Skip to main content

minarrow/kernels/broadcast/
table.rs

1// Copyright 2025 Peter Garfield Bower
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17#[cfg(feature = "scalar_type")]
18use crate::Scalar;
19use crate::enums::error::{KernelError, MinarrowError};
20use crate::enums::operators::ArithmeticOperator;
21use crate::kernels::broadcast::array::broadcast_array_add;
22use crate::kernels::broadcast::broadcast_value;
23use crate::kernels::broadcast::table_view::broadcast_tableview_to_arrayview;
24use crate::kernels::routing::arithmetic::resolve_binary_arithmetic;
25use crate::structs::field_array::create_field_for_array;
26use crate::{Array, ArrayV, Bitmask, Field, FieldArray, Table, TableV, Value};
27#[cfg(feature = "chunked")]
28use crate::{SuperArray, SuperArrayV, SuperTable, SuperTableV};
29
30/// General table broadcasting function that supports all arithmetic operators
31pub fn broadcast_table_with_operator(
32    op: ArithmeticOperator,
33    table_l: impl Into<TableV>,
34    table_r: impl Into<TableV>,
35) -> Result<Table, MinarrowError> {
36    let table_l: TableV = table_l.into();
37    let table_r: TableV = table_r.into();
38
39    // Ensure tables have same number of columns
40    if table_l.cols.len() != table_r.cols.len() {
41        return Err(MinarrowError::ShapeError {
42            message: format!(
43                "Table column count mismatch: {} vs {}",
44                table_l.cols.len(),
45                table_r.cols.len()
46            ),
47        });
48    }
49
50    let mut result_field_arrays = Vec::new();
51
52    for (i, (col_l, col_r)) in table_l.cols.iter().zip(table_r.cols.iter()).enumerate() {
53        // Route through array broadcasting
54        let result_array = resolve_binary_arithmetic(op, col_l.clone(), col_r.clone(), None)?;
55
56        // Create new FieldArray with the field schema from the left table
57        let result_field_array = FieldArray::new(table_l.fields[i].as_ref().clone(), result_array);
58        result_field_arrays.push(result_field_array);
59    }
60
61    Ok(Table::new(table_l.name.clone(), Some(result_field_arrays)))
62}
63
64/// Broadcasts addition over table columns element-wise
65///
66/// Both tables must have the same number of columns and rows.
67/// Addition is applied column-wise between corresponding columns.
68pub fn broadcast_table_add(
69    lhs: impl Into<TableV>,
70    rhs: impl Into<TableV>,
71    null_mask: Option<Arc<Bitmask>>,
72) -> Result<Table, KernelError> {
73    let lhs_table: TableV = lhs.into();
74    let rhs_table: TableV = rhs.into();
75
76    // Check shape compatibility
77    if lhs_table.cols.len() != rhs_table.cols.len() {
78        return Err(KernelError::BroadcastingError(format!(
79            "Table column count mismatch: LHS {} cols, RHS {} cols",
80            lhs_table.cols.len(),
81            rhs_table.cols.len()
82        )));
83    }
84
85    if lhs_table.len != rhs_table.len {
86        return Err(KernelError::BroadcastingError(format!(
87            "Table row count mismatch: LHS {} rows, RHS {} rows",
88            lhs_table.len, rhs_table.len
89        )));
90    }
91
92    // Apply addition column-wise
93    let mut result_cols = Vec::with_capacity(lhs_table.cols.len());
94
95    for (i, (lhs_col, rhs_col)) in lhs_table.cols.iter().zip(rhs_table.cols.iter()).enumerate() {
96        let result_array =
97            broadcast_array_add(lhs_col.clone(), rhs_col.clone(), null_mask.as_deref()).map_err(
98                |e| KernelError::BroadcastingError(format!("Column {} addition failed: {}", i, e)),
99            )?;
100
101        // Create FieldArray with name from left table
102        let field_name = if i < lhs_table.fields.len() {
103            lhs_table.fields[i].name.clone()
104        } else {
105            format!("col_{}", i)
106        };
107
108        let field_dtype = if i < lhs_table.fields.len() {
109            lhs_table.fields[i].dtype.clone()
110        } else {
111            result_array.arrow_type()
112        };
113
114        let field = Field::new(
115            field_name,
116            field_dtype,
117            result_array.null_mask().is_some(), // nullable based on result array
118            None,                               // metadata
119        );
120        let field_array = FieldArray::new(field, result_array);
121
122        result_cols.push(field_array);
123    }
124
125    // Create result table with same name as left table
126    Ok(Table::new(lhs_table.name.clone(), Some(result_cols)))
127}
128
129/// Broadcasts addition over SuperTable chunks (batched tables)
130///
131/// Both SuperTables must have the same number of chunks and compatible shapes.
132/// Addition is applied chunk-wise between corresponding table chunks.
133#[cfg(feature = "chunked")]
134pub fn broadcast_super_table_add(
135    lhs: impl Into<SuperTableV>,
136    rhs: impl Into<SuperTableV>,
137    null_mask: Option<Arc<Bitmask>>,
138) -> Result<SuperTable, KernelError> {
139    let lhs_table: SuperTableV = lhs.into();
140    let rhs_table: SuperTableV = rhs.into();
141
142    // Check chunk count compatibility
143    if lhs_table.slices.len() != rhs_table.slices.len() {
144        return Err(KernelError::BroadcastingError(format!(
145            "SuperTable chunk count mismatch: LHS {} chunks, RHS {} chunks",
146            lhs_table.slices.len(),
147            rhs_table.slices.len()
148        )));
149    }
150
151    // Apply addition chunk-wise
152    let mut result_tables = Vec::with_capacity(lhs_table.slices.len());
153
154    for (i, (lhs_chunk, rhs_chunk)) in lhs_table
155        .slices
156        .iter()
157        .zip(rhs_table.slices.iter())
158        .enumerate()
159    {
160        let result_table =
161            broadcast_table_add(lhs_chunk.clone(), rhs_chunk.clone(), null_mask.clone()).map_err(
162                |e| KernelError::BroadcastingError(format!("Chunk {} addition failed: {}", i, e)),
163            )?;
164
165        result_tables.push(Arc::new(result_table));
166    }
167
168    // Create result SuperTable - use name from first slice if available
169    let name = if !lhs_table.slices.is_empty() && !lhs_table.slices[0].name.is_empty() {
170        lhs_table.slices[0].name.clone()
171    } else {
172        "SuperTable".to_string()
173    };
174    Ok(SuperTable::from_batches(result_tables, Some(name)))
175}
176
177/// Helper function for table-array broadcasting - apply table columns to array
178pub fn broadcast_table_to_array(
179    op: ArithmeticOperator,
180    table: &Table,
181    array: &Array,
182) -> Result<Table, MinarrowError> {
183    let new_cols: Result<Vec<_>, _> = table
184        .cols
185        .iter()
186        .map(|field_array| {
187            let col_array = &field_array.array;
188            let result_array = match (
189                Value::Array(Arc::new(col_array.clone())),
190                Value::Array(Arc::new(array.clone())),
191            ) {
192                (a, b) => broadcast_value(op, a, b)?,
193            };
194
195            match result_array {
196                Value::Array(result_array) => {
197                    let result_array = Arc::unwrap_or_clone(result_array);
198                    // Preserve original field metadata but update type if needed
199                    let new_field = create_field_for_array(
200                        &field_array.field.name,
201                        &result_array,
202                        Some(&array),
203                        Some(field_array.field.metadata.clone()),
204                    );
205                    Ok(FieldArray::new(new_field, result_array))
206                }
207                _ => Err(MinarrowError::TypeError {
208                    from: "table-array broadcasting",
209                    to: "Array result",
210                    message: Some("Expected Array result from broadcasting".to_string()),
211                }),
212            }
213        })
214        .collect();
215
216    let table_out = Table::new(table.name.clone(), Some(new_cols?));
217    #[cfg(feature = "table_metadata")]
218    {
219        let mut t = table_out;
220        t.metadata = table.metadata.clone();
221        return Ok(t);
222    }
223    #[cfg(not(feature = "table_metadata"))]
224    Ok(table_out)
225}
226
227/// Helper function for table-scalar broadcasting - apply table columns to scalar
228#[cfg(feature = "scalar_type")]
229pub fn broadcast_table_to_scalar(
230    op: ArithmeticOperator,
231    table: &Table,
232    scalar: &Scalar,
233) -> Result<Table, MinarrowError> {
234    let new_cols: Result<Vec<_>, _> = table
235        .cols
236        .iter()
237        .map(|field_array| {
238            let col_array = &field_array.array;
239            let result_array = match (
240                Value::Array(Arc::new(col_array.clone())),
241                Value::Scalar(scalar.clone()),
242            ) {
243                (a, b) => broadcast_value(op, a, b)?,
244            };
245
246            match result_array {
247                Value::Array(result_array) => {
248                    let result_array = Arc::unwrap_or_clone(result_array);
249                    // Preserve original field metadata but update type if needed
250                    let new_field = create_field_for_array(
251                        &field_array.field.name,
252                        &result_array,
253                        Some(&col_array),
254                        Some(field_array.field.metadata.clone()),
255                    );
256                    Ok(FieldArray::new(new_field, result_array))
257                }
258                _ => Err(MinarrowError::TypeError {
259                    from: "table-scalar broadcasting",
260                    to: "Array result",
261                    message: Some("Expected Array result from broadcasting".to_string()),
262                }),
263            }
264        })
265        .collect();
266
267    let table_out = Table::new(table.name.clone(), Some(new_cols?));
268    #[cfg(feature = "table_metadata")]
269    {
270        let mut t = table_out;
271        t.metadata = table.metadata.clone();
272        return Ok(t);
273    }
274    #[cfg(not(feature = "table_metadata"))]
275    Ok(table_out)
276}
277
278/// Helper function for table-arrayview broadcasting - work directly with view without conversion
279#[cfg(feature = "views")]
280pub fn broadcast_table_to_arrayview(
281    op: ArithmeticOperator,
282    table: &Table,
283    array_view: &ArrayV,
284) -> Result<Table, MinarrowError> {
285    // Work directly with the view's underlying array and window bounds
286    let new_cols: Result<Vec<_>, _> = table
287        .cols
288        .iter()
289        .map(|field_array| {
290            let col_array = &field_array.array;
291            // Create a view of the column array that matches the input view's window
292            let col_view = ArrayV::new(col_array.clone(), array_view.offset, array_view.len());
293            let result_array = match (
294                Value::ArrayView(Arc::new(col_view)),
295                Value::ArrayView(Arc::new(array_view.clone())),
296            ) {
297                (a, b) => broadcast_value(op, a, b)?,
298            };
299
300            match result_array {
301                Value::Array(result_array) => {
302                    let result_array = Arc::unwrap_or_clone(result_array);
303                    let new_field = create_field_for_array(
304                        &field_array.field.name,
305                        &result_array,
306                        Some(&array_view.array),
307                        Some(field_array.field.metadata.clone()),
308                    );
309                    Ok(FieldArray::new(new_field, result_array))
310                }
311                _ => Err(MinarrowError::TypeError {
312                    from: "table-arrayview broadcasting",
313                    to: "Array result",
314                    message: Some("Expected Array result from view broadcasting".to_string()),
315                }),
316            }
317        })
318        .collect();
319
320    let table_out = Table::new(table.name.clone(), Some(new_cols?));
321    #[cfg(feature = "table_metadata")]
322    {
323        let mut t = table_out;
324        t.metadata = table.metadata.clone();
325        return Ok(t);
326    }
327    #[cfg(not(feature = "table_metadata"))]
328    Ok(table_out)
329}
330
331/// Helper function for Table-SuperArrayView broadcasting - promote Table to aligned SuperTableView
332#[cfg(all(feature = "chunked", feature = "views"))]
333pub fn broadcast_table_to_superarrayview(
334    op: ArithmeticOperator,
335    table: &Table,
336    super_array_view: &SuperArrayV,
337) -> Result<SuperTableV, MinarrowError> {
338    // 1. Validate lengths match
339    if table.n_rows != super_array_view.len {
340        return Err(MinarrowError::ShapeError {
341            message: format!(
342                "Table rows ({}) does not match SuperArrayView length ({})",
343                table.n_rows, super_array_view.len
344            ),
345        });
346    }
347
348    // 2. Promote Table to SuperTableView with aligned chunking
349    let mut current_offset = 0;
350    let mut table_slices = Vec::new();
351
352    for array_slice in super_array_view.slices.iter() {
353        let chunk_len = array_slice.len();
354        let table_slice = TableV::from_table(table.clone(), current_offset, chunk_len);
355        table_slices.push(table_slice);
356        current_offset += chunk_len;
357    }
358
359    let aligned_super_table = SuperTableV {
360        slices: table_slices,
361        len: table.n_rows,
362    };
363
364    // 3. Broadcast per chunk using indexed loops
365    let mut result_slices = Vec::new();
366    for i in 0..aligned_super_table.slices.len() {
367        let table_slice = &aligned_super_table.slices[i];
368        let array_slice = &super_array_view.slices[i];
369        let slice_result = broadcast_tableview_to_arrayview(op, table_slice, array_slice)?;
370        result_slices.push(slice_result);
371    }
372
373    Ok(SuperTableV {
374        slices: result_slices,
375        len: super_array_view.len,
376    })
377}
378
379/// Helper function for Table-SuperArray broadcasting - broadcast table against each chunk
380#[cfg(feature = "chunked")]
381pub fn broadcast_table_to_superarray(
382    op: ArithmeticOperator,
383    table: &Table,
384    super_array: &SuperArray,
385) -> Result<SuperArray, MinarrowError> {
386    let new_chunks: Result<Vec<_>, _> = super_array
387        .chunks()
388        .iter()
389        .map(|chunk| {
390            let chunk_array = chunk;
391            let result_table = broadcast_table_to_array(op, table, chunk_array)?;
392            // Convert result table back to a FieldArray chunk with matching structure
393            if result_table.cols.len() == 1 {
394                Ok(result_table.cols[0].clone())
395            } else {
396                Err(MinarrowError::ShapeError {
397                    message: "Table-SuperArray broadcasting should result in single column"
398                        .to_string(),
399                })
400            }
401        })
402        .collect();
403
404    Ok(SuperArray::from_chunks(new_chunks?))
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::traits::selection::ColumnSelection;
411    use crate::{Array, FieldArray, IntegerArray, vec64};
412
413    fn create_test_table(name: &str, data1: &[i32], data2: &[i32]) -> Table {
414        let col1 = FieldArray::from_arr(
415            "col1",
416            Array::from_int32(IntegerArray::from_slice(&vec64![
417                data1[0], data1[1], data1[2]
418            ])),
419        );
420        let col2 = FieldArray::from_arr(
421            "col2",
422            Array::from_int32(IntegerArray::from_slice(&vec64![
423                data2[0], data2[1], data2[2]
424            ])),
425        );
426
427        Table::new(name.to_string(), Some(vec![col1, col2]))
428    }
429
430    #[test]
431    fn test_table_plus_table() {
432        let table1 = create_test_table("table1", &[1, 2, 3], &[10, 20, 30]);
433        let table2 = create_test_table("table2", &[4, 5, 6], &[40, 50, 60]);
434
435        let result = broadcast_table_add(table1, table2, None).unwrap();
436
437        assert_eq!(result.n_cols(), 2);
438        assert_eq!(result.n_rows(), 3);
439        assert_eq!(result.name, "table1"); // Takes name from left table
440
441        // Check first column: [1,2,3] + [4,5,6] = [5,7,9]
442        if let Some(col1) = result.col_ix(0) {
443            if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &col1.array {
444                assert_eq!(arr.data.as_slice(), &[5, 7, 9]);
445            } else {
446                panic!("Expected Int32 array in first column");
447            }
448        } else {
449            panic!("Could not get first column");
450        }
451
452        // Check second column: [10,20,30] + [40,50,60] = [50,70,90]
453        if let Some(col2) = result.col_ix(1) {
454            if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &col2.array {
455                assert_eq!(arr.data.as_slice(), &[50, 70, 90]);
456            } else {
457                panic!("Expected Int32 array in second column");
458            }
459        } else {
460            panic!("Could not get second column");
461        }
462    }
463
464    #[test]
465    #[should_panic(expected = "column count mismatch")]
466    fn test_mismatched_column_count() {
467        let col1 = FieldArray::from_arr(
468            "col1",
469            Array::from_int32(IntegerArray::from_slice(&vec64![1, 2, 3])),
470        );
471        let table1 = Table::new("table1".to_string(), Some(vec![col1])); // 1 column
472
473        let table2 = create_test_table("table2", &[4, 5, 6], &[40, 50, 60]); // 2 columns
474
475        let _ = broadcast_table_add(table1, table2, None).unwrap();
476    }
477
478    #[test]
479    #[should_panic(expected = "row count mismatch")]
480    fn test_mismatched_row_count() {
481        let col1 = FieldArray::from_arr(
482            "col1",
483            Array::from_int32(IntegerArray::from_slice(&vec64![1, 2])),
484        );
485        let col2 = FieldArray::from_arr(
486            "col2",
487            Array::from_int32(IntegerArray::from_slice(&vec64![10, 20])),
488        );
489        let table1 = Table::new("table1".to_string(), Some(vec![col1, col2])); // 2 rows
490
491        let table2 = create_test_table("table2", &[4, 5, 6], &[40, 50, 60]); // 3 rows
492
493        let _ = broadcast_table_add(table1, table2, None).unwrap();
494    }
495
496    #[test]
497    fn test_broadcast_table_with_operator_multiply() {
498        let table1 = create_test_table("table1", &[2, 3, 4], &[5, 6, 7]);
499        let table2 = create_test_table("table2", &[10, 10, 10], &[2, 2, 2]);
500
501        let result =
502            broadcast_table_with_operator(ArithmeticOperator::Multiply, table1, table2).unwrap();
503
504        // col1: [2,3,4] * [10,10,10] = [20,30,40]
505        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[0].array {
506            assert_eq!(arr.data.as_slice(), &[20, 30, 40]);
507        } else {
508            panic!("Expected Int32 array");
509        }
510
511        // col2: [5,6,7] * [2,2,2] = [10,12,14]
512        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[1].array {
513            assert_eq!(arr.data.as_slice(), &[10, 12, 14]);
514        } else {
515            panic!("Expected Int32 array");
516        }
517    }
518
519    #[test]
520    fn test_broadcast_table_to_array() {
521        let table = create_test_table("table1", &[10, 20, 30], &[100, 200, 300]);
522        let arr = Array::from_int32(IntegerArray::from_slice(&vec64![1, 2, 3]));
523
524        let result = broadcast_table_to_array(ArithmeticOperator::Add, &table, &arr).unwrap();
525
526        // col1: [10,20,30] + [1,2,3] = [11,22,33]
527        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[0].array {
528            assert_eq!(arr.data.as_slice(), &[11, 22, 33]);
529        } else {
530            panic!("Expected Int32 array");
531        }
532
533        // col2: [100,200,300] + [1,2,3] = [101,202,303]
534        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[1].array {
535            assert_eq!(arr.data.as_slice(), &[101, 202, 303]);
536        } else {
537            panic!("Expected Int32 array");
538        }
539    }
540
541    #[cfg(feature = "scalar_type")]
542    #[test]
543    fn test_broadcast_table_to_scalar() {
544        let table = create_test_table("table1", &[10, 20, 30], &[100, 200, 300]);
545        let scalar = Scalar::Int32(5);
546
547        let result =
548            broadcast_table_to_scalar(ArithmeticOperator::Multiply, &table, &scalar).unwrap();
549
550        // col1: [10,20,30] * 5 = [50,100,150]
551        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[0].array {
552            assert_eq!(arr.data.as_slice(), &[50, 100, 150]);
553        } else {
554            panic!("Expected Int32 array");
555        }
556
557        // col2: [100,200,300] * 5 = [500,1000,1500]
558        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[1].array {
559            assert_eq!(arr.data.as_slice(), &[500, 1000, 1500]);
560        } else {
561            panic!("Expected Int32 array");
562        }
563    }
564
565    #[cfg(feature = "views")]
566    #[test]
567    fn test_broadcast_table_to_arrayview() {
568        let table = create_test_table("table1", &[10, 20, 30], &[100, 200, 300]);
569        let arr = Array::from_int32(IntegerArray::from_slice(&vec64![2, 3, 4]));
570        let array_view = ArrayV::from(arr);
571
572        let result =
573            broadcast_table_to_arrayview(ArithmeticOperator::Subtract, &table, &array_view)
574                .unwrap();
575
576        // col1: [10,20,30] - [2,3,4] = [8,17,26]
577        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[0].array {
578            assert_eq!(arr.data.as_slice(), &[8, 17, 26]);
579        } else {
580            panic!("Expected Int32 array");
581        }
582
583        // col2: [100,200,300] - [2,3,4] = [98,197,296]
584        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.cols[1].array {
585            assert_eq!(arr.data.as_slice(), &[98, 197, 296]);
586        } else {
587            panic!("Expected Int32 array");
588        }
589    }
590
591    #[cfg(feature = "chunked")]
592    #[test]
593    fn test_broadcast_super_table_add() {
594        let table1 = create_test_table("table1", &[1, 2, 3], &[10, 20, 30]);
595        let table2 = create_test_table("table2", &[4, 5, 6], &[40, 50, 60]);
596        let table3 = create_test_table("table3", &[7, 8, 9], &[70, 80, 90]);
597        let table4 = create_test_table("table4", &[1, 1, 1], &[2, 2, 2]);
598
599        let super_table1 = SuperTableV {
600            slices: vec![
601                TableV::from_table(table1, 0, 3),
602                TableV::from_table(table2, 0, 3),
603            ],
604            len: 6,
605        };
606
607        let super_table2 = SuperTableV {
608            slices: vec![
609                TableV::from_table(table3, 0, 3),
610                TableV::from_table(table4, 0, 3),
611            ],
612            len: 6,
613        };
614
615        let result = broadcast_super_table_add(super_table1, super_table2, None).unwrap();
616
617        assert_eq!(result.batches.len(), 2);
618
619        // First batch: table1 + table3
620        // col1: [1,2,3] + [7,8,9] = [8,10,12]
621        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) =
622            &result.batches[0].cols[0].array
623        {
624            assert_eq!(arr.data.as_slice(), &[8, 10, 12]);
625        } else {
626            panic!("Expected Int32 array");
627        }
628
629        // col2: [10,20,30] + [70,80,90] = [80,100,120]
630        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) =
631            &result.batches[0].cols[1].array
632        {
633            assert_eq!(arr.data.as_slice(), &[80, 100, 120]);
634        } else {
635            panic!("Expected Int32 array");
636        }
637
638        // Second batch: table2 + table4
639        // col1: [4,5,6] + [1,1,1] = [5,6,7]
640        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) =
641            &result.batches[1].cols[0].array
642        {
643            assert_eq!(arr.data.as_slice(), &[5, 6, 7]);
644        } else {
645            panic!("Expected Int32 array");
646        }
647
648        // col2: [40,50,60] + [2,2,2] = [42,52,62]
649        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) =
650            &result.batches[1].cols[1].array
651        {
652            assert_eq!(arr.data.as_slice(), &[42, 52, 62]);
653        } else {
654            panic!("Expected Int32 array");
655        }
656    }
657
658    #[cfg(feature = "chunked")]
659    #[test]
660    fn test_broadcast_table_to_superarray() {
661        use crate::ffi::arrow_dtype::ArrowType;
662
663        // Table with 3 rows to match each chunk size
664        let table = Table::build(
665            vec![FieldArray::new(
666                Field::new("col1".to_string(), ArrowType::Int32, false, None),
667                Array::from_int32(IntegerArray::from_slice(&vec64![2, 3, 4])),
668            )],
669            3,
670            "test".to_string(),
671        );
672
673        let field = Field::new("data".to_string(), ArrowType::Int32, false, None);
674        let chunks = vec![
675            FieldArray::new(
676                field.clone(),
677                Array::from_int32(IntegerArray::from_slice(&vec64![10, 20, 30])),
678            ),
679            FieldArray::new(
680                field.clone(),
681                Array::from_int32(IntegerArray::from_slice(&vec64![40, 50, 60])),
682            ),
683        ];
684        let super_array = SuperArray::from_chunks(chunks);
685
686        let result =
687            broadcast_table_to_superarray(ArithmeticOperator::Add, &table, &super_array).unwrap();
688
689        assert_eq!(result.chunks().len(), 2);
690
691        // Both chunks: [2,3,4] + [10,20,30] = [12,23,34] and [2,3,4] + [40,50,60] = [42,53,64]
692        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.chunks()[0] {
693            assert_eq!(arr.data.as_slice(), &[12, 23, 34]);
694        } else {
695            panic!("Expected Int32 array");
696        }
697
698        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &result.chunks()[1] {
699            assert_eq!(arr.data.as_slice(), &[42, 53, 64]);
700        } else {
701            panic!("Expected Int32 array");
702        }
703    }
704
705    #[cfg(all(feature = "chunked", feature = "views"))]
706    #[test]
707    fn test_broadcast_table_to_superarrayview() {
708        use crate::ffi::arrow_dtype::ArrowType;
709
710        let table = Table::build(
711            vec![FieldArray::new(
712                Field::new("col1".to_string(), ArrowType::Int32, false, None),
713                Array::from_int32(IntegerArray::from_slice(&vec64![1, 2, 3, 4, 5, 6])),
714            )],
715            6,
716            "test".to_string(),
717        );
718
719        let arr = Array::from_int32(IntegerArray::from_slice(&vec64![10, 20, 30, 40, 50, 60]));
720        let field = Field::new("data".to_string(), ArrowType::Int32, false, None);
721
722        let slices = vec![
723            ArrayV::from(arr.clone()).slice(0, 3),
724            ArrayV::from(arr.clone()).slice(3, 3),
725        ];
726        let super_array_view = SuperArrayV {
727            slices,
728            field: Arc::new(field),
729            len: 6,
730        };
731
732        let result = broadcast_table_to_superarrayview(
733            ArithmeticOperator::Multiply,
734            &table,
735            &super_array_view,
736        )
737        .unwrap();
738
739        assert_eq!(result.slices.len(), 2);
740        assert_eq!(result.len, 6);
741
742        // First slice: [1,2,3] * [10,20,30] = [10,40,90]
743        let slice1 = result.slices[0].to_table();
744        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &slice1.cols[0].array {
745            assert_eq!(arr.data.as_slice(), &[10, 40, 90]);
746        } else {
747            panic!("Expected Int32 array");
748        }
749
750        // Second slice: [4,5,6] * [40,50,60] = [160,250,360]
751        let slice2 = result.slices[1].to_table();
752        if let crate::Array::NumericArray(crate::NumericArray::Int32(arr)) = &slice2.cols[0].array {
753            assert_eq!(arr.data.as_slice(), &[160, 250, 360]);
754        } else {
755            panic!("Expected Int32 array");
756        }
757    }
758}