cj-zhukov opened a new issue, #85:
URL: https://github.com/apache/datafusion-spark/issues/85

   _Moved from apache/datafusion#26185, opened before the `datafusion-spark` 
crate was moved to this repository. The implementation PR 
apache/datafusion#26187 (reviewed there, not merged before the move) is being 
ported here._
   
   ### Is your feature request related to a problem or challenge?
   
   There is no way to turn a struct, or a set of columns, into a JSON string 
column without leaving the lazy plan. Today this needs `collect()`, 
concatenating the batches, serializing with `arrow_json::ArrayWriter`, and 
rebuilding the DataFrame with `read_batch`, which materializes the whole table 
and discards the original plan:
   
   ```rust
   /// Create json like string column new_col from cols
   ///
   /// # Examples
   /// ```
   /// # use color_eyre::Result;
   /// use datafusion::prelude::*;
   /// # use datafusion_example::utils::dataframe::df_cols_to_json;
   /// # use datafusion_example::utils::datasets::ExampleDataset;
   /// # use datafusion::assert_batches_eq;
   /// # #[tokio::main]
   /// # async fn main() -> Result<()> {
   /// let ctx = SessionContext::new();
   /// let cars = ExampleDataset::Cars;
   /// let df = cars.dataframe(&ctx).await?;
   /// let res = df_cols_to_json(&ctx, df, &["car", "speed"], 
