use std::{ptr, sync::Arc};
use crate::{
core::{
column::{Column, ColumnView},
error::{self, Error, ErrorCode, FACT_COLUMN_INDEX, FACT_LEFT_TYPE_ID, FACT_RIGHT_TYPE_ID, Result},
table::{Table, TableView},
},
runtime::{execution_context::Exec, execution_domain::ExecutionDomain, execution_resolver::ResolvedExecution},
};
fn checked_len_to_i32(
len: usize,
operation: &'static str,
message: &'static str,
resource: &'static str,
value_key: &'static str,
remedy: &'static str,
) -> Result<i32> {
i32::try_from(len)
.map_err(|_| error::limit_validation_error(operation, message, resource, value_key, "i32::MAX", len, remedy))
}
#[cfg(any(test, feature = "test-hooks"))]
fn input_column_count_limit_error(operation: &'static str, column_count: usize) -> Error {
error::limit_validation_error(
operation,
"number of input columns exceeds i32::MAX",
"input_columns",
"column_count",
"i32::MAX",
column_count,
"reduce column_count to fit within i32::MAX",
)
}
#[cfg(any(test, feature = "test-hooks"))]
fn input_table_count_limit_error(operation: &'static str, table_count: usize) -> Error {
error::limit_validation_error(
operation,
"number of input tables exceeds i32::MAX",
"input_tables",
"table_count",
"i32::MAX",
table_count,
"reduce table_count to fit within i32::MAX",
)
}
fn concatenate_requires_input_error(operation: &'static str, input_kind: &'static str) -> Error {
Error::new_with(
ErrorCode::UnsupportedOperation,
operation,
format!("{operation} requires at least one input {input_kind}"),
)
.with_detail("reason", "empty_input")
.with_detail("remedy", format!("provide at least one input {input_kind}"))
}
fn validate_column_types(columns: &[&ColumnView<'_>], operation: &'static str) -> Result<()> {
let Some((first, rest)) = columns.split_first() else {
return Ok(());
};
let left_type = first.data_type().map_err(|e| e.reanchor(operation))?.id();
for (offset, column) in rest.iter().enumerate() {
let right_type = column.data_type().map_err(|e| e.reanchor(operation))?.id();
if left_type != right_type {
return Err(Error::new_with(
ErrorCode::TypeMismatch,
operation,
"all input columns must have the same data type for concatenation",
)
.with_fact(FACT_COLUMN_INDEX, offset + 1)
.with_fact(FACT_LEFT_TYPE_ID, left_type)
.with_fact(FACT_RIGHT_TYPE_ID, right_type));
}
}
Ok(())
}
fn validate_table_column_types(tables: &[&TableView<'_>], operation: &'static str) -> Result<()> {
let Some((first, rest)) = tables.split_first() else {
return Ok(());
};
let column_count = first.num_columns().map_err(|e| e.reanchor(operation))?;
for table in rest {
let table_column_count = table.num_columns().map_err(|e| e.reanchor(operation))?;
if table_column_count != column_count {
return Ok(());
}
for index in 0..column_count {
let left_type =
first.column(index).and_then(|column| column.data_type()).map_err(|e| e.reanchor(operation))?.id();
let right_type =
table.column(index).and_then(|column| column.data_type()).map_err(|e| e.reanchor(operation))?.id();
if left_type != right_type {
return Err(Error::new_with(
ErrorCode::TypeMismatch,
operation,
"all input tables must have matching column data types for concatenation",
)
.with_fact(FACT_COLUMN_INDEX, index)
.with_fact(FACT_LEFT_TYPE_ID, left_type)
.with_fact(FACT_RIGHT_TYPE_ID, right_type));
}
}
}
Ok(())
}
fn compatible_column_execution(columns: &[&ColumnView<'_>]) -> Result<ResolvedExecution> {
let Some(first) = columns.first() else {
return Err(concatenate_requires_input_error("concatenate::concatenate_columns", "column"));
};
let ctx = Exec::RETAINED;
ResolvedExecution::many(
first.execution_domain(),
columns[1..].iter().map(|column| column.execution_domain()),
ctx,
"concatenate::concatenate_columns",
)
}
pub(crate) fn concatenate_columns(columns: &[&ColumnView<'_>]) -> Result<Column> {
let execution = compatible_column_execution(columns)?;
concatenate_columns_with_exec(columns, execution.output_domain(), execution.exec_options())
}
pub(crate) fn concatenate_columns_with_exec(
columns: &[&ColumnView<'_>],
output_domain: &Arc<ExecutionDomain>,
exec_options: &cudf_sys::cudf_exec_options_t,
) -> Result<Column> {
validate_column_types(columns, "concatenate::concatenate_columns")?;
let handles: Vec<cudf_sys::cudf_column_view_handle> = columns.iter().map(|c| c.raw_handle()).collect();
let num_columns = checked_len_to_i32(
handles.len(),
"concatenate::concatenate_columns",
"number of input columns exceeds i32::MAX",
"input_columns",
"column_count",
"reduce column_count to fit within i32::MAX",
)?;
let mut out = ptr::null_mut();
error::check_status_in_with_details(
unsafe { cudf_sys::cudf_concatenate_columns_ex(handles.as_ptr(), num_columns, exec_options, &mut out) },
"concatenate::concatenate_columns",
[("num_columns", handles.len().to_string())],
)?;
unsafe { Column::from_raw_with_domain(out, Arc::clone(output_domain)) }
}
pub(crate) fn concatenate_tables_with_exec(
tables: &[&TableView<'_>],
output_domain: &Arc<ExecutionDomain>,
exec_options: &cudf_sys::cudf_exec_options_t,
) -> Result<Table> {
validate_table_column_types(tables, "concatenate::concatenate_tables")?;
let handles: Vec<cudf_sys::cudf_table_view_handle> = tables.iter().map(|t| t.raw_handle()).collect();
let num_tables = checked_len_to_i32(
handles.len(),
"concatenate::concatenate_tables",
"number of input tables exceeds i32::MAX",
"input_tables",
"table_count",
"reduce table_count to fit within i32::MAX",
)?;
let mut out = ptr::null_mut();
error::check_status_in_with_details(
unsafe { cudf_sys::cudf_concatenate_tables_ex(handles.as_ptr(), num_tables, exec_options, &mut out) },
"concatenate::concatenate_tables",
[("num_tables", handles.len().to_string())],
)?;
unsafe { Table::from_raw_with_domain(out, Arc::clone(output_domain)) }
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn trigger_concatenate_column_count_limit_error_for_test() -> Result<()> {
Err(input_column_count_limit_error("concatenate::concatenate_columns", usize::MAX))
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn trigger_concatenate_table_count_limit_error_for_test() -> Result<()> {
Err(input_table_count_limit_error("concatenate::concatenate_tables", usize::MAX))
}