blob: 067d111e5ca70ae405bc9c0c8cb07fce6c1c68c3 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::util::BenchmarkRun;
use crate::util::CommonOpt;
use crate::util::QueryResult;
use arrow::array::{ArrayRef, DictionaryArray, Int32Array, ListArray, StringArray};
use arrow::buffer::OffsetBuffer;
use arrow::datatypes::{DataType, Field, Int32Type, Schema};
use arrow::record_batch::RecordBatch;
use clap::ValueEnum;
use datafusion::common::DataFusionError;
use datafusion::datasource::MemTable;
use datafusion::physical_plan::execute_stream;
use datafusion::prelude::SessionContext;
use datafusion_common::Result;
use datafusion_common::exec_err;
use datafusion_common::instant::Instant;
use futures::StreamExt;
use std::path::PathBuf;
use std::sync::Arc;
const ITEMS_PER_VALUE: usize = 4;
#[derive(Debug, Clone, clap::Parser)]
pub struct RunOpt {
/// Number of rows in the generated table.
#[clap(long, default_value = "1000000")]
pub num_rows: usize,
/// Which query to run (1-based). Omit to run all queries.
#[clap(long)]
pub query: Option<usize>,
/// How string group keys are represented at the aggregate input.
#[clap(long, value_enum, default_value_t = InputEncoding::Dictionary)]
pub input_encoding: InputEncoding,
/// Override the generated number of distinct values.
#[clap(long)]
pub num_distinct: Option<usize>,
/// Split the generated input into batches of this many rows.
#[clap(long)]
pub input_batch_size: Option<usize>,
/// Pad generated strings to at least this many bytes.
#[clap(long)]
pub value_length: Option<usize>,
/// Whether dictionary values are shared by all input batches.
#[clap(long, value_enum, default_value_t = DictionaryValueReuse::Shared)]
pub dictionary_value_reuse: DictionaryValueReuse,
/// Cast dictionary group keys back to strings in the query output.
#[clap(long, default_value_t = false)]
pub normalize_output: bool,
/// Output path for the JSON benchmark summary.
#[arg(short = 'o', long = "output")]
pub output_path: Option<PathBuf>,
#[clap(flatten)]
pub common: CommonOpt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum InputEncoding {
/// Build dictionary arrays before query execution.
Dictionary,
/// Keep the group keys as plain string arrays.
Plain,
/// Keep the group keys as string view arrays.
View,
/// Build plain strings and dictionary-encode them in the query plan.
RuntimeDictionary,
}
impl InputEncoding {
fn label(self) -> &'static str {
match self {
Self::Dictionary => "dictionary",
Self::Plain => "plain",
Self::View => "view",
Self::RuntimeDictionary => "runtime_dictionary",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DictionaryValueReuse {
/// Slice one array so every batch shares the same dictionary values.
Shared,
/// Build an independent values array for every input batch.
PerBatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DictValueType {
Utf8,
ListUtf8,
}
// percent of row that are null
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NullPercent {
Zero,
Fifteen,
}
impl NullPercent {
pub fn null_every(self) -> Option<usize> {
match self {
NullPercent::Zero => None,
NullPercent::Fifteen => Some(7), // 1-in-7 ≈ 14.3 %
}
}
}
/// Fraction of `size` that becomes the number of distinct dictionary entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cardinality {
Five,
Ten,
TwentyFive,
}
impl Cardinality {
pub fn num_distinct(self, size: usize) -> usize {
match self {
Cardinality::Five => (size / 20).max(1),
Cardinality::Ten => (size / 10).max(1),
Cardinality::TwentyFive => (size / 4).max(1),
}
}
}
/// `generate_dict` arguments for one dictionary column.
#[derive(Debug)]
pub struct DictColParams {
pub value_type: DictValueType,
pub null_percent: NullPercent,
pub cardinality: Cardinality,
}
#[derive(Debug)]
pub struct DictionaryQuery {
pub name: &'static str,
pub col: DictColParams,
/// Params for a second dictionary column; `None` for single-column queries.
pub col2: Option<DictColParams>,
pub sql: &'static str,
}
pub const DICTIONARY_QUERIES: &[DictionaryQuery] = &[
// single-column group-by: Utf8
DictionaryQuery {
name: "group_by_utf8_card5_no_nulls",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Five,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_utf8_card10_no_nulls",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Ten,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_utf8_card25_no_nulls",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::TwentyFive,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_utf8_card5_null15",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::Five,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_utf8_card25_null15",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::TwentyFive,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
// currently not supported by GroupValuesRows,
// https://github.com/apache/datafusion/pull/21765 Intends to address this.
// commenting out these benchmarks fornow
/*
DictionaryQuery {
name: "group_by_list_utf8_card5_no_nulls",
col: DictColParams {
value_type: DictValueType::ListUtf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Five,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_list_utf8_card10_no_nulls",
col: DictColParams {
value_type: DictValueType::ListUtf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Ten,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
DictionaryQuery {
name: "group_by_list_utf8_card25_null15",
col: DictColParams {
value_type: DictValueType::ListUtf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::TwentyFive,
},
col2: None,
sql: r#"SELECT dict_col, COUNT(*) FROM test_data GROUP BY dict_col"#,
},
*/
DictionaryQuery {
name: "group_by_two_utf8_card5_no_nulls",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Five,
},
col2: Some(DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::Ten,
}),
sql: r#"SELECT dict_col, dict_col2, COUNT(*) FROM test_data GROUP BY dict_col, dict_col2"#,
},
DictionaryQuery {
name: "group_by_two_utf8_card25_null15",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::TwentyFive,
},
col2: Some(DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Five,
}),
sql: r#"SELECT dict_col, dict_col2, COUNT(*) FROM test_data GROUP BY dict_col, dict_col2"#,
},
// --- multi-column group-by: Utf8 + List<Utf8> ----------------------------
/*
DictionaryQuery {
name: "group_by_utf8_and_list_utf8_card10_null15",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::Ten,
},
col2: Some(DictColParams {
value_type: DictValueType::ListUtf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::Five,
}),
sql: r#"SELECT dict_col, dict_col2, COUNT(*) FROM test_data GROUP BY dict_col, dict_col2"#,
},
DictionaryQuery {
name: "group_by_utf8_and_list_utf8_card25_no_nulls",
col: DictColParams {
value_type: DictValueType::Utf8,
null_percent: NullPercent::Zero,
cardinality: Cardinality::TwentyFive,
},
col2: Some(DictColParams {
value_type: DictValueType::ListUtf8,
null_percent: NullPercent::Fifteen,
cardinality: Cardinality::Ten,
}),
sql: r#"SELECT dict_col, dict_col2, COUNT(*) FROM test_data GROUP BY dict_col, dict_col2"#,
},
*/
];
pub fn generate_dict(
value_type: DictValueType,
cardinality: Cardinality,
null_percent: NullPercent,
size: usize,
) -> ArrayRef {
generate_dict_with_offset(value_type, cardinality, None, None, null_percent, size, 0)
}
fn generate_dict_with_offset(
value_type: DictValueType,
cardinality: Cardinality,
num_distinct: Option<usize>,
value_length: Option<usize>,
null_percent: NullPercent,
size: usize,
row_offset: usize,
) -> ArrayRef {
let num_distinct = num_distinct
.unwrap_or_else(|| cardinality.num_distinct(size))
.clamp(1, i32::MAX as usize);
let dict_values: ArrayRef = match value_type {
DictValueType::Utf8 => {
let strings: StringArray = (0..num_distinct)
.map(|i| padded_value(format!("value_{i}"), value_length))
.collect::<Vec<_>>()
.into();
Arc::new(strings)
}
DictValueType::ListUtf8 => {
let flat: StringArray = (0..num_distinct)
.flat_map(|i| {
(0..ITEMS_PER_VALUE).map(move |j| {
padded_value(format!("value_{i}_{j}"), value_length)
})
})
.collect::<Vec<_>>()
.into();
let offsets: Vec<i32> = (0..=(num_distinct * ITEMS_PER_VALUE))
.step_by(ITEMS_PER_VALUE)
.map(|o| o as i32)
.collect();
Arc::new(ListArray::new(
Arc::new(Field::new("item", DataType::Utf8, false)),
OffsetBuffer::new(offsets.into()),
Arc::new(flat),
None,
))
}
};
let null_every = null_percent.null_every();
let mut key_builder = Int32Array::builder(size);
for i in 0..size {
let row_index = row_offset + i;
if null_every.is_some_and(|n| row_index.is_multiple_of(n)) {
key_builder.append_null();
} else {
key_builder.append_value((row_index % num_distinct) as i32);
}
}
let keys = key_builder.finish();
Arc::new(
DictionaryArray::<Int32Type>::try_new(keys, dict_values)
.expect("valid dictionary array"),
)
}
fn padded_value(mut value: String, value_length: Option<usize>) -> String {
if let Some(value_length) = value_length {
value.extend(std::iter::repeat_n(
'x',
value_length.saturating_sub(value.len()),
));
}
value
}
impl RunOpt {
pub async fn run(self) -> Result<()> {
println!(
"Running dictionary encoding benchmarks with the following options: {self:#?}\n"
);
let query_range = match self.query {
Some(query_id) => {
if query_id >= 1 && query_id <= DICTIONARY_QUERIES.len() {
query_id..=query_id
} else {
return exec_err!(
"Query {query_id} not found. Available queries: 1 to {}",
DICTIONARY_QUERIES.len()
);
}
}
None => 1..=DICTIONARY_QUERIES.len(),
};
let config = self.common.config()?;
let rt = self.common.build_runtime()?;
let ctx = SessionContext::new_with_config_rt(config, rt);
let mut benchmark_run = BenchmarkRun::new();
benchmark_run.set_memory_pool(&ctx.runtime_env().memory_pool);
for query_id in query_range {
let query = &DICTIONARY_QUERIES[query_id - 1];
let case_name = if self.input_encoding == InputEncoding::Dictionary
&& !self.normalize_output
&& self.dictionary_value_reuse == DictionaryValueReuse::Shared
{
query.name.to_string()
} else {
let output = if self.normalize_output {
"_normalized"
} else {
""
};
let dictionary_values =
if self.dictionary_value_reuse == DictionaryValueReuse::PerBatch {
"_per_batch_values"
} else {
""
};
format!(
"{}_{}{output}{dictionary_values}",
query.name,
self.input_encoding.label()
)
};
benchmark_run.start_new_case(&case_name);
let query_run = self.benchmark_query(query, &ctx).await;
match query_run {
Ok(query_results) => {
for iter in query_results {
benchmark_run.write_iter(iter.elapsed, iter.row_count);
}
}
Err(e) => {
return Err(DataFusionError::Context(
format!("Dictionary benchmark '{}' failed:", query.name),
Box::new(e),
));
}
}
}
benchmark_run.maybe_write_json(self.output_path.as_ref())?;
Ok(())
}
async fn benchmark_query(
&self,
query: &DictionaryQuery,
ctx: &SessionContext,
) -> Result<Vec<QueryResult>> {
let batches = self.make_record_batches(query)?;
let schema = batches.first().expect("at least one input batch").schema();
ctx.deregister_table("test_data")?;
ctx.register_table(
"test_data",
Arc::new(MemTable::try_new(schema, vec![batches])?),
)?;
let sql = self.query_sql(query);
let mut query_results = vec![];
for i in 0..self.common.iterations {
let start = Instant::now();
let row_count =
Self::execute_query_without_result_buffering(&sql, ctx).await?;
let elapsed = start.elapsed();
println!(
"Query '{}' iteration {i} returned {row_count} rows in {elapsed:?}",
query.name
);
query_results.push(QueryResult { elapsed, row_count });
}
Ok(query_results)
}
fn query_sql(&self, query: &DictionaryQuery) -> String {
if self.input_encoding == InputEncoding::Plain
|| (matches!(
self.input_encoding,
InputEncoding::Dictionary | InputEncoding::View
) && !self.normalize_output)
{
return query.sql.to_string();
}
let group_col = match self.input_encoding {
InputEncoding::Dictionary | InputEncoding::View => "dict_col",
InputEncoding::RuntimeDictionary => {
"arrow_cast(dict_col, 'Dictionary(Int32, Utf8)')"
}
InputEncoding::Plain => unreachable!(),
};
let select_col = if self.normalize_output {
format!("arrow_cast({group_col}, 'Utf8') AS dict_col")
} else {
group_col.to_string()
};
if query.col2.is_some() {
let group_col2 = match self.input_encoding {
InputEncoding::Dictionary | InputEncoding::View => "dict_col2",
InputEncoding::RuntimeDictionary => {
"arrow_cast(dict_col2, 'Dictionary(Int32, Utf8)')"
}
InputEncoding::Plain => unreachable!(),
};
let select_col2 = if self.normalize_output {
format!("arrow_cast({group_col2}, 'Utf8') AS dict_col2")
} else {
group_col2.to_string()
};
format!(
"SELECT {select_col}, {select_col2}, COUNT(*) FROM test_data GROUP BY {group_col}, {group_col2}"
)
} else {
format!("SELECT {select_col}, COUNT(*) FROM test_data GROUP BY {group_col}")
}
}
fn make_record_batches(&self, query: &DictionaryQuery) -> Result<Vec<RecordBatch>> {
let size = self.num_rows;
if size == 0 {
return exec_err!("num_rows must be greater than zero");
}
if let Some(num_distinct) = self.num_distinct {
if num_distinct == 0 {
return exec_err!("num_distinct must be greater than zero");
}
if num_distinct > size {
return exec_err!(
"num_distinct ({num_distinct}) must not exceed num_rows ({size})"
);
}
if num_distinct > i32::MAX as usize {
return exec_err!(
"num_distinct must fit in the Int32 dictionary key type"
);
}
}
let input_batch_size = self.input_batch_size.unwrap_or(size);
if input_batch_size == 0 {
return exec_err!("input_batch_size must be greater than zero");
}
if self.input_encoding != InputEncoding::Dictionary
&& self.dictionary_value_reuse == DictionaryValueReuse::PerBatch
{
return exec_err!(
"dictionary_value_reuse=per-batch requires input_encoding=dictionary"
);
}
if self.input_encoding == InputEncoding::Dictionary
&& self.dictionary_value_reuse == DictionaryValueReuse::PerBatch
{
return (0..size)
.step_by(input_batch_size)
.map(|offset| {
self.make_record_batch(
query,
input_batch_size.min(size - offset),
offset,
)
})
.collect();
}
let batch = self.make_record_batch(query, size, 0)?;
Ok((0..size)
.step_by(input_batch_size)
.map(|offset| batch.slice(offset, input_batch_size.min(size - offset)))
.collect())
}
fn make_record_batch(
&self,
query: &DictionaryQuery,
size: usize,
row_offset: usize,
) -> Result<RecordBatch> {
let num_distinct = |params: &DictColParams| {
Some(
self.num_distinct
.unwrap_or_else(|| params.cardinality.num_distinct(self.num_rows)),
)
};
let col1 = generate_dict_with_offset(
query.col.value_type,
query.col.cardinality,
num_distinct(&query.col),
self.value_length,
query.col.null_percent,
size,
row_offset,
);
let col1 = match self.input_encoding {
InputEncoding::Dictionary => col1,
InputEncoding::Plain | InputEncoding::RuntimeDictionary => {
arrow::compute::cast(col1.as_ref(), &DataType::Utf8)?
}
InputEncoding::View => {
arrow::compute::cast(col1.as_ref(), &DataType::Utf8View)?
}
};
let (schema, columns): (Schema, Vec<ArrayRef>) = match &query.col2 {
None => {
let schema = Schema::new(vec![Field::new(
"dict_col",
col1.data_type().clone(),
true,
)]);
(schema, vec![col1])
}
Some(col2_params) => {
let col2 = generate_dict_with_offset(
col2_params.value_type,
col2_params.cardinality,
num_distinct(col2_params),
self.value_length,
col2_params.null_percent,
size,
row_offset,
);
let col2 = match self.input_encoding {
InputEncoding::Dictionary => col2,
InputEncoding::Plain | InputEncoding::RuntimeDictionary => {
arrow::compute::cast(col2.as_ref(), &DataType::Utf8)?
}
InputEncoding::View => {
arrow::compute::cast(col2.as_ref(), &DataType::Utf8View)?
}
};
let schema = Schema::new(vec![
Field::new("dict_col", col1.data_type().clone(), true),
Field::new("dict_col2", col2.data_type().clone(), true),
]);
(schema, vec![col1, col2])
}
};
Ok(RecordBatch::try_new(Arc::new(schema), columns)?)
}
async fn execute_query_without_result_buffering(
sql: &str,
ctx: &SessionContext,
) -> Result<usize> {
let mut row_count = 0;
let df = ctx.sql(sql).await?;
let physical_plan = df.create_physical_plan().await?;
let mut stream = execute_stream(physical_plan, ctx.task_ctx())?;
while let Some(batch) = stream.next().await {
row_count += batch?.num_rows();
}
Ok(row_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Array, Int64Array};
use std::collections::BTreeMap;
fn run_opt(input_encoding: InputEncoding) -> RunOpt {
RunOpt {
num_rows: 10,
query: Some(1),
input_encoding,
num_distinct: Some(3),
input_batch_size: Some(4),
value_length: Some(32),
dictionary_value_reuse: DictionaryValueReuse::Shared,
normalize_output: true,
output_path: None,
common: CommonOpt {
iterations: 1,
partitions: Some(1),
batch_size: None,
mem_pool_type: "fair".to_string(),
memory_limit: None,
sort_spill_reservation_bytes: None,
debug: false,
simulate_latency: false,
},
}
}
#[test]
fn dictionary_batches_share_values_and_preserve_padding() {
let batches = run_opt(InputEncoding::Dictionary)
.make_record_batches(&DICTIONARY_QUERIES[0])
.unwrap();
assert_eq!(
batches
.iter()
.map(RecordBatch::num_rows)
.collect::<Vec<_>>(),
vec![4, 4, 2]
);
let first = batches[0].column(0);
let second = batches[1].column(0);
let first = first
.as_any()
.downcast_ref::<DictionaryArray<Int32Type>>()
.unwrap();
let second = second
.as_any()
.downcast_ref::<DictionaryArray<Int32Type>>()
.unwrap();
assert!(Arc::ptr_eq(first.values(), second.values()));
let values = first
.values()
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(values.len(), 3);
assert!(values.iter().flatten().all(|value| value.len() == 32));
}
#[test]
fn query_sql_keeps_user_visible_string_type() {
let query = &DICTIONARY_QUERIES[0];
assert_eq!(run_opt(InputEncoding::Plain).query_sql(query), query.sql);
let mut default_dictionary = run_opt(InputEncoding::Dictionary);
default_dictionary.normalize_output = false;
assert_eq!(default_dictionary.query_sql(query), query.sql);
assert_eq!(
run_opt(InputEncoding::Dictionary).query_sql(query),
"SELECT arrow_cast(dict_col, 'Utf8') AS dict_col, COUNT(*) FROM test_data GROUP BY dict_col"
);
assert_eq!(
run_opt(InputEncoding::View).query_sql(query),
"SELECT arrow_cast(dict_col, 'Utf8') AS dict_col, COUNT(*) FROM test_data GROUP BY dict_col"
);
assert_eq!(
run_opt(InputEncoding::RuntimeDictionary).query_sql(query),
"SELECT arrow_cast(arrow_cast(dict_col, 'Dictionary(Int32, Utf8)'), 'Utf8') AS dict_col, COUNT(*) FROM test_data GROUP BY arrow_cast(dict_col, 'Dictionary(Int32, Utf8)')"
);
}
#[test]
fn per_batch_dictionaries_keep_global_row_pattern() {
let mut opt = run_opt(InputEncoding::Dictionary);
opt.dictionary_value_reuse = DictionaryValueReuse::PerBatch;
let batches = opt.make_record_batches(&DICTIONARY_QUERIES[0]).unwrap();
let first = batches[0]
.column(0)
.as_any()
.downcast_ref::<DictionaryArray<Int32Type>>()
.unwrap();
let second = batches[1]
.column(0)
.as_any()
.downcast_ref::<DictionaryArray<Int32Type>>()
.unwrap();
assert!(!Arc::ptr_eq(first.values(), second.values()));
assert_eq!(first.keys().value(0), 0);
assert_eq!(second.keys().value(0), 1);
}
#[test]
fn rejects_zero_input_batch_size() {
let mut opt = run_opt(InputEncoding::Dictionary);
opt.input_batch_size = Some(0);
let err = opt.make_record_batches(&DICTIONARY_QUERIES[0]).unwrap_err();
assert!(
err.to_string()
.contains("input_batch_size must be greater than zero")
);
}
#[test]
fn rejects_invalid_distinct_count_and_dictionary_reuse() {
let query = &DICTIONARY_QUERIES[0];
let mut zero_distinct = run_opt(InputEncoding::Dictionary);
zero_distinct.num_distinct = Some(0);
assert!(
zero_distinct
.make_record_batches(query)
.unwrap_err()
.to_string()
.contains("num_distinct must be greater than zero")
);
let mut excess_distinct = run_opt(InputEncoding::Dictionary);
excess_distinct.num_distinct = Some(excess_distinct.num_rows + 1);
assert!(
excess_distinct
.make_record_batches(query)
.unwrap_err()
.to_string()
.contains("must not exceed num_rows")
);
let mut plain_per_batch = run_opt(InputEncoding::Plain);
plain_per_batch.dictionary_value_reuse = DictionaryValueReuse::PerBatch;
assert!(
plain_per_batch
.make_record_batches(query)
.unwrap_err()
.to_string()
.contains("requires input_encoding=dictionary")
);
}
async fn grouped_counts(opt: &RunOpt) -> BTreeMap<String, i64> {
let query = &DICTIONARY_QUERIES[0];
let batches = opt.make_record_batches(query).unwrap();
let schema = batches[0].schema();
let ctx = SessionContext::new();
ctx.register_table(
"test_data",
Arc::new(MemTable::try_new(schema, vec![batches]).unwrap()),
)
.unwrap();
let batches = ctx
.sql(&opt.query_sql(query))
.await
.unwrap()
.collect()
.await
.unwrap();
let mut counts = BTreeMap::new();
for batch in batches {
let keys = batch
.column(0)
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
let values = batch
.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap();
for row in 0..batch.num_rows() {
counts.insert(keys.value(row).to_string(), values.value(row));
}
}
counts
}
#[tokio::test]
async fn encodings_produce_identical_grouped_counts() {
let plain = run_opt(InputEncoding::Plain);
let shared = run_opt(InputEncoding::Dictionary);
let mut per_batch = run_opt(InputEncoding::Dictionary);
per_batch.dictionary_value_reuse = DictionaryValueReuse::PerBatch;
let runtime = run_opt(InputEncoding::RuntimeDictionary);
let view = run_opt(InputEncoding::View);
let expected = grouped_counts(&plain).await;
assert_eq!(
expected.values().copied().collect::<Vec<_>>(),
vec![4, 3, 3]
);
assert_eq!(grouped_counts(&shared).await, expected);
assert_eq!(grouped_counts(&per_batch).await, expected);
assert_eq!(grouped_counts(&runtime).await, expected);
assert_eq!(grouped_counts(&view).await, expected);
}
}