@@ -179,10 +179,15 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
179179 queryId,
180180 } ;
181181
182- if ( isClickhouseQuotaError ( clickhouseError ) ) {
183- this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
184- } else {
185- this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
182+ switch ( classifyClickhouseError ( clickhouseError , false ) ) {
183+ case "quota" :
184+ this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
185+ break ;
186+ case "invalid-sql" :
187+ this . logger . warn ( "ClickHouse rejected an invalid query" , errorLogFields ) ;
188+ break ;
189+ default :
190+ this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
186191 }
187192
188193 recordClickhouseError ( span , clickhouseError ) ;
@@ -271,6 +276,11 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
271276 * record what produced the SQL, e.g. the TSQL a caller actually wrote.
272277 */
273278 logFields ?: Record < string , unknown > ;
279+ /**
280+ * Set when the SQL originates from whoever made the request rather than
281+ * from us. Invalid-SQL rejections are then their mistake, not a bug.
282+ */
283+ userAuthoredQuery ?: boolean ;
274284 } ) : ClickhouseQueryWithStatsFunction < z . input < TIn > , z . output < TOut > > {
275285 return async ( params , options ) => {
276286 const queryId = randomUUID ( ) ;
@@ -340,10 +350,15 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
340350 queryId,
341351 } ;
342352
343- if ( isClickhouseQuotaError ( clickhouseError ) ) {
344- this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
345- } else {
346- this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
353+ switch ( classifyClickhouseError ( clickhouseError , req . userAuthoredQuery ) ) {
354+ case "quota" :
355+ this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
356+ break ;
357+ case "invalid-sql" :
358+ this . logger . warn ( "ClickHouse rejected an invalid query" , errorLogFields ) ;
359+ break ;
360+ default :
361+ this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
347362 }
348363
349364 recordClickhouseError ( span , clickhouseError ) ;
@@ -479,10 +494,15 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
479494 queryId,
480495 } ;
481496
482- if ( isClickhouseQuotaError ( clickhouseError ) ) {
483- this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
484- } else {
485- this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
497+ switch ( classifyClickhouseError ( clickhouseError , false ) ) {
498+ case "quota" :
499+ this . logger . warn ( "Query exceeded a ClickHouse limit" , errorLogFields ) ;
500+ break ;
501+ case "invalid-sql" :
502+ this . logger . warn ( "ClickHouse rejected an invalid query" , errorLogFields ) ;
503+ break ;
504+ default :
505+ this . logger . error ( "Error querying clickhouse" , errorLogFields ) ;
486506 }
487507
488508 recordClickhouseError ( span , clickhouseError ) ;
@@ -631,7 +651,7 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
631651 queryId,
632652 } ;
633653
634- if ( error instanceof Error && isClickhouseQuotaError ( error ) ) {
654+ if ( error instanceof Error && classifyClickhouseError ( error , false ) === "quota" ) {
635655 self . logger . warn ( "Streamed query exceeded a ClickHouse limit" , errorLogFields ) ;
636656 } else {
637657 self . logger . error ( "Error streaming clickhouse" , errorLogFields ) ;
@@ -1043,15 +1063,51 @@ const CLICKHOUSE_QUOTA_ERROR_TYPES = new Set([
10431063 "TOO_MANY_ROWS" ,
10441064 "TOO_MANY_BYTES" ,
10451065 "TOO_MANY_ROWS_OR_BYTES" ,
1046- "QUERY_WAS_CANCELLED" ,
10471066] ) ;
10481067
1049- function isClickhouseQuotaError ( error : Error ) : boolean {
1050- return (
1051- error instanceof ClickHouseError &&
1052- error . type !== undefined &&
1053- CLICKHOUSE_QUOTA_ERROR_TYPES . has ( error . type )
1054- ) ;
1068+ /**
1069+ * ClickHouse error types that mean the SQL itself is wrong. Only treated as the
1070+ * caller's fault when the query was written by the caller — the same error on a
1071+ * query we generated is our bug and has to keep alerting.
1072+ */
1073+ const CLICKHOUSE_INVALID_SQL_ERROR_TYPES = new Set ( [
1074+ "NOT_AN_AGGREGATE" ,
1075+ "ILLEGAL_AGGREGATION" ,
1076+ "UNKNOWN_IDENTIFIER" ,
1077+ "UNKNOWN_FUNCTION" ,
1078+ "UNKNOWN_TABLE" ,
1079+ "AMBIGUOUS_COLUMN_NAME" ,
1080+ "MULTIPLE_EXPRESSIONS_FOR_ALIAS" ,
1081+ "SYNTAX_ERROR" ,
1082+ "BAD_ARGUMENTS" ,
1083+ "TYPE_MISMATCH" ,
1084+ "NO_COMMON_TYPE" ,
1085+ "ILLEGAL_TYPE_OF_ARGUMENT" ,
1086+ "ILLEGAL_COLUMN" ,
1087+ "CANNOT_CONVERT_TYPE" ,
1088+ "CANNOT_PARSE_TEXT" ,
1089+ "CANNOT_PARSE_NUMBER" ,
1090+ "CANNOT_PARSE_DATE" ,
1091+ "CANNOT_PARSE_DATETIME" ,
1092+ "CANNOT_PARSE_INPUT_ASSERTION_FAILED" ,
1093+ ] ) ;
1094+
1095+ type ClickhouseErrorCategory = "quota" | "invalid-sql" | "fault" ;
1096+
1097+ function classifyClickhouseError (
1098+ error : Error ,
1099+ userAuthoredQuery : boolean | undefined
1100+ ) : ClickhouseErrorCategory {
1101+ if ( ! ( error instanceof ClickHouseError ) || error . type === undefined ) {
1102+ return "fault" ;
1103+ }
1104+ if ( CLICKHOUSE_QUOTA_ERROR_TYPES . has ( error . type ) ) {
1105+ return "quota" ;
1106+ }
1107+ if ( userAuthoredQuery && CLICKHOUSE_INVALID_SQL_ERROR_TYPES . has ( error . type ) ) {
1108+ return "invalid-sql" ;
1109+ }
1110+ return "fault" ;
10551111}
10561112
10571113function recordClickhouseError ( span : Span , error : Error ) : void {
0 commit comments