Skip to content

Commit eae79b4

Browse files
authored
Add detailed diagnostics for validate_sql API (#1123)
1 parent fa575e4 commit eae79b4

16 files changed

Lines changed: 297 additions & 44 deletions

File tree

crates/arroyo-api/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ impl IntoResponse for HttpError {
356356
GlobalUdf,
357357
GlobalUdfCollection,
358358
BadData,
359+
SqlDiagnostic,
360+
SqlSpan,
361+
SqlLocation
359362
)),
360363
tags(
361364
(name = "ping", description = "Ping endpoint"),

crates/arroyo-api/src/pipelines.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use arroyo_datastream::logical::{
2828
ChainedLogicalOperator, LogicalNode, LogicalProgram, OperatorChain, OperatorName,
2929
};
3030
use arroyo_formats::ser::ArrowSerializer;
31-
use arroyo_planner::{ArroyoSchemaProvider, CompiledSql, SqlConfig};
31+
use arroyo_planner::{ArroyoSchemaProvider, CompiledSql, PlannerError, SqlConfig};
3232
use arroyo_rpc::formats::Format;
3333
use arroyo_rpc::grpc::rpc::compiler_grpc_client::CompilerGrpcClient;
3434
use arroyo_rpc::public_ids::{IdTypes, generate_id};
@@ -65,7 +65,7 @@ async fn compile_sql(
6565
auth_data: &AuthData,
6666
validate_only: bool,
6767
db: &DatabaseSource,
68-
) -> Result<CompiledSql, ErrorResp> {
68+
) -> Result<Result<CompiledSql, PlannerError>, ErrorResp> {
6969
let mut schema_provider = ArroyoSchemaProvider::new();
7070

7171
let global_udfs = fetch_get_udfs(&db.client().await?, &auth_data.organization_id)
@@ -174,15 +174,14 @@ async fn compile_sql(
174174
schema_provider.add_connection_profile(profile);
175175
}
176176

177-
arroyo_planner::parse_and_get_program(
177+
Ok(arroyo_planner::parse_and_get_program(
178178
&query,
179179
schema_provider,
180180
SqlConfig {
181181
default_parallelism: parallelism,
182182
},
183183
)
184-
.await
185-
.map_err(|err| bad_request(err.to_string()))
184+
.await)
186185
}
187186

188187
fn set_parallelism(program: &mut LogicalProgram, parallelism: usize) {
@@ -569,15 +568,15 @@ pub async fn validate_query(
569568
true,
570569
&state.database,
571570
)
572-
.await
571+
.await?
573572
{
574573
Ok(CompiledSql { program, .. }) => QueryValidationResult {
575574
graph: Some(program.try_into().map_err(log_and_map)?),
576575
errors: vec![],
577576
},
578577
Err(e) => QueryValidationResult {
579578
graph: None,
580-
errors: vec![e.message],
579+
errors: e.diagnostics,
581580
},
582581
};
583582

@@ -660,7 +659,7 @@ async fn create_pipeline_inner(
660659
false,
661660
&state.database,
662661
)
663-
.await?;
662+
.await??;
664663

665664
let pipeline_id = create_pipeline_int(
666665
pipeline_post.name,
@@ -718,7 +717,7 @@ pub async fn create_preview_pipeline(
718717
false,
719718
&state.database,
720719
)
721-
.await?;
720+
.await??;
722721

723722
let pipeline_id = create_pipeline_int(
724723
format!("preview_{}", to_millis(SystemTime::now())),

crates/arroyo-api/src/rest_utils.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{AuthData, cloud};
2+
use arroyo_planner::PlannerError;
23
use arroyo_rpc::log_event;
34
use axum::Json;
45
use axum::extract::rejection::JsonRejection;
@@ -67,6 +68,20 @@ pub fn map_delete_err(name: &str, user: &str, error: DbError) -> ErrorResp {
6768
}
6869
}
6970

71+
impl From<PlannerError> for ErrorResp {
72+
fn from(value: PlannerError) -> Self {
73+
let mut message = "Failed to plan query:".to_string();
74+
for d in &value.diagnostics {
75+
message.push_str(&format!("\n * {}", d.message));
76+
}
77+
78+
ErrorResp {
79+
status_code: StatusCode::BAD_REQUEST,
80+
message,
81+
}
82+
}
83+
}
84+
7085
impl From<DbError> for ErrorResp {
7186
fn from(value: DbError) -> Self {
7287
match value {

crates/arroyo-planner/src/lib.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ use arroyo_connectors::connectors;
6464
use arroyo_datastream::logical::LogicalProgram;
6565
use arroyo_datastream::optimizers::ChainingOptimizer;
6666
use arroyo_operator::connector::Connection;
67+
use arroyo_rpc::api_types::pipelines::{SqlDiagnostic, SqlLocation, SqlSpan};
6768
use arroyo_rpc::df::ArroyoSchema;
6869
use arroyo_rpc::{TIMESTAMP_FIELD, duration_from_sql};
6970
use arroyo_udf_host::ParsedUdfFile;
@@ -548,17 +549,52 @@ impl Default for SqlConfig {
548549
}
549550
}
550551

552+
#[derive(Debug)]
553+
pub struct PlannerError {
554+
pub diagnostics: Vec<SqlDiagnostic>,
555+
}
556+
551557
pub async fn parse_and_get_program(
552558
query: &str,
553559
schema_provider: ArroyoSchemaProvider,
554560
config: SqlConfig,
555-
) -> Result<CompiledSql> {
561+
) -> Result<CompiledSql, PlannerError> {
556562
let query = query.to_string();
557563

558564
if query.trim().is_empty() {
559-
return plan_err!("Query is empty");
560-
}
561-
parse_and_get_arrow_program(query, schema_provider, config).await
565+
return Err(PlannerError {
566+
diagnostics: vec![SqlDiagnostic::message("Query is empty!")],
567+
});
568+
}
569+
parse_and_get_arrow_program(query, schema_provider, config)
570+
.await
571+
.map_err(|e| {
572+
let diagnostics = e
573+
.iter()
574+
.map(|error| {
575+
let diagnostic = error.diagnostic();
576+
SqlDiagnostic {
577+
message: diagnostic
578+
.map(|diagnostic| diagnostic.message.clone())
579+
.unwrap_or_else(|| error.to_string()),
580+
span: diagnostic.and_then(|diagnostic| {
581+
diagnostic.span.map(|span| SqlSpan {
582+
start: SqlLocation {
583+
line: span.start.line,
584+
column: span.start.column,
585+
},
586+
end: SqlLocation {
587+
line: span.end.line,
588+
column: span.end.column,
589+
},
590+
})
591+
}),
592+
}
593+
})
594+
.collect();
595+
596+
PlannerError { diagnostics }
597+
})
562598
}
563599

564600
#[derive(Debug, Clone, PartialEq, Eq, Hash)]

crates/arroyo-planner/src/tables.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ use datafusion::{
5757
},
5858
};
5959
use itertools::Itertools;
60-
use sqlparser::ast;
61-
use sqlparser::ast::TableConstraint;
60+
use sqlparser::ast::{self, TableConstraint};
6261
use std::sync::Arc;
6362
use std::{collections::HashMap, time::Duration};
6463
use tracing::warn;
@@ -140,7 +139,10 @@ fn produce_optimized_plan(
140139
statement: &Statement,
141140
schema_provider: &ArroyoSchemaProvider,
142141
) -> Result<LogicalPlan> {
143-
let sql_to_rel = SqlToRel::new(schema_provider);
142+
let sql_to_rel = SqlToRel::new_with_options(
143+
schema_provider,
144+
datafusion::sql::planner::ParserOptions::new().with_collect_spans(true),
145+
);
144146

145147
let plan = sql_to_rel.sql_statement_to_plan(statement.clone())?;
146148

crates/arroyo-planner/src/test/plan_tests.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,21 @@ async fn validate_query(path: &Path) {
4747
if fail {
4848
let err = result.unwrap_err();
4949
if let Some(error_message) = error_message {
50-
let err_s = err.to_string();
51-
let err: Vec<_> = err_s.split_whitespace().collect();
50+
let err: Vec<_> = err
51+
.diagnostics
52+
.first()
53+
.unwrap()
54+
.message
55+
.split_whitespace()
56+
.collect();
5257
let err = err.join(" ");
5358
assert!(
5459
err.contains(error_message),
5560
"expected error message '{error_message}' not found; instead got '{err}'"
5661
);
5762
}
5863
} else if let Err(e) = result {
59-
println!("{e}");
60-
panic!("{}", e);
64+
println!("{e:?}");
65+
panic!("{e:?}");
6166
}
6267
}

crates/arroyo-planner/src/test/queries/virtual_bad_schema.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
--fail=Schema error: No field named notfield. Valid fields are input.length.
1+
--fail='notfield' not found
22
create table input (
33
length JSON,
44
diff INT GENERATED ALWAYS AS (notfield) STORED

crates/arroyo-planner/src/types.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
use std::{sync::Arc, time::SystemTime};
2-
31
use arrow::datatypes::{DataType, Field};
4-
use datafusion::common::{Result, plan_datafusion_err, plan_err};
5-
62
use arrow_schema::{DECIMAL_DEFAULT_SCALE, DECIMAL128_MAX_PRECISION, IntervalUnit, TimeUnit};
73
use arroyo_types::ArroyoExtensionType;
8-
use datafusion::error::DataFusionError;
4+
use datafusion::common::{
5+
DataFusionError, Diagnostic, Result, Span, plan_datafusion_err, plan_err,
6+
};
97
use datafusion::sql::sqlparser::ast::{
10-
ArrayElemTypeDef, DataType as SQLDataType, ExactNumberInfo, TimezoneInfo,
8+
ArrayElemTypeDef, DataType as SQLDataType, ExactNumberInfo, Spanned, TimezoneInfo,
119
};
1210
use itertools::Itertools;
11+
use std::{sync::Arc, time::SystemTime};
1312
// Pulled from DataFusion
1413

1514
pub(crate) fn convert_data_type(
@@ -117,7 +116,13 @@ fn convert_simple_data_type(
117116

118117
Ok(DataType::Struct(fields.into()))
119118
}
120-
_ => return plan_err!("Unsupported SQL type {sql_type:?}"),
119+
SQLDataType::Custom(name, _) => {
120+
let message = format!("Unsupported SQL type '{name}'");
121+
let diagnostic =
122+
Diagnostic::new_error(message.clone(), Span::try_from_sqlparser_span(name.span()));
123+
return plan_err!("{message}"; diagnostic = diagnostic);
124+
}
125+
_ => return plan_err!("Unsupported SQL type '{sql_type}'"),
121126
};
122127

123128
Ok((dt?, None))

crates/arroyo-rpc/src/api_types/pipelines.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
use std::collections::HashMap;
2-
31
use crate::api_types::udfs::Udf;
42
use crate::errors::ErrorDomain;
53
use crate::grpc as grpc_proto;
64
use serde::{Deserialize, Serialize};
5+
use std::collections::HashMap;
76
use utoipa::ToSchema;
87

98
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
@@ -17,7 +16,34 @@ pub struct ValidateQueryPost {
1716
#[serde(rename_all = "snake_case")]
1817
pub struct QueryValidationResult {
1918
pub graph: Option<PipelineGraph>,
20-
pub errors: Vec<String>,
19+
pub errors: Vec<SqlDiagnostic>,
20+
}
21+
22+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
23+
pub struct SqlLocation {
24+
pub line: u64,
25+
pub column: u64,
26+
}
27+
28+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
29+
pub struct SqlSpan {
30+
pub start: SqlLocation,
31+
pub end: SqlLocation,
32+
}
33+
34+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
35+
pub struct SqlDiagnostic {
36+
pub message: String,
37+
#[serde(skip_serializing_if = "Option::is_none")]
38+
pub span: Option<SqlSpan>,
39+
}
40+
impl SqlDiagnostic {
41+
pub fn message(message: impl Into<String>) -> Self {
42+
Self {
43+
message: message.into(),
44+
span: None,
45+
}
46+
}
2147
}
2248

2349
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]

crates/arroyo/src/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ async fn run_pipeline(
167167
if !errors.errors.is_empty() {
168168
eprintln!("There were some issues with the provided query");
169169
for error in errors.errors {
170-
eprintln!(" * {error}");
170+
eprintln!(" * {}", error.message);
171171
}
172172
exit(1);
173173
}

0 commit comments

Comments
 (0)