Skip to content

Commit 60c5d5a

Browse files
authored
test: improve planner orm and error coverage (#370)
* test: improve planner orm and error coverage * ci: set codecov patch target
1 parent 7cbf9fe commit 60c5d5a

7 files changed

Lines changed: 1933 additions & 2 deletions

File tree

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CODECOV_OUTPUT ?= lcov.info
1414
COVERAGE_PROFILE_DIR ?= target/grcov/profraw
1515
COVERAGE_HTML_DIR ?= target/grcov/html
1616
COVERAGE_RUSTFLAGS ?= -Cinstrument-coverage
17+
COVERAGE_TEST_FEATURES ?= decimal,orm
1718
COVERAGE_REPORT_ARGS ?= --llvm --ignore-not-existing --keep-only 'src/**' --ignore 'src/**/tests/**' --ignore 'tests/**' --ignore 'tpcc/**' --excl-start 'GRCOV_EXCL_START' --excl-stop 'GRCOV_EXCL_STOP'
1819

1920
.PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-sqlite-practical tpcc-sqlite-balanced tpcc-dual cargo-check build wasm-examples native-examples fmt clippy
@@ -55,7 +56,7 @@ codecov:
5556
command -v $(GRCOV) >/dev/null || { echo 'grcov is not installed; run: cargo install grcov'; exit 1; }; \
5657
rm -rf '$(COVERAGE_PROFILE_DIR)' '$(CODECOV_OUTPUT)'; \
5758
mkdir -p '$(COVERAGE_PROFILE_DIR)'; \
58-
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) test --all --features decimal; \
59+
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) test --all --features '$(COVERAGE_TEST_FEATURES)'; \
5960
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) run -p sqllogictest-test -- --path '$(SQLLOGIC_PATH)'; \
6061
$(GRCOV) . --binary-path \"$${CARGO_TARGET_DIR:-target}/debug\" -s . -t lcov $(COVERAGE_REPORT_ARGS) -o '$(CODECOV_OUTPUT)'"
6162

@@ -65,7 +66,7 @@ codecov-html:
6566
command -v $(GRCOV) >/dev/null || { echo 'grcov is not installed; run: cargo install grcov'; exit 1; }; \
6667
rm -rf '$(COVERAGE_PROFILE_DIR)' '$(COVERAGE_HTML_DIR)'; \
6768
mkdir -p '$(COVERAGE_PROFILE_DIR)' '$(COVERAGE_HTML_DIR)'; \
68-
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) test --all --features decimal; \
69+
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) test --all --features '$(COVERAGE_TEST_FEATURES)'; \
6970
CARGO_INCREMENTAL=0 RUSTFLAGS=\"$${RUSTFLAGS:-} $(COVERAGE_RUSTFLAGS)\" LLVM_PROFILE_FILE='$(COVERAGE_PROFILE_DIR)/kitesql-%p-%m.profraw' $(CARGO) run -p sqllogictest-test -- --path '$(SQLLOGIC_PATH)'; \
7071
$(GRCOV) . --binary-path \"$${CARGO_TARGET_DIR:-target}/debug\" -s . -t html $(COVERAGE_REPORT_ARGS) -o '$(COVERAGE_HTML_DIR)'"
7172
@echo "Coverage report: $(COVERAGE_HTML_DIR)/index.html"

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ coverage:
55
informational: true
66
patch:
77
default:
8+
target: 90%
89
informational: true
910

1011
comment:

src/errors.rs

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 *\nfrom missing\nwhere 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\ncustom 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\n1,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

Comments
 (0)