micromegas_datafusion_extensions/jsonb/
each.rs1use crate::jsonb::extract::object_or_array_entries;
2use async_trait::async_trait;
3use datafusion::arrow::array::{Array, ArrayRef, BinaryArray, DictionaryArray, GenericBinaryArray};
4use datafusion::arrow::datatypes::{DataType, Field, Int32Type, Schema, SchemaRef};
5use datafusion::arrow::record_batch::RecordBatch;
6use datafusion::catalog::Session;
7use datafusion::catalog::TableFunctionArgs;
8use datafusion::catalog::TableFunctionImpl;
9use datafusion::catalog::TableProvider;
10use datafusion::datasource::TableType;
11use datafusion::datasource::memory::{DataSourceExec, MemorySourceConfig};
12use datafusion::error::DataFusionError;
13use datafusion::logical_expr::{LogicalPlan, LogicalPlanBuilder};
14use datafusion::physical_plan::ExecutionPlan;
15use datafusion::prelude::Expr;
16use datafusion::scalar::ScalarValue;
17use std::sync::Arc;
18
19#[derive(Debug)]
31pub struct JsonbEachTableFunction {}
32
33impl JsonbEachTableFunction {
34 pub fn new() -> Self {
35 Self {}
36 }
37}
38
39impl Default for JsonbEachTableFunction {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45#[derive(Debug, Clone)]
47enum JsonbSource {
48 Literal(ScalarValue),
49 Subquery(Arc<LogicalPlan>),
50}
51
52impl TableFunctionImpl for JsonbEachTableFunction {
53 fn call_with_args(
54 &self,
55 args: TableFunctionArgs,
56 ) -> datafusion::error::Result<Arc<dyn TableProvider>> {
57 let args = args.exprs();
58 if args.len() != 1 {
59 return Err(DataFusionError::Plan(
60 "jsonb_each requires exactly one argument (a JSONB object)".into(),
61 ));
62 }
63
64 let source = match &args[0] {
65 Expr::Literal(scalar, _metadata) => JsonbSource::Literal(scalar.clone()),
66 Expr::ScalarSubquery(subquery) => JsonbSource::Subquery(subquery.subquery.clone()),
67 other => {
68 let plan = LogicalPlanBuilder::empty(true)
69 .project(vec![other.clone()])?
70 .build()?;
71 JsonbSource::Subquery(Arc::new(plan))
72 }
73 };
74
75 Ok(Arc::new(JsonbEachTableProvider { source }))
76 }
77}
78
79fn output_schema() -> SchemaRef {
80 Arc::new(Schema::new(vec![
81 Field::new("key", DataType::Utf8, false),
82 Field::new("value", DataType::Binary, false),
83 ]))
84}
85
86fn extract_entries_from_jsonb(
91 jsonb_bytes: &[u8],
92) -> Result<Vec<(String, Vec<u8>)>, DataFusionError> {
93 object_or_array_entries(jsonb_bytes)?.ok_or_else(|| {
94 DataFusionError::Execution("jsonb_each: input is not a JSONB object or array".into())
95 })
96}
97
98fn entries_to_batch(entries: &[(String, Vec<u8>)]) -> Result<RecordBatch, DataFusionError> {
99 if entries.is_empty() {
100 return Ok(RecordBatch::new_empty(output_schema()));
101 }
102
103 let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
104 let values: Vec<&[u8]> = entries.iter().map(|(_, v)| v.as_slice()).collect();
105
106 let key_array: ArrayRef = Arc::new(datafusion::arrow::array::StringArray::from(keys));
107 let value_array: ArrayRef = Arc::new(BinaryArray::from(values));
108
109 RecordBatch::try_new(output_schema(), vec![key_array, value_array])
110 .map_err(|e| DataFusionError::External(e.into()))
111}
112
113fn scalar_to_entries(scalar: &ScalarValue) -> Result<Vec<(String, Vec<u8>)>, DataFusionError> {
114 match scalar {
115 ScalarValue::Binary(Some(bytes)) => extract_entries_from_jsonb(bytes),
116 ScalarValue::Binary(None) => Ok(vec![]),
117 ScalarValue::Dictionary(_, inner) => scalar_to_entries(inner.as_ref()),
118 _ => Err(DataFusionError::Plan(format!(
119 "jsonb_each argument must be Binary (JSONB), got: {:?}",
120 scalar.data_type()
121 ))),
122 }
123}
124
125fn extract_all_jsonb_bytes_from_column(column: &ArrayRef) -> Result<Vec<Vec<u8>>, DataFusionError> {
128 match column.data_type() {
129 DataType::Binary => {
130 let binary_array = column
131 .as_any()
132 .downcast_ref::<GenericBinaryArray<i32>>()
133 .ok_or_else(|| {
134 DataFusionError::Internal("failed to cast column to BinaryArray".into())
135 })?;
136 Ok((0..binary_array.len())
137 .filter(|&i| !binary_array.is_null(i))
138 .map(|i| binary_array.value(i).to_vec())
139 .collect())
140 }
141 DataType::Dictionary(_, value_type) if matches!(value_type.as_ref(), DataType::Binary) => {
142 let dict_array = column
143 .as_any()
144 .downcast_ref::<DictionaryArray<Int32Type>>()
145 .ok_or_else(|| {
146 DataFusionError::Internal(
147 "failed to cast column to DictionaryArray<Int32, Binary>".into(),
148 )
149 })?;
150 let binary_values = dict_array
151 .values()
152 .as_any()
153 .downcast_ref::<GenericBinaryArray<i32>>()
154 .ok_or_else(|| {
155 DataFusionError::Internal("dictionary values are not a binary array".into())
156 })?;
157 Ok((0..dict_array.len())
158 .filter(|&i| !dict_array.is_null(i))
159 .map(|i| {
160 let key_index = dict_array.keys().value(i) as usize;
161 binary_values.value(key_index).to_vec()
162 })
163 .collect())
164 }
165 other => Err(DataFusionError::Execution(format!(
166 "jsonb_each subquery must return a Binary or Dictionary<Int32, Binary> column, got: {other:?}"
167 ))),
168 }
169}
170
171#[derive(Debug)]
173pub struct JsonbEachTableProvider {
174 source: JsonbSource,
175}
176
177impl JsonbEachTableProvider {
178 pub fn from_scalar(scalar: ScalarValue) -> Result<Self, DataFusionError> {
180 if !matches!(&scalar, ScalarValue::Binary(Some(_))) {
181 return Err(DataFusionError::Plan(format!(
182 "jsonb_each argument must be Binary (JSONB), got: {:?}",
183 scalar.data_type()
184 )));
185 }
186 Ok(Self {
187 source: JsonbSource::Literal(scalar),
188 })
189 }
190}
191
192#[async_trait]
193impl TableProvider for JsonbEachTableProvider {
194 fn schema(&self) -> SchemaRef {
195 output_schema()
196 }
197
198 fn table_type(&self) -> TableType {
199 TableType::Temporary
200 }
201
202 async fn scan(
203 &self,
204 state: &dyn Session,
205 projection: Option<&Vec<usize>>,
206 _filters: &[Expr],
207 limit: Option<usize>,
208 ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
209 let entries = match &self.source {
210 JsonbSource::Literal(scalar) => scalar_to_entries(scalar)?,
211 JsonbSource::Subquery(plan) => {
212 let physical_plan = state.create_physical_plan(plan).await?;
213 let task_ctx = state.task_ctx();
214 let batches = datafusion::physical_plan::collect(physical_plan, task_ctx).await?;
215
216 if batches.is_empty() || batches.iter().all(|b| b.num_rows() == 0) {
217 return Err(DataFusionError::Execution(
218 "jsonb_each subquery returned no rows".into(),
219 ));
220 }
221
222 let mut all_entries = Vec::new();
223 for batch in &batches {
224 if batch.num_columns() != 1 {
225 return Err(DataFusionError::Execution(format!(
226 "jsonb_each subquery must return exactly one column, got {}",
227 batch.num_columns()
228 )));
229 }
230 for jsonb_bytes in extract_all_jsonb_bytes_from_column(batch.column(0))? {
231 all_entries.extend(extract_entries_from_jsonb(&jsonb_bytes)?);
232 }
233 }
234 all_entries
235 }
236 };
237
238 let mut record_batch = entries_to_batch(&entries)?;
239
240 if let Some(n) = limit
242 && n < record_batch.num_rows()
243 {
244 record_batch = record_batch.slice(0, n);
245 }
246
247 let source = MemorySourceConfig::try_new(
248 &[vec![record_batch]],
249 self.schema(),
250 projection.map(|v| v.to_owned()),
251 )?;
252 Ok(DataSourceExec::from_data_source(source))
253 }
254}