@@ -518,3 +518,334 @@ fn build_sql_highlight(sql: &str, span: &SqlErrorSpan) -> Option<String> {
518518
519519 Some ( out. trim_end ( ) . to_string ( ) )
520520}
521+
522+ // GRCOV_EXCL_START
523+ #[ cfg( test) ]
524+ mod tests {
525+ use super :: * ;
526+ use crate :: expression:: { BinaryOperator , UnaryOperator } ;
527+ use crate :: types:: value:: DataValue ;
528+ use std:: error:: Error ;
529+
530+ fn span ( line : usize , start : usize , end : usize ) -> SqlErrorSpan {
531+ SqlErrorSpan {
532+ line,
533+ start,
534+ end,
535+ highlight : None ,
536+ }
537+ }
538+
539+ #[ test]
540+ fn span_aware_errors_format_location_and_highlight ( ) {
541+ let err = DatabaseError :: column_not_found ( "missing" ) . with_span ( span ( 2 , 8 , 14 ) ) ;
542+ assert_eq ! (
543+ err. to_string( ) ,
544+ "column: `missing` not found at line 2, range 8..14"
545+ ) ;
546+ assert_eq ! ( err. sql_error_span( ) . unwrap( ) . line, 2 ) ;
547+
548+ let err = err. with_sql_context ( "select *\n from missing\n where id = 1" ) ;
549+ let highlight = err
550+ . sql_error_span ( )
551+ . and_then ( |span| span. highlight . as_ref ( ) )
552+ . expect ( "highlight is added from SQL context" ) ;
553+ assert ! ( highlight. contains( "--> line 2" ) ) ;
554+ assert ! ( highlight. contains( "2 | from missing" ) ) ;
555+ assert ! ( highlight. lines( ) . any( |line| line. contains( '^' ) ) ) ;
556+ assert ! ( err. to_string( ) . contains( "\n --> line 2" ) ) ;
557+ }
558+
559+ #[ test]
560+ fn sql_context_preserves_existing_highlight_and_ignores_invalid_spans ( ) {
561+ let mut existing = span ( 1 , 1 , 3 ) ;
562+ existing. highlight = Some ( "custom highlight" . to_string ( ) ) ;
563+ let err = DatabaseError :: not_null_column ( "id" )
564+ . with_span ( existing)
565+ . with_sql_context ( "select id" ) ;
566+ assert_eq ! (
567+ err. sql_error_span( ) . unwrap( ) . highlight. as_deref( ) ,
568+ Some ( "custom highlight" )
569+ ) ;
570+ assert_eq ! (
571+ err. to_string( ) ,
572+ "column: `id` cannot be null\n custom highlight"
573+ ) ;
574+
575+ let err = DatabaseError :: invalid_table ( "t" )
576+ . with_span ( span ( 0 , 1 , 2 ) )
577+ . with_sql_context ( "select * from t" ) ;
578+ assert ! ( err. sql_error_span( ) . unwrap( ) . highlight. is_none( ) ) ;
579+ assert_eq ! ( err. to_string( ) , "invalid table: `t` at line 0, range 1..2" ) ;
580+
581+ let err = DatabaseError :: invalid_table ( "missing" )
582+ . with_span ( span ( 3 , 1 , 4 ) )
583+ . with_sql_context ( "select 1" ) ;
584+ assert ! ( err. sql_error_span( ) . unwrap( ) . highlight. is_none( ) ) ;
585+ assert_eq ! (
586+ err. to_string( ) ,
587+ "invalid table: `missing` at line 3, range 1..4"
588+ ) ;
589+ }
590+
591+ #[ test]
592+ fn constructors_attach_expected_payloads ( ) {
593+ assert_eq ! (
594+ DatabaseError :: invalid_column( "c" ) . to_string( ) ,
595+ "invalid column: `c`"
596+ ) ;
597+ assert_eq ! (
598+ DatabaseError :: function_not_found( "lower" ) . to_string( ) ,
599+ "function: `lower` not found"
600+ ) ;
601+ assert_eq ! (
602+ DatabaseError :: parameter_not_found( "p" ) . to_string( ) ,
603+ "parameter: `p` not found"
604+ ) ;
605+ assert_eq ! ( DatabaseError :: not_null( ) . to_string( ) , "cannot be null" ) ;
606+ assert_eq ! (
607+ DatabaseError :: not_null_column( "name" ) . to_string( ) ,
608+ "column: `name` cannot be null"
609+ ) ;
610+
611+ assert ! ( DatabaseError :: TableNotFound
612+ . with_span( span( 1 , 1 , 1 ) )
613+ . sql_error_span( )
614+ . is_none( ) ) ;
615+ }
616+
617+ #[ test]
618+ fn span_helpers_attach_context_to_remaining_span_aware_variants ( ) {
619+ let cast = DatabaseError :: CastFail {
620+ from : LogicalType :: Boolean ,
621+ to : LogicalType :: Integer ,
622+ span : None ,
623+ }
624+ . with_span ( span ( 1 , 8 , 11 ) ) ;
625+ assert_eq ! (
626+ cast. to_string( ) ,
627+ "cast fail: Boolean -> Integer at line 1, range 8..11"
628+ ) ;
629+ assert_eq ! ( cast. sql_error_span( ) . unwrap( ) . start, 8 ) ;
630+
631+ let cast = cast. with_sql_context ( "select true" ) ;
632+ assert ! ( cast. to_string( ) . contains( "\n --> line 1" ) ) ;
633+
634+ let parameter = DatabaseError :: parameter_not_found ( "tenant_id" )
635+ . with_span ( span ( 1 , 15 , 24 ) )
636+ . with_sql_context ( "select :tenant_id" ) ;
637+ assert_eq ! ( parameter. sql_error_span( ) . unwrap( ) . line, 1 ) ;
638+ assert ! ( parameter
639+ . to_string( )
640+ . contains( "parameter: `tenant_id` not found\n --> line 1" ) ) ;
641+ }
642+
643+ #[ test]
644+ fn display_formats_common_error_variants ( ) {
645+ let cases = [
646+ ( DatabaseError :: AggMiss ( "sum" . into ( ) ) , "agg miss: sum" ) ,
647+ ( DatabaseError :: CacheSizeOverFlow , "cache size overflow" ) ,
648+ (
649+ DatabaseError :: CastFail {
650+ from : LogicalType :: Integer ,
651+ to : LogicalType :: Varchar ( None , crate :: types:: CharLengthUnits :: Characters ) ,
652+ span : None ,
653+ } ,
654+ "cast fail: Integer -> Varchar(None, CHARACTERS)" ,
655+ ) ,
656+ (
657+ DatabaseError :: ColumnIdNotFound ( "7" . into ( ) ) ,
658+ "column id: `7` not found" ,
659+ ) ,
660+ (
661+ DatabaseError :: DuplicateColumn ( "id" . into ( ) ) ,
662+ "column: `id` already exists" ,
663+ ) ,
664+ (
665+ DatabaseError :: DuplicateSourceHash ( "v" . into ( ) ) ,
666+ "table or view: `v` hash already exists" ,
667+ ) ,
668+ (
669+ DatabaseError :: DuplicateIndex ( "idx" . into ( ) ) ,
670+ "index: `idx` already exists" ,
671+ ) ,
672+ (
673+ DatabaseError :: Incomparable ( LogicalType :: Integer , LogicalType :: Boolean ) ,
674+ "can not compare two types: Integer and Boolean" ,
675+ ) ,
676+ (
677+ DatabaseError :: InvalidValue ( "NaN" . into ( ) ) ,
678+ "invalid value: NaN" ,
679+ ) ,
680+ (
681+ DatabaseError :: MisMatch ( "left" , "right" ) ,
682+ "left and right do not match" ,
683+ ) ,
684+ (
685+ DatabaseError :: TupleIdNotFound ( DataValue :: Int32 ( 3 ) ) ,
686+ "tuple id: 3 not found" ,
687+ ) ,
688+ (
689+ DatabaseError :: TooManyBuckets ( 8 , 3 ) ,
690+ "there are more buckets: 8 than elements: 3" ,
691+ ) ,
692+ (
693+ DatabaseError :: UnsupportedUnaryOperator ( LogicalType :: Integer , UnaryOperator :: Not ) ,
694+ "unsupported unary operator: Integer cannot support ! for calculations" ,
695+ ) ,
696+ (
697+ DatabaseError :: UnsupportedBinaryOperator (
698+ LogicalType :: Boolean ,
699+ BinaryOperator :: Plus ,
700+ ) ,
701+ "unsupported binary operator: Boolean cannot support + for calculations" ,
702+ ) ,
703+ (
704+ DatabaseError :: ValuesLenMismatch ( 2 , 3 ) ,
705+ "values length not match, expect 2, got 3" ,
706+ ) ,
707+ ] ;
708+
709+ for ( err, expected) in cases {
710+ assert_eq ! ( err. to_string( ) , expected) ;
711+ }
712+ }
713+
714+ #[ test]
715+ fn display_formats_remaining_simple_error_variants ( ) {
716+ let cases = [
717+ ( DatabaseError :: ChannelClose , "channel close" ) ,
718+ ( DatabaseError :: ColumnsEmpty , "columns empty" ) ,
719+ (
720+ DatabaseError :: DefaultNotColumnRef ,
721+ "default cannot be a column related to the table" ,
722+ ) ,
723+ ( DatabaseError :: DefaultNotExist , "default does not exist" ) ,
724+ ( DatabaseError :: DuplicatePrimaryKey , "duplicate primary key" ) ,
725+ ( DatabaseError :: EmptyPlan , "empty plan" ) ,
726+ ( DatabaseError :: EmptyStatement , "sql statement is empty" ) ,
727+ ( DatabaseError :: EvaluatorNotFound , "evaluator not found" ) ,
728+ ( DatabaseError :: InvalidIndex , "invalid index" ) ,
729+ ( DatabaseError :: InvalidType , "invalid type" ) ,
730+ (
731+ DatabaseError :: NeedNullAbleOrDefault ,
732+ "add column must be nullable or specify a default value" ,
733+ ) ,
734+ ( DatabaseError :: NoTransactionBegin , "no transaction begin" ) ,
735+ ( DatabaseError :: OverFlow , "over flow" ) ,
736+ (
737+ DatabaseError :: PrimaryKeyNotFound ,
738+ "must contain primary key!" ,
739+ ) ,
740+ (
741+ DatabaseError :: PrimaryKeyTooManyLayers ,
742+ "primaryKey only allows single or multiple values" ,
743+ ) ,
744+ (
745+ DatabaseError :: SharedNotAlign ,
746+ "the number of caches cannot be divisible by the number of shards" ,
747+ ) ,
748+ ( DatabaseError :: SourceNotFound , "the table or view not found" ) ,
749+ ( DatabaseError :: TableExists , "the table already exists" ) ,
750+ ( DatabaseError :: TableNotFound , "the table not found" ) ,
751+ (
752+ DatabaseError :: TransactionAlreadyExists ,
753+ "transaction already exists" ,
754+ ) ,
755+ ( DatabaseError :: TooLong , "too long" ) ,
756+ (
757+ DatabaseError :: UnsupportedStmt ( "merge" . into ( ) ) ,
758+ "unsupported statement: merge" ,
759+ ) ,
760+ ( DatabaseError :: ViewExists , "the view already exists" ) ,
761+ ( DatabaseError :: ViewNotFound , "the view not found" ) ,
762+ ] ;
763+
764+ for ( err, expected) in cases {
765+ assert_eq ! ( err. to_string( ) , expected) ;
766+ }
767+ }
768+
769+ #[ test]
770+ fn source_returns_inner_errors_for_conversion_variants ( ) {
771+ let parse_int: DatabaseError = "not-int" . parse :: < i32 > ( ) . unwrap_err ( ) . into ( ) ;
772+ let parse_float: DatabaseError = "not-float" . parse :: < f64 > ( ) . unwrap_err ( ) . into ( ) ;
773+ let parse_bool: DatabaseError = "not-bool" . parse :: < bool > ( ) . unwrap_err ( ) . into ( ) ;
774+ let bytes = vec ! [ 0xff ] ;
775+ let utf8: DatabaseError = std:: str:: from_utf8 ( & bytes) . unwrap_err ( ) . into ( ) ;
776+ let from_utf8: DatabaseError = String :: from_utf8 ( vec ! [ 0xff ] ) . unwrap_err ( ) . into ( ) ;
777+ let io: DatabaseError = std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , "disk" ) . into ( ) ;
778+ let try_from_int: DatabaseError = u8:: try_from ( 300_u16 ) . unwrap_err ( ) . into ( ) ;
779+
780+ for err in [
781+ parse_int,
782+ parse_float,
783+ parse_bool,
784+ utf8,
785+ from_utf8,
786+ io,
787+ try_from_int,
788+ ] {
789+ assert ! ( Error :: source( & err) . is_some( ) , "{err}" ) ;
790+ }
791+ assert ! ( Error :: source( & DatabaseError :: TableNotFound ) . is_none( ) ) ;
792+ }
793+
794+ #[ test]
795+ fn display_formats_conversion_error_variants ( ) {
796+ let parse_int: DatabaseError = "not-int" . parse :: < i32 > ( ) . unwrap_err ( ) . into ( ) ;
797+ let parse_float: DatabaseError = "not-float" . parse :: < f64 > ( ) . unwrap_err ( ) . into ( ) ;
798+ let parse_bool: DatabaseError = "not-bool" . parse :: < bool > ( ) . unwrap_err ( ) . into ( ) ;
799+ let bytes = vec ! [ 0xff ] ;
800+ let utf8: DatabaseError = std:: str:: from_utf8 ( & bytes) . unwrap_err ( ) . into ( ) ;
801+ let from_utf8: DatabaseError = String :: from_utf8 ( vec ! [ 0xff ] ) . unwrap_err ( ) . into ( ) ;
802+ let io: DatabaseError = std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , "disk" ) . into ( ) ;
803+ let try_from_int: DatabaseError = u8:: try_from ( 300_u16 ) . unwrap_err ( ) . into ( ) ;
804+
805+ assert ! ( parse_int. to_string( ) . starts_with( "parser int:" ) ) ;
806+ assert ! ( parse_float. to_string( ) . starts_with( "parser float:" ) ) ;
807+ assert ! ( parse_bool. to_string( ) . starts_with( "parser bool:" ) ) ;
808+ assert ! ( utf8. to_string( ) . starts_with( "utf8:" ) ) ;
809+ assert ! ( from_utf8. to_string( ) . starts_with( "from utf8:" ) ) ;
810+ assert_eq ! ( io. to_string( ) , "io: disk" ) ;
811+ assert ! ( try_from_int. to_string( ) . starts_with( "try from int:" ) ) ;
812+
813+ #[ cfg( feature = "copy" ) ]
814+ {
815+ let mut reader = csv:: Reader :: from_reader ( "a\n 1,2\n " . as_bytes ( ) ) ;
816+ let csv_err = reader. records ( ) . next ( ) . unwrap ( ) . unwrap_err ( ) ;
817+ let err = DatabaseError :: from ( csv_err) ;
818+ assert ! ( err. to_string( ) . starts_with( "csv error:" ) ) ;
819+ assert ! ( Error :: source( & err) . is_some( ) ) ;
820+ }
821+
822+ #[ cfg( feature = "time" ) ]
823+ {
824+ let err: DatabaseError = chrono:: NaiveDate :: parse_from_str ( "not-a-date" , "%Y-%m-%d" )
825+ . unwrap_err ( )
826+ . into ( ) ;
827+ assert ! ( err. to_string( ) . starts_with( "parser date:" ) ) ;
828+ assert ! ( Error :: source( & err) . is_some( ) ) ;
829+ }
830+
831+ #[ cfg( feature = "parser" ) ]
832+ {
833+ let dialect = sqlparser:: dialect:: GenericDialect { } ;
834+ let err: DatabaseError = sqlparser:: parser:: Parser :: parse_sql ( & dialect, "select" )
835+ . unwrap_err ( )
836+ . into ( ) ;
837+ assert ! ( err. to_string( ) . starts_with( "parser sql:" ) ) ;
838+ assert ! ( Error :: source( & err) . is_some( ) ) ;
839+ }
840+
841+ #[ cfg( feature = "decimal" ) ]
842+ {
843+ let err: DatabaseError = rust_decimal:: Decimal :: from_str_exact ( "not-a-decimal" )
844+ . unwrap_err ( )
845+ . into ( ) ;
846+ assert ! ( err. to_string( ) . starts_with( "try from decimal:" ) ) ;
847+ assert ! ( Error :: source( & err) . is_some( ) ) ;
848+ }
849+ }
850+ }
851+ // GRCOV_EXCL_STOP
0 commit comments