mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
1295 lines
42 KiB
Rust
1295 lines
42 KiB
Rust
// Copyright 2023 Zinc Labs Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
use std::{str::FromStr, sync::Arc};
|
|
|
|
use ahash::AHashMap as HashMap;
|
|
use datafusion::{
|
|
arrow::{
|
|
datatypes::{DataType, Schema},
|
|
json as arrowJson,
|
|
record_batch::RecordBatch,
|
|
},
|
|
common::{FileType, GetExt},
|
|
datasource::{
|
|
file_format::{json::JsonFormat, parquet::ParquetFormat},
|
|
listing::{ListingOptions, ListingTable, ListingTableConfig, ListingTableUrl},
|
|
object_store::{DefaultObjectStoreRegistry, ObjectStoreRegistry},
|
|
MemTable,
|
|
},
|
|
error::{DataFusionError, Result},
|
|
execution::{
|
|
context::SessionConfig,
|
|
memory_pool::{FairSpillPool, GreedyMemoryPool},
|
|
runtime_env::{RuntimeConfig, RuntimeEnv},
|
|
},
|
|
logical_expr::expr::Alias,
|
|
prelude::{cast, col, lit, Expr, SessionContext},
|
|
scalar::ScalarValue,
|
|
};
|
|
use once_cell::sync::Lazy;
|
|
use parquet::arrow::ArrowWriter;
|
|
use regex::Regex;
|
|
|
|
use super::{
|
|
storage::{file_list, StorageType},
|
|
transform_udf::get_all_transform,
|
|
};
|
|
use crate::{
|
|
common::{
|
|
infra::{
|
|
cache::tmpfs,
|
|
config::{CONFIG, PARQUET_BATCH_SIZE},
|
|
},
|
|
meta::{
|
|
common::{FileKey, FileMeta},
|
|
functions::VRLResultResolver,
|
|
search::{SearchType, Session as SearchSession},
|
|
sql,
|
|
},
|
|
utils::{flatten, json},
|
|
},
|
|
service::{schema::filter_schema_null_fields, search::sql::Sql},
|
|
};
|
|
|
|
const AGGREGATE_UDF_LIST: [&str; 7] = [
|
|
"min",
|
|
"max",
|
|
"count",
|
|
"avg",
|
|
"sum",
|
|
"array_agg",
|
|
"approx_percentile_cont",
|
|
];
|
|
|
|
static RE_WHERE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i) where (.*)").unwrap());
|
|
static RE_FIELD_FN: Lazy<Regex> =
|
|
Lazy::new(|| Regex::new(r#"(?i)([a-zA-Z0-9_]+)\((['"\ a-zA-Z0-9,._*]+)"#).unwrap());
|
|
|
|
pub async fn sql(
|
|
session: &SearchSession,
|
|
schema: Arc<Schema>,
|
|
rules: &HashMap<String, DataType>,
|
|
sql: &Arc<Sql>,
|
|
files: &[FileKey],
|
|
in_records_batches: Option<Vec<RecordBatch>>,
|
|
file_type: FileType,
|
|
) -> Result<HashMap<String, Vec<RecordBatch>>> {
|
|
if files.is_empty() {
|
|
return Ok(HashMap::new());
|
|
}
|
|
|
|
let start = std::time::Instant::now();
|
|
let session_id = session.id.clone();
|
|
let mut ctx = if !file_type.eq(&FileType::ARROW) {
|
|
register_table(session, schema.clone(), "tbl", files, file_type.clone()).await?
|
|
} else {
|
|
let ctx = prepare_datafusion_context(&session.search_type)?;
|
|
|
|
let record_batches = in_records_batches.unwrap();
|
|
let schema = if let Some(first_batch) = record_batches.first() {
|
|
first_batch.schema()
|
|
} else {
|
|
log::error!("No record batches found");
|
|
return Ok(HashMap::new());
|
|
};
|
|
let mem_table = Arc::new(MemTable::try_new(schema, vec![record_batches])?);
|
|
|
|
// Register the MemTable as a table in the DataFusion context
|
|
ctx.register_table("tbl", mem_table)?;
|
|
ctx
|
|
};
|
|
|
|
// register UDF
|
|
register_udf(&mut ctx, &sql.org_id).await;
|
|
|
|
let mut result: HashMap<String, Vec<RecordBatch>> = HashMap::new();
|
|
|
|
// query sql
|
|
result.insert(
|
|
"query".to_string(),
|
|
exec_query(
|
|
&ctx,
|
|
session,
|
|
schema.clone(),
|
|
rules,
|
|
sql,
|
|
files,
|
|
file_type.clone(),
|
|
)
|
|
.await?,
|
|
);
|
|
|
|
// get alias from context query for agg sql
|
|
let meta_sql = sql::Sql::new(&sql.query_context);
|
|
|
|
for (name, orig_agg_sql) in sql.aggs.iter() {
|
|
// Debug SQL
|
|
if CONFIG.common.print_key_sql {
|
|
log::info!(
|
|
"[session_id {session_id}] Query agg sql: {}",
|
|
orig_agg_sql.0
|
|
);
|
|
}
|
|
|
|
let mut agg_sql = orig_agg_sql.0.to_owned();
|
|
if meta_sql.is_ok() {
|
|
for alias in &meta_sql.as_ref().unwrap().field_alias {
|
|
replace_in_query(&alias.1, &mut agg_sql, true);
|
|
}
|
|
}
|
|
|
|
let mut df = match ctx.sql(&agg_sql).await {
|
|
Ok(df) => df,
|
|
Err(e) => {
|
|
log::error!(
|
|
"aggs sql execute failed, session: {:?}, sql: {}, err: {:?}",
|
|
session,
|
|
agg_sql,
|
|
e
|
|
);
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
if !rules.is_empty() {
|
|
let fields = df.schema().fields();
|
|
let mut exprs = Vec::with_capacity(fields.len());
|
|
for field in fields {
|
|
if let Some(v) = field.qualifier() {
|
|
if v.to_string() != "tbl" {
|
|
exprs.push(col(field.name()));
|
|
continue;
|
|
}
|
|
}
|
|
exprs.push(match rules.get(field.name()) {
|
|
Some(rule) => Expr::Alias(Alias::new(
|
|
cast(col(field.name()), rule.clone()),
|
|
None::<&str>,
|
|
field.name().to_string(),
|
|
)),
|
|
None => col(field.name()),
|
|
});
|
|
}
|
|
df = df.select(exprs)?;
|
|
}
|
|
let batches = df.collect().await?;
|
|
result.insert(format!("agg_{name}"), batches);
|
|
log::info!(
|
|
"[session_id {session_id}] Query agg:{name} took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
}
|
|
|
|
// drop table
|
|
ctx.deregister_table("tbl")?;
|
|
log::info!(
|
|
"[session_id {session_id}] Query all took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn exec_query(
|
|
ctx: &SessionContext,
|
|
session: &SearchSession,
|
|
schema: Arc<Schema>,
|
|
rules: &HashMap<String, DataType>,
|
|
sql: &Arc<Sql>,
|
|
files: &[FileKey],
|
|
file_type: FileType,
|
|
) -> Result<Vec<RecordBatch>> {
|
|
let start = std::time::Instant::now();
|
|
let session_id = session.id.clone();
|
|
|
|
let mut fast_mode = false;
|
|
let (q_ctx, schema) = if sql.fast_mode && session.storage_type != StorageType::Tmpfs {
|
|
fast_mode = true;
|
|
get_fast_mode_ctx(session, schema, sql, files, file_type).await?
|
|
} else {
|
|
(ctx.clone(), schema.clone())
|
|
};
|
|
|
|
// get used UDF
|
|
let mut field_fns = vec![];
|
|
let mut sql_parts = vec![];
|
|
for fn_name in crate::common::utils::functions::get_all_transform_keys(&sql.org_id).await {
|
|
if sql.origin_sql.contains(&format!("{}(", fn_name)) {
|
|
field_fns.push(fn_name.clone());
|
|
}
|
|
}
|
|
|
|
if !fast_mode && (!field_fns.is_empty() || sql.query_fn.is_some()) {
|
|
if let Some(caps) = RE_WHERE.captures(&sql.origin_sql) {
|
|
sql_parts.insert(
|
|
0,
|
|
sql.origin_sql
|
|
.strip_suffix(caps.get(0).unwrap().as_str())
|
|
.unwrap(),
|
|
);
|
|
sql_parts.insert(1, caps.get(1).unwrap().as_str());
|
|
};
|
|
}
|
|
|
|
// query
|
|
let query = if !&sql.query_context.is_empty() {
|
|
sql.query_context.replace(&sql.stream_name, "tbl").clone()
|
|
} else if !fast_mode
|
|
&& ((!field_fns.is_empty() && !sql_parts.is_empty()) || sql.query_fn.is_some())
|
|
{
|
|
match sql.meta.time_range {
|
|
Some(ts_range) => format!(
|
|
"{} where {} >= {} AND {} < {}",
|
|
sql_parts[0],
|
|
CONFIG.common.column_timestamp,
|
|
ts_range.0,
|
|
CONFIG.common.column_timestamp,
|
|
ts_range.1
|
|
),
|
|
None => sql_parts[0].to_owned(),
|
|
}
|
|
} else {
|
|
sql.origin_sql.clone()
|
|
};
|
|
|
|
// Debug SQL
|
|
if CONFIG.common.print_key_sql {
|
|
log::info!("[session_id {session_id}] Query sql: {}", query);
|
|
}
|
|
|
|
let mut df = match q_ctx.sql(&query).await {
|
|
Ok(df) => df,
|
|
Err(e) => {
|
|
log::error!(
|
|
"[session_id {session_id}] query sql execute failed, session: {:?}, sql: {}, err: {:?}",
|
|
session,
|
|
sql.origin_sql,
|
|
e
|
|
);
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
if !rules.is_empty() {
|
|
let fields = df.schema().fields();
|
|
let mut exprs = Vec::with_capacity(fields.len());
|
|
for field in fields {
|
|
if let Some(v) = field.qualifier() {
|
|
if v.to_string() != "tbl" {
|
|
exprs.push(col(field.name()));
|
|
continue;
|
|
}
|
|
}
|
|
exprs.push(match rules.get(field.name()) {
|
|
Some(rule) => Expr::Alias(Alias::new(
|
|
cast(col(field.name()), rule.clone()),
|
|
None::<&str>,
|
|
field.name().to_string(),
|
|
)),
|
|
None => col(field.name()),
|
|
});
|
|
}
|
|
df = df.select(exprs)?;
|
|
}
|
|
|
|
if field_fns.is_empty() && sql.query_fn.is_none() {
|
|
let batches = df.clone().collect().await?;
|
|
log::info!(
|
|
"[session_id {session_id}] Query took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
return Ok(batches);
|
|
}
|
|
|
|
if !sql.query_context.is_empty() {
|
|
q_ctx.deregister_table("tbl")?;
|
|
q_ctx.register_table("tbl", df.clone().into_view())?;
|
|
// re-register to ctx
|
|
if !fast_mode {
|
|
ctx.deregister_table("tbl")?;
|
|
ctx.register_table("tbl", df.into_view())?;
|
|
}
|
|
} else if sql.query_fn.is_some() {
|
|
let batches = df.collect().await?;
|
|
let batches_ref: Vec<&RecordBatch> = batches.iter().collect();
|
|
match handle_query_fn(sql.query_fn.clone().unwrap(), &batches_ref, &sql.org_id) {
|
|
Err(err) => {
|
|
return Err(datafusion::error::DataFusionError::Execution(format!(
|
|
"Error applying query function: {err} "
|
|
)));
|
|
}
|
|
Ok(resp) => {
|
|
if !resp.is_empty() {
|
|
let mem_table = datafusion::datasource::MemTable::try_new(
|
|
resp.first().unwrap().schema(),
|
|
vec![resp],
|
|
)?;
|
|
q_ctx.deregister_table("tbl")?;
|
|
q_ctx.register_table("tbl", Arc::new(mem_table))?;
|
|
// -- fix mem table, add missing columns
|
|
let mut tmp_df = q_ctx.table("tbl").await?;
|
|
let tmp_fields = tmp_df
|
|
.schema()
|
|
.field_names()
|
|
.iter()
|
|
.map(|f| f.strip_prefix("tbl.").unwrap().to_string())
|
|
.collect::<Vec<String>>();
|
|
let need_add_columns = schema
|
|
.fields()
|
|
.iter()
|
|
.filter(|field| !tmp_fields.contains(field.name()))
|
|
.collect::<Vec<_>>();
|
|
if !need_add_columns.is_empty() {
|
|
for column in need_add_columns {
|
|
if column.data_type() == &DataType::Utf8 {
|
|
tmp_df = tmp_df.with_column(
|
|
&column.name().to_string(),
|
|
lit(ScalarValue::Utf8(None)),
|
|
)?;
|
|
} else if let Ok(v) = ScalarValue::new_zero(column.data_type()) {
|
|
tmp_df = tmp_df.with_column(&column.name().to_string(), lit(v))?;
|
|
}
|
|
}
|
|
q_ctx.deregister_table("tbl")?;
|
|
q_ctx.register_table("tbl", tmp_df.clone().into_view())?;
|
|
// re-register to ctx
|
|
if !fast_mode {
|
|
ctx.deregister_table("tbl")?;
|
|
ctx.register_table("tbl", tmp_df.into_view())?;
|
|
}
|
|
}
|
|
// -- fix done
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return Err(datafusion::error::DataFusionError::Execution(
|
|
"BUG -> this shouldn't be happen".to_string(),
|
|
));
|
|
}
|
|
|
|
let mut where_query = if !sql_parts.is_empty() {
|
|
format!("select * from tbl where {}", &sql_parts[1])
|
|
} else {
|
|
sql.origin_sql.clone()
|
|
};
|
|
for alias in &sql.meta.field_alias {
|
|
replace_in_query(&alias.1, &mut where_query, true);
|
|
}
|
|
let additional_clause = where_query.clone();
|
|
|
|
let df = match q_ctx.sql(&additional_clause).await {
|
|
Ok(df) => df,
|
|
Err(e) => {
|
|
log::error!(
|
|
"query sql execute failed, session: {:?}, sql: {}, err: {:?}",
|
|
session,
|
|
sql.origin_sql,
|
|
e
|
|
);
|
|
return Err(e);
|
|
}
|
|
};
|
|
let batches = df.clone().collect().await?;
|
|
log::info!(
|
|
"[session_id {session_id}] Query took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
Ok(batches)
|
|
}
|
|
|
|
async fn get_fast_mode_ctx(
|
|
session: &SearchSession,
|
|
schema: Arc<Schema>,
|
|
sql: &Arc<Sql>,
|
|
files: &[FileKey],
|
|
file_type: FileType,
|
|
) -> Result<(SessionContext, Arc<Schema>)> {
|
|
let mut files = files.to_vec();
|
|
files.sort_by(|a, b| a.meta.min_ts.cmp(&b.meta.min_ts));
|
|
|
|
let mut loaded_records = 0;
|
|
let mut new_files = Vec::new();
|
|
let needs = sql.meta.limit + sql.meta.offset;
|
|
for i in (0..files.len()).rev() {
|
|
loaded_records += files.get(i).unwrap().meta.records as usize;
|
|
new_files.push(files[i].clone());
|
|
if loaded_records >= needs {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let fast_session = SearchSession {
|
|
id: format!("{}-fast", session.id),
|
|
storage_type: session.storage_type.clone(),
|
|
search_type: session.search_type.clone(),
|
|
};
|
|
let mut ctx =
|
|
register_table(&fast_session, schema.clone(), "tbl", &new_files, file_type).await?;
|
|
|
|
// register UDF
|
|
register_udf(&mut ctx, &sql.org_id).await;
|
|
|
|
Ok((ctx, schema))
|
|
}
|
|
|
|
fn replace_in_query(replace_pat: &str, where_query: &mut String, is_alias: bool) {
|
|
let re1 = Regex::new(&format!("(?i){}_([a-zA-Z0-9_-]*)", replace_pat)).unwrap();
|
|
if let Some(caps) = re1.captures(&*where_query) {
|
|
let cap_str = caps.get(0).unwrap().as_str();
|
|
let field = caps.get(1).unwrap().as_str();
|
|
if is_alias {
|
|
*where_query = where_query.replace(cap_str, &format!("{replace_pat}['{field}']"));
|
|
} else {
|
|
let local_pattern = replace_pat
|
|
.replace("tbl_", "")
|
|
.replacen('_', "(", 1)
|
|
.replace('_', ")");
|
|
*where_query = where_query.replace(cap_str, &format!("{local_pattern}['{field}']"));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn merge(
|
|
org_id: &str,
|
|
offset: usize,
|
|
limit: usize,
|
|
sql: &str,
|
|
batches: &Vec<Vec<RecordBatch>>,
|
|
) -> Result<Vec<RecordBatch>> {
|
|
if batches.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
if offset == 0 && batches.len() == 1 && batches[0].len() <= 1 {
|
|
return Ok(batches[0].to_owned());
|
|
}
|
|
|
|
// let start = std::time::Instant::now();
|
|
|
|
// write temp file
|
|
let (schema, work_dir) = merge_write_recordbatch(batches)?;
|
|
// log::info!(
|
|
// "merge_write_recordbatch took {:.3} seconds.",
|
|
// start.elapsed().as_secs_f64()
|
|
// );
|
|
if schema.fields().is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
// rewrite sql
|
|
let query_sql = match merge_rewrite_sql(sql, schema) {
|
|
Ok(sql) => {
|
|
if offset > 0
|
|
&& sql.to_uppercase().contains(" LIMIT ")
|
|
&& !sql.to_uppercase().contains(" OFFSET ")
|
|
{
|
|
sql.replace(
|
|
&format!(" LIMIT {}", limit + offset),
|
|
&format!(" LIMIT {limit} OFFSET {offset}"),
|
|
)
|
|
} else {
|
|
sql
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
// log::info!(
|
|
// "merge_rewrite_sql took {:.3} seconds.",
|
|
// start.elapsed().as_secs_f64()
|
|
// );
|
|
|
|
// query data
|
|
let mut ctx = prepare_datafusion_context(&SearchType::Normal)?;
|
|
// Configure listing options
|
|
let file_format = ParquetFormat::default();
|
|
let listing_options = ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::PARQUET.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num);
|
|
let list_url = format!("tmpfs://{work_dir}");
|
|
let prefix = match ListingTableUrl::parse(list_url) {
|
|
Ok(url) => url,
|
|
Err(e) => {
|
|
return Err(datafusion::error::DataFusionError::Execution(format!(
|
|
"ListingTableUrl error: {e}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
let mut config = ListingTableConfig::new(prefix).with_listing_options(listing_options);
|
|
config = match config.infer_schema(&ctx.state()).await {
|
|
Ok(config) => config,
|
|
Err(e) => {
|
|
return Err(datafusion::error::DataFusionError::Execution(format!(
|
|
"infer_schema error: {e}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
let table = ListingTable::try_new(config)?;
|
|
ctx.register_table("tbl", Arc::new(table))?;
|
|
|
|
// register UDF
|
|
register_udf(&mut ctx, org_id).await;
|
|
|
|
// Debug SQL
|
|
if CONFIG.common.print_key_sql {
|
|
log::info!("Merge sql: {query_sql}");
|
|
}
|
|
|
|
let df = match ctx.sql(&query_sql).await {
|
|
Ok(df) => df,
|
|
Err(e) => {
|
|
log::error!("merge sql execute failed, sql: {}, err: {:?}", query_sql, e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
let mut batches = df.collect().await?;
|
|
if batches.len() > 1 {
|
|
batches.retain(|batch| batch.num_rows() > 0);
|
|
}
|
|
ctx.deregister_table("tbl")?;
|
|
|
|
// log::info!("Merge took {:.3} seconds.", start.elapsed().as_secs_f64());
|
|
|
|
// clear temp file
|
|
tmpfs::delete(&work_dir, true).unwrap();
|
|
|
|
Ok(batches)
|
|
}
|
|
|
|
fn merge_write_recordbatch(batches: &[Vec<RecordBatch>]) -> Result<(Arc<Schema>, String)> {
|
|
let mut i = 0;
|
|
let work_dir = format!("/tmp/merge/{}/", chrono::Utc::now().timestamp_micros());
|
|
let mut schema = Schema::empty();
|
|
for item in batches.iter() {
|
|
if item.is_empty() {
|
|
continue;
|
|
}
|
|
for row in item.iter() {
|
|
if row.num_rows() == 0 {
|
|
continue;
|
|
}
|
|
i += 1;
|
|
let row_schema = row.schema();
|
|
schema = Schema::try_merge(vec![schema, row_schema.as_ref().clone()])?;
|
|
let file_name = format!("{work_dir}{i}.parquet");
|
|
let mut buf_parquet = Vec::new();
|
|
let mut writer = ArrowWriter::try_new(&mut buf_parquet, row_schema, None)?;
|
|
writer.write(row)?;
|
|
writer.close()?;
|
|
tmpfs::set(&file_name, buf_parquet.into()).expect("tmpfs set success");
|
|
}
|
|
}
|
|
filter_schema_null_fields(&mut schema); // fix schema
|
|
Ok((Arc::new(schema), work_dir))
|
|
}
|
|
|
|
fn merge_rewrite_sql(sql: &str, schema: Arc<Schema>) -> Result<String> {
|
|
let mut sql = sql.to_string();
|
|
let mut fields = Vec::new();
|
|
let mut from_pos = 0;
|
|
let sql_chars = sql.chars().collect::<Vec<char>>();
|
|
let sql_chars_len = sql_chars.len();
|
|
let mut start_pos = 0;
|
|
let mut in_word = false;
|
|
let mut brackets = 0;
|
|
let mut quotes = 0;
|
|
let mut quote_now = '\"';
|
|
for i in 0..sql_chars_len {
|
|
let c = sql_chars.get(i).unwrap();
|
|
if *c == '(' {
|
|
brackets += 1;
|
|
continue;
|
|
}
|
|
if *c == ')' {
|
|
brackets -= 1;
|
|
continue;
|
|
}
|
|
if *c == '"' || *c == '\'' {
|
|
if quotes == 0 {
|
|
quotes += 1;
|
|
quote_now = *c;
|
|
if !in_word {
|
|
start_pos = i;
|
|
in_word = true;
|
|
}
|
|
continue;
|
|
}
|
|
if quotes == 1 && quote_now == *c {
|
|
quotes = 0;
|
|
continue;
|
|
}
|
|
}
|
|
if *c == ',' || *c == ' ' {
|
|
if brackets > 0 || quotes > 0 {
|
|
continue;
|
|
}
|
|
if in_word {
|
|
let field = sql_chars[start_pos..i].iter().collect::<String>();
|
|
if field.to_lowercase().eq("from") {
|
|
from_pos = i;
|
|
break;
|
|
} else if field.to_lowercase().eq("over") {
|
|
continue;
|
|
}
|
|
fields.push(field);
|
|
}
|
|
in_word = false;
|
|
continue;
|
|
}
|
|
if in_word {
|
|
continue;
|
|
}
|
|
start_pos = i;
|
|
in_word = true;
|
|
}
|
|
|
|
let mut new_fields = Vec::new();
|
|
let mut sel_fields_name = Vec::new();
|
|
let mut sel_fields_has_star = false;
|
|
let mut last_is_as = false;
|
|
let mut last_is_distinct = false;
|
|
for (_, field) in fields.iter().enumerate() {
|
|
let field = if last_is_distinct {
|
|
last_is_distinct = false;
|
|
format!("DISTINCT {}", field.trim())
|
|
} else {
|
|
field.trim().to_string()
|
|
};
|
|
if field.to_lowercase().eq("select") {
|
|
continue;
|
|
}
|
|
if field.to_lowercase().eq("distinct") {
|
|
last_is_distinct = true;
|
|
continue;
|
|
}
|
|
if field.to_lowercase().eq("as") {
|
|
last_is_as = true;
|
|
continue;
|
|
}
|
|
if field.to_lowercase().starts_with("over") && field.contains('(') {
|
|
// replace previouse field with over
|
|
let prev_field = new_fields.pop().unwrap();
|
|
new_fields.push(format!("{} {}", prev_field, field));
|
|
continue;
|
|
}
|
|
if last_is_as {
|
|
let prev_field = new_fields.pop().unwrap();
|
|
new_fields.push(format!("{prev_field} AS {field}"));
|
|
sel_fields_name.remove(sel_fields_name.len() - 1);
|
|
sel_fields_name.push(field.to_string().replace('"', "").replace(", ", ","));
|
|
last_is_as = false;
|
|
continue;
|
|
}
|
|
if field.eq("*") {
|
|
sel_fields_has_star = true;
|
|
}
|
|
new_fields.push(field.to_string());
|
|
sel_fields_name.push(field.to_string().replace('"', "").replace(", ", ","));
|
|
}
|
|
|
|
// handle select *
|
|
let mut fields = new_fields;
|
|
if fields.len() == 1 && sel_fields_has_star {
|
|
return Ok(sql);
|
|
}
|
|
if sel_fields_has_star {
|
|
let mut new_fields = Vec::with_capacity(fields.len());
|
|
for field in fields.iter() {
|
|
if field.eq("*") {
|
|
for f in schema.fields() {
|
|
let f_name = f.name().replace("tbl.", "");
|
|
if sel_fields_name.contains(&f_name) {
|
|
continue;
|
|
}
|
|
let field_name = if f.name().contains('@') {
|
|
"_PLACEHOLDER_"
|
|
} else {
|
|
f.name()
|
|
};
|
|
new_fields.push(format!("\"{field_name}\""));
|
|
}
|
|
} else {
|
|
new_fields.push(field.to_string());
|
|
}
|
|
}
|
|
fields = new_fields;
|
|
}
|
|
if fields.len() != schema.fields().len() {
|
|
log::error!(
|
|
"in sql fields: {:?}",
|
|
fields
|
|
.iter()
|
|
.map(|f| f.trim_matches('"').to_string())
|
|
.collect::<Vec<String>>()
|
|
);
|
|
log::error!(
|
|
"schema fields: {:?}",
|
|
schema
|
|
.fields()
|
|
.iter()
|
|
.map(|f| f.name().clone())
|
|
.collect::<Vec<String>>()
|
|
);
|
|
return Err(datafusion::error::DataFusionError::Execution(
|
|
"merge arrow files error: schema and SQL fields mismatch".to_string(),
|
|
));
|
|
}
|
|
|
|
let mut need_rewrite = false;
|
|
for i in 0..fields.len() {
|
|
let field = fields.get(i).unwrap();
|
|
let schema_field = schema.field(i).name();
|
|
if !field.contains('(') {
|
|
if field.contains(" AS ") {
|
|
need_rewrite = true;
|
|
if field.contains("DISTINCT ") {
|
|
fields[i] = format!("DISTINCT \"{}\" AS \"{}\"", schema_field, schema_field);
|
|
} else {
|
|
fields[i] = format!("\"{}\"", schema_field);
|
|
}
|
|
} else if field != schema_field && *field == schema_field.replace("tbl.", "") {
|
|
need_rewrite = true;
|
|
fields[i] = format!("\"{}\" AS \"{}\"", schema_field, field);
|
|
}
|
|
continue;
|
|
}
|
|
need_rewrite = true;
|
|
let cap = match RE_FIELD_FN.captures(field) {
|
|
Some(caps) => caps,
|
|
None => {
|
|
fields[i] = format!("\"{}\"", schema_field);
|
|
continue;
|
|
}
|
|
};
|
|
let mut fn_name = cap.get(1).unwrap().as_str().to_lowercase();
|
|
if !AGGREGATE_UDF_LIST.contains(&fn_name.as_str()) {
|
|
fields[i] = format!("\"{}\"", schema_field);
|
|
continue;
|
|
}
|
|
|
|
let over_as = if field.to_lowercase().contains("over") && field.contains('(') {
|
|
field[field.to_lowercase().find("over").unwrap()..].to_string()
|
|
} else {
|
|
"AS \"".to_string() + schema_field + "\""
|
|
};
|
|
if fn_name == "count" {
|
|
fn_name = "sum".to_string();
|
|
}
|
|
if fn_name == "approx_percentile_cont" {
|
|
let percentile = cap
|
|
.get(2)
|
|
.unwrap()
|
|
.as_str()
|
|
.splitn(2, ',')
|
|
.last()
|
|
.unwrap()
|
|
.trim();
|
|
fields[i] = format!(
|
|
"{fn_name}(\"{}\", {}) {}",
|
|
schema_field, percentile, over_as
|
|
);
|
|
} else {
|
|
fields[i] = format!("{fn_name}(\"{}\") {}", schema_field, over_as);
|
|
}
|
|
}
|
|
|
|
if need_rewrite {
|
|
sql = format!("SELECT {} FROM {}", &fields.join(", "), &sql[from_pos..]);
|
|
if sql.contains("_PLACEHOLDER_") {
|
|
sql = sql.replace(r#""_PLACEHOLDER_", "#, "");
|
|
sql = sql.replace(r#", "_PLACEHOLDER_""#, "");
|
|
}
|
|
}
|
|
|
|
// delete where from sql
|
|
let mut where_str = match RE_WHERE.captures(&sql) {
|
|
Some(caps) => caps[0].to_string(),
|
|
None => "".to_string(),
|
|
};
|
|
if !where_str.is_empty() {
|
|
let mut where_str_lower = where_str.to_lowercase();
|
|
for key in [" order by", " group by", " offset", " limit"].iter() {
|
|
if let Some(pos) = where_str_lower.find(key) {
|
|
where_str = where_str[..pos].to_string();
|
|
where_str_lower = where_str.to_lowercase();
|
|
}
|
|
}
|
|
sql = sql.replace(&where_str, " ");
|
|
}
|
|
|
|
Ok(sql)
|
|
}
|
|
|
|
pub async fn convert_parquet_file(
|
|
session_id: &str,
|
|
buf: &mut Vec<u8>,
|
|
schema: Arc<Schema>,
|
|
bloom_filter_fields: &[String],
|
|
rules: HashMap<String, DataType>,
|
|
file_type: FileType,
|
|
) -> Result<()> {
|
|
let start = std::time::Instant::now();
|
|
// query data
|
|
let ctx = prepare_datafusion_context(&SearchType::Normal)?;
|
|
|
|
// Configure listing options
|
|
let listing_options = match file_type {
|
|
FileType::PARQUET => {
|
|
let file_format = ParquetFormat::default();
|
|
ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::PARQUET.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num)
|
|
}
|
|
FileType::JSON => {
|
|
let file_format = JsonFormat::default();
|
|
ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::JSON.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num)
|
|
}
|
|
_ => {
|
|
return Err(DataFusionError::Execution(format!(
|
|
"Unsupported file type scheme {file_type:?}",
|
|
)));
|
|
}
|
|
};
|
|
|
|
let prefix = match ListingTableUrl::parse(format!("tmpfs:///{session_id}/")) {
|
|
Ok(url) => url,
|
|
Err(e) => {
|
|
return Err(datafusion::error::DataFusionError::Execution(format!(
|
|
"ListingTableUrl error: {e}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
let config = ListingTableConfig::new(prefix)
|
|
.with_listing_options(listing_options)
|
|
.with_schema(schema.clone());
|
|
|
|
let table = ListingTable::try_new(config)?;
|
|
ctx.register_table("tbl", Arc::new(table))?;
|
|
|
|
// get all sorted data
|
|
let query_sql = format!(
|
|
"SELECT * FROM tbl ORDER BY {} DESC",
|
|
CONFIG.common.column_timestamp
|
|
);
|
|
let mut df = match ctx.sql(&query_sql).await {
|
|
Ok(df) => df,
|
|
Err(e) => {
|
|
log::error!(
|
|
"convert sql execute failed, sql: {}, err: {:?}",
|
|
query_sql,
|
|
e
|
|
);
|
|
return Err(e);
|
|
}
|
|
};
|
|
if !rules.is_empty() {
|
|
let fields = df.schema().fields();
|
|
let mut exprs = Vec::with_capacity(fields.len());
|
|
for field in fields {
|
|
if let Some(v) = field.qualifier() {
|
|
if v.to_string() != "tbl" {
|
|
exprs.push(col(field.name()));
|
|
continue;
|
|
}
|
|
}
|
|
exprs.push(match rules.get(field.name()) {
|
|
Some(rule) => Expr::Alias(Alias::new(
|
|
cast(col(field.name()), rule.clone()),
|
|
None::<&str>,
|
|
field.name().to_string(),
|
|
)),
|
|
None => col(field.name()),
|
|
});
|
|
}
|
|
df = df.select(exprs)?;
|
|
}
|
|
let schema: Schema = df.schema().into();
|
|
let schema = Arc::new(schema);
|
|
let batches = df.collect().await?;
|
|
let file_meta = FileMeta::default();
|
|
let mut writer = super::new_parquet_writer(buf, &schema, bloom_filter_fields, &file_meta);
|
|
for batch in batches {
|
|
writer.write(&batch)?;
|
|
}
|
|
writer.close().unwrap();
|
|
ctx.deregister_table("tbl")?;
|
|
drop(ctx);
|
|
|
|
log::info!(
|
|
"convert_parquet_file took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn merge_parquet_files(
|
|
session_id: &str,
|
|
buf: &mut Vec<u8>,
|
|
schema: Arc<Schema>,
|
|
bloom_filter_fields: &[String],
|
|
original_size: i64,
|
|
) -> Result<FileMeta> {
|
|
let start = std::time::Instant::now();
|
|
// query data
|
|
let runtime_env = create_runtime_env()?;
|
|
let session_config = create_session_config(&SearchType::Normal)?;
|
|
let ctx = SessionContext::new_with_config_rt(session_config, Arc::new(runtime_env));
|
|
|
|
// Configure listing options
|
|
let file_format = ParquetFormat::default();
|
|
let listing_options = ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::PARQUET.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num);
|
|
let prefix = ListingTableUrl::parse(format!("tmpfs:///{session_id}/"))?;
|
|
let config = ListingTableConfig::new(prefix)
|
|
.with_listing_options(listing_options)
|
|
.with_schema(schema);
|
|
|
|
let table = ListingTable::try_new(config)?;
|
|
ctx.register_table("tbl", Arc::new(table))?;
|
|
|
|
// get meta data
|
|
let meta_sql = format!(
|
|
"SELECT MIN({}) as min_ts, MAX({}) as max_ts, COUNT(1) as num_records FROM tbl",
|
|
CONFIG.common.column_timestamp, CONFIG.common.column_timestamp
|
|
);
|
|
let df = ctx.sql(&meta_sql).await?;
|
|
let batches = df.collect().await?;
|
|
let batches_ref: Vec<&RecordBatch> = batches.iter().collect();
|
|
let result = arrowJson::writer::record_batches_to_json_rows(&batches_ref).unwrap();
|
|
let record = result.first().unwrap();
|
|
let file_meta = if record.is_empty() {
|
|
FileMeta::default()
|
|
} else {
|
|
FileMeta {
|
|
min_ts: record["min_ts"].as_i64().unwrap(),
|
|
max_ts: record["max_ts"].as_i64().unwrap(),
|
|
records: record["num_records"].as_i64().unwrap(),
|
|
original_size,
|
|
compressed_size: 0,
|
|
}
|
|
};
|
|
|
|
// get all sorted data
|
|
let query_sql = format!(
|
|
"SELECT * FROM tbl ORDER BY {} DESC",
|
|
CONFIG.common.column_timestamp
|
|
);
|
|
let df = ctx.sql(&query_sql).await?;
|
|
let schema: Schema = df.schema().into();
|
|
let schema = Arc::new(schema);
|
|
let batches = df.collect().await?;
|
|
|
|
let mut writer = super::new_parquet_writer(buf, &schema, bloom_filter_fields, &file_meta);
|
|
for batch in batches {
|
|
writer.write(&batch)?;
|
|
}
|
|
writer.close().unwrap();
|
|
ctx.deregister_table("tbl")?;
|
|
drop(ctx);
|
|
|
|
log::info!(
|
|
"merge_parquet_files took {:.3} seconds.",
|
|
start.elapsed().as_secs_f64()
|
|
);
|
|
|
|
Ok(file_meta)
|
|
}
|
|
|
|
pub fn create_session_config(search_type: &SearchType) -> Result<SessionConfig> {
|
|
let mut config = SessionConfig::from_env()?
|
|
.with_batch_size(PARQUET_BATCH_SIZE)
|
|
.with_information_schema(true);
|
|
if search_type == &SearchType::Normal {
|
|
config = config.set_bool("datafusion.execution.parquet.pushdown_filters", true);
|
|
config = config.set_bool("datafusion.execution.parquet.reorder_filters", true);
|
|
}
|
|
if CONFIG.common.bloom_filter_enabled {
|
|
config = config.set_bool("datafusion.execution.parquet.bloom_filter_enabled", true);
|
|
}
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn create_runtime_env() -> Result<RuntimeEnv> {
|
|
let object_store_registry = DefaultObjectStoreRegistry::new();
|
|
|
|
let memory = super::storage::memory::FS::new();
|
|
let memory_url = url::Url::parse("memory:///").unwrap();
|
|
object_store_registry.register_store(&memory_url, Arc::new(memory));
|
|
|
|
let tmpfs = super::storage::tmpfs::Tmpfs::new();
|
|
let tmpfs_url = url::Url::parse("tmpfs:///").unwrap();
|
|
object_store_registry.register_store(&tmpfs_url, Arc::new(tmpfs));
|
|
|
|
let rn_config =
|
|
RuntimeConfig::new().with_object_store_registry(Arc::new(object_store_registry));
|
|
let rn_config = if CONFIG.memory_cache.datafusion_max_size > 0 {
|
|
let mem_pool = super::MemoryPoolType::from_str(&CONFIG.memory_cache.datafusion_memory_pool)
|
|
.map_err(|e| {
|
|
DataFusionError::Execution(format!("Invalid datafusion memory pool type: {}", e))
|
|
})?;
|
|
match mem_pool {
|
|
super::MemoryPoolType::Greedy => rn_config.with_memory_pool(Arc::new(
|
|
GreedyMemoryPool::new(CONFIG.memory_cache.datafusion_max_size),
|
|
)),
|
|
super::MemoryPoolType::Fair => rn_config.with_memory_pool(Arc::new(
|
|
FairSpillPool::new(CONFIG.memory_cache.datafusion_max_size),
|
|
)),
|
|
super::MemoryPoolType::None => rn_config,
|
|
}
|
|
} else {
|
|
rn_config
|
|
};
|
|
RuntimeEnv::new(rn_config)
|
|
}
|
|
|
|
pub fn prepare_datafusion_context(
|
|
search_type: &SearchType,
|
|
) -> Result<SessionContext, DataFusionError> {
|
|
let session_config = create_session_config(search_type)?;
|
|
let runtime_env = create_runtime_env()?;
|
|
Ok(SessionContext::new_with_config_rt(
|
|
session_config,
|
|
Arc::new(runtime_env),
|
|
))
|
|
}
|
|
|
|
async fn register_udf(ctx: &mut SessionContext, _org_id: &str) {
|
|
ctx.register_udf(super::match_udf::MATCH_UDF.clone());
|
|
ctx.register_udf(super::match_udf::MATCH_IGNORE_CASE_UDF.clone());
|
|
ctx.register_udf(super::regexp_udf::REGEX_MATCH_UDF.clone());
|
|
ctx.register_udf(super::regexp_udf::REGEX_NOT_MATCH_UDF.clone());
|
|
ctx.register_udf(super::time_range_udf::TIME_RANGE_UDF.clone());
|
|
ctx.register_udf(super::date_format_udf::DATE_FORMAT_UDF.clone());
|
|
|
|
{
|
|
let udf_list = get_all_transform(_org_id).await;
|
|
for udf in udf_list {
|
|
ctx.register_udf(udf.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn register_table(
|
|
session: &SearchSession,
|
|
schema: Arc<Schema>,
|
|
table_name: &str,
|
|
files: &[FileKey],
|
|
file_type: FileType,
|
|
) -> Result<SessionContext> {
|
|
let ctx = prepare_datafusion_context(&session.search_type)?;
|
|
// Configure listing options
|
|
let listing_options = match file_type {
|
|
FileType::PARQUET => {
|
|
let file_format = ParquetFormat::default();
|
|
ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::PARQUET.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num)
|
|
}
|
|
FileType::JSON => {
|
|
let file_format = JsonFormat::default();
|
|
ListingOptions::new(Arc::new(file_format))
|
|
.with_file_extension(FileType::JSON.get_ext())
|
|
.with_target_partitions(CONFIG.limit.cpu_num)
|
|
}
|
|
_ => {
|
|
return Err(DataFusionError::Execution(format!(
|
|
"Unsupported file type scheme {file_type:?}",
|
|
)));
|
|
}
|
|
};
|
|
|
|
let prefix = if session.storage_type.eq(&StorageType::Memory) {
|
|
file_list::set(&session.id, files).await;
|
|
format!("memory:///{}/", session.id)
|
|
} else if session.storage_type.eq(&StorageType::Tmpfs) {
|
|
format!("tmpfs:///{}/", session.id)
|
|
} else {
|
|
return Err(DataFusionError::Execution(format!(
|
|
"Unsupported file type scheme {file_type:?}",
|
|
)));
|
|
};
|
|
let prefix = match ListingTableUrl::parse(prefix) {
|
|
Ok(url) => url,
|
|
Err(e) => {
|
|
return Err(datafusion::error::DataFusionError::Execution(format!(
|
|
"ListingTableUrl error: {e}",
|
|
)));
|
|
}
|
|
};
|
|
|
|
let config = ListingTableConfig::new(prefix)
|
|
.with_listing_options(listing_options)
|
|
.with_schema(schema);
|
|
let table = ListingTable::try_new(config)?;
|
|
ctx.register_table(table_name, Arc::new(table))?;
|
|
|
|
Ok(ctx)
|
|
}
|
|
|
|
fn handle_query_fn(
|
|
query_fn: String,
|
|
batches: &[&RecordBatch],
|
|
org_id: &str,
|
|
) -> Result<Vec<RecordBatch>> {
|
|
match datafusion::arrow::json::writer::record_batches_to_json_rows(batches) {
|
|
Ok(json_rows) => apply_query_fn(query_fn, json_rows, org_id),
|
|
Err(err) => Err(DataFusionError::Execution(format!(
|
|
"Error converting record batches to json rows: {}",
|
|
err
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn apply_query_fn(
|
|
query_fn_src: String,
|
|
in_batch: Vec<json::Map<String, json::Value>>,
|
|
org_id: &str,
|
|
) -> Result<Vec<RecordBatch>> {
|
|
use vector_enrichment::TableRegistry;
|
|
|
|
let mut resp = vec![];
|
|
let mut runtime = crate::common::utils::functions::init_vrl_runtime();
|
|
match crate::service::ingestion::compile_vrl_function(&query_fn_src, org_id) {
|
|
Ok(program) => {
|
|
let registry = program.config.get_custom::<TableRegistry>().unwrap();
|
|
registry.finish_load();
|
|
|
|
let rows_val: Vec<json::Value> = in_batch
|
|
.iter()
|
|
.filter_map(|hit| {
|
|
let ret_val = crate::service::ingestion::apply_vrl_fn(
|
|
&mut runtime,
|
|
&VRLResultResolver {
|
|
program: program.program.clone(),
|
|
fields: program.fields.clone(),
|
|
},
|
|
&json::Value::Object(hit.clone()),
|
|
);
|
|
(!ret_val.is_null()).then_some(flatten::flatten(&ret_val).unwrap_or(ret_val))
|
|
})
|
|
.collect();
|
|
|
|
let value_iter = rows_val.iter().map(Ok);
|
|
let mut inferred_schema =
|
|
arrow::json::reader::infer_json_schema_from_iterator(value_iter).unwrap();
|
|
filter_schema_null_fields(&mut inferred_schema);
|
|
let mut decoder =
|
|
arrow::json::ReaderBuilder::new(Arc::new(inferred_schema)).build_decoder()?;
|
|
|
|
for value in rows_val {
|
|
decoder
|
|
.decode(json::to_string(&value).unwrap().as_bytes())
|
|
.unwrap();
|
|
resp.push(decoder.flush()?.unwrap());
|
|
}
|
|
Ok(resp)
|
|
}
|
|
Err(err) => Err(DataFusionError::Execution(format!(
|
|
"Error compiling VRL function: {}",
|
|
err
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use arrow::array::{Int32Array, NullArray, StringArray};
|
|
use arrow_schema::Field;
|
|
|
|
use super::*;
|
|
|
|
#[actix_web::test]
|
|
async fn test_register_udf() {
|
|
let mut ctx = SessionContext::new();
|
|
let _ = register_udf(&mut ctx, "nexus").await;
|
|
// assert!(res)
|
|
}
|
|
|
|
#[actix_web::test]
|
|
async fn test_merge_write_recordbatch() {
|
|
// define a schema.
|
|
let schema1 = Arc::new(Schema::new(vec![
|
|
Field::new("f", DataType::Int32, false),
|
|
Field::new("g", DataType::Utf8, false),
|
|
]));
|
|
let schema2 = Arc::new(Schema::new(vec![
|
|
Field::new("f", DataType::Int32, false),
|
|
Field::new("g", DataType::Null, false),
|
|
]));
|
|
// define data.
|
|
let batch2 = RecordBatch::try_new(
|
|
schema2.clone(),
|
|
vec![
|
|
Arc::new(Int32Array::from(vec![1, 10, 10, 100])),
|
|
Arc::new(NullArray::new(4)),
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
let batch1 = RecordBatch::try_new(
|
|
schema1.clone(),
|
|
vec![
|
|
Arc::new(Int32Array::from(vec![2, 20, 20, 200])),
|
|
Arc::new(StringArray::from(vec!["2", "20", "20", "200"])),
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
let res = merge_write_recordbatch(&[vec![batch1, batch2]]).unwrap();
|
|
assert!(!res.0.fields().is_empty());
|
|
assert!(!res.1.is_empty())
|
|
}
|
|
|
|
#[actix_web::test]
|
|
async fn test_merge() {
|
|
// define a schema.
|
|
let schema = Arc::new(Schema::new(vec![Field::new("f", DataType::Int32, false)]));
|
|
// define data.
|
|
let batch = RecordBatch::try_new(
|
|
schema.clone(),
|
|
vec![Arc::new(Int32Array::from(vec![1, 10, 10, 100]))],
|
|
)
|
|
.unwrap();
|
|
|
|
let batch2 = RecordBatch::try_new(
|
|
schema.clone(),
|
|
vec![Arc::new(Int32Array::from(vec![2, 20, 20, 200]))],
|
|
)
|
|
.unwrap();
|
|
|
|
let res = merge(
|
|
"dummy",
|
|
1,
|
|
100,
|
|
"select * from tbl limit 10",
|
|
&vec![vec![batch, batch2]],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(!res.is_empty())
|
|
}
|
|
}
|