Skip to content

Commit a980af3

Browse files
committed
Add detailed diagnostics for validate_sql API
1 parent fa575e4 commit a980af3

14 files changed

Lines changed: 300 additions & 42 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: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{compiler_service, connection_profiles, jobs, types};
1717
use arroyo_datastream::default_sink;
1818
use arroyo_rpc::api_types::pipelines::{
1919
FailureReason, Job, Pipeline, PipelinePatch, PipelinePost, PipelineRestart, PreviewPost,
20-
QueryValidationResult, StopType, ValidateQueryPost,
20+
QueryValidationResult, SqlDiagnostic, SqlLocation, SqlSpan, StopType, ValidateQueryPost,
2121
};
2222
use arroyo_rpc::api_types::udfs::{GlobalUdf, Udf, UdfLanguage};
2323
use arroyo_rpc::api_types::{JobCollection, PaginationQueryParams, PipelineCollection};
@@ -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 = format!("Failed to plan query: {}", value.error);
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: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ use arroyo_datastream::WindowType;
2424

2525
use builder::NamedNode;
2626
use datafusion::common::tree_node::TreeNode;
27-
use datafusion::common::{Column, DFSchema, Result, ScalarValue, not_impl_err, plan_err};
27+
use datafusion::common::{
28+
Column, DFSchema, Diagnostic, Location, Result, ScalarValue, Span, not_impl_err, plan_err,
29+
};
2830
use datafusion::datasource::DefaultTableSource;
2931
#[allow(deprecated)]
3032
use datafusion::prelude::SessionConfig;
@@ -64,6 +66,7 @@ use arroyo_connectors::connectors;
6466
use arroyo_datastream::logical::LogicalProgram;
6567
use arroyo_datastream::optimizers::ChainingOptimizer;
6668
use arroyo_operator::connector::Connection;
69+
use arroyo_rpc::api_types::pipelines::{SqlDiagnostic, SqlLocation, SqlSpan};
6770
use arroyo_rpc::df::ArroyoSchema;
6871
use arroyo_rpc::{TIMESTAMP_FIELD, duration_from_sql};
6972
use arroyo_udf_host::ParsedUdfFile;
@@ -79,6 +82,7 @@ use datafusion::prelude::col;
7982
use sqlparser::ast::{OneOrManyWithParens, Statement};
8083
use sqlparser::dialect::ArroyoDialect;
8184
use sqlparser::parser::{Parser, ParserError};
85+
use sqlparser::tokenizer::{TokenWithSpan, Tokenizer};
8286
use std::any::Any;
8387
use std::time::{Duration, SystemTime};
8488
use std::{collections::HashMap, sync::Arc};
@@ -548,17 +552,57 @@ impl Default for SqlConfig {
548552
}
549553
}
550554

555+
#[derive(Debug)]
556+
pub struct PlannerError {
557+
pub error: String,
558+
pub diagnostics: Vec<SqlDiagnostic>,
559+
}
560+
551561
pub async fn parse_and_get_program(
552562
query: &str,
553563
schema_provider: ArroyoSchemaProvider,
554564
config: SqlConfig,
555-
) -> Result<CompiledSql> {
565+
) -> Result<CompiledSql, PlannerError> {
556566
let query = query.to_string();
557567

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

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

crates/arroyo-planner/src/tables.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use arroyo_rpc::formats::{BadData, Format, Framing, JsonCompression, JsonFormat}
1818
use arroyo_rpc::grpc::api::ConnectorOp;
1919
use arroyo_types::ArroyoExtensionType;
2020
use datafusion::common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor};
21-
use datafusion::common::{Column, DataFusionError, plan_err};
21+
use datafusion::common::{Column, DataFusionError, Diagnostic, Span, plan_err};
2222
use datafusion::common::{
2323
DFSchema, Result, ScalarValue, config::ConfigOptions, plan_datafusion_err,
2424
};
@@ -57,8 +57,8 @@ use datafusion::{
5757
},
5858
};
5959
use itertools::Itertools;
60-
use sqlparser::ast;
61-
use sqlparser::ast::TableConstraint;
60+
use sqlparser::ast::{self, FunctionArguments, Spanned, TableConstraint};
61+
use std::ops::ControlFlow;
6262
use std::sync::Arc;
6363
use std::{collections::HashMap, time::Duration};
6464
use tracing::warn;
@@ -140,7 +140,10 @@ fn produce_optimized_plan(
140140
statement: &Statement,
141141
schema_provider: &ArroyoSchemaProvider,
142142
) -> Result<LogicalPlan> {
143-
let sql_to_rel = SqlToRel::new(schema_provider);
143+
let sql_to_rel = SqlToRel::new_with_options(
144+
schema_provider,
145+
datafusion::sql::planner::ParserOptions::new().with_collect_spans(true),
146+
);
144147

145148
let plan = sql_to_rel.sql_statement_to_plan(statement.clone())?;
146149

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: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
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;
6+
use std::fmt::{Display, Formatter};
77
use utoipa::ToSchema;
88

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

2350
#[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
}

crates/integ/tests/api_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ async fn basic_pipeline() {
223223
.unwrap()
224224
.into_inner();
225225

226-
assert_eq!(valid.errors, Vec::<String>::new());
226+
assert!(valid.errors.is_empty());
227227
assert!(valid.graph.is_some());
228228

229229
let (pipeline_id, job_id, _) = start_and_monitor(test_id, &query, &[], 10).await.unwrap();

webui/src/gen/api-types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ export interface components {
907907
message_name?: string | null;
908908
};
909909
QueryValidationResult: {
910-
errors: string[];
910+
errors: components["schemas"]["SqlDiagnostic"][];
911911
graph?: components["schemas"]["PipelineGraph"] | null;
912912
};
913913
RawBytesFormat: Record<string, never>;
@@ -936,6 +936,20 @@ export interface components {
936936
required?: boolean;
937937
readonly sql_name?: string | null;
938938
});
939+
SqlDiagnostic: {
940+
message: string;
941+
span?: components["schemas"]["SqlSpan"] | null;
942+
};
943+
SqlLocation: {
944+
/** Format: int64 */
945+
column: number;
946+
/** Format: int64 */
947+
line: number;
948+
};
949+
SqlSpan: {
950+
end: components["schemas"]["SqlLocation"];
951+
start: components["schemas"]["SqlLocation"];
952+
};
939953
/** @enum {string} */
940954
StopType: "none" | "checkpoint" | "graceful" | "immediate" | "force";
941955
StructField: {

0 commit comments

Comments
 (0)