"new_col").await?.sort(vec![col("time").sort(true, true)])?;
   /// assert_batches_eq!(
   ///  &[  
   ///     "+---------------------+------------------------------+",
   ///     "| time                | new_col                      |",
   ///     "+---------------------+------------------------------+",
   ///     "| 1996-04-12T12:05:03 | {\"car\":\"red\",\"speed\":20.0}   |",
   ///     "| 1996-04-12T12:05:03 | {\"car\":\"green\",\"speed\":10.0} |",
   ///     "| 1996-04-12T12:05:04 | {\"car\":\"red\",\"speed\":20.3}   |",
   ///     "| 1996-04-12T12:05:04 | {\"car\":\"green\",\"speed\":10.3} |",
   ///     "| 1996-04-12T12:05:05 | {\"car\":\"red\",\"speed\":21.4}   |",
   ///     "| 1996-04-12T12:05:05 | {\"car\":\"green\",\"speed\":10.4} |",
   ///     "| 1996-04-12T12:05:06 | {\"car\":\"red\",\"speed\":21.5}   |",
   ///     "| 1996-04-12T12:05:06 | {\"car\":\"green\",\"speed\":10.5} |",
   ///     "| 1996-04-12T12:05:07 | {\"car\":\"red\",\"speed\":19.0}   |",
   ///     "| 1996-04-12T12:05:07 | {\"car\":\"green\",\"speed\":11.0} |",
   ///     "| 1996-04-12T12:05:08 | {\"car\":\"red\",\"speed\":18.0}   |",
   ///     "| 1996-04-12T12:05:08 | {\"car\":\"green\",\"speed\":12.0} |",
   ///     "| 1996-04-12T12:05:09 | {\"car\":\"red\",\"speed\":17.0}   |",
   ///     "| 1996-04-12T12:05:09 | {\"car\":\"green\",\"speed\":14.0} |",
   ///     "| 1996-04-12T12:05:10 | {\"car\":\"red\",\"speed\":7.0}    |",
   ///     "| 1996-04-12T12:05:10 | {\"car\":\"green\",\"speed\":15.0} |",
   ///     "| 1996-04-12T12:05:11 | {\"car\":\"red\",\"speed\":7.1}    |",
   ///     "| 1996-04-12T12:05:11 | {\"car\":\"green\",\"speed\":15.1} |",
   ///     "| 1996-04-12T12:05:12 | {\"car\":\"red\",\"speed\":7.2}    |",
   ///     "| 1996-04-12T12:05:12 | {\"car\":\"green\",\"speed\":15.2} |",
   ///     "| 1996-04-12T12:05:13 | {\"car\":\"red\",\"speed\":3.0}    |",
   ///     "| 1996-04-12T12:05:13 | {\"car\":\"green\",\"speed\":8.0}  |",
   ///     "| 1996-04-12T12:05:14 | {\"car\":\"red\",\"speed\":1.0}    |",
   ///     "| 1996-04-12T12:05:14 | {\"car\":\"green\",\"speed\":2.0}  |",
   ///     "| 1996-04-12T12:05:15 | {\"car\":\"red\",\"speed\":0.0}    |",
   ///     "+---------------------+------------------------------+",
   ///  ],
   ///    &res.collect().await?
   ///  );
   /// # Ok(())
   /// # }
   /// ```
   pub async fn df_cols_to_json(
       ctx: &SessionContext,
       df: DataFrame,
       cols: &[&str],
       new_col: &str,
   ) -> Result<DataFrame, UtilsError> {
       let schema_ref = Arc::new(df.schema().as_arrow().clone());
       let arrays = concat_arrays(df).await?;
       let batch = RecordBatch::try_new(schema_ref.clone(), arrays)?;
   
       let selected_arrays: Vec<ArrayRef> = cols
           .iter()
           .map(|col| {
               batch
                   .column_by_name(col)
                   .ok_or_else(|| DataFusionError::Plan(format!("column {col} 
not found")))
                   .cloned()
           })
           .collect::<Result<_, _>>()?;
   
       let selected_fields: Vec<Field> = cols
           .iter()
           .map(|col| schema_ref.field_with_name(col).cloned())
           .collect::<Result<_, _>>()?;
   
       let selected_schema = Arc::new(Schema::new(selected_fields));
       let selected_batch = RecordBatch::try_new(selected_schema.clone(), 
selected_arrays)?;
   
       let mut writer = arrow_json::ArrayWriter::new(Vec::new());
       writer.write(&selected_batch)?;
       writer.finish()?;
   
       let json_data = writer.into_inner();
       let json_rows: Vec<Map<String, Value>> = 
serde_json::from_reader(json_data.as_slice())?;
       let str_rows: Vec<String> = json_rows
           .iter()
           .map(serde_json::to_string)
           .collect::<Result<_, _>>()?;
   
       let mut all_columns = batch.columns().to_vec();
       all_columns.push(Arc::new(StringArray::from(str_rows)) as ArrayRef);
   
       let mut new_fields: Vec<Field> = schema_ref
           .fields()
           .iter()
           .map(|f| f.as_ref().clone())
           .collect();
       new_fields.push(Field::new(new_col, DataType::Utf8, true));
       let new_schema = Arc::new(Schema::new(new_fields));
   
       let new_batch = RecordBatch::try_new(new_schema, all_columns)?;
       let res = ctx.read_batch(new_batch)?.drop_columns(cols)?;
       Ok(res)
   }
   ```
   
   Spark exposes this as an ordinary expression, `to_json(struct)`. DataFusion 
already has the first half (`named_struct`, `struct`) but no serializer.
   
   ### Describe the solution you'd like
   
   A Spark-compatible `to_json` scalar function in this crate, plus a small 
`Expr` builder so it can be used with `with_column` either on an existing 
struct column or on a list of column names:
   
   ```rust
   use datafusion::prelude::*;
   use datafusion_spark::expr_fn::to_json;
   
   let df = dataframe!(
       "id"  => [10, 20, 30],
       "foo" => [1, 2, 3],
       "bar" => [None, Some("bak"), Some("baz")],
   )?;
   
   // pack columns into a struct and serialize it
   let df = df.with_column("json", to_json(&["foo", "bar"]))?;
   
   // or serialize an existing struct column
   // let df = df.with_column("json", to_json(col("event")))?;
   
   df.show().await?;
   ```
   
   ```
   +----+-----+-----+-----------------------+
   | id | foo | bar | json                  |
   +----+-----+-----+-----------------------+
   | 10 | 1   |     | {"foo":1}             |
   | 20 | 2   | bak | {"foo":2,"bar":"bak"} |
   | 30 | 3   | baz | {"foo":3,"bar":"baz"} |
   +----+-----+-----+-----------------------+
   ```
   
   The same function from SQL:
   
   ```sql
   SELECT id, to_json(named_struct('foo', foo, 'bar', bar)) AS json FROM t;
   ```
   
   Semantics follow Spark's defaults: NULL struct fields are omitted 
(`ignoreNullFields = true`) while NULL map entries are kept, map keys are 
written as strings, binary is Base64, non-finite floats are the strings `"NaN"` 
/ `"Infinity"` / `"-Infinity"`, a NULL struct produces NULL, and nested 
structs, lists and maps are serialized recursively. `to_json(&[...])` is plain 
sugar for `to_json(named_struct(...))`, so `with_column` needs no changes and 
the result stays a lazy projection (serialization runs per batch with the 
arrow-json row encoder, no `collect`).
   
   Not in scope for a first version: the Spark `options` argument 
(`ignoreNullFields`, `timestampFormat`) and map/array top-level inputs.
   
   ### Describe alternatives you've considered
   
   - Keeping the `collect()`-based helper in user code: works, but executes the 
plan and loses laziness, filter pushdown and the original lineage.
   - `arrow_cast(struct_col, 'Utf8')`: produces the Arrow display format (`{a: 
1, b: x}`), not JSON.
   
   ### Additional context
   
   - Spark `to_json`: 
https://spark.apache.org/docs/latest/api/sql/index.html#to_json
   - Original issue and review history: apache/datafusion#26185, 
apache/datafusion#26187
   
   ### Willingness to contribute
   
   I plan to contribute to this feature independently.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to