Skip to content

Commit f3bf727

Browse files
authored
Merge branch 'main' into fix/dedup-stringview-buffers-collectleft-joins
2 parents 6c8683c + e1aa7d9 commit f3bf727

28 files changed

Lines changed: 984 additions & 249 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎benchmarks/src/statistics.rs‎

Lines changed: 141 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ use serde::{Deserialize, Serialize};
4141

4242
/// Generate reports that compare planning statistics with runtime metrics.
4343
///
44-
/// Parser options are captured when the run starts, so `SET` statements do not
45-
/// affect parsing in later query files.
44+
/// Parser options are refreshed before each query file is parsed, so `SET`
45+
/// statements affect later files. Because each file is parsed before any of its
46+
/// statements are executed, parser-setting changes do not affect later
47+
/// statements in the same file.
4648
#[derive(Debug, Args)]
4749
#[command(verbatim_doc_comment)]
4850
pub struct RunOpt {
@@ -68,7 +70,6 @@ impl RunOpt {
6870
let mut config = SessionConfig::from_env()?.with_collect_statistics(true);
6971
config.options_mut().optimizer.prefer_hash_join = true;
7072
let ctx = SessionContext::new_with_config(config);
71-
let sql_parser_options = ctx.state().config_options().sql_parser.clone();
7273
register_parquet_files(&ctx, &self.path).await?;
7374

7475
let branch = current_branch_name();
@@ -87,48 +88,8 @@ impl RunOpt {
8788

8889
let mut reports = vec![];
8990
for query_path in query_files(&self.query_path, self.query.as_deref())? {
90-
let query = query_path
91-
.file_stem()
92-
.expect("query file has a filename")
93-
.to_string_lossy()
94-
.to_string();
95-
let sql = fs::read_to_string(query_path)?;
96-
let statements = match sql_statements(&sql, &sql_parser_options) {
97-
Ok(statements) => statements,
98-
Err(error) => {
99-
let report = QueryReport {
100-
query: query.clone(),
101-
statement: 1,
102-
operators: vec![],
103-
success: false,
104-
error: Some(error.to_string()),
105-
};
106-
print_query_report(&report, previous.as_deref());
107-
reports.push(report);
108-
continue;
109-
}
110-
};
111-
for (statement, sql) in statements.into_iter().enumerate() {
112-
let statement = statement + 1;
113-
let report = match self.report_statement(&ctx, sql).await {
114-
Ok(operators) => QueryReport {
115-
query: query.clone(),
116-
statement,
117-
operators,
118-
success: true,
119-
error: None,
120-
},
121-
Err(error) => QueryReport {
122-
query: query.clone(),
123-
statement,
124-
operators: vec![],
125-
success: false,
126-
error: Some(error.to_string()),
127-
},
128-
};
129-
print_query_report(&report, previous.as_deref());
130-
reports.push(report);
131-
}
91+
self.report_query_file(&ctx, &query_path, previous.as_deref(), &mut reports)
92+
.await?;
13293
}
13394
store_report(&result_path, &reports)?;
13495
print_q_error_summary(
@@ -140,6 +101,59 @@ impl RunOpt {
140101
Ok(())
141102
}
142103

104+
async fn report_query_file(
105+
&self,
106+
ctx: &SessionContext,
107+
query_path: &Path,
108+
previous: Option<&[QueryReport]>,
109+
reports: &mut Vec<QueryReport>,
110+
) -> Result<()> {
111+
let query = query_path
112+
.file_stem()
113+
.expect("query file has a filename")
114+
.to_string_lossy()
115+
.to_string();
116+
let sql = fs::read_to_string(query_path)?;
117+
let sql_parser_options = ctx.state().config_options().sql_parser.clone();
118+
let statements = match sql_statements(&sql, &sql_parser_options) {
119+
Ok(statements) => statements,
120+
Err(error) => {
121+
let report = QueryReport {
122+
query,
123+
statement: 1,
124+
operators: vec![],
125+
success: false,
126+
error: Some(error.to_string()),
127+
};
128+
print_query_report(&report, previous);
129+
reports.push(report);
130+
return Ok(());
131+
}
132+
};
133+
for (statement, sql) in statements.into_iter().enumerate() {
134+
let statement = statement + 1;
135+
let report = match self.report_statement(ctx, sql).await {
136+
Ok(operators) => QueryReport {
137+
query: query.clone(),
138+
statement,
139+
operators,
140+
success: true,
141+
error: None,
142+
},
143+
Err(error) => QueryReport {
144+
query: query.clone(),
145+
statement,
146+
operators: vec![],
147+
success: false,
148+
error: Some(error.to_string()),
149+
},
150+
};
151+
print_query_report(&report, previous);
152+
reports.push(report);
153+
}
154+
Ok(())
155+
}
156+
143157
fn report_path(&self, branch: &str) -> PathBuf {
144158
let report_name = self.query.as_ref().map_or_else(
145159
|| "statistics.json".to_string(),
@@ -772,6 +786,88 @@ mod tests {
772786
assert!(!reports.is_empty());
773787
}
774788

789+
async fn report_query_files(
790+
options: &RunOpt,
791+
ctx: &SessionContext,
792+
) -> Vec<QueryReport> {
793+
let mut reports = vec![];
794+
for path in query_files(&options.query_path, options.query.as_deref()).unwrap() {
795+
options
796+
.report_query_file(ctx, &path, None, &mut reports)
797+
.await
798+
.unwrap();
799+
}
800+
reports
801+
}
802+
803+
#[tokio::test]
804+
async fn refreshes_sql_dialect_between_query_files() {
805+
let directory = tempdir().unwrap();
806+
fs::write(
807+
directory.path().join("01.sql"),
808+
"SET datafusion.sql_parser.dialect = 'MySQL'",
809+
)
810+
.unwrap();
811+
fs::write(directory.path().join("02.sql"), "# MySQL comment\nSELECT 1").unwrap();
812+
fs::write(
813+
directory.path().join("03.sql"),
814+
"RESET datafusion.sql_parser.dialect",
815+
)
816+
.unwrap();
817+
fs::write(directory.path().join("04.sql"), "SELECT 1,").unwrap();
818+
let options = RunOpt {
819+
query: None,
820+
compare: None,
821+
path: directory.path().to_path_buf(),
822+
query_path: directory.path().to_path_buf(),
823+
};
824+
825+
let reports = report_query_files(&options, &SessionContext::new()).await;
826+
827+
assert_eq!(reports.len(), 4);
828+
assert!(reports.iter().all(|report| report.success));
829+
assert_eq!(reports[1].query, "02");
830+
assert!(!reports[1].operators.is_empty());
831+
assert_eq!(reports[3].query, "04");
832+
assert!(!reports[3].operators.is_empty());
833+
}
834+
835+
#[tokio::test]
836+
async fn refreshes_parser_recursion_limit_between_query_files() {
837+
let directory = tempdir().unwrap();
838+
fs::write(
839+
directory.path().join("01.sql"),
840+
"SET datafusion.sql_parser.recursion_limit = 2",
841+
)
842+
.unwrap();
843+
fs::write(
844+
directory.path().join("02.sql"),
845+
"SELECT (((((((((((1)))))))))))",
846+
)
847+
.unwrap();
848+
let options = RunOpt {
849+
query: None,
850+
compare: None,
851+
path: directory.path().to_path_buf(),
852+
query_path: directory.path().to_path_buf(),
853+
};
854+
855+
let reports = report_query_files(&options, &SessionContext::new()).await;
856+
857+
assert_eq!(reports.len(), 2);
858+
assert!(reports[0].success);
859+
assert!(!reports[1].success);
860+
assert_eq!(reports[1].query, "02");
861+
assert!(
862+
reports[1]
863+
.error
864+
.as_deref()
865+
.is_some_and(|error| error.contains("RecursionLimitExceeded")),
866+
"unexpected report: {:?}",
867+
reports[1]
868+
);
869+
}
870+
775871
#[test]
776872
fn persists_failed_reports() {
777873
let directory = tempdir().unwrap();

‎datafusion/common/src/config.rs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ config_namespace! {
10591059
/// Guarantees a minimum level of output files running in parallel.
10601060
/// RecordBatches will be distributed in round robin fashion to each
10611061
/// parallel writer. Each writer is closed and a new file opened once
1062-
/// soft_max_rows_per_output_file is reached.
1062+
/// soft_max_rows_per_output_file or soft_max_bytes_per_output_file is reached.
10631063
pub minimum_parallel_output_files: ConfigNonZeroUsize, default = non_zero_usize_default(4)
10641064

10651065
/// Target number of rows in output files when writing multiple.
@@ -1068,6 +1068,13 @@ config_namespace! {
10681068
/// number of rows written is not roughly divisible by the soft max
10691069
pub soft_max_rows_per_output_file: ConfigNonZeroUsize, default = non_zero_usize_default(50000000)
10701070

1071+
/// Target encoded size in bytes of output files when writing multiple.
1072+
/// Writers asynchronously report the cumulative encoded size as they
1073+
/// process RecordBatches. The final file size may exceed this limit due
1074+
/// to batches buffered before the limit is observed, the size of a batch,
1075+
/// and file metadata written when the file is finalized.
1076+
pub soft_max_bytes_per_output_file: ConfigNonZeroUsize, default = non_zero_usize_default(4294967295)
1077+
10711078
/// This is the maximum number of RecordBatches buffered
10721079
/// for each output file being worked. Higher values can potentially
10731080
/// give faster write performance at the cost of higher peak

0 commit comments

Comments
 (0)