mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-16 04:50:38 +00:00
@@ -13,6 +13,8 @@ This compatibility test is inspired by [Databend](https://github.com/datafuselab
|
||||
|
||||
## Usage
|
||||
|
||||
### Legacy script flow
|
||||
|
||||
```shell
|
||||
tests/compat/test-compat.sh <old_ver>
|
||||
```
|
||||
@@ -20,6 +22,45 @@ tests/compat/test-compat.sh <old_ver>
|
||||
E.g. `tests/compat/test-compat.sh 0.6.0` tests if the data written by GreptimeDB **v0.6.0** can be read by **current**
|
||||
version of GreptimeDB, and vice-versa. By "current", it's meant the fresh binary built by current codes.
|
||||
|
||||
### Sqlness compat command (recommended)
|
||||
|
||||
```shell
|
||||
cargo sqlness compat --from=1.0.0-beta.1 --to=current --preserve-state
|
||||
```
|
||||
|
||||
The compat command runs cases in three phases:
|
||||
|
||||
1. `1.feature` on `--from`
|
||||
2. `2.verify` on `--to`
|
||||
3. `3.cleanup` on `--to`
|
||||
|
||||
## Case markers
|
||||
|
||||
Compatibility cases can use sqlness markers:
|
||||
|
||||
- `-- SQLNESS SINCE <version>`
|
||||
- `-- SQLNESS TILL <version>`
|
||||
|
||||
For `since`/`till` skips, runner rewrites the statement before execution to avoid running skipped SQL.
|
||||
|
||||
## Filter behavior
|
||||
|
||||
`--test-filter` still accepts sqlness regex. Compat runner also recognizes optional leading path prefixes:
|
||||
|
||||
- phase prefix: `1.feature/`, `2.verify/`, `3.cleanup/`
|
||||
- mode prefix: `standalone/`, `distributed/`
|
||||
- group prefix: `common/`, `only/`
|
||||
|
||||
Examples:
|
||||
|
||||
```shell
|
||||
# run one standalone case path
|
||||
cargo sqlness compat --from=1.0.0-beta.1 --to=current --test-filter="standalone/common/show-create-table.sql"
|
||||
|
||||
# same target with explicit phase prefix
|
||||
cargo sqlness compat --from=1.0.0-beta.1 --to=current --test-filter="2.verify/distributed/common/show-create-table.sql"
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Current version of GreptimeDB's binaries must reside in `./bins`:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- SQLNESS ARG since=0.15.0
|
||||
-- SQLNESS SINCE 0.15.0
|
||||
CREATE TABLE granularity_and_false_positive_rate (
|
||||
ts timestamp time index,
|
||||
val double
|
||||
@@ -9,3 +9,23 @@ CREATE TABLE granularity_and_false_positive_rate (
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
-- SQLNESS SINCE 99.0.0
|
||||
SELECT * FROM __sqlness_since_till_should_not_exist__;
|
||||
|
||||
-- SQLNESS_SKIP: target version 1.0.0-beta.1 < 99.0.0
|
||||
|
||||
-- SQLNESS TILL 0.1.0
|
||||
SELECT * FROM __sqlness_since_till_should_not_exist__;
|
||||
|
||||
-- SQLNESS_SKIP: target version 1.0.0-beta.1 > 0.1.0
|
||||
|
||||
-- SQLNESS SINCE 0.1.0
|
||||
-- SQLNESS TILL 99.0.0
|
||||
SELECT 1;
|
||||
|
||||
+----------+
|
||||
| Int64(1) |
|
||||
+----------+
|
||||
| 1 |
|
||||
+----------+
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- SQLNESS ARG since=0.15.0
|
||||
-- SQLNESS SINCE 0.15.0
|
||||
CREATE TABLE granularity_and_false_positive_rate (
|
||||
ts timestamp time index,
|
||||
val double
|
||||
@@ -6,3 +6,13 @@ CREATE TABLE granularity_and_false_positive_rate (
|
||||
"index.granularity" = "8192",
|
||||
"index.false_positive_rate" = "0.01"
|
||||
);
|
||||
|
||||
-- SQLNESS SINCE 99.0.0
|
||||
SELECT * FROM __sqlness_since_till_should_not_exist__;
|
||||
|
||||
-- SQLNESS TILL 0.1.0
|
||||
SELECT * FROM __sqlness_since_till_should_not_exist__;
|
||||
|
||||
-- SQLNESS SINCE 0.1.0
|
||||
-- SQLNESS TILL 99.0.0
|
||||
SELECT 1;
|
||||
|
||||
@@ -22,7 +22,6 @@ use sqlness::{ConfigBuilder, Runner};
|
||||
|
||||
use crate::cmd::SqlnessConfig;
|
||||
use crate::env::bare::{Env, ServiceProvider, StoreConfig, WalConfig};
|
||||
use crate::interceptors::ignore_result;
|
||||
use crate::{protocol_interceptor, util};
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
@@ -123,10 +122,6 @@ impl BareCommand {
|
||||
protocol_interceptor::PREFIX,
|
||||
Arc::new(protocol_interceptor::ProtocolInterceptorFactory),
|
||||
);
|
||||
interceptor_registry.register(
|
||||
ignore_result::PREFIX,
|
||||
Arc::new(ignore_result::IgnoreResultInterceptorFactory),
|
||||
);
|
||||
|
||||
if let Some(d) = &self.config.case_dir {
|
||||
ensure!(d.is_dir(), "{} is not a directory", d.display());
|
||||
|
||||
@@ -21,7 +21,7 @@ use sqlness::{ConfigBuilder, Runner};
|
||||
use crate::cmd::bare::ServerAddr;
|
||||
use crate::env::bare::{StoreConfig, WalConfig};
|
||||
use crate::env::compat::Env;
|
||||
use crate::interceptors::{ignore_result, since, till};
|
||||
use crate::interceptors::{since, till};
|
||||
use crate::version::Version;
|
||||
use crate::{protocol_interceptor, util};
|
||||
|
||||
@@ -81,27 +81,27 @@ impl CompatibilityRunner {
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> anyhow::Result<()> {
|
||||
self.run_phase(&self.from_version, "1.feature").await?;
|
||||
self.run_phase(&self.to_version, "2.verify").await?;
|
||||
self.run_phase(&self.to_version, "3.cleanup").await?;
|
||||
self.run_phase(&self.from_version, Phase::Feature).await?;
|
||||
self.run_phase(&self.to_version, Phase::Verify).await?;
|
||||
self.run_phase(&self.to_version, Phase::Cleanup).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_phase(&self, version: &Version, phase: &str) -> anyhow::Result<()> {
|
||||
async fn run_phase(&self, version: &Version, phase: Phase) -> anyhow::Result<()> {
|
||||
if !self.case_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mode_filter, remainder) = Self::split_filter(self.test_filter.as_str());
|
||||
let case_root = self.case_dir.join(phase);
|
||||
let case_root = self.case_dir.join(phase.dir_name());
|
||||
if !case_root.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bins_dir = Self::resolve_bins_dir(version).await?;
|
||||
let mut store_config = self.store_config.clone();
|
||||
store_config.setup_etcd = phase == "1.feature";
|
||||
store_config.setup_etcd = phase.should_setup_etcd();
|
||||
let env = Env::new_bare(
|
||||
self.data_dir.clone(),
|
||||
self.server_addr.clone(),
|
||||
@@ -117,10 +117,6 @@ impl CompatibilityRunner {
|
||||
protocol_interceptor::PREFIX,
|
||||
Arc::new(protocol_interceptor::ProtocolInterceptorFactory),
|
||||
);
|
||||
interceptor_registry.register(
|
||||
ignore_result::PREFIX,
|
||||
Arc::new(ignore_result::IgnoreResultInterceptorFactory),
|
||||
);
|
||||
interceptor_registry.register(
|
||||
since::PREFIX,
|
||||
Arc::new(since::SinceInterceptorFactory::new(version.clone())),
|
||||
@@ -138,7 +134,7 @@ impl CompatibilityRunner {
|
||||
.unwrap_or_else(|| "<build>".to_string());
|
||||
println!(
|
||||
"compat phase={} version={} case_dir={} test_filter={} env_filter={} phase_root={} bins_dir={}",
|
||||
phase,
|
||||
phase.dir_name(),
|
||||
version,
|
||||
self.case_dir.display(),
|
||||
self.test_filter,
|
||||
@@ -169,46 +165,76 @@ impl CompatibilityRunner {
|
||||
return (ModeFilter::Any, String::new());
|
||||
}
|
||||
|
||||
let mut normalized = trimmed;
|
||||
let normalized = Self::trim_anchors(trimmed);
|
||||
let (mode, mut rest) = Self::extract_mode_and_path(normalized);
|
||||
rest = Self::trim_case_group_prefixes(rest);
|
||||
|
||||
(mode, rest.to_string())
|
||||
}
|
||||
|
||||
fn trim_anchors(filter: &str) -> &str {
|
||||
let mut normalized = filter;
|
||||
if let Some(rest) = normalized.strip_prefix('^') {
|
||||
normalized = rest;
|
||||
}
|
||||
if let Some(rest) = normalized.strip_suffix('$') {
|
||||
normalized = rest;
|
||||
}
|
||||
|
||||
if let Some(rest) = normalized.strip_prefix("standalone/") {
|
||||
return (ModeFilter::Standalone, Self::strip_path_prefixes(rest));
|
||||
}
|
||||
if let Some(rest) = normalized.strip_prefix("distributed/") {
|
||||
return (ModeFilter::Distributed, Self::strip_path_prefixes(rest));
|
||||
}
|
||||
if normalized == "standalone" {
|
||||
return (ModeFilter::Standalone, String::new());
|
||||
}
|
||||
if normalized == "distributed" {
|
||||
return (ModeFilter::Distributed, String::new());
|
||||
}
|
||||
|
||||
(ModeFilter::Any, Self::strip_path_prefixes(normalized))
|
||||
normalized
|
||||
}
|
||||
|
||||
fn strip_path_prefixes(input: &str) -> String {
|
||||
let mut rest = input;
|
||||
for prefix in [
|
||||
"1.feature/",
|
||||
"2.verify/",
|
||||
"3.cleanup/",
|
||||
"standalone/",
|
||||
"distributed/",
|
||||
"common/",
|
||||
"only/",
|
||||
] {
|
||||
if let Some(stripped) = rest.strip_prefix(prefix) {
|
||||
rest = stripped;
|
||||
fn extract_mode_and_path(filter: &str) -> (ModeFilter, &str) {
|
||||
if let Some((mode, rest)) = Self::extract_mode_prefix(filter) {
|
||||
return (mode, rest);
|
||||
}
|
||||
|
||||
if let Some(after_phase) = Self::strip_phase_prefix(filter)
|
||||
&& let Some((mode, rest)) = Self::extract_mode_prefix(after_phase)
|
||||
{
|
||||
return (mode, rest);
|
||||
}
|
||||
|
||||
if let Some(after_phase) = Self::strip_phase_prefix(filter) {
|
||||
return (ModeFilter::Any, after_phase);
|
||||
}
|
||||
|
||||
(ModeFilter::Any, filter)
|
||||
}
|
||||
|
||||
fn extract_mode_prefix(filter: &str) -> Option<(ModeFilter, &str)> {
|
||||
if let Some(rest) = filter.strip_prefix("standalone/") {
|
||||
return Some((ModeFilter::Standalone, rest));
|
||||
}
|
||||
if let Some(rest) = filter.strip_prefix("distributed/") {
|
||||
return Some((ModeFilter::Distributed, rest));
|
||||
}
|
||||
if filter == "standalone" {
|
||||
return Some((ModeFilter::Standalone, ""));
|
||||
}
|
||||
if filter == "distributed" {
|
||||
return Some((ModeFilter::Distributed, ""));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn strip_phase_prefix(filter: &str) -> Option<&str> {
|
||||
for phase in ["1.feature/", "2.verify/", "3.cleanup/"] {
|
||||
if let Some(rest) = filter.strip_prefix(phase) {
|
||||
return Some(rest);
|
||||
}
|
||||
}
|
||||
rest.to_string()
|
||||
None
|
||||
}
|
||||
|
||||
fn trim_case_group_prefixes(mut path: &str) -> &str {
|
||||
for prefix in ["common/", "only/"] {
|
||||
if let Some(rest) = path.strip_prefix(prefix) {
|
||||
path = rest;
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
fn env_filter(mode_filter: ModeFilter) -> String {
|
||||
@@ -251,3 +277,69 @@ enum ModeFilter {
|
||||
Standalone,
|
||||
Distributed,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Phase {
|
||||
Feature,
|
||||
Verify,
|
||||
Cleanup,
|
||||
}
|
||||
|
||||
impl Phase {
|
||||
fn dir_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Feature => "1.feature",
|
||||
Self::Verify => "2.verify",
|
||||
Self::Cleanup => "3.cleanup",
|
||||
}
|
||||
}
|
||||
|
||||
fn should_setup_etcd(self) -> bool {
|
||||
matches!(self, Self::Feature)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_filter_mode_only() {
|
||||
let (mode, remainder) = CompatibilityRunner::split_filter("standalone");
|
||||
assert!(matches!(mode, ModeFilter::Standalone));
|
||||
assert!(remainder.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_filter_mode_and_case_path() {
|
||||
let (mode, remainder) = CompatibilityRunner::split_filter("distributed/common/show.sql");
|
||||
assert!(matches!(mode, ModeFilter::Distributed));
|
||||
assert_eq!(remainder, "show.sql");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_filter_phase_then_mode() {
|
||||
let (mode, remainder) =
|
||||
CompatibilityRunner::split_filter("1.feature/standalone/only/a.sql");
|
||||
assert!(matches!(mode, ModeFilter::Standalone));
|
||||
assert_eq!(remainder, "a.sql");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_filter_regex_anchors() {
|
||||
let (mode, remainder) =
|
||||
CompatibilityRunner::split_filter("^2.verify/distributed/common/t.sql$");
|
||||
assert!(matches!(mode, ModeFilter::Distributed));
|
||||
assert_eq!(remainder, "t.sql");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_filter_passthrough_regex_with_colon() {
|
||||
let (mode, remainder) = CompatibilityRunner::split_filter(".*:foo/bar");
|
||||
assert!(matches!(mode, ModeFilter::Any));
|
||||
assert_eq!(
|
||||
CompatibilityRunner::build_test_filter(&remainder),
|
||||
".*:foo/bar"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use sqlness::SqlnessError;
|
||||
use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef};
|
||||
|
||||
pub const PREFIX: &str = "IGNORE_RESULT";
|
||||
|
||||
/// Interceptor that ignores result matching for a query.
|
||||
///
|
||||
/// Usage: `-- SQLNESS IGNORE_RESULT`
|
||||
///
|
||||
/// When this interceptor is used, the test runner will only check that the query
|
||||
/// executes successfully, without comparing the actual output to the expected result.
|
||||
/// This is useful for operations where the exact output may vary (e.g., timestamps,
|
||||
/// auto-generated IDs) or when testing that a feature doesn't crash.
|
||||
pub struct IgnoreResultInterceptor;
|
||||
|
||||
impl Interceptor for IgnoreResultInterceptor {
|
||||
fn after_execute(&self, result: &mut String) {
|
||||
*result = "-- IGNORE_RESULT: Query executed successfully".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IgnoreResultInterceptorFactory;
|
||||
|
||||
impl InterceptorFactory for IgnoreResultInterceptorFactory {
|
||||
fn try_new(&self, _ctx: &str) -> Result<InterceptorRef, SqlnessError> {
|
||||
Ok(Box::new(IgnoreResultInterceptor))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,5 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod ignore_result;
|
||||
pub mod since;
|
||||
pub mod till;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use sqlness::SqlnessError;
|
||||
use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef};
|
||||
use sqlness::{SKIP_MARKER_PREFIX, SqlnessError};
|
||||
|
||||
use crate::version::Version;
|
||||
|
||||
@@ -36,27 +36,38 @@ impl SinceInterceptor {
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_rewrite_to_skip_sql(&self, sql: &mut Vec<String>) {
|
||||
if self.target_version < self.since_version {
|
||||
let skip_marker = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason());
|
||||
sql.clear();
|
||||
sql.push(format!("SELECT '{}';", skip_marker));
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_reason(&self) -> String {
|
||||
format!(
|
||||
"target version {} < {}",
|
||||
self.target_version, self.since_version
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_skip_result(&self, result: &mut String) {
|
||||
if result.contains(SKIP_MARKER_PREFIX) {
|
||||
*result = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for SinceInterceptor {
|
||||
fn before_execute(&self, sql: &mut Vec<String>, _ctx: &mut sqlness::QueryContext) {
|
||||
// Skip execution if target version is less than since version
|
||||
if self.target_version < self.since_version {
|
||||
sql.clear();
|
||||
}
|
||||
self.maybe_rewrite_to_skip_sql(sql);
|
||||
}
|
||||
|
||||
fn after_execute(&self, result: &mut String) {
|
||||
result.clear();
|
||||
*result = format!(
|
||||
"{} target version {} < {}",
|
||||
sqlness::SKIP_MARKER_PREFIX,
|
||||
self.target_version,
|
||||
self.since_version
|
||||
);
|
||||
self.normalize_skip_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SinceInterceptorFactory {
|
||||
target_version: Version,
|
||||
}
|
||||
@@ -80,3 +91,68 @@ impl InterceptorFactory for SinceInterceptorFactory {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_before_execute_keeps_sql_when_not_skipped() {
|
||||
let interceptor = SinceInterceptor::new(
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut sql = vec!["SELECT 1;".to_string()];
|
||||
|
||||
interceptor.maybe_rewrite_to_skip_sql(&mut sql);
|
||||
|
||||
assert_eq!(sql, vec!["SELECT 1;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_execute_rewrites_sql_when_skipped() {
|
||||
let interceptor = SinceInterceptor::new(
|
||||
Version::parse("0.16.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut sql = vec!["SELECT 1;".to_string()];
|
||||
|
||||
interceptor.maybe_rewrite_to_skip_sql(&mut sql);
|
||||
|
||||
assert_eq!(sql.len(), 1);
|
||||
assert!(sql[0].contains(SKIP_MARKER_PREFIX));
|
||||
assert!(sql[0].contains("target version 0.15.0 < 0.16.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_execute_normalizes_skip_result() {
|
||||
let interceptor = SinceInterceptor::new(
|
||||
Version::parse("0.16.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut result = format!(
|
||||
"+----------------+\n| {} target version 0.15.0 < 0.16.0 |\n+----------------+",
|
||||
SKIP_MARKER_PREFIX
|
||||
);
|
||||
|
||||
interceptor.normalize_skip_result(&mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
format!("{} target version 0.15.0 < 0.16.0", SKIP_MARKER_PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_execute_keeps_non_skip_result() {
|
||||
let interceptor = SinceInterceptor::new(
|
||||
Version::parse("0.16.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut result = "Affected Rows: 1".to_string();
|
||||
|
||||
interceptor.normalize_skip_result(&mut result);
|
||||
|
||||
assert_eq!(result, "Affected Rows: 1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use sqlness::SqlnessError;
|
||||
use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef};
|
||||
use sqlness::{SKIP_MARKER_PREFIX, SqlnessError};
|
||||
|
||||
use crate::version::Version;
|
||||
|
||||
@@ -36,26 +36,38 @@ impl TillInterceptor {
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_rewrite_to_skip_sql(&self, sql: &mut Vec<String>) {
|
||||
if self.target_version > self.till_version {
|
||||
let skip_marker = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason());
|
||||
sql.clear();
|
||||
sql.push(format!("SELECT '{}';", skip_marker));
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_reason(&self) -> String {
|
||||
format!(
|
||||
"target version {} > {}",
|
||||
self.target_version, self.till_version
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_skip_result(&self, result: &mut String) {
|
||||
if result.contains(SKIP_MARKER_PREFIX) {
|
||||
*result = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for TillInterceptor {
|
||||
fn before_execute(&self, sql: &mut Vec<String>, _ctx: &mut sqlness::QueryContext) {
|
||||
if self.target_version > self.till_version {
|
||||
sql.clear();
|
||||
}
|
||||
self.maybe_rewrite_to_skip_sql(sql);
|
||||
}
|
||||
|
||||
fn after_execute(&self, result: &mut String) {
|
||||
result.clear();
|
||||
*result = format!(
|
||||
"{} target version {} > {}",
|
||||
sqlness::SKIP_MARKER_PREFIX,
|
||||
self.target_version,
|
||||
self.till_version
|
||||
);
|
||||
self.normalize_skip_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TillInterceptorFactory {
|
||||
target_version: Version,
|
||||
}
|
||||
@@ -79,3 +91,68 @@ impl InterceptorFactory for TillInterceptorFactory {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_before_execute_keeps_sql_when_not_skipped() {
|
||||
let interceptor = TillInterceptor::new(
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut sql = vec!["SELECT 1;".to_string()];
|
||||
|
||||
interceptor.maybe_rewrite_to_skip_sql(&mut sql);
|
||||
|
||||
assert_eq!(sql, vec!["SELECT 1;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_execute_rewrites_sql_when_skipped() {
|
||||
let interceptor = TillInterceptor::new(
|
||||
Version::parse("0.14.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut sql = vec!["SELECT 1;".to_string()];
|
||||
|
||||
interceptor.maybe_rewrite_to_skip_sql(&mut sql);
|
||||
|
||||
assert_eq!(sql.len(), 1);
|
||||
assert!(sql[0].contains(SKIP_MARKER_PREFIX));
|
||||
assert!(sql[0].contains("target version 0.15.0 > 0.14.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_execute_normalizes_skip_result() {
|
||||
let interceptor = TillInterceptor::new(
|
||||
Version::parse("0.14.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut result = format!(
|
||||
"+----------------+\n| {} target version 0.15.0 > 0.14.0 |\n+----------------+",
|
||||
SKIP_MARKER_PREFIX
|
||||
);
|
||||
|
||||
interceptor.normalize_skip_result(&mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
format!("{} target version 0.15.0 > 0.14.0", SKIP_MARKER_PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_execute_keeps_non_skip_result() {
|
||||
let interceptor = TillInterceptor::new(
|
||||
Version::parse("0.14.0").unwrap(),
|
||||
Version::parse("0.15.0").unwrap(),
|
||||
);
|
||||
let mut result = "Affected Rows: 1".to_string();
|
||||
|
||||
interceptor.normalize_skip_result(&mut result);
|
||||
|
||||
assert_eq!(result, "Affected Rows: 1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use semver::{Prerelease, Version as SemverVersion};
|
||||
use semver::Version as SemverVersion;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, VersionError>;
|
||||
|
||||
@@ -81,88 +81,10 @@ impl Version {
|
||||
let v2 = other.to_semantic();
|
||||
|
||||
match (v1, v2) {
|
||||
(Some(v1), Some(v2)) => Self::compare_semantic(&v1, &v2),
|
||||
(Some(v1), Some(v2)) => v1.cmp(&v2),
|
||||
(None, _) | (_, None) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_semantic(v1: &SemverVersion, v2: &SemverVersion) -> Ordering {
|
||||
match v1
|
||||
.major
|
||||
.cmp(&v2.major)
|
||||
.then_with(|| v1.minor.cmp(&v2.minor))
|
||||
.then_with(|| v1.patch.cmp(&v2.patch))
|
||||
{
|
||||
Ordering::Equal => Self::compare_pre_release(v1, v2),
|
||||
ordering => ordering,
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_pre_release(v1: &SemverVersion, v2: &SemverVersion) -> Ordering {
|
||||
let pre1_empty = v1.pre.is_empty();
|
||||
let pre2_empty = v2.pre.is_empty();
|
||||
|
||||
match (pre1_empty, pre2_empty) {
|
||||
(true, true) => Ordering::Equal,
|
||||
(true, false) => Ordering::Greater,
|
||||
(false, true) => Ordering::Less,
|
||||
(false, false) => {
|
||||
let rank1 = Self::pre_release_rank(&v1.pre);
|
||||
let rank2 = Self::pre_release_rank(&v2.pre);
|
||||
rank1
|
||||
.cmp(&rank2)
|
||||
.then_with(|| Self::compare_pre_release_identifiers(&v1.pre, &v2.pre))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_pre_release_identifiers(pre1: &Prerelease, pre2: &Prerelease) -> Ordering {
|
||||
let mut parts1 = pre1.as_str().split('.');
|
||||
let mut parts2 = pre2.as_str().split('.');
|
||||
|
||||
loop {
|
||||
match (parts1.next(), parts2.next()) {
|
||||
(None, None) => return Ordering::Equal,
|
||||
(None, Some(_)) => return Ordering::Less,
|
||||
(Some(_), None) => return Ordering::Greater,
|
||||
(Some(a), Some(b)) => {
|
||||
let ordering = Self::compare_pre_release_part(a, b);
|
||||
if ordering != Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_pre_release_part(a: &str, b: &str) -> Ordering {
|
||||
let a_numeric = a.chars().all(|c| c.is_ascii_digit());
|
||||
let b_numeric = b.chars().all(|c| c.is_ascii_digit());
|
||||
|
||||
match (a_numeric, b_numeric) {
|
||||
(true, true) => {
|
||||
let a_num = a.parse::<u64>().unwrap_or(u64::MAX);
|
||||
let b_num = b.parse::<u64>().unwrap_or(u64::MAX);
|
||||
a_num.cmp(&b_num)
|
||||
}
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
(false, false) => a.cmp(b),
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_release_rank(pre: &Prerelease) -> u8 {
|
||||
let s = pre.as_str();
|
||||
if s.starts_with("alpha") {
|
||||
0
|
||||
} else if s.starts_with("beta") {
|
||||
1
|
||||
} else if s.starts_with("rc") {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
|
||||
Reference in New Issue
Block a user