diff --git a/Cargo.lock b/Cargo.lock index 9c0e332c..ce30f0d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,7 +191,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -235,7 +235,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -497,6 +497,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util 0.7.8", +] + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -825,6 +839,7 @@ dependencies = [ "configparser", "dotenv", "futures", + "kafka", "mongodb", "mysql-binlog-connector-rust", "nom", @@ -835,6 +850,7 @@ dependencies = [ "serde_yaml 0.9.22", "sqlx", "strum", + "thiserror", "tokio", ] @@ -844,9 +860,11 @@ version = "0.1.0" dependencies = [ "async-mutex", "async-recursion", + "async-std", "async-trait", "byteorder", "bytes", + "chrono", "concurrent-queue", "dt-common", "dt-meta", @@ -859,6 +877,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rdkafka", + "redis", "regex", "reqwest", "rusoto_core", @@ -869,6 +888,7 @@ dependencies = [ "serde_json", "sqlx", "strum", + "thiserror", "tokio", "tokio-postgres", "url", @@ -946,11 +966,15 @@ name = "dt-precheck" version = "0.1.0" dependencies = [ "async-trait", + "concurrent-queue", "configparser", "dt-common", + "dt-connector", "dt-meta", + "dt-task", "futures", "mongodb", + "redis", "regex", "sqlx", "strum", @@ -977,6 +1001,7 @@ dependencies = [ "mongodb", "project-root", "rdkafka", + "redis", "regex", "reqwest", "rusoto_core", @@ -1008,6 +1033,9 @@ dependencies = [ "log4rs", "mongodb", "project-root", + "rand", + "rdkafka", + "redis", "regex", "rusoto_core", "rusoto_credential", @@ -1222,7 +1250,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -1933,6 +1961,7 @@ dependencies = [ "async-std", "byteorder", "dotenv", + "lazy_static", "num_enum", "openssl", "serde", @@ -1940,6 +1969,7 @@ dependencies = [ "serial_test 1.0.0", "sha-1", "sha2 0.10.7", + "thiserror", "url", "zstd", ] @@ -2119,7 +2149,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -2371,9 +2401,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -2412,9 +2442,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.29" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2457,9 +2487,9 @@ dependencies = [ [[package]] name = "rdkafka" -version = "0.29.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c5d6d17442bcb9f943aae96d67d98c6d36af60442dd5da62aaa7fcbb25c48" +checksum = "053adfa02fab06e86c01d586cc68aa47ee0ff4489a59469081dc12cbcde578bf" dependencies = [ "futures-channel", "futures-util", @@ -2475,9 +2505,9 @@ dependencies = [ [[package]] name = "rdkafka-sys" -version = "4.5.0+1.9.2" +version = "4.6.0+2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb0676c2112342ac7165decdedbc4e7086c0af384479ccce534546b10687a5d" +checksum = "ad63c279fca41a27c231c450a2d2ad18288032e9cbb159ad16c9d96eba35aaaf" dependencies = [ "libc", "libz-sys", @@ -2485,6 +2515,27 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "redis" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6543a7bc6428396845f6854ccf3d1ae8823816592e2cbe74f20f50f209d02" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.4.9", + "tokio", + "tokio-util 0.7.8", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2974,7 +3025,7 @@ checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -3095,7 +3146,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -3120,6 +3171,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.9.9" @@ -3416,9 +3473,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -3453,22 +3510,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -3563,7 +3620,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -3690,7 +3747,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", ] [[package]] @@ -3931,7 +3988,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -3965,7 +4022,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4178,9 +4235,9 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zstd" -version = "0.12.3+zstd.1.5.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ "zstd-safe", ] diff --git a/Cargo.toml b/Cargo.toml index bfa99e84..c506e286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ async-mutex = "1.4.0" project-root = "0.2.2" strum = { version = "0.24.1", features = ["derive"] } regex = "1.5.4" -rdkafka = "0.29.0" +rdkafka = "0.34.0" kafka = "0.9.0" reqwest = "0.11.16" rusoto_core = "0.48.0" @@ -52,4 +52,7 @@ rusoto_credential = "0.48.0" uuid = { version = "1.3.1", features = ["v4"] } nom = "7.1.3" mongodb = { version = "2.5.0" } -dotenv = "0.15.0" \ No newline at end of file +dotenv = "0.15.0" +redis = { version = "0.23.1", features = ["tokio-comp"] } +thiserror = "1.0.44" +async-std = "1.12.0" \ No newline at end of file diff --git a/dt-common/Cargo.toml b/dt-common/Cargo.toml index f9bc7b32..cfcbe228 100644 --- a/dt-common/Cargo.toml +++ b/dt-common/Cargo.toml @@ -22,4 +22,6 @@ futures = { workspace = true } serde_yaml = { workspace = true } regex = { workspace = true } nom = { workspace = true } -tokio = { workspace = true } \ No newline at end of file +tokio = { workspace = true } +thiserror = { workspace = true } +kafka = { workspace = true } \ No newline at end of file diff --git a/dt-common/src/config/config_enums.rs b/dt-common/src/config/config_enums.rs index abc43e47..5c162365 100644 --- a/dt-common/src/config/config_enums.rs +++ b/dt-common/src/config/config_enums.rs @@ -1,6 +1,10 @@ +use std::str::FromStr; + use strum::{Display, EnumString, IntoStaticStr}; -#[derive(Clone, Display, EnumString, IntoStaticStr, Debug)] +use crate::error::Error; + +#[derive(Clone, Display, EnumString, IntoStaticStr, Debug, PartialEq, Eq)] pub enum DbType { #[strum(serialize = "mysql")] Mysql, @@ -14,9 +18,11 @@ pub enum DbType { Foxlake, #[strum(serialize = "mongo")] Mongo, + #[strum(serialize = "redis")] + Redis, } -#[derive(Display, EnumString, IntoStaticStr)] +#[derive(Display, EnumString, IntoStaticStr, Debug, Clone)] pub enum ExtractType { #[strum(serialize = "snapshot")] Snapshot, @@ -26,11 +32,9 @@ pub enum ExtractType { CheckLog, #[strum(serialize = "struct")] Struct, - #[strum(serialize = "basic")] - Basic, } -#[derive(EnumString, IntoStaticStr)] +#[derive(Display, EnumString, IntoStaticStr)] pub enum SinkType { #[strum(serialize = "write")] Write, @@ -38,8 +42,6 @@ pub enum SinkType { Check, #[strum(serialize = "struct")] Struct, - #[strum(serialize = "basic")] - Basic, } #[derive(EnumString, IntoStaticStr, Clone, Display)] @@ -58,14 +60,8 @@ pub enum ParallelType { Table, #[strum(serialize = "mongo")] Mongo, -} - -#[derive(EnumString, IntoStaticStr, Display)] -pub enum PipelineType { - #[strum(serialize = "basic")] - Basic, - #[strum(serialize = "transaction")] - Transaction, + #[strum(serialize = "redis")] + Redis, } pub enum RouteType { @@ -73,10 +69,20 @@ pub enum RouteType { Tb, } -#[derive(EnumString, Clone, IntoStaticStr)] +#[derive(Clone, Debug, IntoStaticStr)] pub enum ConflictPolicyEnum { #[strum(serialize = "ignore")] Ignore, #[strum(serialize = "interrupt")] Interrupt, } + +impl FromStr for ConflictPolicyEnum { + type Err = Error; + fn from_str(str: &str) -> Result { + match str { + "ignore" => Ok(Self::Ignore), + _ => Ok(Self::Interrupt), + } + } +} diff --git a/dt-common/src/config/config_token_parser.rs b/dt-common/src/config/config_token_parser.rs index f1f7bd2e..d06bc33a 100644 --- a/dt-common/src/config/config_token_parser.rs +++ b/dt-common/src/config/config_token_parser.rs @@ -47,15 +47,17 @@ impl ConfigTokenParser { delimiters: &[char], ) -> (String, usize) { let mut token = String::new(); + let mut read_count = 0; for c in chars.iter().skip(start_index) { if delimiters.contains(c) { break; } else { token.push(*c); + read_count += 1; } } - let next_index = start_index + token.len(); + let next_index = start_index + read_count; (token, next_index) } @@ -66,9 +68,11 @@ impl ConfigTokenParser { ) -> (String, usize) { let mut start = false; let mut token = String::new(); + let mut read_count = 0; for c in chars.iter().skip(start_index) { if start && *c == escape_pair.1 { token.push(*c); + read_count += 1; break; } if *c == escape_pair.0 { @@ -76,10 +80,14 @@ impl ConfigTokenParser { } if start { token.push(*c); + read_count += 1; } } - let next_index = start_index + token.len(); + // when there are emojs in the token, the read_count may be less than token.len(), for example: + // in chars, πŸ˜€ only takes 1 slot, which is '\u{1f600}' + // in token, πŸ˜€ takes 4 slots, which are: 240, 159, 152, 128 + let next_index = start_index + read_count; (token, next_index) } } @@ -184,4 +192,26 @@ mod tests { assert_eq!(tokens[6], "db_4"); assert_eq!(tokens[7], r#""tb`4""#); } + + #[test] + fn test_parse_emoj_config_tokens() { + let config = r#"SET "set_key_3_ πŸ˜€" "val_2_ πŸ˜€""#; + let delimiters = vec![' ']; + let escape_pairs = vec![('"', '"')]; + let tokens = ConfigTokenParser::parse(config, &delimiters, &escape_pairs); + assert_eq!(tokens.len(), 3); + assert_eq!(tokens[0], "SET"); + assert_eq!(tokens[1], r#""set_key_3_ πŸ˜€""#); + assert_eq!(tokens[2], r#""val_2_ πŸ˜€""#); + + let config = r#"ZADD key 2 val_2_δΈ­ζ–‡ 3 "val_3_ πŸ˜€""#; + let tokens = ConfigTokenParser::parse(config, &delimiters, &escape_pairs); + assert_eq!(tokens.len(), 6); + assert_eq!(tokens[0], "ZADD"); + assert_eq!(tokens[1], "key"); + assert_eq!(tokens[2], "2"); + assert_eq!(tokens[3], "val_2_δΈ­ζ–‡"); + assert_eq!(tokens[4], "3"); + assert_eq!(tokens[5], r#""val_3_ πŸ˜€""#); + } } diff --git a/dt-common/src/config/extractor_config.rs b/dt-common/src/config/extractor_config.rs index 2ba4c86c..8eaea26a 100644 --- a/dt-common/src/config/extractor_config.rs +++ b/dt-common/src/config/extractor_config.rs @@ -1,12 +1,7 @@ -use super::config_enums::DbType; +use super::config_enums::{DbType, ExtractType}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum ExtractorConfig { - Basic { - url: String, - db_type: DbType, - }, - MysqlStruct { url: String, db: String, @@ -64,10 +59,42 @@ pub enum ExtractorConfig { MongoCdc { url: String, resume_token: String, - start_timestamp: i64, + start_timestamp: u32, + // op_log, change_stream + source: String, + }, + + RedisSnapshot { + url: String, + repl_port: u64, + }, + + RedisCdc { + url: String, + run_id: String, + repl_offset: u64, + repl_port: u64, + heartbeat_interval_secs: u64, + now_db_id: i64, + }, + + Kafka { + url: String, + group: String, + topic: String, + partition: i32, + offset: i64, + ack_interval_secs: u64, }, } +#[derive(Clone, Debug)] +pub struct ExtractorBasicConfig { + pub db_type: DbType, + pub extract_type: ExtractType, + pub url: String, +} + impl ExtractorConfig { pub fn get_db_type(&self) -> DbType { match self { @@ -75,14 +102,31 @@ impl ExtractorConfig { | Self::MysqlSnapshot { .. } | Self::MysqlCdc { .. } | Self::MysqlCheck { .. } => DbType::Mysql, - Self::PgStruct { .. } | Self::PgSnapshot { .. } | Self::PgCdc { .. } | Self::PgCheck { .. } => DbType::Pg, - Self::MongoSnapshot { .. } | Self::MongoCdc { .. } => DbType::Mongo, - Self::Basic { db_type, .. } => db_type.clone(), + Self::RedisSnapshot { .. } | Self::RedisCdc { .. } => DbType::Redis, + Self::Kafka { .. } => DbType::Kafka, + } + } + + pub fn get_url(&self) -> String { + match self { + Self::MysqlStruct { url, .. } + | Self::MysqlSnapshot { url, .. } + | Self::MysqlCdc { url, .. } + | Self::MysqlCheck { url, .. } + | Self::PgStruct { url, .. } + | Self::PgSnapshot { url, .. } + | Self::PgCdc { url, .. } + | Self::PgCheck { url, .. } + | Self::MongoSnapshot { url, .. } + | Self::MongoCdc { url, .. } + | Self::RedisSnapshot { url, .. } + | Self::RedisCdc { url, .. } + | Self::Kafka { url, .. } => url.to_owned(), } } diff --git a/dt-common/src/config/sinker_config.rs b/dt-common/src/config/sinker_config.rs index 36e0d665..8fa79e73 100644 --- a/dt-common/src/config/sinker_config.rs +++ b/dt-common/src/config/sinker_config.rs @@ -1,12 +1,7 @@ use super::config_enums::{ConflictPolicyEnum, DbType}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum SinkerConfig { - Basic { - url: String, - db_type: DbType, - }, - Mysql { url: String, batch_size: usize, @@ -65,6 +60,19 @@ pub enum SinkerConfig { region: String, root_dir: String, }, + + Redis { + url: String, + batch_size: usize, + method: String, + }, +} + +#[derive(Clone, Debug)] +pub struct SinkerBasicConfig { + pub db_type: DbType, + pub url: String, + pub batch_size: usize, } impl SinkerConfig { @@ -78,7 +86,23 @@ impl SinkerConfig { Self::Foxlake { .. } => DbType::Foxlake, Self::MysqlStruct { .. } => DbType::Mysql, Self::PgStruct { .. } => DbType::Pg, - Self::Basic { db_type, .. } => db_type.clone(), + Self::Redis { .. } => DbType::Redis, + } + } + + pub fn get_url(&self) -> String { + match self { + Self::Mysql { url, .. } + | Self::MysqlCheck { url, .. } + | Self::Pg { url, .. } + | Self::PgCheck { url, .. } + | Self::Mongo { url, .. } + | Self::Kafka { url, .. } + | Self::OpenFaas { url, .. } + | Self::MysqlStruct { url, .. } + | Self::PgStruct { url, .. } + | Self::Redis { url, .. } => url.to_owned(), + Self::Foxlake { .. } => String::new(), } } diff --git a/dt-common/src/config/task_config.rs b/dt-common/src/config/task_config.rs index 8e4a59c7..40d17163 100644 --- a/dt-common/src/config/task_config.rs +++ b/dt-common/src/config/task_config.rs @@ -6,19 +6,21 @@ use crate::error::Error; use super::{ config_enums::{ConflictPolicyEnum, DbType, ExtractType, ParallelType, PipelineType, SinkType}, - extractor_config::ExtractorConfig, + extractor_config::{ExtractorBasicConfig, ExtractorConfig}, filter_config::FilterConfig, parallelizer_config::ParallelizerConfig, pipeline_config::{ExtraConfig, PipelineConfig}, resumer_config::ResumerConfig, router_config::RouterConfig, runtime_config::RuntimeConfig, - sinker_config::SinkerConfig, + sinker_config::{SinkerBasicConfig, SinkerConfig}, }; #[derive(Clone)] pub struct TaskConfig { + pub extractor_basic: ExtractorBasicConfig, pub extractor: ExtractorConfig, + pub sinker_basic: SinkerBasicConfig, pub sinker: SinkerConfig, pub runtime: RuntimeConfig, pub parallelizer: ParallelizerConfig, @@ -52,11 +54,15 @@ impl TaskConfig { let mut ini = Ini::new(); ini.read(config_str).unwrap(); + let (extractor_basic, extractor) = Self::load_extractor_config(&ini).unwrap(); + let (sinker_basic, sinker) = Self::load_sinker_config(&ini).unwrap(); Self { - extractor: Self::load_extractor_config(&ini).unwrap(), + extractor_basic, + extractor, parallelizer: Self::load_paralleizer_config(&ini), - sinker: Self::load_sinker_config(&ini).unwrap(), pipeline: Self::load_pipeline_config(&ini), + sinker_basic, + sinker, runtime: Self::load_runtime_config(&ini).unwrap(), filter: Self::load_filter_config(&ini).unwrap(), router: Self::load_router_config(&ini).unwrap(), @@ -64,50 +70,53 @@ impl TaskConfig { } } - fn load_extractor_config(ini: &Ini) -> Result { + fn load_extractor_config(ini: &Ini) -> Result<(ExtractorBasicConfig, ExtractorConfig), Error> { let db_type = DbType::from_str(&ini.get(EXTRACTOR, DB_TYPE).unwrap()).unwrap(); let extract_type = ExtractType::from_str(&ini.get(EXTRACTOR, "extract_type").unwrap()).unwrap(); let url = ini.get(EXTRACTOR, URL).unwrap(); + let basic = ExtractorBasicConfig { + db_type: db_type.clone(), + extract_type: extract_type.clone(), + url: url.clone(), + }; - match db_type { + let sinker = match db_type { DbType::Mysql => match extract_type { - ExtractType::Snapshot => Ok(ExtractorConfig::MysqlSnapshot { + ExtractType::Snapshot => ExtractorConfig::MysqlSnapshot { url, db: String::new(), tb: String::new(), - }), + }, - ExtractType::Cdc => Ok(ExtractorConfig::MysqlCdc { + ExtractType::Cdc => ExtractorConfig::MysqlCdc { url, binlog_filename: ini.get(EXTRACTOR, "binlog_filename").unwrap(), binlog_position: ini.getuint(EXTRACTOR, "binlog_position").unwrap().unwrap() as u32, server_id: ini.getuint(EXTRACTOR, "server_id").unwrap().unwrap(), - }), + }, - ExtractType::CheckLog => Ok(ExtractorConfig::MysqlCheck { + ExtractType::CheckLog => ExtractorConfig::MysqlCheck { url, check_log_dir: ini.get(EXTRACTOR, CHECK_LOG_DIR).unwrap(), batch_size: ini.getuint(EXTRACTOR, BATCH_SIZE).unwrap().unwrap() as usize, - }), + }, - ExtractType::Struct => Ok(ExtractorConfig::MysqlStruct { + ExtractType::Struct => ExtractorConfig::MysqlStruct { url, db: String::new(), - }), - - ExtractType::Basic => Ok(ExtractorConfig::Basic { url, db_type }), + }, }, DbType::Pg => match extract_type { - ExtractType::Snapshot => Ok(ExtractorConfig::PgSnapshot { + ExtractType::Snapshot => ExtractorConfig::PgSnapshot { url, db: String::new(), tb: String::new(), - }), + }, - ExtractType::Cdc => Ok(ExtractorConfig::PgCdc { + ExtractType::Cdc => ExtractorConfig::PgCdc { url, slot_name: ini.get(EXTRACTOR, "slot_name").unwrap(), start_lsn: ini.get(EXTRACTOR, "start_lsn").unwrap(), @@ -115,141 +124,174 @@ impl TaskConfig { .getuint(EXTRACTOR, "heartbeat_interval_secs") .unwrap() .unwrap(), - }), + }, - ExtractType::CheckLog => Ok(ExtractorConfig::PgCheck { + ExtractType::CheckLog => ExtractorConfig::PgCheck { url, check_log_dir: ini.get(EXTRACTOR, CHECK_LOG_DIR).unwrap(), batch_size: ini.getuint(EXTRACTOR, BATCH_SIZE).unwrap().unwrap() as usize, - }), + }, - ExtractType::Struct => Ok(ExtractorConfig::PgStruct { + ExtractType::Struct => ExtractorConfig::PgStruct { url, db: String::new(), - }), - - ExtractType::Basic => Ok(ExtractorConfig::Basic { url, db_type }), + }, }, DbType::Mongo => match extract_type { - ExtractType::Snapshot => Ok(ExtractorConfig::MongoSnapshot { + ExtractType::Snapshot => ExtractorConfig::MongoSnapshot { url, db: String::new(), tb: String::new(), - }), - - ExtractType::Cdc => { - let start_timestamp: i64 = match ini.getint(EXTRACTOR, "start_timestamp") { - Ok(ts_option) => { - if let Some(ts) = ts_option { - ts - } else { - 0 - } - } - Err(_) => 0, - }; - let resume_token: String = match ini.get(EXTRACTOR, "resume_token") { - Some(val) => val, - None => String::from(""), - }; - Ok(ExtractorConfig::MongoCdc { - url, - resume_token, - start_timestamp, - }) + }, + + ExtractType::Cdc => ExtractorConfig::MongoCdc { + url, + resume_token: Self::get_optional_value(ini, EXTRACTOR, "resume_token"), + start_timestamp: Self::get_optional_value(ini, EXTRACTOR, "start_timestamp"), + source: Self::get_optional_value(ini, EXTRACTOR, "source"), + }, + + extract_type => { + return Err(Error::ConfigError(format!( + "extract type: {} not supported", + extract_type + ))) } + }, - ExtractType::Basic => Ok(ExtractorConfig::Basic { url, db_type }), + DbType::Redis => { + let repl_port = ini.getuint(EXTRACTOR, "repl_port").unwrap().unwrap(); + match extract_type { + ExtractType::Snapshot => ExtractorConfig::RedisSnapshot { url, repl_port }, - _ => Err(Error::Unexpected { - error: "extractor db type not supported".to_string(), - }), + ExtractType::Cdc => ExtractorConfig::RedisCdc { + url, + repl_port, + run_id: ini.get(EXTRACTOR, "run_id").unwrap(), + repl_offset: ini.getuint(EXTRACTOR, "repl_offset").unwrap().unwrap(), + heartbeat_interval_secs: ini + .getuint(EXTRACTOR, "heartbeat_interval_secs") + .unwrap() + .unwrap(), + now_db_id: ini.getint(EXTRACTOR, "now_db_id").unwrap().unwrap(), + }, + + extract_type => { + return Err(Error::ConfigError(format!( + "extract type: {} not supported", + extract_type + ))) + } + } + } + + DbType::Kafka => ExtractorConfig::Kafka { + url, + group: ini.get(EXTRACTOR, "group").unwrap(), + topic: ini.get(EXTRACTOR, "topic").unwrap(), + partition: Self::get_optional_value(ini, EXTRACTOR, "partition"), + offset: Self::get_optional_value(ini, EXTRACTOR, "offset"), + ack_interval_secs: Self::get_optional_value(ini, EXTRACTOR, "ack_interval_secs"), }, - _ => Err(Error::Unexpected { - error: "extractor db type not supported".to_string(), - }), - } + db_type => { + return Err(Error::ConfigError(format!( + "extractor db type: {} not supported", + db_type + ))) + } + }; + Ok((basic, sinker)) } - fn load_sinker_config(ini: &Ini) -> Result { + fn load_sinker_config(ini: &Ini) -> Result<(SinkerBasicConfig, SinkerConfig), Error> { let db_type = DbType::from_str(&ini.get(SINKER, DB_TYPE).unwrap()).unwrap(); let sink_type = SinkType::from_str(&ini.get(SINKER, "sink_type").unwrap()).unwrap(); let url = ini.get(SINKER, URL).unwrap(); let batch_size: usize = Self::get_optional_value(ini, SINKER, BATCH_SIZE); + let basic = SinkerBasicConfig { + db_type: db_type.clone(), + url: url.clone(), + batch_size, + }; + let conflict_policy_str: String = Self::get_optional_value(ini, SINKER, "conflict_policy"); let conflict_policy = ConflictPolicyEnum::from_str(&conflict_policy_str) .unwrap_or(ConflictPolicyEnum::Interrupt); - match db_type { + let sinker = match db_type { DbType::Mysql => match sink_type { - SinkType::Write => Ok(SinkerConfig::Mysql { url, batch_size }), + SinkType::Write => SinkerConfig::Mysql { url, batch_size }, - SinkType::Check => Ok(SinkerConfig::MysqlCheck { + SinkType::Check => SinkerConfig::MysqlCheck { url, batch_size, check_log_dir: ini.get(SINKER, CHECK_LOG_DIR), - }), + }, - SinkType::Struct => Ok(SinkerConfig::MysqlStruct { + SinkType::Struct => SinkerConfig::MysqlStruct { url, conflict_policy, - }), - - SinkType::Basic => Ok(SinkerConfig::Basic { url, db_type }), + }, }, DbType::Pg => match sink_type { - SinkType::Write => Ok(SinkerConfig::Pg { url, batch_size }), + SinkType::Write => SinkerConfig::Pg { url, batch_size }, - SinkType::Check => Ok(SinkerConfig::PgCheck { + SinkType::Check => SinkerConfig::PgCheck { url, batch_size, check_log_dir: ini.get(SINKER, CHECK_LOG_DIR), - }), + }, - SinkType::Struct => Ok(SinkerConfig::PgStruct { + SinkType::Struct => SinkerConfig::PgStruct { url, conflict_policy, - }), - - SinkType::Basic => Ok(SinkerConfig::Basic { url, db_type }), + }, }, DbType::Mongo => match sink_type { - SinkType::Write => Ok(SinkerConfig::Mongo { url, batch_size }), + SinkType::Write => SinkerConfig::Mongo { url, batch_size }, - SinkType::Basic => Ok(SinkerConfig::Basic { url, db_type }), - - _ => Err(Error::Unexpected { - error: "sinker db type not supported".to_string(), - }), + db_type => { + return Err(Error::ConfigError(format!( + "sinker db type: {} not supported", + db_type + ))) + } }, - DbType::Kafka => Ok(SinkerConfig::Kafka { + DbType::Kafka => SinkerConfig::Kafka { url, batch_size, ack_timeout_secs: ini.getuint(SINKER, "ack_timeout_secs").unwrap().unwrap(), required_acks: ini.get(SINKER, "required_acks").unwrap(), - }), + }, - DbType::OpenFaas => Ok(SinkerConfig::OpenFaas { + DbType::OpenFaas => SinkerConfig::OpenFaas { url, batch_size, timeout_secs: ini.getuint(SINKER, "timeout_secs").unwrap().unwrap(), - }), + }, - DbType::Foxlake => Ok(SinkerConfig::Foxlake { + DbType::Foxlake => SinkerConfig::Foxlake { batch_size, bucket: ini.get(SINKER, "bucket").unwrap(), access_key: ini.get(SINKER, "access_key").unwrap(), secret_key: ini.get(SINKER, "secret_key").unwrap(), region: ini.get(SINKER, "region").unwrap(), root_dir: ini.get(SINKER, "root_dir").unwrap(), - }), - } + }, + + DbType::Redis => SinkerConfig::Redis { + url, + batch_size, + method: Self::get_optional_value(ini, SINKER, "method"), + }, + }; + Ok((basic, sinker)) } fn load_paralleizer_config(ini: &Ini) -> ParallelizerConfig { diff --git a/dt-common/src/error.rs b/dt-common/src/error.rs index 93a1235c..a19ee6ba 100644 --- a/dt-common/src/error.rs +++ b/dt-common/src/error.rs @@ -1,90 +1,49 @@ -use core::fmt; +use thiserror::Error; -#[derive(Debug)] +#[derive(Error, Debug)] pub enum Error { - ConfigError { - error: String, - }, + #[error("config error: {0}")] + ConfigError(String), - BinlogError { - error: mysql_binlog_connector_rust::binlog_error::BinlogError, - }, + #[error("extractor error: {0}")] + ExtractorError(String), - SqlxError { - error: sqlx::Error, - }, + #[error("sinker error: {0}")] + SinkerError(String), - Unexpected { - error: String, - }, + #[error("pull mysql binlog error: {0}")] + BinlogError(#[from] mysql_binlog_connector_rust::binlog_error::BinlogError), - MetadataError { - error: String, - }, + #[error("sqlx error: {0}")] + SqlxError(#[from] sqlx::Error), - IoError { - error: std::io::Error, - }, + #[error("unexpected error: {0}")] + Unexpected(String), - YamlError { - error: serde_yaml::Error, - }, + #[error("parse redis rdb error: {0}")] + RedisRdbError(String), - EnvVarError { - error: std::env::VarError, - }, + #[error("metadata error: {0}")] + MetadataError(String), - StructError { - error: String, - }, + #[error("io error: {0}")] + IoError(#[from] std::io::Error), - ColumnNotMatch, -} + #[error("yaml error: {0}")] + YamlError(#[from] serde_yaml::Error), -impl From for Error { - fn from(err: mysql_binlog_connector_rust::binlog_error::BinlogError) -> Self { - Self::BinlogError { error: err } - } -} + #[error("from utf8 error: {0}")] + FromUtf8Error(#[from] std::string::FromUtf8Error), -impl From for Error { - fn from(err: sqlx::Error) -> Self { - Self::SqlxError { error: err } - } -} + #[error("mongodb error: {0}")] + MongodbError(#[from] mongodb::error::Error), -impl From for Error { - fn from(err: std::io::Error) -> Self { - Self::IoError { error: err } - } -} + #[error("struct error: {0}")] + StructError(String), -impl From for Error { - fn from(err: serde_yaml::Error) -> Self { - Self::YamlError { error: err } - } -} - -impl From for Error { - fn from(err: std::env::VarError) -> Self { - Self::EnvVarError { error: err } - } -} + #[error("precheck error: {0}")] + PreCheckError(String), -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let err_msg: String = match self { - Error::ConfigError { error } => error.to_owned(), - Error::BinlogError { .. } => String::from("binlog error"), - Error::SqlxError { error } => error.to_string(), - Error::Unexpected { error } => error.to_owned(), - Error::MetadataError { error } => error.to_owned(), - Error::IoError { error } => error.to_string(), - Error::YamlError { error } => error.to_string(), - Error::EnvVarError { error } => error.to_string(), - Error::StructError { error } => error.to_owned(), - Error::ColumnNotMatch => String::from("column not match"), - }; - write!(f, "err_msg:{}", err_msg) - } + #[error("kafka error: {0}")] + KafkaError(#[from] kafka::Error), } diff --git a/dt-common/src/lib.rs b/dt-common/src/lib.rs index 5e5ecc21..23038e30 100644 --- a/dt-common/src/lib.rs +++ b/dt-common/src/lib.rs @@ -1,7 +1,4 @@ -// extern crate dt-meta; - pub mod config; -pub mod constants; pub mod error; pub mod logger; pub mod monitor; diff --git a/dt-common/src/test/config/cdc_config.ini b/dt-common/src/test/config/cdc_config.ini deleted file mode 100644 index 56c1fccb..00000000 --- a/dt-common/src/test/config/cdc_config.ini +++ /dev/null @@ -1,47 +0,0 @@ -[extractor] -db_type=mysql -extract_type=cdc -binlog_position=1073349 -binlog_filename=binlog.000022 -server_id=10086 -url=mysql://root:123456@127.0.0.1:3306 - -[filter] -ignore_dbs= -do_dbs= -do_tbs=source_db.multi_datas -ignore_tbs= -do_events=insert,update,delete - -[sinker] -db_type=mysql -sink_type=write -batch_size=200 -url=mysql://root:123456@127.0.0.1:3307 - -[router] -tb_map= -field_map= -db_map= - -[parallelizer] -parallel_type=rdb_merge -parallel_size=16 - -[pipeline] -type=transaction -buffer_size=16000 -checkpoint_interval_secs=2 - -transaction_db=ape_trans_mysql -transaction_table=topo1_node1_to_node2 -transaction_express=(?.*)_(?.*)_to_(?.*) -transaction_command=update `ape_trans_mysql`.`topo1_node1_to_node2` set `n` = `n` + 1; -topology_key=topo1 -white_nodes= -black_nodes=node2 - -[runtime] -log_dir=./logs -log_level=info -log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-common/src/utils/config_url_util.rs b/dt-common/src/utils/config_url_util.rs deleted file mode 100644 index 4be5df71..00000000 --- a/dt-common/src/utils/config_url_util.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::env; - -pub struct ConfigUrlUtil {} - -impl ConfigUrlUtil { - // get_username: try to get the username from a databaseUrl - // postgres://postgres:123456@127.0.0.1:5431/dt_test - // mysql://root:123456@127.0.0.1:3306?ssl-mode=disabled - pub fn get_username(database_url: String) -> Option { - if database_url.is_empty() { - return None; - } - match database_url.split(':').nth(1) { - Some(username) => { - let byte_arr = username.as_bytes(); - Some(String::from_utf8(byte_arr[2..].to_vec()).unwrap()) - } - None => None, - } - } - - // convert_with_envs: format the database_url with envs, such as: - // change: mysql://{test_user}:{test_password}@{test_url} - // to: mysql://test:123456@127.0.0.1:3306 - // when have the envs: test_user=test, test_password=123456, test_url=127.0.0.1:3306 - pub fn convert_with_envs(database_url: String) -> Option { - if database_url.is_empty() { - return None; - } - let (mut new_url_bytes, mut pos, mut left_pos): (Vec, i64, i64) = (vec![], 0, -1); - - for ch in database_url.chars() { - if ch == '{' { - left_pos = pos; - } else if ch == '}' && pos > left_pos && left_pos >= 0 { - let new_env = String::from_utf8( - database_url.as_bytes()[(left_pos + 1) as usize..pos as usize].to_vec(), - ) - .unwrap(); - if env::var(&new_env).is_ok() { - let env_val_tmp = env::var(new_env).unwrap(); - new_url_bytes.extend_from_slice(env_val_tmp.as_bytes()); - } - left_pos = -1; - } else if left_pos == -1 { - new_url_bytes.push(ch as u8); - } - pos += 1; - } - - Some(String::from_utf8(new_url_bytes).unwrap()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn get_username_test() { - assert_eq!( - ConfigUrlUtil::get_username(String::from( - "postgres://postgres:123456@127.0.0.1:5431/dt_test" - )) - .unwrap(), - "postgres" - ); - - assert_eq!( - ConfigUrlUtil::get_username(String::from( - "mysql://root:123456@127.0.0.1:3306?ssl-mode=disabled" - )) - .unwrap(), - "root" - ); - - // unnormal case - assert_eq!( - ConfigUrlUtil::get_username(String::from( - "mysql:///root:123456@127.0.0.1:3306?ssl-mode=disabled" - )) - .unwrap(), - "/root" - ); - } - - #[test] - fn convert_with_envs() { - env::set_var("test_user", "test"); - env::set_var("test_password", "123456"); - env::set_var("test_url", "127.0.0.1:3306"); - - let mut opt: Option; - opt = ConfigUrlUtil::convert_with_envs(String::from( - "mysql://{test_user}:{test_password}@{test_url}?ssl-mode=disabled", - )); - assert!( - opt.is_some() && opt.unwrap() == "mysql://test:123456@127.0.0.1:3306?ssl-mode=disabled" - ); - - opt = ConfigUrlUtil::convert_with_envs(String::from( - "mysql://test:123456@127.0.0.1:3306?ssl-mode=disabled", - )); - assert!( - opt.is_some() && opt.unwrap() == "mysql://test:123456@127.0.0.1:3306?ssl-mode=disabled" - ); - - // unnormal case - env::set_var("test_wrong", "wrong"); - opt = ConfigUrlUtil::convert_with_envs(String::from( - "mysql://}test:123456{test_wrong}{@127.0.0.1:3306?ssl-mode=disabled", - )); - assert!(opt.is_some() && opt.unwrap() == "mysql://}test:123456wrong"); - - env::remove_var("test_user"); - env::remove_var("test_password"); - env::remove_var("test_url"); - env::remove_var("test_wrong"); - } -} diff --git a/dt-common/src/utils/database_mock.rs b/dt-common/src/utils/database_mock.rs deleted file mode 100644 index 51b66a2a..00000000 --- a/dt-common/src/utils/database_mock.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::{ - fs::File, - io::{BufRead, BufReader}, - path::Path, - time::Duration, -}; - -use sqlx::{mysql::MySqlPoolOptions, postgres::PgPoolOptions, Error, MySql, Pool, Postgres}; - -use crate::config::config_enums::DbType; - -use super::work_dir_util::WorkDirUtil; - -// DatabaseMockUtils: Mock Database struct& data for test. -pub struct DatabaseMockUtils { - pub pg_pool: Option>, - pub mysql_pool: Option>, - pub db_type: DbType, -} - -impl DatabaseMockUtils { - pub async fn new( - connection_url: String, - db_type: DbType, - pool_size: u32, - connection_timeout_sec: u64, - ) -> Result { - match db_type { - DbType::Mysql => { - let db_pool_result = MySqlPoolOptions::new() - .max_connections(pool_size) - .acquire_timeout(Duration::from_secs(connection_timeout_sec)) - .connect(connection_url.as_str()) - .await; - match db_pool_result { - Ok(pool) => Ok(Self { - mysql_pool: Option::Some(pool), - pg_pool: None, - db_type, - }), - Err(error) => Err(error), - } - } - DbType::Pg => { - let db_pool_result = PgPoolOptions::new() - .max_connections(pool_size) - .acquire_timeout(Duration::from_secs(connection_timeout_sec)) - .connect(connection_url.as_str()) - .await; - match db_pool_result { - Ok(pool) => Ok(Self { - mysql_pool: None, - pg_pool: Option::Some(pool), - db_type, - }), - Err(error) => Err(error), - } - } - _ => Ok(Self { - mysql_pool: None, - pg_pool: None, - db_type, - }), - } - } - - pub async fn load_data(&self, relative_data_path: &str) -> Result<(), Error> { - match self.db_type { - DbType::Mysql => { - if self.mysql_pool.is_none() { - println!("mysql_pool is empty."); - return Ok(()); - } - } - DbType::Pg => { - if self.pg_pool.is_none() { - println!("pg_pool is empty."); - return Ok(()); - } - } - _ => return Ok(()), - } - - let absolute_path = WorkDirUtil::get_absolute_by_relative(relative_data_path).unwrap(); - let file_path = Path::new(&absolute_path); - if !file_path.exists() || !file_path.is_file() { - println!("path:{} is empty.", absolute_path); - return Ok(()); - } - let file = File::open(absolute_path).unwrap(); - for sql in BufReader::new(file).lines().flatten() { - if sql.is_empty() || sql.starts_with("--") { - continue; - } - match self.db_type { - DbType::Mysql => { - let pool: &Pool = match &self.mysql_pool { - Some(p) => p, - _ => return Ok(()), - }; - _ = sqlx::query(&sql).execute(pool).await; - } - DbType::Pg => { - let pool: &Pool = match &self.pg_pool { - Some(p) => p, - _ => return Ok(()), - }; - _ = sqlx::query(&sql).execute(pool).await; - } - _ => return Ok(()), - } - println!("executed sql: {}", sql); - } - Ok(()) - } - - pub async fn release_pool(self) { - if let Some(p) = self.mysql_pool { - p.close().await; - } - if let Some(p) = self.pg_pool { - p.close().await; - } - } -} diff --git a/dt-common/src/utils/mod.rs b/dt-common/src/utils/mod.rs index c4ff1bd6..13b4c69b 100644 --- a/dt-common/src/utils/mod.rs +++ b/dt-common/src/utils/mod.rs @@ -1,5 +1,3 @@ -pub mod config_url_util; -pub mod database_mock; pub mod position_util; pub mod rdb_filter; pub mod sql_util; diff --git a/dt-common/src/utils/rdb_filter.rs b/dt-common/src/utils/rdb_filter.rs index 365fa55c..a5aeed40 100644 --- a/dt-common/src/utils/rdb_filter.rs +++ b/dt-common/src/utils/rdb_filter.rs @@ -28,6 +28,8 @@ pub struct RdbFilter { pub transaction_worker: TransactionWorker, } +const DDL: &str = "ddl"; + impl RdbFilter { pub fn from_config(config: &FilterConfig, db_type: DbType) -> Result { match config { @@ -122,12 +124,20 @@ impl RdbFilter { } pub fn filter_event(&mut self, db: &str, tb: &str, row_type: &str) -> bool { - if self.do_events.is_empty() || !self.do_events.contains(&row_type.to_string()) { + if self.do_events.is_empty() || !self.do_events.contains(row_type) { return false; } self.filter_transaction_tb(db, tb) } + pub fn filter_ddl(&mut self) -> bool { + // filter ddl by default + if self.do_events.is_empty() { + return true; + } + !self.do_events.contains(DDL) + } + fn contain_tb( set: &HashSet<(String, String)>, db: &str, @@ -217,9 +227,10 @@ impl RdbFilter { let tokens = ConfigTokenParser::parse(config_str, &delimiters, escape_pairs); for token in tokens.iter() { if !SqlUtil::is_valid_token(token, db_type, escape_pairs) { - return Err(Error::ConfigError { - error: format!("invalid filter config, check error near: {}", token), - }); + return Err(Error::ConfigError(format!( + "invalid filter config, check error near: {}", + token + ))); } } Ok(tokens) diff --git a/dt-common/src/utils/work_dir_util.rs b/dt-common/src/utils/work_dir_util.rs deleted file mode 100644 index f2bab818..00000000 --- a/dt-common/src/utils/work_dir_util.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub struct WorkDirUtil {} - -impl WorkDirUtil { - pub fn get_project_root() -> Option { - let mut project_root: Option = None; - - if let Ok(pr) = project_root::get_project_root() { - project_root = Some(String::from(pr.to_str().unwrap())); - } - - project_root - } - - pub fn get_absolute_by_relative(relative_path: &str) -> Option { - let path = if !relative_path.starts_with('/') { - format!("/{}", relative_path) - } else { - String::from(relative_path) - }; - - Some(format!( - "{}{}", - WorkDirUtil::get_project_root().unwrap(), - path - )) - } -} - -#[cfg(test)] -mod tests { - use super::WorkDirUtil; - - #[test] - fn get_project_root_test() { - let pr_option = WorkDirUtil::get_project_root(); - assert!(pr_option.is_some()); - println!("{}", pr_option.unwrap()); - } - - #[test] - fn get_absolute_by_relative_test() { - let mut path_option: Option; - path_option = WorkDirUtil::get_absolute_by_relative("fold/file.rs"); - assert!(path_option.is_some() && path_option.unwrap().ends_with("/fold/file.rs")); - path_option = WorkDirUtil::get_absolute_by_relative("/fold/file.rs"); - assert!(path_option.is_some() && path_option.unwrap().ends_with("/fold/file.rs")); - } -} diff --git a/dt-connector/Cargo.toml b/dt-connector/Cargo.toml index ecb6caad..71844e21 100644 --- a/dt-connector/Cargo.toml +++ b/dt-connector/Cargo.toml @@ -35,4 +35,8 @@ rdkafka = { workspace = true } kafka = { workspace = true } url = { workspace = true } log = { workspace = true } -log4rs = { workspace = true } \ No newline at end of file +log4rs = { workspace = true } +redis = { workspace = true } +thiserror = { workspace = true } +async-std = { workspace = true } +chrono = { workspace = true } \ No newline at end of file diff --git a/dt-connector/src/extractor/base_check_extractor.rs b/dt-connector/src/extractor/base_check_extractor.rs index 72090494..1416d34f 100644 --- a/dt-connector/src/extractor/base_check_extractor.rs +++ b/dt-connector/src/extractor/base_check_extractor.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::{atomic::AtomicBool, Arc}; use concurrent_queue::ConcurrentQueue; @@ -12,14 +12,14 @@ use crate::{ use super::base_extractor::BaseExtractor; -pub struct BaseCheckExtractor<'a> { +pub struct BaseCheckExtractor { pub check_log_dir: String, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub batch_size: usize, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } -impl BaseCheckExtractor<'_> { +impl BaseCheckExtractor { pub async fn extract( &mut self, extractor: &mut (dyn BatchCheckExtractor + Send), @@ -48,7 +48,7 @@ impl BaseCheckExtractor<'_> { } Self::batch_extract_and_clear(extractor, &mut batch).await; - BaseExtractor::wait_task_finish(self.buffer, self.shut_down).await + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await } async fn batch_extract_and_clear( diff --git a/dt-connector/src/extractor/kafka/kafka_extractor.rs b/dt-connector/src/extractor/kafka/kafka_extractor.rs new file mode 100644 index 00000000..f817e82b --- /dev/null +++ b/dt-connector/src/extractor/kafka/kafka_extractor.rs @@ -0,0 +1,67 @@ +use std::sync::{atomic::AtomicBool, Arc, Mutex}; + +use crate::{extractor::base_extractor::BaseExtractor, Extractor}; + +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::{error::Error, log_info, syncer::Syncer}; +use dt_meta::dt_data::DtData; +use rdkafka::{ + consumer::{Consumer, StreamConsumer}, + ClientConfig, Message, Offset, TopicPartitionList, +}; + +pub struct KafkaExtractor { + pub buffer: Arc>, + pub shut_down: Arc, + pub url: String, + pub group: String, + pub topic: String, + pub partition: i32, + pub offset: i64, + pub ack_interval_secs: u64, + pub syncer: Arc>, +} + +#[async_trait] +impl Extractor for KafkaExtractor { + async fn extract(&mut self) -> Result<(), Error> { + let consumer = self.create_consumer(); + log_info!("KafkaCdcExtractor starts"); + loop { + let msg = consumer.recv().await.unwrap(); + let msg_position = format!("offset:{}", msg.offset()); + if let Some(payload) = msg.payload() { + let mut dt_data: DtData = serde_json::from_slice(payload).unwrap(); + match &mut dt_data { + DtData::Commit { position, .. } => *position = msg_position, + DtData::Dml { row_data } => row_data.position = msg_position, + _ => {} + }; + BaseExtractor::push_dt_data(&self.buffer, dt_data).await?; + } + } + } +} + +impl KafkaExtractor { + fn create_consumer(&self) -> StreamConsumer { + let mut config = ClientConfig::new(); + config.set("bootstrap.servers", &self.url); + config.set("group.id", &self.group); + config.set("auto.offset.reset", "latest"); + config.set("session.timeout.ms", "10000"); + + let consumer: StreamConsumer = config.create().unwrap(); + // only support extract data from one topic, one partition + let mut tpl = TopicPartitionList::new(); + if self.offset > 0 { + tpl.add_partition_offset(&self.topic, self.partition, Offset::Offset(self.offset)) + .unwrap(); + } else { + tpl.add_partition(&self.topic, self.partition); + } + consumer.assign(&tpl).unwrap(); + consumer + } +} diff --git a/dt-connector/src/extractor/kafka/mod.rs b/dt-connector/src/extractor/kafka/mod.rs new file mode 100644 index 00000000..6a9432fc --- /dev/null +++ b/dt-connector/src/extractor/kafka/mod.rs @@ -0,0 +1 @@ +pub mod kafka_extractor; diff --git a/dt-connector/src/extractor/mod.rs b/dt-connector/src/extractor/mod.rs index 814eb14e..eca3e7f8 100644 --- a/dt-connector/src/extractor/mod.rs +++ b/dt-connector/src/extractor/mod.rs @@ -1,6 +1,8 @@ pub mod base_check_extractor; pub mod base_extractor; +pub mod kafka; pub mod mongo; pub mod mysql; pub mod pg; +pub mod redis; pub mod snapshot_resumer; diff --git a/dt-connector/src/extractor/mongo/mongo_cdc_extractor.rs b/dt-connector/src/extractor/mongo/mongo_cdc_extractor.rs index 6e2dd4ee..bc0ff770 100644 --- a/dt-connector/src/extractor/mongo/mongo_cdc_extractor.rs +++ b/dt-connector/src/extractor/mongo/mongo_cdc_extractor.rs @@ -1,16 +1,25 @@ -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use async_trait::async_trait; +use chrono::Utc; use concurrent_queue::ConcurrentQueue; use dt_common::{ - constants::MongoConstants, error::Error, log_info, utils::{position_util::PositionUtil, rdb_filter::RdbFilter}, }; -use dt_meta::{col_value::ColValue, dt_data::DtData, row_data::RowData, row_type::RowType}; +use dt_meta::{ + col_value::ColValue, + dt_data::DtData, + mongo::{mongo_cdc_source::MongoCdcSource, mongo_constant::MongoConstants}, + row_data::RowData, + row_type::RowType, +}; use mongodb::{ - bson::{doc, Timestamp}, + bson::{doc, Document, Timestamp}, change_stream::event::{OperationType, ResumeToken}, options::{ChangeStreamOptions, FullDocumentBeforeChangeType, FullDocumentType}, Client, @@ -19,52 +28,154 @@ use serde_json::json; use crate::{extractor::base_extractor::BaseExtractor, Extractor}; -pub struct MongoCdcExtractor<'a> { - pub buffer: &'a ConcurrentQueue, +const SYSTEM_DBS: [&str; 3] = ["admin", "config", "local"]; + +pub struct MongoCdcExtractor { + pub buffer: Arc>, pub filter: RdbFilter, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, pub resume_token: String, - pub start_timestamp: i64, + pub start_timestamp: u32, + pub source: MongoCdcSource, pub mongo_client: Client, } #[async_trait] -impl Extractor for MongoCdcExtractor<'_> { +impl Extractor for MongoCdcExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( - "MongoCdcExtractor starts, resume_token: {} ", - self.resume_token + "MongoCdcExtractor starts, resume_token: {}, start_timestamp: {}, source: {:?} ", + self.resume_token, + self.start_timestamp, + self.source, ); - self.extract_internal().await - } - async fn close(&mut self) -> Result<(), Error> { - Ok(()) + match self.source { + MongoCdcSource::OpLog => self.extract_oplog().await, + MongoCdcSource::ChangeStream => self.extract_change_stream().await, + } } } -impl MongoCdcExtractor<'_> { - async fn extract_internal(&mut self) -> Result<(), Error> { - let mut start_timestamp_option: Option = None; - let mut start_after: Option = None; - - if self.resume_token.is_empty() { - start_timestamp_option = if self.start_timestamp > 0 { - Some(Timestamp { - time: self.start_timestamp as u32, - increment: 0, - }) +impl MongoCdcExtractor { + async fn extract_oplog(&mut self) -> Result<(), Error> { + let start_timestamp = self.parse_start_timestamp(); + let filter = doc! { + "ts": { "$gte": start_timestamp } + }; + let options = mongodb::options::FindOptions::builder() + .cursor_type(mongodb::options::CursorType::TailableAwait) + .build(); + + let oplog = self + .mongo_client + .database("local") + .collection::("oplog.rs"); + let mut cursor = oplog.find(filter, options).await.unwrap(); + + while cursor.advance().await.unwrap() { + let doc: Document = cursor.deserialize_current().unwrap(); + // https://github.com/mongodb/mongo/blob/master/src/mongo/db/repl/oplog.cpp + // op: + // "i" insert + // "u" update + // "d" delete + // "c" db cmd + // "n" no op + // "xi" insert global index key + // "xd" delete global index key + let op = if let Some(op) = doc.get("op") { + op.as_str().unwrap() } else { - None + "" }; + + let mut row_type = RowType::Insert; + let mut before = HashMap::new(); + let mut after = HashMap::new(); + let o = doc.get("o"); + let o2 = doc.get("o2"); + match op { + "i" => { + after.insert( + MongoConstants::DOC.to_string(), + ColValue::MongoDoc(o.unwrap().as_document().unwrap().clone()), + ); + } + "u" => { + row_type = RowType::Update; + // for update op log, doc.o contains only diff instead of full doc + let after_doc = o.unwrap().as_document().unwrap(); + let diff_doc = after_doc.get("diff").unwrap().as_document().unwrap(); + let u_doc = diff_doc.get("u").unwrap().as_document().unwrap(); + after.insert( + MongoConstants::DIFF_DOC.to_string(), + ColValue::MongoDoc(u_doc.clone()), + ); + before.insert( + MongoConstants::DOC.to_string(), + ColValue::MongoDoc(o2.unwrap().as_document().unwrap().clone()), + ); + } + "d" => { + row_type = RowType::Delete; + before.insert( + MongoConstants::DOC.to_string(), + ColValue::MongoDoc(o.unwrap().as_document().unwrap().clone()), + ); + } + // TODO, DDL + "c" | "xi" | "xd" => { + continue; + } + "n" => { + // TODO, heartbeat + // Document({"op": String("n"), "ns": String(""), "o": Document({"msg": String("periodic noop")}), "ts": Timestamp { time: 1693470874, increment: 1 }, "t": Int64(67), "v": Int64(2), "wall": DateTime(2023-08-31 8:34:34.19 +00:00:00)}) + continue; + } + _ => { + continue; + } + } + + // get db & tb + let ns = doc.get("ns").unwrap().as_str().unwrap(); + let tokens: Vec<&str> = ns.split(".").collect(); + let db: String = tokens[0].into(); + let tb: String = ns[db.len() + 1..].into(); + + // get ts for position + let ts = doc.get("ts").unwrap().as_timestamp().unwrap(); + let position = format!( + "resume_token:,operation_time:{},timestamp:{}", + ts.time, + PositionUtil::format_timestamp_millis(ts.time as i64 * 1000) + ); + + let row_data = RowData { + schema: db, + tb, + row_type, + position, + before: Some(before), + after: Some(after), + }; + self.push_row_to_buf(row_data).await.unwrap(); + } + Ok(()) + } + + async fn extract_change_stream(&mut self) -> Result<(), Error> { + let (resume_token, start_timestamp) = if self.resume_token.is_empty() { + (None, Some(self.parse_start_timestamp())) } else { let token: ResumeToken = serde_json::from_str(&self.resume_token).unwrap(); - start_after = Some(token) + (Some(token), None) }; let stream_options = ChangeStreamOptions::builder() - .start_at_operation_time(start_timestamp_option) - .start_after(start_after) + .start_at_operation_time(start_timestamp) + .start_after(resume_token) .full_document(Some(FullDocumentType::UpdateLookup)) .full_document_before_change(Some(FullDocumentBeforeChangeType::WhenAvailable)) .build(); @@ -118,25 +229,22 @@ impl MongoCdcExtractor<'_> { OperationType::Update | OperationType::Replace => { row_type = RowType::Update; - if let Some(document) = doc.full_document { - let id = document.get_object_id(MongoConstants::ID).unwrap(); - - let before_doc = doc! {MongoConstants::ID: id}; - let after_doc = document; - before.insert( MongoConstants::DOC.to_string(), - ColValue::MongoDoc(before_doc), + ColValue::MongoDoc(doc.document_key.unwrap()), ); after.insert( MongoConstants::DOC.to_string(), - ColValue::MongoDoc(after_doc), + ColValue::MongoDoc(document), ); } } - _ => {} + // TODO, heartbeat and DDL + _ => { + continue; + } } let row_data = RowData { @@ -151,17 +259,26 @@ impl MongoCdcExtractor<'_> { } } } -} -impl MongoCdcExtractor<'_> { async fn push_row_to_buf(&mut self, row_data: RowData) -> Result<(), Error> { - if self.filter.filter_event( - &row_data.schema, - &row_data.tb, - &row_data.row_type.to_string(), - ) { + if SYSTEM_DBS.contains(&row_data.schema.as_str()) + || self.filter.filter_event( + &row_data.schema, + &row_data.tb, + &row_data.row_type.to_string(), + ) + { return Ok(()); } - BaseExtractor::push_row(self.buffer, row_data).await + BaseExtractor::push_row(self.buffer.as_ref(), row_data).await + } + + fn parse_start_timestamp(&mut self) -> Timestamp { + let time = if self.start_timestamp > 0 { + self.start_timestamp + } else { + Utc::now().timestamp() as u32 + }; + Timestamp { time, increment: 0 } } } diff --git a/dt-connector/src/extractor/mongo/mongo_snapshot_extractor.rs b/dt-connector/src/extractor/mongo/mongo_snapshot_extractor.rs index 4a78f923..34ebccdd 100644 --- a/dt-connector/src/extractor/mongo/mongo_snapshot_extractor.rs +++ b/dt-connector/src/extractor/mongo/mongo_snapshot_extractor.rs @@ -1,14 +1,20 @@ use std::{ collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; -use dt_common::{constants::MongoConstants, error::Error, log_info, utils::time_util::TimeUtil}; -use dt_meta::{col_value::ColValue, dt_data::DtData, row_data::RowData, row_type::RowType}; +use dt_common::{error::Error, log_info, utils::time_util::TimeUtil}; +use dt_meta::{ + col_value::ColValue, dt_data::DtData, mongo::mongo_constant::MongoConstants, row_data::RowData, + row_type::RowType, +}; use mongodb::{ - bson::{doc, oid::ObjectId, Document}, + bson::{doc, oid::ObjectId, Bson, Document}, options::FindOptions, Client, }; @@ -18,17 +24,17 @@ use crate::{ Extractor, }; -pub struct MongoSnapshotExtractor<'a> { +pub struct MongoSnapshotExtractor { pub resumer: SnapshotResumer, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub db: String, pub tb: String, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, pub mongo_client: Client, } #[async_trait] -impl Extractor for MongoSnapshotExtractor<'_> { +impl Extractor for MongoSnapshotExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "MongoSnapshotExtractor starts, schema: {}, tb: {}", @@ -37,13 +43,9 @@ impl Extractor for MongoSnapshotExtractor<'_> { ); self.extract_internal().await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } -impl MongoSnapshotExtractor<'_> { +impl MongoSnapshotExtractor { pub async fn extract_internal(&mut self) -> Result<(), Error> { log_info!("start extracting data from {}.{}", self.db, self.tb); @@ -70,8 +72,7 @@ impl MongoSnapshotExtractor<'_> { let mut cursor = collection.find(filter, find_options).await.unwrap(); while cursor.advance().await.unwrap() { let doc = cursor.deserialize_current().unwrap(); - - let id = doc.get_object_id(MongoConstants::ID).unwrap().to_string(); + let id = Self::get_object_id(&doc); let mut after = HashMap::new(); after.insert(MongoConstants::DOC.to_string(), ColValue::MongoDoc(doc)); @@ -84,7 +85,7 @@ impl MongoSnapshotExtractor<'_> { before: None, }; - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); all_count += 1; @@ -105,4 +106,14 @@ impl MongoSnapshotExtractor<'_> { self.shut_down.store(true, Ordering::Release); Ok(()) } + + fn get_object_id(doc: &Document) -> String { + if let Some(id) = doc.get(MongoConstants::ID) { + match id { + Bson::ObjectId(v) => return v.to_string(), + _ => return String::new(), + } + } + String::new() + } } diff --git a/dt-connector/src/extractor/mysql/mysql_cdc_extractor.rs b/dt-connector/src/extractor/mysql/mysql_cdc_extractor.rs index 187c2b35..22fb0190 100644 --- a/dt-connector/src/extractor/mysql/mysql_cdc_extractor.rs +++ b/dt-connector/src/extractor/mysql/mysql_cdc_extractor.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use async_recursion::async_recursion; use async_trait::async_trait; @@ -13,34 +16,34 @@ use dt_meta::{ use mysql_binlog_connector_rust::{ binlog_client::BinlogClient, event::{ - event_data::EventData, event_header::EventHeader, row_event::RowEvent, - table_map_event::TableMapEvent, + event_data::EventData, event_header::EventHeader, query_event::QueryEvent, + row_event::RowEvent, table_map_event::TableMapEvent, }, }; use dt_common::{ error::Error, - log_info, + log_error, log_info, utils::{position_util::PositionUtil, rdb_filter::RdbFilter}, }; use crate::{extractor::base_extractor::BaseExtractor, Extractor}; -pub struct MysqlCdcExtractor<'a> { +pub struct MysqlCdcExtractor { pub meta_manager: MysqlMetaManager, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub filter: RdbFilter, pub url: String, pub binlog_filename: String, pub binlog_position: u32, pub server_id: u64, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } const QUERY_BEGIN: &str = "BEGIN"; #[async_trait] -impl Extractor for MysqlCdcExtractor<'_> { +impl Extractor for MysqlCdcExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "MysqlCdcExtractor starts, binlog_filename: {}, binlog_position: {}", @@ -49,13 +52,9 @@ impl Extractor for MysqlCdcExtractor<'_> { ); self.extract_internal().await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } -impl MysqlCdcExtractor<'_> { +impl MysqlCdcExtractor { async fn extract_internal(&mut self) -> Result<(), Error> { let mut client = BinlogClient { url: self.url.clone(), @@ -167,22 +166,7 @@ impl MysqlCdcExtractor<'_> { } EventData::Query(query) => { - let mut ddl_data = DdlData { - schema: query.schema, - query: query.query.clone(), - ddl_type: DdlType::Unknown, - meta: None, - }; - - if query.query != QUERY_BEGIN { - if let Ok((ddl_type, schema, _)) = DdlParser::parse(&query.query) { - ddl_data.ddl_type = ddl_type; - if let Some(schema) = schema { - ddl_data.schema = schema; - } - } - BaseExtractor::push_dt_data(self.buffer, DtData::Ddl { ddl_data }).await?; - } + self.handle_query_event(query).await?; } EventData::Xid(xid) => { @@ -190,7 +174,7 @@ impl MysqlCdcExtractor<'_> { xid: xid.xid.to_string(), position: position.to_string(), }; - BaseExtractor::push_dt_data(self.buffer, commit).await?; + BaseExtractor::push_dt_data(self.buffer.as_ref(), commit).await?; } _ => {} @@ -207,7 +191,7 @@ impl MysqlCdcExtractor<'_> { ) { return Ok(()); } - BaseExtractor::push_row(self.buffer, row_data).await + BaseExtractor::push_row(self.buffer.as_ref(), row_data).await } async fn parse_row_data( @@ -222,7 +206,9 @@ impl MysqlCdcExtractor<'_> { .await?; if included_columns.len() != event.column_values.len() { - return Err(Error::ColumnNotMatch); + return Err(Error::ExtractorError( + "included_columns not match column_values in binlog".into(), + )); } let mut data = HashMap::new(); @@ -235,9 +221,66 @@ impl MysqlCdcExtractor<'_> { let col_type = tb_meta.col_type_map.get(key).unwrap(); let raw_value = event.column_values.remove(i); - let value = MysqlColValueConvertor::from_binlog(col_type, raw_value); + let value = MysqlColValueConvertor::from_binlog(col_type, raw_value)?; data.insert(key.clone(), value); } Ok(data) } + + async fn handle_query_event(&mut self, query: QueryEvent) -> Result<(), Error> { + if query.query == QUERY_BEGIN { + return Ok(()); + } + + log_info!("received ddl: {:?}", query); + let mut ddl_data = DdlData { + schema: query.schema, + tb: String::new(), + query: query.query.clone(), + ddl_type: DdlType::Unknown, + meta: None, + }; + + let parse_result = DdlParser::parse(&query.query); + if let Err(error) = parse_result { + // clear all metadata cache + self.meta_manager.invalidate_cache("", ""); + log_error!( + "failed to parse ddl, will try ignore it, please execute the ddl manually in target, sql: {}, error: {}", + ddl_data.query, + error + ); + return Ok(()); + } + + // case 1, execute: use db_1; create table tb_1(id int); + // binlog query.schema == db_1, schema from DdlParser == None + // case 2, execute: create table db_1.tb_1(id int); + // binlog query.schema == empty, schema from DdlParser == db_1 + // case 3, execute: use db_1; create table db_2.tb_1(id int); + // binlog query.schema == db_1, schema from DdlParser == db_2 + let (ddl_type, schema, tb) = parse_result.unwrap(); + ddl_data.ddl_type = ddl_type; + if let Some(schema) = schema { + ddl_data.schema = schema; + } + if let Some(tb) = tb { + ddl_data.tb = tb; + } + + // invalidate metadata cache + self.meta_manager + .invalidate_cache(&ddl_data.schema, &ddl_data.tb); + + let filter = if ddl_data.tb.is_empty() { + self.filter.filter_db(&ddl_data.schema) + } else { + self.filter.filter_tb(&ddl_data.schema, &ddl_data.tb) + }; + + if !self.filter.filter_ddl() && !filter { + BaseExtractor::push_dt_data(self.buffer.as_ref(), DtData::Ddl { ddl_data }).await?; + } + Ok(()) + } } diff --git a/dt-connector/src/extractor/mysql/mysql_check_extractor.rs b/dt-connector/src/extractor/mysql/mysql_check_extractor.rs index 0e4fa6ac..a7277cfa 100644 --- a/dt-connector/src/extractor/mysql/mysql_check_extractor.rs +++ b/dt-connector/src/extractor/mysql/mysql_check_extractor.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -21,17 +24,17 @@ use crate::{ BatchCheckExtractor, Extractor, }; -pub struct MysqlCheckExtractor<'a> { +pub struct MysqlCheckExtractor { pub conn_pool: Pool, pub meta_manager: MysqlMetaManager, pub check_log_dir: String, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub batch_size: usize, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for MysqlCheckExtractor<'_> { +impl Extractor for MysqlCheckExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "MysqlCheckExtractor starts, check_log_dir: {}", @@ -40,21 +43,17 @@ impl Extractor for MysqlCheckExtractor<'_> { let mut base_check_extractor = BaseCheckExtractor { check_log_dir: self.check_log_dir.clone(), - buffer: self.buffer, + buffer: self.buffer.clone(), batch_size: self.batch_size, - shut_down: self.shut_down, + shut_down: self.shut_down.clone(), }; base_check_extractor.extract(self).await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } #[async_trait] -impl BatchCheckExtractor for MysqlCheckExtractor<'_> { +impl BatchCheckExtractor for MysqlCheckExtractor { async fn batch_extract(&mut self, check_logs: &[CheckLog]) -> Result<(), Error> { if check_logs.is_empty() { return Ok(()); @@ -84,7 +83,7 @@ impl BatchCheckExtractor for MysqlCheckExtractor<'_> { row_data.before = row_data.after.clone(); } - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); } @@ -92,7 +91,7 @@ impl BatchCheckExtractor for MysqlCheckExtractor<'_> { } } -impl MysqlCheckExtractor<'_> { +impl MysqlCheckExtractor { fn build_check_row_datas( check_logs: &[CheckLog], tb_meta: &MysqlTbMeta, diff --git a/dt-connector/src/extractor/mysql/mysql_snapshot_extractor.rs b/dt-connector/src/extractor/mysql/mysql_snapshot_extractor.rs index 085e529d..ff415fa5 100644 --- a/dt-connector/src/extractor/mysql/mysql_snapshot_extractor.rs +++ b/dt-connector/src/extractor/mysql/mysql_snapshot_extractor.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::{atomic::AtomicBool, Arc}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -23,19 +23,19 @@ use crate::{ Extractor, }; -pub struct MysqlSnapshotExtractor<'a> { +pub struct MysqlSnapshotExtractor { pub conn_pool: Pool, pub meta_manager: MysqlMetaManager, pub resumer: SnapshotResumer, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub slice_size: usize, pub db: String, pub tb: String, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for MysqlSnapshotExtractor<'_> { +impl Extractor for MysqlSnapshotExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "MysqlSnapshotExtractor starts, schema: `{}`, tb: `{}`, slice_size: {}", @@ -55,7 +55,7 @@ impl Extractor for MysqlSnapshotExtractor<'_> { } } -impl MysqlSnapshotExtractor<'_> { +impl MysqlSnapshotExtractor { async fn extract_internal(&mut self) -> Result<(), Error> { let tb_meta = self.meta_manager.get_tb_meta(&self.db, &self.tb).await?; @@ -75,7 +75,7 @@ impl MysqlSnapshotExtractor<'_> { self.extract_all(&tb_meta).await?; } - BaseExtractor::wait_task_finish(self.buffer, self.shut_down).await + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await } async fn extract_all(&mut self, tb_meta: &MysqlTbMeta) -> Result<(), Error> { @@ -90,7 +90,7 @@ impl MysqlSnapshotExtractor<'_> { let mut rows = sqlx::query(&sql).fetch(&self.conn_pool); while let Some(row) = rows.try_next().await.unwrap() { let row_data = RowData::from_mysql_row(&row, tb_meta); - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); all_count += 1; @@ -146,7 +146,7 @@ impl MysqlSnapshotExtractor<'_> { if let Some(value) = start_value.to_option_string() { row_data.position = format!("`{}`:{}", order_col, value) } - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); slice_count += 1; diff --git a/dt-connector/src/extractor/mysql/mysql_struct_extractor.rs b/dt-connector/src/extractor/mysql/mysql_struct_extractor.rs index 4bd969a2..51eecc6f 100644 --- a/dt-connector/src/extractor/mysql/mysql_struct_extractor.rs +++ b/dt-connector/src/extractor/mysql/mysql_struct_extractor.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::{atomic::AtomicBool, Arc}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -14,27 +14,23 @@ use crate::{ meta_fetcher::mysql::mysql_struct_fetcher::MysqlStructFetcher, Extractor, }; -pub struct MysqlStructExtractor<'a> { +pub struct MysqlStructExtractor { pub conn_pool: Pool, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub db: String, pub filter: RdbFilter, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for MysqlStructExtractor<'_> { +impl Extractor for MysqlStructExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!("MysqlStructExtractor starts, schema: {}", self.db,); self.extract_internal().await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } -impl MysqlStructExtractor<'_> { +impl MysqlStructExtractor { pub async fn extract_internal(&mut self) -> Result<(), Error> { let mut mysql_fetcher = MysqlStructFetcher { conn_pool: self.conn_pool.to_owned(), @@ -54,17 +50,18 @@ impl MysqlStructExtractor<'_> { self.push_dt_data(&meta).await; } - BaseExtractor::wait_task_finish(self.buffer, self.shut_down).await + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await } pub async fn push_dt_data(&mut self, meta: &StructModel) { let ddl_data = DdlData { schema: self.db.clone(), + tb: String::new(), query: String::new(), meta: Some(meta.to_owned()), ddl_type: DdlType::Unknown, }; - BaseExtractor::push_dt_data(self.buffer, DtData::Ddl { ddl_data }) + BaseExtractor::push_dt_data(self.buffer.as_ref(), DtData::Ddl { ddl_data }) .await .unwrap() } diff --git a/dt-connector/src/extractor/pg/pg_cdc_client.rs b/dt-connector/src/extractor/pg/pg_cdc_client.rs index 66000d57..e136a323 100644 --- a/dt-connector/src/extractor/pg/pg_cdc_client.rs +++ b/dt-connector/src/extractor/pg/pg_cdc_client.rs @@ -81,9 +81,10 @@ impl PgCdcClient { start_lsn = if let Row(row) = &res[0] { row.get("consistent_point").unwrap().to_string() } else { - return Err(Error::MetadataError { - error: format!("failed in: {}", query), - }); + return Err(Error::ExtractorError(format!( + "failed to create replication slot by query: {}", + query + ))); }; } diff --git a/dt-connector/src/extractor/pg/pg_cdc_extractor.rs b/dt-connector/src/extractor/pg/pg_cdc_extractor.rs index c4f27fb8..c811aa2d 100644 --- a/dt-connector/src/extractor/pg/pg_cdc_extractor.rs +++ b/dt-connector/src/extractor/pg/pg_cdc_extractor.rs @@ -41,22 +41,22 @@ use dt_meta::{ row_type::RowType, }; -pub struct PgCdcExtractor<'a> { +pub struct PgCdcExtractor { pub meta_manager: PgMetaManager, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub filter: RdbFilter, pub url: String, pub slot_name: String, pub start_lsn: String, pub heartbeat_interval_secs: u64, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, pub syncer: Arc>, } const SECS_FROM_1970_TO_2000: i64 = 946_684_800; #[async_trait] -impl Extractor for PgCdcExtractor<'_> { +impl Extractor for PgCdcExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "PgCdcExtractor starts, slot_name: {}, start_lsn: {}, heartbeat_interval_secs: {}", @@ -67,13 +67,9 @@ impl Extractor for PgCdcExtractor<'_> { self.extract_internal().await.unwrap(); Ok(()) } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } -impl PgCdcExtractor<'_> { +impl PgCdcExtractor { async fn extract_internal(&mut self) -> Result<(), Error> { let mut cdc_client = PgCdcClient { url: self.url.clone(), @@ -335,9 +331,9 @@ impl PgCdcExtractor<'_> { } TupleData::UnchangedToast => { - return Err(Error::Unexpected { - error: "unexpected UnchangedToast value received".to_string(), - }) + return Err(Error::ExtractorError( + "unexpected UnchangedToast value received".into(), + )) } } } diff --git a/dt-connector/src/extractor/pg/pg_check_extractor.rs b/dt-connector/src/extractor/pg/pg_check_extractor.rs index 37e8b425..9c67a3c9 100644 --- a/dt-connector/src/extractor/pg/pg_check_extractor.rs +++ b/dt-connector/src/extractor/pg/pg_check_extractor.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -25,17 +28,17 @@ use crate::{ BatchCheckExtractor, Extractor, }; -pub struct PgCheckExtractor<'a> { +pub struct PgCheckExtractor { pub conn_pool: Pool, pub meta_manager: PgMetaManager, pub check_log_dir: String, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub batch_size: usize, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for PgCheckExtractor<'_> { +impl Extractor for PgCheckExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( "PgCheckExtractor starts, check_log_dir: {}", @@ -44,21 +47,17 @@ impl Extractor for PgCheckExtractor<'_> { let mut base_check_extractor = BaseCheckExtractor { check_log_dir: self.check_log_dir.clone(), - buffer: self.buffer, + buffer: self.buffer.clone(), batch_size: self.batch_size, - shut_down: self.shut_down, + shut_down: self.shut_down.clone(), }; base_check_extractor.extract(self).await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } #[async_trait] -impl BatchCheckExtractor for PgCheckExtractor<'_> { +impl BatchCheckExtractor for PgCheckExtractor { async fn batch_extract(&mut self, check_logs: &[CheckLog]) -> Result<(), Error> { if check_logs.is_empty() { return Ok(()); @@ -88,7 +87,7 @@ impl BatchCheckExtractor for PgCheckExtractor<'_> { row_data.before = row_data.after.clone(); } - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); } @@ -97,7 +96,7 @@ impl BatchCheckExtractor for PgCheckExtractor<'_> { } } -impl PgCheckExtractor<'_> { +impl PgCheckExtractor { fn build_check_row_datas( &mut self, check_logs: &[CheckLog], diff --git a/dt-connector/src/extractor/pg/pg_snapshot_extractor.rs b/dt-connector/src/extractor/pg/pg_snapshot_extractor.rs index a6f85749..3575849b 100644 --- a/dt-connector/src/extractor/pg/pg_snapshot_extractor.rs +++ b/dt-connector/src/extractor/pg/pg_snapshot_extractor.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::{atomic::AtomicBool, Arc}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -24,19 +24,19 @@ use crate::{ Extractor, }; -pub struct PgSnapshotExtractor<'a> { +pub struct PgSnapshotExtractor { pub conn_pool: Pool, pub meta_manager: PgMetaManager, pub resumer: SnapshotResumer, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub slice_size: usize, pub schema: String, pub tb: String, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for PgSnapshotExtractor<'_> { +impl Extractor for PgSnapshotExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!( r#"PgSnapshotExtractor starts, schema: "{}", tb: "{}", slice_size: {}"#, @@ -56,7 +56,7 @@ impl Extractor for PgSnapshotExtractor<'_> { } } -impl PgSnapshotExtractor<'_> { +impl PgSnapshotExtractor { async fn extract_internal(&mut self) -> Result<(), Error> { let tb_meta = self .meta_manager @@ -82,7 +82,7 @@ impl PgSnapshotExtractor<'_> { self.extract_all(&tb_meta).await?; } - BaseExtractor::wait_task_finish(self.buffer, self.shut_down).await + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await } async fn extract_all(&mut self, tb_meta: &PgTbMeta) -> Result<(), Error> { @@ -97,7 +97,7 @@ impl PgSnapshotExtractor<'_> { let mut rows = sqlx::query(&sql).fetch(&self.conn_pool); while let Some(row) = rows.try_next().await.unwrap() { let row_data = RowData::from_pg_row(&row, tb_meta); - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); all_count += 1; @@ -146,7 +146,7 @@ impl PgSnapshotExtractor<'_> { if let Some(value) = start_value.to_option_string() { row_data.position = format!("{}:{}", order_col, value) } - BaseExtractor::push_row(self.buffer, row_data) + BaseExtractor::push_row(self.buffer.as_ref(), row_data) .await .unwrap(); slice_count += 1; diff --git a/dt-connector/src/extractor/pg/pg_struct_extractor.rs b/dt-connector/src/extractor/pg/pg_struct_extractor.rs index 1678c4a7..0bc98a75 100644 --- a/dt-connector/src/extractor/pg/pg_struct_extractor.rs +++ b/dt-connector/src/extractor/pg/pg_struct_extractor.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::{atomic::AtomicBool, Arc}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; @@ -15,27 +15,23 @@ use crate::{ Extractor, }; -pub struct PgStructExtractor<'a> { +pub struct PgStructExtractor { pub conn_pool: Pool, - pub buffer: &'a ConcurrentQueue, + pub buffer: Arc>, pub db: String, pub filter: RdbFilter, - pub shut_down: &'a AtomicBool, + pub shut_down: Arc, } #[async_trait] -impl Extractor for PgStructExtractor<'_> { +impl Extractor for PgStructExtractor { async fn extract(&mut self) -> Result<(), Error> { log_info!("PgStructExtractor starts, schema: {}", self.db,); self.extract_internal().await } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } -impl PgStructExtractor<'_> { +impl PgStructExtractor { pub async fn extract_internal(&mut self) -> Result<(), Error> { let mut pg_fetcher = PgStructFetcher { conn_pool: self.conn_pool.to_owned(), @@ -71,17 +67,18 @@ impl PgStructExtractor<'_> { self.push_dt_data(&column_comment).await; } - BaseExtractor::wait_task_finish(self.buffer, self.shut_down).await + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await } pub async fn push_dt_data(&mut self, meta: &StructModel) { let ddl_data = DdlData { schema: self.db.clone(), + tb: String::new(), query: String::new(), meta: Some(meta.to_owned()), ddl_type: DdlType::Unknown, }; - BaseExtractor::push_dt_data(self.buffer, DtData::Ddl { ddl_data }) + BaseExtractor::push_dt_data(self.buffer.as_ref(), DtData::Ddl { ddl_data }) .await .unwrap() } diff --git a/dt-connector/src/extractor/redis/mod.rs b/dt-connector/src/extractor/redis/mod.rs new file mode 100644 index 00000000..c59dfe38 --- /dev/null +++ b/dt-connector/src/extractor/redis/mod.rs @@ -0,0 +1,23 @@ +use std::io::{Cursor, Read}; + +use dt_common::error::Error; + +pub mod rdb; +pub mod redis_cdc_extractor; +pub mod redis_client; +pub mod redis_psync_extractor; +pub mod redis_resp_reader; +pub mod redis_resp_types; +pub mod redis_snapshot_extractor; + +pub trait RawByteReader { + fn read_raw(&mut self, size: usize) -> Result, Error>; +} + +impl RawByteReader for Cursor<&[u8]> { + fn read_raw(&mut self, size: usize) -> Result, Error> { + let mut buf = vec![0; size]; + self.read_exact(&mut buf)?; + Ok(buf) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/entry_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/entry_parser.rs new file mode 100644 index 00000000..22ddc832 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/entry_parser.rs @@ -0,0 +1,66 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{RedisObject, RedisString}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +use super::{ + hash_parser::HashLoader, list_parser::ListLoader, module2_parser::ModuleLoader, + set_parser::SetLoader, stream_parser::StreamLoader, string_parser::StringLoader, + zset_parser::ZsetLoader, +}; + +pub struct EntryParser {} + +impl EntryParser { + pub fn parse_object( + reader: &mut RdbReader, + type_byte: u8, + key: RedisString, + ) -> Result { + let obj = match type_byte { + super::RDB_TYPE_STRING => { + RedisObject::String(StringLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_LIST + | super::RDB_TYPE_LIST_ZIP_LIST + | super::RDB_TYPE_LIST_QUICK_LIST + | super::RDB_TYPE_LIST_QUICK_LIST_2 => { + RedisObject::List(ListLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_SET | super::RDB_TYPE_SET_INT_SET => { + RedisObject::Set(SetLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_ZSET + | super::RDB_TYPE_ZSET_2 + | super::RDB_TYPE_ZSET_ZIP_LIST + | super::RDB_TYPE_ZSET_LIST_PACK => { + RedisObject::Zset(ZsetLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_HASH + | super::RDB_TYPE_HASH_ZIP_MAP + | super::RDB_TYPE_HASH_ZIP_LIST + | super::RDB_TYPE_HASH_LIST_PACK => { + RedisObject::Hash(HashLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_STREAM_LIST_PACKS | super::RDB_TYPE_STREAM_LIST_PACKS_2 => { + RedisObject::Stream(StreamLoader::load_from_buffer(reader, key, type_byte)?) + } + + super::RDB_TYPE_MODULE | super::RDB_TYPE_MODULE_2 => { + RedisObject::Module(ModuleLoader::load_from_buffer(reader, key, type_byte)?) + } + + _ => { + log::error!("unknown type byte: {}", type_byte); + RedisObject::Unknown + } + }; + + Ok(obj) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/hash_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/hash_parser.rs new file mode 100644 index 00000000..aff68f87 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/hash_parser.rs @@ -0,0 +1,69 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{HashObject, RedisString}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +pub struct HashLoader {} + +impl HashLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + type_byte: u8, + ) -> Result { + let mut obj = HashObject::new(); + obj.key = key; + + match type_byte { + super::RDB_TYPE_HASH => Self::read_hash(&mut obj, reader)?, + super::RDB_TYPE_HASH_ZIP_MAP => Self::read_hash_zip_map(&mut obj, reader)?, + super::RDB_TYPE_HASH_ZIP_LIST => Self::read_hash_zip_list(&mut obj, reader)?, + super::RDB_TYPE_HASH_LIST_PACK => Self::read_hash_list_pack(&mut obj, reader)?, + _ => { + return Err(Error::RedisRdbError(format!( + "unknown hash type. type_byte=[{}]", + type_byte + ))) + } + } + Ok(obj) + } + + fn read_hash(obj: &mut HashObject, reader: &mut RdbReader) -> Result<(), Error> { + let size = reader.read_length()?; + for _ in 0..size { + let key = reader.read_string()?; + let value = reader.read_string()?; + obj.value.insert(key, value); + } + Ok(()) + } + + fn read_hash_zip_map(_obj: &mut HashObject, _reader: &mut RdbReader) -> Result<(), Error> { + Err(Error::RedisRdbError( + "not implemented rdb_type_zip_map".to_string(), + )) + } + + fn read_hash_zip_list(obj: &mut HashObject, reader: &mut RdbReader) -> Result<(), Error> { + let list = reader.read_zip_list()?; + let size = list.len(); + for i in (0..size).step_by(2) { + let key = list[i].clone(); + let value = list[i + 1].clone(); + obj.value.insert(key, value); + } + Ok(()) + } + + pub fn read_hash_list_pack(obj: &mut HashObject, reader: &mut RdbReader) -> Result<(), Error> { + let list = reader.read_list_pack()?; + let size = list.len(); + for i in (0..size).step_by(2) { + let key = list[i].clone(); + let value = list[i + 1].clone(); + obj.value.insert(key, value); + } + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/list_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/list_parser.rs new file mode 100644 index 00000000..38a35620 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/list_parser.rs @@ -0,0 +1,79 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{ListObject, RedisString}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +const QUICKLIST_NODE_CONTAINER_PLAIN: u64 = 1; +const QUICKLIST_NODE_CONTAINER_PACKED: u64 = 2; + +pub struct ListLoader {} + +impl ListLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + type_byte: u8, + ) -> Result { + let mut obj = ListObject::new(); + obj.key = key; + + match type_byte { + super::RDB_TYPE_LIST => Self::read_list(&mut obj, reader)?, + super::RDB_TYPE_LIST_ZIP_LIST => obj.elements = reader.read_zip_list()?, + super::RDB_TYPE_LIST_QUICK_LIST => Self::read_quick_list(&mut obj, reader)?, + super::RDB_TYPE_LIST_QUICK_LIST_2 => Self::read_quick_list_2(&mut obj, reader)?, + _ => { + return Err(Error::RedisRdbError(format!( + "unknown list type {}", + type_byte + ))) + } + } + Ok(obj) + } + + fn read_list(obj: &mut ListObject, reader: &mut RdbReader) -> Result<(), Error> { + let size = reader.read_length()?; + for _ in 0..size { + let ele = reader.read_string()?; + obj.elements.push(ele); + } + Ok(()) + } + + fn read_quick_list(obj: &mut ListObject, reader: &mut RdbReader) -> Result<(), Error> { + let size = reader.read_length()?; + for _ in 0..size { + let zip_list_elements = reader.read_zip_list()?; + obj.elements.extend(zip_list_elements); + } + Ok(()) + } + + fn read_quick_list_2(obj: &mut ListObject, reader: &mut RdbReader) -> Result<(), Error> { + let size = reader.read_length()?; + + for _ in 0..size { + let container = reader.read_length()?; + match container { + QUICKLIST_NODE_CONTAINER_PLAIN => { + let ele = reader.read_string()?; + obj.elements.push(ele); + } + + QUICKLIST_NODE_CONTAINER_PACKED => { + let listpack_elements = reader.read_list_pack()?; + obj.elements.extend(listpack_elements); + } + + _ => { + return Err(Error::RedisRdbError(format!( + "unknown quicklist container {}", + container + ))); + } + } + } + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/mod.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/mod.rs new file mode 100644 index 00000000..4c36c8fd --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/mod.rs @@ -0,0 +1,39 @@ +pub mod entry_parser; +pub mod hash_parser; +pub mod list_parser; +pub mod module2_parser; +pub mod set_parser; +pub mod stream_parser; +pub mod string_parser; +pub mod zset_parser; + +const RDB_TYPE_STRING: u8 = 0; +const RDB_TYPE_LIST: u8 = 1; +const RDB_TYPE_SET: u8 = 2; +const RDB_TYPE_ZSET: u8 = 3; +const RDB_TYPE_HASH: u8 = 4; +const RDB_TYPE_ZSET_2: u8 = 5; +const RDB_TYPE_MODULE: u8 = 6; +const RDB_TYPE_MODULE_2: u8 = 7; + +const RDB_TYPE_HASH_ZIP_MAP: u8 = 9; +const RDB_TYPE_LIST_ZIP_LIST: u8 = 10; +const RDB_TYPE_SET_INT_SET: u8 = 11; +const RDB_TYPE_ZSET_ZIP_LIST: u8 = 12; +const RDB_TYPE_HASH_ZIP_LIST: u8 = 13; +const RDB_TYPE_LIST_QUICK_LIST: u8 = 14; +const RDB_TYPE_STREAM_LIST_PACKS: u8 = 15; +const RDB_TYPE_HASH_LIST_PACK: u8 = 16; +const RDB_TYPE_ZSET_LIST_PACK: u8 = 17; +const RDB_TYPE_LIST_QUICK_LIST_2: u8 = 18; +const RDB_TYPE_STREAM_LIST_PACKS_2: u8 = 19; + +const MODULE_TYPE_NAME_CHAR_SET: &'static str = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +const RDB_MODULE_OPCODE_EOF: u8 = 0; +const RDB_MODULE_OPCODE_SINT: u8 = 1; +const RDB_MODULE_OPCODE_UINT: u8 = 2; +const RDB_MODULE_OPCODE_FLOAT: u8 = 3; +const RDB_MODULE_OPCODE_DOUBLE: u8 = 4; +const RDB_MODULE_OPCODE_STRING: u8 = 5; diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/module2_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/module2_parser.rs new file mode 100644 index 00000000..5deafa80 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/module2_parser.rs @@ -0,0 +1,70 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{ModuleObject, RedisString}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +pub struct ModuleLoader {} + +impl ModuleLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + type_byte: u8, + ) -> Result { + let obj = ModuleObject::new(); + + if type_byte == super::RDB_TYPE_MODULE { + return Err(Error::RedisRdbError(format!( + "module type with version 1 is not supported, key=[{}]", + String::from(key) + ))); + } + + let module_id = reader.read_length()?; + let module_name = Self::module_type_name_by_id(module_id); + let mut op_code = reader.read_byte()?; + while op_code != super::RDB_MODULE_OPCODE_EOF { + match op_code { + super::RDB_MODULE_OPCODE_SINT | super::RDB_MODULE_OPCODE_UINT => { + reader.read_length()?; + } + + super::RDB_MODULE_OPCODE_FLOAT => { + reader.read_float()?; + } + + super::RDB_MODULE_OPCODE_DOUBLE => { + reader.read_double()?; + } + + super::RDB_MODULE_OPCODE_STRING => { + reader.read_string()?; + } + + _ => { + return Err(Error::RedisRdbError(format!( + "unknown module opcode=[{}], module name=[{}]", + op_code, module_name + ))); + } + } + op_code = reader.read_byte()?; + } + + Ok(obj) + } + + fn module_type_name_by_id(module_id: u64) -> String { + let mut name_list: Vec = vec![0; 9]; + let mut module_id = module_id >> 10; + let name_char_set = super::MODULE_TYPE_NAME_CHAR_SET + .chars() + .collect::>(); + + for i in (0..9).rev() { + name_list[i] = name_char_set[(module_id & 63) as usize] as u8; + module_id >>= 6; + } + String::from_utf8(name_list).unwrap() + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/set_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/set_parser.rs new file mode 100644 index 00000000..1e042976 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/set_parser.rs @@ -0,0 +1,64 @@ +use std::io::Cursor; + +use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; +use dt_common::error::Error; +use dt_meta::redis::redis_object::{RedisString, SetObject}; + +use crate::extractor::redis::{rdb::reader::rdb_reader::RdbReader, RawByteReader}; + +pub struct SetLoader {} + +impl SetLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + type_byte: u8, + ) -> Result { + let mut obj = SetObject::new(); + obj.key = key; + + match type_byte { + super::RDB_TYPE_SET => Self::read_str_set(&mut obj, reader)?, + super::RDB_TYPE_SET_INT_SET => Self::read_int_set(&mut obj, reader)?, + _ => { + return Err(Error::RedisRdbError(format!( + "unknown set type. type_byte=[{}]", + type_byte + ))) + } + } + Ok(obj) + } + + pub fn read_str_set(obj: &mut SetObject, reader: &mut RdbReader) -> Result<(), Error> { + let size = reader.read_length()? as usize; + for _i in 0..size { + obj.elements.push(reader.read_string()?); + } + Ok(()) + } + + pub fn read_int_set(obj: &mut SetObject, reader: &mut RdbReader) -> Result<(), Error> { + let buf = reader.read_string()?; + let mut reader = Cursor::new(buf.as_bytes()); + + let encoding_type = reader.read_u32::()? as usize; + let size = reader.read_u32::()?; + for _ in 0..size { + let buf = reader.read_raw(encoding_type)?; + let int_str = match encoding_type { + 2 => LittleEndian::read_i16(&buf).to_string(), + 4 => LittleEndian::read_i32(&buf).to_string(), + 8 => LittleEndian::read_i64(&buf).to_string(), + _ => { + return Err(Error::RedisRdbError(format!( + "unknown int encoding type: {:x}", + encoding_type + ))); + } + }; + obj.elements.push(RedisString::from(int_str)); + } + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/stream_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/stream_parser.rs new file mode 100644 index 00000000..e2116926 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/stream_parser.rs @@ -0,0 +1,234 @@ +use std::collections::HashMap; + +use byteorder::{BigEndian, ByteOrder}; +use dt_common::error::Error; +use dt_meta::redis::redis_object::{RedisCmd, RedisString, StreamObject}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +pub struct StreamLoader {} + +impl StreamLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + master_key: RedisString, + type_byte: u8, + ) -> Result { + let mut obj = StreamObject::new(); + obj.key = master_key.clone(); + + // 1. length(number of listpack), k1, v1, k2, v2, ..., number, ms, seq + + // Load the number of Listpack. + let n_list_pack = reader.read_length()?; + for _ in 0..n_list_pack { + // Load key + // key is streamId, like: 1612181627287-0 + let key = reader.read_string()?; + let master_ms = BigEndian::read_i64(&key.as_bytes()[..8]); // ms + let master_seq = BigEndian::read_i64(&key.as_bytes()[8..]); + + // value is a listpack + let elements = reader.read_list_pack()?; + let mut inx = 0usize; + + // The front of stream listpack is master entry + // Parse the master entry + let mut count = Self::next_integer(&mut inx, &elements); // count + let mut deleted = Self::next_integer(&mut inx, &elements); // deleted + let num_fields = Self::next_integer(&mut inx, &elements) as usize; // num-fields + + let fields = &elements[3..3 + num_fields]; // fields + inx = 3 + num_fields; + + // master entry end by zero + let last_entry = String::from(Self::next(&mut inx, &elements).clone()); + if last_entry != "0" { + return Err(Error::RedisRdbError(format!( + "master entry not ends by zero. lastEntry=[{}]", + last_entry + ))); + } + + // Parse entries + while count != 0 || deleted != 0 { + let flags = Self::next_integer(&mut inx, &elements); // [is_same_fields|is_deleted] + let entry_ms = Self::next_integer(&mut inx, &elements); + let entry_seq = Self::next_integer(&mut inx, &elements); + let value = &format!("{}-{}", entry_ms + master_ms, entry_seq + master_seq); + + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("xadd"); + cmd.add_redis_arg(&master_key); + cmd.add_str_arg(&value); + + if flags & 2 == 2 { + // same fields, get field from master entry. + for j in 0..num_fields { + cmd.add_redis_arg(&fields[j]); + cmd.add_redis_arg(Self::next(&mut inx, &elements)); + } + } else { + // get field by lp.Next() + let num = Self::next_integer(&mut inx, &elements) as usize; + for ele in elements[inx..inx + num * 2].iter() { + cmd.add_redis_arg(ele); + } + inx += num * 2; + } + + Self::next(&mut inx, &elements); // lp_count + + if flags & 1 == 1 { + // is_deleted + deleted -= 1; + } else { + count -= 1; + obj.cmds.push(cmd); + } + } + } + + // Load total number of items inside the stream. + reader.read_length()?; + // Load the last entry ID. + let last_ms = reader.read_length()?; + let last_seq = reader.read_length()?; + let last_id = format!("{}-{}", last_ms, last_seq); + if n_list_pack == 0 { + // Use the XADD MAXLEN 0 trick to generate an empty stream if + // the key we are serializing is an empty string, which is possible + // for the Stream type. + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("XADD"); + cmd.add_redis_arg(&master_key); + cmd.add_str_arg("MAXLEN"); + cmd.add_str_arg("0"); + cmd.add_str_arg(&last_id); + cmd.add_str_arg("x"); + cmd.add_str_arg("y"); + obj.cmds.push(cmd); + } + + // Append XSETID after XADD, make sure lastid is correct, + // in case of XDEL lastid. + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("XSETID"); + cmd.add_redis_arg(&master_key); + cmd.add_str_arg(&last_id); + obj.cmds.push(cmd); + + if type_byte == super::RDB_TYPE_STREAM_LIST_PACKS_2 { + // Load the first entry ID. + let _ = reader.read_length()?; // first_ms + let _ = reader.read_length()?; // first_seq + + /* Load the maximal deleted entry ID. */ + let _ = reader.read_length()?; // max_deleted_ms + let _ = reader.read_length()?; // max_deleted_seq + + /* Load the offset. */ + let _ = reader.read_length()?; // offset + } + + // 2. nConsumerGroup, groupName, ms, seq, PEL, Consumers + + // Load the number of groups. + let n_consumer_group = reader.read_length()?; + for _i in 0..n_consumer_group { + // Load groupName + let group_name = reader.read_string()?; + + /* Load the last ID */ + let last_ms = reader.read_length()?; + let last_seq = reader.read_length()?; + let last_id = format!("{}-{}", last_ms, last_seq); + + /* Create Group */ + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("XGROUP"); + cmd.add_str_arg("CREATE"); + cmd.add_redis_arg(&master_key); + cmd.add_redis_arg(&group_name); + cmd.add_str_arg(&last_id); + obj.cmds.push(cmd); + + /* Load group offset. */ + if type_byte == super::RDB_TYPE_STREAM_LIST_PACKS_2 { + reader.read_length()?; // offset + } + + /* Load the global PEL */ + let n_pel = u64::from(reader.read_length()?); + let mut map_id_to_time = HashMap::new(); + let mut map_id_to_count = HashMap::new(); + + for _ in 0..n_pel { + // Load streamId + let ms = reader.read_be_u64()?; + let seq = reader.read_be_u64()?; + let stream_id = format!("{}-{}", ms, seq); + + // Load deliveryTime + let delivery_time = reader.read_u64()?.to_string(); + + // Load deliveryCount + let delivery_count = reader.read_length()?.to_string(); + + // Save deliveryTime and deliveryCount + map_id_to_time.insert(stream_id.clone(), delivery_time); + map_id_to_count.insert(stream_id, delivery_count); + } + + // Generate XCLAIMs for each consumer that happens to + // have pending entries. Empty consumers are discarded. + let n_consumer = reader.read_length()?; + for _i in 0..n_consumer { + /* Load consumerName */ + let consumer_name = reader.read_string()?; + + /* Load lastSeenTime */ + let _ = reader.read_u64()?; + + /* Consumer PEL */ + let n_pel = reader.read_length()?; + for _i in 0..n_pel { + // Load streamId + let ms = reader.read_be_u64()?; + let seq = reader.read_be_u64()?; + let stream_id = format!("{}-{}", ms, seq); + + /* Send */ + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("XCLAIM"); + cmd.add_redis_arg(&master_key); + cmd.add_redis_arg(&group_name); + cmd.add_redis_arg(&consumer_name); + cmd.add_str_arg("0"); + cmd.add_str_arg(&stream_id); + cmd.add_str_arg("TIME"); + cmd.add_str_arg(map_id_to_time.get(&stream_id).unwrap()); + cmd.add_str_arg("RETRYCOUNT"); + cmd.add_str_arg(map_id_to_count.get(&stream_id).unwrap()); + cmd.add_str_arg("JUSTID"); + cmd.add_str_arg("FORCE"); + obj.cmds.push(cmd); + } + } + } + + Ok(obj) + } + + fn next_integer(inx: &mut usize, elements: &Vec) -> i64 { + let ele = &elements[*inx]; + *inx += 1; + String::from(ele.clone()).parse::().unwrap() + } + + fn next<'a>(inx: &mut usize, elements: &'a Vec) -> &'a RedisString { + let ele = &elements[*inx as usize]; + *inx += 1; + &ele + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/string_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/string_parser.rs new file mode 100644 index 00000000..a2bf110b --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/string_parser.rs @@ -0,0 +1,19 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{RedisString, StringObject}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +pub struct StringLoader {} + +impl StringLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + _type_byte: u8, + ) -> Result { + let mut obj = StringObject::new(); + obj.key = key; + obj.value = reader.read_string()?; + Ok(obj) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/entry_parser/zset_parser.rs b/dt-connector/src/extractor/redis/rdb/entry_parser/zset_parser.rs new file mode 100644 index 00000000..a98c4ebe --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/entry_parser/zset_parser.rs @@ -0,0 +1,81 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::{RedisString, ZSetEntry, ZsetObject}; + +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; + +pub struct ZsetLoader {} + +impl ZsetLoader { + pub fn load_from_buffer( + reader: &mut RdbReader, + key: RedisString, + type_byte: u8, + ) -> Result { + let mut obj = ZsetObject::new(); + obj.key = key; + + match type_byte { + super::RDB_TYPE_ZSET => Self::read_zset(&mut obj, reader, false)?, + super::RDB_TYPE_ZSET_2 => Self::read_zset(&mut obj, reader, true)?, + super::RDB_TYPE_ZSET_ZIP_LIST => Self::read_zset_zip_list(&mut obj, reader)?, + super::RDB_TYPE_ZSET_LIST_PACK => Self::read_zset_list_pack(&mut obj, reader)?, + _ => { + return Err(Error::RedisRdbError(format!( + "unknown zset type. type_byte=[{}]", + type_byte + ))); + } + } + Ok(obj) + } + + fn read_zset( + obj: &mut ZsetObject, + reader: &mut RdbReader, + is_zset_2: bool, + ) -> Result<(), Error> { + let size = reader.read_length()?; + for _ in 0..size { + let member = reader.read_string()?; + let score = if is_zset_2 { + reader.read_double()?.to_string() + } else { + reader.read_float()?.to_string() + }; + + let entry = ZSetEntry { + member, + score: RedisString::from(score), + }; + obj.elements.push(entry); + } + Ok(()) + } + + fn read_zset_zip_list(obj: &mut ZsetObject, reader: &mut RdbReader) -> Result<(), Error> { + let list = reader.read_zip_list()?; + Self::parse_zset_result(obj, list) + } + + fn read_zset_list_pack(obj: &mut ZsetObject, reader: &mut RdbReader) -> Result<(), Error> { + let list = reader.read_list_pack()?; + Self::parse_zset_result(obj, list) + } + + fn parse_zset_result(obj: &mut ZsetObject, list: Vec) -> Result<(), Error> { + let size = list.len(); + if size % 2 != 0 { + return Err(Error::RedisRdbError(format!( + "zset list pack size is not even. size=[{}]", + size + ))); + } + + for i in (0..size).step_by(2) { + let member = list[i].clone(); + let score = list[i + 1].clone(); + obj.elements.push(ZSetEntry { member, score }); + } + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/mod.rs b/dt-connector/src/extractor/redis/rdb/mod.rs new file mode 100644 index 00000000..6b3f3c68 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/mod.rs @@ -0,0 +1,3 @@ +pub mod entry_parser; +pub mod rdb_loader; +pub mod reader; diff --git a/dt-connector/src/extractor/redis/rdb/rdb_loader.rs b/dt-connector/src/extractor/redis/rdb/rdb_loader.rs new file mode 100644 index 00000000..213b40ab --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/rdb_loader.rs @@ -0,0 +1,159 @@ +use dt_common::{error::Error, log_info}; +use dt_meta::redis::{redis_entry::RedisEntry, redis_object::RedisCmd}; +use sqlx::types::chrono; + +use crate::extractor::redis::RawByteReader; + +use super::{entry_parser::entry_parser::EntryParser, reader::rdb_reader::RdbReader}; + +const _K_FLAG_FUNCTION2: u8 = 245; // function library data +const _K_FLAG_FUNCTION: u8 = 246; // old function library data for 7.0 rc1 and rc2 +const _K_FLAG_MODULE_AUX: u8 = 247; // Module auxiliary data. +const K_FLAG_IDLE: u8 = 0xf8; // LRU idle time. +const K_FLAG_FREQ: u8 = 0xf9; // LFU frequency. +const K_FLAG_AUX: u8 = 0xfa; // RDB aux field. +const K_FLAG_RESIZE_DB: u8 = 0xfb; // Hash table resize hint. +const K_FLAG_EXPIRE_MS: u8 = 0xfc; // Expire time in milliseconds. +const K_FLAG_EXPIRE: u8 = 0xfd; // Old expire time in seconds. +const K_FLAG_SELECT: u8 = 0xfe; // DB number of the following keys. +const K_EOF: u8 = 0xff; // End of the RDB file. + +pub struct RdbLoader<'a> { + pub reader: RdbReader<'a>, + pub repl_stream_db_id: i64, + pub now_db_id: i64, + pub expire_ms: i64, + pub idle: i64, + pub freq: i64, + + pub is_end: bool, +} + +impl RdbLoader<'_> { + pub fn load_meta(&mut self) -> Result { + // magic + let mut buf = self.reader.read_raw(5)?; + let magic = String::from_utf8(buf).unwrap(); + if magic != "REDIS" { + return Err(Error::RedisRdbError("invalid rdb format".to_string())); + } + + // version + buf = self.reader.read_raw(4)?; + let version = String::from_utf8(buf).unwrap(); + Ok(version) + } + + pub fn load_entry(&mut self) -> Result, Error> { + let type_byte = self.reader.read_byte()?; + + match type_byte { + K_FLAG_IDLE => { + // OBJECT IDELTIME NOT captured in rdb snapshot + self.idle = self.reader.read_length()? as i64; + } + + K_FLAG_FREQ => { + // OBJECT FREQ NOT captured in rdb snapshot + self.freq = self.reader.read_u8()? as i64; + } + + K_FLAG_AUX => { + let key = String::from(self.reader.read_string()?); + let value = self.reader.read_string()?; + match key.as_str() { + "repl-stream-db" => { + let value = String::from(value); + self.repl_stream_db_id = value.parse::().unwrap(); + log_info!("RDB repl-stream-db: {}", self.repl_stream_db_id); + } + + "lua" => { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("script"); + cmd.add_str_arg("load"); + cmd.add_redis_arg(&value); + log_info!("LUA script: {:?}", value); + + let mut entry = RedisEntry::new(); + entry.is_base = true; + entry.db_id = self.now_db_id; + entry.cmd = cmd; + return Ok(Some(entry)); + } + + _ => { + log_info!("RDB AUX fields. key=[{}], value=[{:?}]", key, value); + } + } + } + + K_FLAG_RESIZE_DB => { + let db_size = self.reader.read_length()?; + let expire_size = self.reader.read_length()?; + log_info!( + "RDB resize db. db_size=[{}], expire_size=[{}]", + db_size, + expire_size + ) + } + + K_FLAG_EXPIRE_MS => { + let mut expire_ms = self.reader.read_u64()? as i64; + expire_ms -= chrono::Utc::now().timestamp_millis(); + if expire_ms < 0 { + expire_ms = 1 + } + self.expire_ms = expire_ms; + } + + K_FLAG_EXPIRE => { + let mut expire_ms = self.reader.read_u32()? as i64 * 1000; + expire_ms -= chrono::Utc::now().timestamp_millis(); + if expire_ms < 0 { + expire_ms = 1 + } + self.expire_ms = expire_ms; + } + + K_FLAG_SELECT => { + self.now_db_id = self.reader.read_length()? as i64; + } + + K_EOF => { + self.is_end = true; + self.reader + .read_raw(self.reader.rdb_length - self.reader.position)?; + } + + _ => { + let key = self.reader.read_string()?; + self.reader.copy_raw = true; + let value = EntryParser::parse_object(&mut self.reader, type_byte, key.clone()); + self.reader.copy_raw = false; + + if let Err(error) = value { + panic!( + "parsing rdb failed, key: {:?}, error: {:?}", + String::from(key), + error + ); + } else { + let mut entry = RedisEntry::new(); + entry.is_base = true; + entry.db_id = self.now_db_id; + entry.raw_bytes = self.reader.drain_raw_bytes(); + entry.key = key; + entry.value = value.unwrap(); + entry.value_type_byte = type_byte; + entry.expire_ms = self.expire_ms; + // reset expire_ms + self.expire_ms = 0; + return Ok(Some(entry)); + } + } + } + + Ok(None) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/byte.rs b/dt-connector/src/extractor/redis/rdb/reader/byte.rs new file mode 100644 index 00000000..72baf8c2 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/byte.rs @@ -0,0 +1,12 @@ +use dt_common::error::Error; + +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; + +impl RdbReader<'_> { + pub fn read_byte(&mut self) -> Result { + let buf = self.read_raw(1).unwrap(); + Ok(buf[0]) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/float.rs b/dt-connector/src/extractor/redis/rdb/reader/float.rs new file mode 100644 index 00000000..5e2aa478 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/float.rs @@ -0,0 +1,29 @@ +use byteorder::{ByteOrder, LittleEndian}; +use dt_common::error::Error; + +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; + +impl RdbReader<'_> { + pub fn read_float(&mut self) -> Result { + let n = self.read_u8()?; + let v = match n { + 253 => f64::NAN, + 254 => f64::INFINITY, + 255 => f64::NEG_INFINITY, + _ => { + let buf = self.read_raw(n as usize)?; + let s = String::from_utf8(buf).unwrap(); + let v: f64 = s.parse().unwrap(); + v + } + }; + Ok(v) + } + + pub fn read_double(&mut self) -> Result { + let buf = self.read_raw(8)?; + Ok(LittleEndian::read_f64(&buf)) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/int.rs b/dt-connector/src/extractor/redis/rdb/reader/int.rs new file mode 100644 index 00000000..60ef8725 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/int.rs @@ -0,0 +1,65 @@ +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use dt_common::error::Error; + +impl RdbReader<'_> { + pub fn read_u8(&mut self) -> Result { + self.read_byte() + } + + pub fn read_u16(&mut self) -> Result { + let buf = self.read_raw(2)?; + Ok(LittleEndian::read_u16(&buf)) + } + + pub fn read_u24(&mut self) -> Result { + let buf = self.read_raw(3)?; + Ok(LittleEndian::read_u24(&buf)) + } + + pub fn read_u32(&mut self) -> Result { + let buf = self.read_raw(4)?; + Ok(LittleEndian::read_u32(&buf)) + } + + pub fn read_u64(&mut self) -> Result { + let buf = self.read_raw(8)?; + Ok(LittleEndian::read_u64(&buf)) + } + + pub fn read_be_u64(&mut self) -> Result { + let buf = self.read_raw(8)?; + Ok(BigEndian::read_u64(&buf)) + } + + pub fn read_i8(&mut self) -> Result { + Ok(self.read_byte()? as i8) + } + + pub fn read_i16(&mut self) -> Result { + let buf = self.read_raw(2)?; + Ok(LittleEndian::read_i16(&buf)) + } + + pub fn read_i24(&mut self) -> Result { + let buf = self.read_raw(3)?; + Ok(LittleEndian::read_i24(&buf)) + } + + pub fn read_i32(&mut self) -> Result { + let buf = self.read_raw(4)?; + Ok(LittleEndian::read_i32(&buf)) + } + + pub fn read_i64(&mut self) -> Result { + let buf = self.read_raw(8)?; + Ok(LittleEndian::read_i64(&buf)) + } + + pub fn read_be_i64(&mut self) -> Result { + let buf = self.read_raw(8)?; + Ok(BigEndian::read_i64(&buf)) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/length.rs b/dt-connector/src/extractor/redis/rdb/reader/length.rs new file mode 100644 index 00000000..595affe0 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/length.rs @@ -0,0 +1,69 @@ +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; +use byteorder::{BigEndian, ByteOrder}; +use dt_common::error::Error; + +const RDB_6_BIT_LEN: u8 = 0; +const RDB_14_BIT_LEN: u8 = 1; +const RDB_32_OR_64_BIT_LEN: u8 = 2; +const RDB_SPECIAL_LEN: u8 = 3; +const RDB_32_BIT_LEN: u8 = 0x80; +const RDB_64_BIT_LEN: u8 = 0x81; + +impl RdbReader<'_> { + pub fn read_length(&mut self) -> Result { + let (len, special) = self.read_encoded_length()?; + if special { + Err(Error::RedisRdbError("illegal length special=true".into())) + } else { + Ok(len) + } + } + + pub fn read_encoded_length(&mut self) -> Result<(u64, bool), Error> { + let first_byte = self.read_byte()?; + let first_2_bits = (first_byte & 0xc0) >> 6; + match first_2_bits { + RDB_6_BIT_LEN => { + let len = u64::from(first_byte) & 0x3f; + Ok((len, false)) + } + + RDB_14_BIT_LEN => { + let next_byte = self.read_byte()?; + let len = (u64::from(first_byte) & 0x3f) << 8 | u64::from(next_byte); + Ok((len, false)) + } + + RDB_32_OR_64_BIT_LEN => match first_byte { + RDB_32_BIT_LEN => { + let next_bytes = self.read_raw(4)?; + let len = BigEndian::read_u32(&next_bytes) as u64; + Ok((len, false)) + } + + RDB_64_BIT_LEN => { + let next_bytes = self.read_raw(8)?; + let len = BigEndian::read_u64(&next_bytes) as u64; + Ok((len, false)) + } + + _ => Err(Error::RedisRdbError(format!( + "illegal length encoding: {:x}", + first_byte + ))), + }, + + RDB_SPECIAL_LEN => { + let len = u64::from(first_byte) & 0x3f; + Ok((len, true)) + } + + _ => Err(Error::RedisRdbError(format!( + "illegal length encoding: {:x}", + first_byte + ))), + } + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/list_pack.rs b/dt-connector/src/extractor/redis/rdb/reader/list_pack.rs new file mode 100644 index 00000000..4cbdc4e0 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/list_pack.rs @@ -0,0 +1,168 @@ +use std::io::Cursor; + +use byteorder::{LittleEndian, ReadBytesExt}; +use dt_common::error::Error; +use dt_meta::redis::redis_object::RedisString; + +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; + +const LP_ENCODING_7BIT_UINT_MASK: u8 = 0x80; // 10000000 +const LP_ENCODING_7BIT_UINT: u8 = 0x00; // 00000000 + +const LP_ENCODING_6BIT_STR_MASK: u8 = 0xC0; // 11000000 +const LP_ENCODING_6BIT_STR: u8 = 0x80; // 10000000 + +const LP_ENCODING_13BIT_INT_MASK: u8 = 0xE0; // 11100000 +const LP_ENCODING_13BIT_INT: u8 = 0xC0; // 11000000 + +const LP_ENCODING_12BIT_STR_MASK: u8 = 0xF0; // 11110000 +const LP_ENCODING_12BIT_STR: u8 = 0xE0; // 11100000 + +const LP_ENCODING_16BIT_INT_MASK: u8 = 0xFF; // 11111111 +const LP_ENCODING_16BIT_INT: u8 = 0xF1; // 11110001 + +const LP_ENCODING_24BIT_INT_MASK: u8 = 0xFF; // 11111111 +const LP_ENCODING_24BIT_INT: u8 = 0xF2; // 11110010 + +const LP_ENCODING_32BIT_INT_MASK: u8 = 0xFF; // 11111111 +const LP_ENCODING_32BIT_INT: u8 = 0xF3; // 11110011 + +const LP_ENCODING_64BIT_INT_MASK: u8 = 0xFF; // 11111111 +const LP_ENCODING_64BIT_INT: u8 = 0xF4; // 11110100 + +const LP_ENCODING_32BIT_STR_MASK: u8 = 0xFF; // 11111111 +const LP_ENCODING_32BIT_STR: u8 = 0xF0; // 11110000 + +impl RdbReader<'_> { + pub fn read_list_pack(&mut self) -> Result, Error> { + let buf = self.read_string()?; + let mut reader = Cursor::new(buf.as_bytes()); + + let _all_bytes = reader.read_u32::()?; // discard the number of bytes + let size = reader.read_u16::()?; + + let mut elements = Vec::new(); + for _ in 0..size { + let ele = Self::read_listpack_entry(&mut reader)?; + elements.push(ele); + } + + let last_byte = reader.read_u8()?; + if last_byte != 0xFF { + return Err(Error::RedisRdbError( + "read_listpack: last byte is not 0xFF".into(), + )); + } + Ok(elements) + } + + // https://github.com/redis/redis/blob/unstable/src/listpack.c lpGetWithSize + fn read_listpack_entry(reader: &mut Cursor<&[u8]>) -> Result { + let mut val: i64; + let mut uval: u64; + let negstart: u64; + let negmax: u64; + + let first_byte = reader.read_u8()?; + if (first_byte & LP_ENCODING_7BIT_UINT_MASK) == LP_ENCODING_7BIT_UINT { + // 7bit uint + uval = u64::from(first_byte & 0x7f); // 0x7f is 01111111 + negmax = 0; + negstart = u64::MAX; // 7 bit ints are always positive + let _ = reader.read_raw(Self::lp_encode_backlen(1))?; // encode: 1 byte + } else if (first_byte & LP_ENCODING_6BIT_STR_MASK) == LP_ENCODING_6BIT_STR { + // 6bit length str + let length = usize::from(first_byte & 0x3f); // 0x3f is 00111111 + let ele = reader.read_raw(length)?; + let _ = reader.read_raw(Self::lp_encode_backlen(1 + length)); // encode: 1byte, str: length + + let ele = RedisString::from(ele); + return Ok(ele); + // return Ok(RedisString::from(ele)); + } else if (first_byte & LP_ENCODING_13BIT_INT_MASK) == LP_ENCODING_13BIT_INT { + // 13bit int + let second_byte = reader.read_u8()?; + uval = (u64::from(first_byte & 0x1f) << 8) | u64::from(second_byte); // 5bit + 8bit, 0x1f is 00011111 + negstart = (1 as u64) << 12; + negmax = 8191; // uint13_max + let _ = reader.read_raw(Self::lp_encode_backlen(2)); + } else if (first_byte & LP_ENCODING_16BIT_INT_MASK) == LP_ENCODING_16BIT_INT { + // 16bit int + uval = reader.read_u16::()? as u64; + negstart = (1 as u64) << 15; + negmax = u16::MAX as u64; + let _ = reader.read_raw(Self::lp_encode_backlen(2)); // encode: 1byte, int: 2 + } else if (first_byte & LP_ENCODING_24BIT_INT_MASK) == LP_ENCODING_24BIT_INT { + // 24bit int + uval = reader.read_u24::()? as u64; + negstart = (1 as u64) << 23; + negmax = (u32::MAX >> 8) as u64; // uint24_max + let _ = reader.read_raw(Self::lp_encode_backlen(1 + 3)); // encode: 1byte, int: 3byte + } else if (first_byte & LP_ENCODING_32BIT_INT_MASK) == LP_ENCODING_32BIT_INT { + // 32bit int + uval = reader.read_u32::()? as u64; + negstart = (1 as u64) << 31; + negmax = u32::MAX as u64; // uint32_max + let _ = reader.read_raw(Self::lp_encode_backlen(1 + 4)); // encode: 1byte, int: 4byte + } else if (first_byte & LP_ENCODING_64BIT_INT_MASK) == LP_ENCODING_64BIT_INT { + // 64bit int + uval = reader.read_u64::()?; + negstart = (1 as u64) << 63; + negmax = u64::MAX; // uint64_max + let _ = reader.read_raw(Self::lp_encode_backlen(1 + 8)); // encode: 1byte, int: 8byte + } else if (first_byte & LP_ENCODING_12BIT_STR_MASK) == LP_ENCODING_12BIT_STR { + // 12bit length str + let second_byte = reader.read_u8()?; + let length = (((first_byte as usize) & 0x0f) << 8) + second_byte as usize; // 4bit + 8bit + let ele = reader.read_raw(length)?; + let _ = reader.read_raw(Self::lp_encode_backlen(2 + length)); // encode: 2byte, str: length + return Ok(RedisString::from(ele)); + } else if (first_byte & LP_ENCODING_32BIT_STR_MASK) == LP_ENCODING_32BIT_STR { + // 32bit length str + let length = reader.read_u32::()? as usize; + let ele = reader.read_raw(length)?; + let _ = reader.read_raw(Self::lp_encode_backlen(5 + length)); // encode: 1byte, length: 4byte, str: length + return Ok(RedisString::from(ele)); + } else { + // redis use this value, don't know why + // uval = 12345678900000000 + uint64(fireByte) + // negstart = math.MaxUint64 + // negmax = 0 + return Err(Error::RedisRdbError(format!( + "unknown encoding: {}", + first_byte + ))); + } + + // We reach this code path only for integer encodings. + // Convert the unsigned value to the signed one using two's complement + // rule. + if uval >= negstart { + // This three steps conversion should avoid undefined behaviors + // in the unsigned -> signed conversion. + uval = negmax - uval; + val = uval as i64; + val = -val - 1; + } else { + val = uval as i64; + } + Ok(RedisString::from(val.to_string())) + } + + /// Return length(bytes) for encoding backlen in Redis protocol + fn lp_encode_backlen(len: usize) -> usize { + if len <= 127 { + 1 + } else if len < 16383 { + 2 + } else if len < 2097151 { + 3 + } else if len < 268435455 { + 4 + } else { + 5 + } + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/mod.rs b/dt-connector/src/extractor/redis/rdb/reader/mod.rs new file mode 100644 index 00000000..c04ba977 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/mod.rs @@ -0,0 +1,8 @@ +pub mod byte; +pub mod float; +pub mod int; +pub mod length; +pub mod list_pack; +pub mod rdb_reader; +pub mod string; +pub mod zip_list; diff --git a/dt-connector/src/extractor/redis/rdb/reader/rdb_reader.rs b/dt-connector/src/extractor/redis/rdb/reader/rdb_reader.rs new file mode 100644 index 00000000..31361b80 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/rdb_reader.rs @@ -0,0 +1,28 @@ +use crate::extractor::redis::{redis_client::RedisClient, RawByteReader}; +use dt_common::error::Error; +use futures::executor::block_on; + +pub struct RdbReader<'a> { + pub conn: &'a mut RedisClient, + pub rdb_length: usize, + pub position: usize, + pub copy_raw: bool, + pub raw_bytes: Vec, +} + +impl RdbReader<'_> { + pub fn drain_raw_bytes(&mut self) -> Vec { + self.raw_bytes.drain(..).collect() + } +} + +impl RawByteReader for RdbReader<'_> { + fn read_raw(&mut self, length: usize) -> Result, Error> { + let buf = block_on(self.conn.read_raw(length)).unwrap(); + self.position += length; + if self.copy_raw { + self.raw_bytes.extend_from_slice(&buf); + } + Ok(buf) + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/string.rs b/dt-connector/src/extractor/redis/rdb/reader/string.rs new file mode 100644 index 00000000..961722d3 --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/string.rs @@ -0,0 +1,85 @@ +use dt_common::error::Error; +use dt_meta::redis::redis_object::RedisString; + +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; + +const RDB_ENC_INT8: u8 = 0; +const RDB_ENC_INT16: u8 = 1; +const RDB_ENC_INT32: u8 = 2; +const RDB_ENC_LZF: u8 = 3; + +impl RdbReader<'_> { + pub fn read_string(&mut self) -> Result { + let (len, special) = self.read_encoded_length()?; + let bytes = if special { + match len as u8 { + RDB_ENC_INT8 => self.read_i8()?.to_string().as_bytes().to_vec(), + + RDB_ENC_INT16 => self.read_i16()?.to_string().as_bytes().to_vec(), + + RDB_ENC_INT32 => self.read_i32()?.to_string().as_bytes().to_vec(), + + RDB_ENC_LZF => { + let in_len = self.read_length()?; + let out_len = self.read_length()?; + let in_buf = self.read_raw(in_len as usize)?; + self.lzf_decompress(&in_buf, out_len as usize)? + } + + _ => { + return Err(Error::RedisRdbError(format!( + "Unknown string encode type {}", + len + ))) + } + } + } else { + self.read_raw(len as usize)? + }; + Ok(RedisString { bytes }) + } + + fn lzf_decompress(&self, in_buf: &[u8], out_len: usize) -> Result, Error> { + let mut out = vec![0u8; out_len]; + + let mut i = 0; + let mut o = 0; + while i < in_buf.len() { + let ctrl = in_buf[i] as usize; + i += 1; + if ctrl < 32 { + for _x in 0..=ctrl { + out[o] = in_buf[i]; + i += 1; + o += 1; + } + } else { + let mut length = ctrl >> 5; + if length == 7 { + length += in_buf[i] as usize; + i += 1; + } + + let mut ref_ = o - ((ctrl & 0x1f) << 8) - in_buf[i] as usize - 1; + i += 1; + + for _x in 0..=length + 1 { + out[o] = out[ref_]; + ref_ += 1; + o += 1; + } + } + } + + if o != out_len { + Err(Error::RedisRdbError(format!( + "lzf decompress failed: out_len: {}, o: {}", + out_len, o + ))) + } else { + Ok(out) + } + } +} diff --git a/dt-connector/src/extractor/redis/rdb/reader/zip_list.rs b/dt-connector/src/extractor/redis/rdb/reader/zip_list.rs new file mode 100644 index 00000000..33233e5c --- /dev/null +++ b/dt-connector/src/extractor/redis/rdb/reader/zip_list.rs @@ -0,0 +1,146 @@ +use std::io::Cursor; + +use byteorder::{BigEndian, ByteOrder, LittleEndian, ReadBytesExt}; +use dt_common::error::Error; +use dt_meta::redis::redis_object::RedisString; + +use crate::extractor::redis::RawByteReader; + +use super::rdb_reader::RdbReader; + +const ZIP_STR_06B: u8 = 0x00; +const ZIP_STR_14B: u8 = 0x01; +const ZIP_STR_32B: u8 = 0x02; + +const ZIP_INT_04B: u8 = 0x0f; + +const ZIP_INT_08B: u8 = 0xfe; +const ZIP_INT_16B: u8 = 0xc0; +const ZIP_INT_24B: u8 = 0xf0; +const ZIP_INT_32B: u8 = 0xd0; +const ZIP_INT_64B: u8 = 0xe0; + +impl RdbReader<'_> { + pub fn read_zip_list(&mut self) -> Result, Error> { + // The general layout of the ziplist is as follows: + // ... + + let buf = self.read_string()?; + let mut reader = Cursor::new(buf.as_bytes()); + + let _ = reader.read_u32::()?; // zlbytes + let _ = reader.read_u32::()?; // zltail + + let size = reader.read_u16::()? as usize; + let mut elements = Vec::new(); + if size == 65535 { + // 2^16-1, we need to traverse the entire list to know how many items it holds. + loop { + let first_byte = reader.read_u8()?; + if first_byte == 0xFE { + break; + } + let ele = Self::read_zip_list_entry(&mut reader, first_byte)?; + elements.push(ele); + } + } else { + for _ in 0..size { + let first_byte = reader.read_u8()?; + let ele = Self::read_zip_list_entry(&mut reader, first_byte)?; + elements.push(ele); + } + + let last_byte = reader.read_u8()?; + if last_byte != 0xFF { + return Err(Error::RedisRdbError(format!( + "invalid zipList lastByte encoding: {}", + last_byte + ))); + } + } + + Ok(elements) + } + + fn read_zip_list_entry( + reader: &mut Cursor<&[u8]>, + first_byte: u8, + ) -> Result { + // read prevlen + if first_byte == 0xFE { + let _prevlen = reader.read_u32::()?; + } + + // read encoding + let first_byte = reader.read_raw(1)?[0]; + let first_2_bits = (first_byte & 0xc0) >> 6; // first 2 bits of encoding + match first_2_bits { + ZIP_STR_06B => { + let length = (first_byte & 0x3f) as usize; // 0x3f = 00111111 + let buf = reader.read_raw(length)?; + return Ok(RedisString::from(buf)); + } + + ZIP_STR_14B => { + let second_byte = reader.read_u8()?; + let length = (((first_byte & 0x3f) as u16) << 8) | second_byte as u16; + let buf = reader.read_raw(length as usize)?; + return Ok(RedisString::from(buf)); + } + + ZIP_STR_32B => { + let mut buf = reader.read_raw(4)?; + let length = BigEndian::read_u32(&buf); + buf = reader.read_raw(length as usize)?; + return Ok(RedisString::from(buf)); + } + + _ => {} + } + + match first_byte { + ZIP_INT_08B => { + let v = reader.read_i8()?; + return Ok(RedisString::from(v.to_string())); + } + + ZIP_INT_16B => { + let v = reader.read_i16::()?; + return Ok(RedisString::from(v.to_string())); + } + + ZIP_INT_24B => { + let v = reader.read_i24::()?; + return Ok(RedisString::from(v.to_string())); + } + + ZIP_INT_32B => { + let v = reader.read_i32::()?; + return Ok(RedisString::from(v.to_string())); + } + + ZIP_INT_64B => { + let v = reader.read_i64::()?; + return Ok(RedisString::from(v.to_string())); + } + + _ => {} + } + + if first_byte >> 4 == ZIP_INT_04B { + let v = (first_byte & 0x0f) as i8 - 1; + if v < 0 || v > 12 { + return Err(Error::RedisRdbError(format!( + "invalid zipInt04B encoding: {}", + v + ))); + } + return Ok(RedisString::from(v.to_string())); + } + + Err(Error::RedisRdbError(format!( + "invalid encoding: {}", + first_byte + ))) + } +} diff --git a/dt-connector/src/extractor/redis/redis_cdc_extractor.rs b/dt-connector/src/extractor/redis/redis_cdc_extractor.rs new file mode 100644 index 00000000..df768ad2 --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_cdc_extractor.rs @@ -0,0 +1,149 @@ +use super::redis_client::RedisClient; +use super::redis_psync_extractor::RedisPsyncExtractor; +use super::redis_resp_types::Value; +use crate::Extractor; +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::error::Error; +use dt_common::log_error; +use dt_common::log_info; +use dt_common::syncer::Syncer; +use dt_common::utils::position_util::PositionUtil; +use dt_common::utils::time_util::TimeUtil; +use dt_meta::dt_data::DtData; +use dt_meta::redis::redis_entry::RedisEntry; +use dt_meta::redis::redis_object::RedisCmd; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Instant; + +pub struct RedisCdcExtractor { + pub conn: RedisClient, + pub buffer: Arc>, + pub run_id: String, + pub repl_offset: u64, + pub repl_port: u64, + pub now_db_id: i64, + pub heartbeat_interval_secs: u64, + pub shut_down: Arc, + pub syncer: Arc>, +} + +#[async_trait] +impl Extractor for RedisCdcExtractor { + async fn extract(&mut self) -> Result<(), Error> { + let mut psync_extractor = RedisPsyncExtractor { + conn: &mut self.conn, + buffer: self.buffer.clone(), + run_id: self.run_id.clone(), + repl_offset: self.repl_offset, + repl_port: self.repl_port, + now_db_id: self.now_db_id, + }; + + // receive rdb data if needed + psync_extractor.extract().await?; + self.run_id = psync_extractor.run_id; + self.repl_offset = psync_extractor.repl_offset; + + self.receive_aof().await + } + + async fn close(&mut self) -> Result<(), Error> { + self.conn.close().await + } +} + +impl RedisCdcExtractor { + async fn receive_aof(&mut self) -> Result<(), Error> { + let mut start_time = Instant::now(); + loop { + // heartbeat + if start_time.elapsed().as_secs() > self.heartbeat_interval_secs { + self.heartbeat().await?; + start_time = Instant::now(); + } + + let (value, n) = self.conn.read_with_len().await.unwrap(); + if Value::Nil == value { + continue; + } + + self.repl_offset += n as u64; + let cmd = self.handle_redis_value(value).await.unwrap(); + + if !cmd.args.is_empty() { + if cmd.get_name().eq_ignore_ascii_case("select") { + self.now_db_id = String::from_utf8(cmd.args[1].clone()) + .unwrap() + .parse::() + .unwrap(); + continue; + } + + // build entry and push it to buffer + let mut entry = RedisEntry::new(); + entry.cmd = cmd; + entry.db_id = self.now_db_id; + entry.position = format!( + "run_id:{},repl_offset:{},now_db_id:{}", + self.run_id, self.repl_offset, self.now_db_id + ); + self.push_to_buf(entry).await; + } + } + } + + async fn push_to_buf(&mut self, entry: RedisEntry) { + while self.buffer.is_full() { + TimeUtil::sleep_millis(1).await; + } + self.buffer.push(DtData::Redis { entry }).unwrap(); + } + + async fn handle_redis_value(&mut self, value: Value) -> Result { + let mut cmd = RedisCmd::new(); + match value { + Value::Bulk(values) => { + for v in values { + match v { + Value::Data(data) => cmd.add_arg(data), + _ => { + log_error!("received unexpected value in aof bulk: {:?}", v); + break; + } + } + } + } + v => { + return Err(Error::RedisRdbError(format!( + "received unexpected aof value: {:?}", + v + ))); + } + } + Ok(cmd) + } + + async fn heartbeat(&mut self) -> Result<(), Error> { + let position = self.syncer.lock().unwrap().checkpoint_position.clone(); + let repl_offset = if !position.is_empty() { + let position_info = PositionUtil::parse(&position); + position_info + .get("repl_offset") + .unwrap() + .parse::() + .unwrap() + } else { + self.repl_offset as u64 + }; + + let repl_offset = &repl_offset.to_string(); + let args = vec!["replconf", "ack", repl_offset]; + let cmd = RedisCmd::from_str_args(&args); + log_info!("heartbeat cmd: {:?}", cmd); + let _ = self.conn.send(&cmd).await; + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/redis_client.rs b/dt-connector/src/extractor/redis/redis_client.rs new file mode 100644 index 00000000..cc185f28 --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_client.rs @@ -0,0 +1,89 @@ +use super::redis_resp_reader::RedisRespReader; +use super::redis_resp_types::Value; +use crate::sinker::redis::cmd_encoder::CmdEncoder; +use async_std::io::BufReader; +use async_std::net::TcpStream; +use async_std::prelude::*; +use dt_common::error::Error; +use dt_meta::redis::redis_object::RedisCmd; + +use url::Url; + +pub struct RedisClient { + stream: BufReader, +} + +impl RedisClient { + pub async fn new(url: &str) -> Result { + let url_info = Url::parse(url).unwrap(); + let host = url_info.host_str().unwrap(); + let port = url_info.port().unwrap(); + let username = url_info.username(); + let password = url_info.password(); + + let stream = TcpStream::connect(format!("{}:{}", host, port)) + .await + .unwrap(); + let mut me = Self { + stream: BufReader::new(stream), + }; + + if let Some(password) = password { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("AUTH"); + if !username.is_empty() { + cmd.add_str_arg(username); + } + cmd.add_str_arg(password); + + me.send(&cmd).await?; + if let Ok(value) = me.read().await { + if let Value::Okay = value { + return Ok(me); + } + } + return Err(Error::Unexpected(format!("can't cnnect redis: {}", url))); + } + + Ok(me) + } + + pub async fn close(&mut self) -> Result<(), Error> { + self.stream.get_mut().shutdown(std::net::Shutdown::Both)?; + Ok(()) + } + + pub async fn send_packed(&mut self, packed_cmd: &[u8]) -> Result<(), Error> { + self.stream.get_mut().write_all(packed_cmd).await?; + Ok(()) + } + + pub async fn send(&mut self, cmd: &RedisCmd) -> Result<(), Error> { + self.send_packed(&CmdEncoder::encode(cmd)).await + } + + pub async fn read(&mut self) -> Result { + let mut resp_reader = RedisRespReader { read_len: 0 }; + resp_reader.decode(&mut self.stream).await + } + + pub async fn read_with_len(&mut self) -> Result<(Value, usize), String> { + let mut resp_reader = RedisRespReader { read_len: 0 }; + let value = resp_reader.decode(&mut self.stream).await?; + Ok((value, resp_reader.read_len)) + } + + pub async fn read_raw(&mut self, length: usize) -> Result, Error> { + let mut buf = vec![0; length]; + // if length is bigger than buffer size of BufReader, the buf will be filled by 0, + // so here we must read from inner TcpStream instead of BufReader + // let n = self.stream.read(&mut buf).await.unwrap(); + let mut read_count = 0; + while read_count < length { + // use async_std::net::TcpStream instead of tokio::net::TcpStream, tokio TcpStream may stuck + // when trying to get big data by multiple read. + read_count += self.stream.get_mut().read(&mut buf[read_count..]).await?; + } + Ok(buf) + } +} diff --git a/dt-connector/src/extractor/redis/redis_psync_extractor.rs b/dt-connector/src/extractor/redis/redis_psync_extractor.rs new file mode 100644 index 00000000..8fcbb7f9 --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_psync_extractor.rs @@ -0,0 +1,149 @@ +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::utils::time_util::TimeUtil; +use dt_common::{error::Error, log_info}; +use dt_meta::dt_data::DtData; +use dt_meta::redis::redis_object::RedisCmd; + +use std::sync::Arc; + +use crate::extractor::redis::rdb::rdb_loader::RdbLoader; +use crate::extractor::redis::rdb::reader::rdb_reader::RdbReader; +use crate::extractor::redis::redis_resp_types::Value; +use crate::Extractor; + +use super::redis_client::RedisClient; + +pub struct RedisPsyncExtractor<'a> { + pub conn: &'a mut RedisClient, + pub buffer: Arc>, + pub run_id: String, + pub repl_offset: u64, + pub now_db_id: i64, + pub repl_port: u64, +} + +#[async_trait] +impl Extractor for RedisPsyncExtractor<'_> { + async fn extract(&mut self) -> Result<(), Error> { + log_info!( + "RedisPsyncExtractor starts, run_id: {}, repl_offset: {}, now_db_id: {}", + self.run_id, + self.repl_offset, + self.now_db_id + ); + if self.start_psync().await? { + // server won't send rdb if it's NOT full sync + self.receive_rdb().await?; + } + Ok(()) + } +} + +impl RedisPsyncExtractor<'_> { + pub async fn start_psync(&mut self) -> Result { + // replconf listening-port [port] + let repl_port = self.repl_port.to_string(); + let repl_cmd = RedisCmd::from_str_args(&vec!["replconf", "listening-port", &repl_port]); + self.conn.send(&repl_cmd).await.unwrap(); + if let Value::Okay = self.conn.read().await.unwrap() { + } else { + return Err(Error::ExtractorError( + "replconf listening-port response is not Ok".into(), + )); + } + + let full_sync = self.run_id.is_empty() && self.repl_offset == 0; + let (run_id, repl_offset) = if full_sync { + ("?".to_string(), "-1".to_string()) + } else { + (self.run_id.clone(), self.repl_offset.to_string()) + }; + + // PSYNC [run_id] [offset] + let psync_cmd = RedisCmd::from_str_args(&vec!["PSYNC", &run_id, &repl_offset]); + self.conn.send(&psync_cmd).await.unwrap(); + let value = self.conn.read().await.unwrap(); + + if let Value::Status(s) = value { + log_info!("PSYNC command response status: {:?}", s); + if full_sync { + let tokens: Vec<&str> = s.split_whitespace().collect(); + self.run_id = tokens[1].to_string(); + self.repl_offset = tokens[2].parse::().unwrap(); + } else if s != "CONTINUE" { + return Err(Error::ExtractorError( + "PSYNC command response is NOT CONTINUE".into(), + )); + } + } else { + return Err(Error::ExtractorError( + "PSYNC command response is NOT status".into(), + )); + }; + Ok(full_sync) + } + + async fn receive_rdb(&mut self) -> Result<(), Error> { + // format: \n\n\n$\r\n + loop { + let buf = self.conn.read_raw(1).await.unwrap(); + if buf[0] == b'\n' { + continue; + } + if buf[0] != b'$' { + panic!("invalid rdb format"); + } + break; + } + + // length of rdb data + let mut rdb_length_str = String::new(); + loop { + let buf = self.conn.read_raw(1).await.unwrap(); + if buf[0] == b'\n' { + break; + } + if buf[0] != b'\r' { + rdb_length_str.push(buf[0] as char); + } + } + let rdb_length = rdb_length_str.parse::().unwrap(); + + let reader = RdbReader { + conn: &mut self.conn, + rdb_length, + position: 0, + copy_raw: false, + raw_bytes: Vec::new(), + }; + + let mut loader = RdbLoader { + reader, + repl_stream_db_id: 0, + now_db_id: self.now_db_id, + expire_ms: 0, + idle: 0, + freq: 0, + is_end: false, + }; + + let version = loader.load_meta()?; + log_info!("source redis version: {:?}", version); + + loop { + if let Some(entry) = loader.load_entry()? { + while self.buffer.is_full() { + TimeUtil::sleep_millis(1).await; + } + self.now_db_id = entry.db_id; + self.buffer.push(DtData::Redis { entry }).unwrap(); + } + + if loader.is_end { + break; + } + } + Ok(()) + } +} diff --git a/dt-connector/src/extractor/redis/redis_resp_reader.rs b/dt-connector/src/extractor/redis/redis_resp_reader.rs new file mode 100644 index 00000000..4d8881fb --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_resp_reader.rs @@ -0,0 +1,111 @@ +use async_recursion::async_recursion; +use async_std::io::BufReader; +use async_std::net::TcpStream; +use async_std::prelude::*; + +use super::redis_resp_types::Value; + +pub struct RedisRespReader { + pub read_len: usize, +} + +/// up to 512 MB in length +const RESP_MAX_SIZE: i64 = 512 * 1024 * 1024; +const OK_RESPONSE: &[u8] = &[79, 75]; + +impl RedisRespReader { + #[async_recursion] + pub async fn decode(&mut self, reader: &mut BufReader) -> Result { + let mut res: Vec = Vec::new(); + reader + .read_until(b'\n', &mut res) + .await + .map_err(|e| e.to_string())?; + + let len = res.len(); + self.read_len += len; + + if len == 1 { + return Ok(Value::Nil); + } + if len < 3 { + return Err(format!("too short: {}", len)); + } + if !is_crlf(res[len - 2], res[len - 1]) { + return Err(format!("invalid CRLF: {:?}", res)); + } + + let bytes = res[1..len - 2].as_ref(); + match res[0] { + // Value::String + b'+' => match bytes { + OK_RESPONSE => Ok(Value::Okay), + bytes => String::from_utf8(bytes.to_vec()) + .map_err(|e| e.to_string()) + .map(Value::Status), + }, + // Value::Error + b'-' => match String::from_utf8(bytes.to_vec()) { + Ok(value) => Err(value), + Err(e) => Err(e.to_string()), + }, + // Value::Integer + b':' => parse_integer(bytes).map(Value::Int), + // Value::Bulk + b'$' => { + let int: i64 = parse_integer(bytes)?; + if int == -1 { + // Nil bulk + return Ok(Value::Nil); + } + if int < -1 || int >= RESP_MAX_SIZE { + return Err(format!("invalid bulk length: {}", int)); + } + + let int = int as usize; + let mut buf: Vec = vec![0; int + 2]; + reader + .read_exact(buf.as_mut_slice()) + .await + .map_err(|e| e.to_string())?; + if !is_crlf(buf[int], buf[int + 1]) { + return Err(format!("invalid CRLF: {:?}", buf)); + } + self.read_len += int + 2; + buf.truncate(int); + Ok(Value::Data(buf)) + } + // Value::Array + b'*' => { + let int = parse_integer(bytes)?; + if int == -1 { + // Null array + return Ok(Value::Nil); + } + if int < -1 || int >= RESP_MAX_SIZE { + return Err(format!("invalid array length: {}", int)); + } + + let mut array: Vec = Vec::with_capacity(int as usize); + for _ in 0..int { + let val = self.decode(reader).await?; + array.push(val); + } + Ok(Value::Bulk(array)) + } + prefix => Err(format!("invalid RESP type: {:?}", prefix)), + } + } +} + +#[inline] +fn is_crlf(a: u8, b: u8) -> bool { + a == b'\r' && b == b'\n' +} + +#[inline] +fn parse_integer(bytes: &[u8]) -> std::result::Result { + String::from_utf8(bytes.to_vec()) + .map_err(|e| e.to_string()) + .and_then(|value| value.parse::().map_err(|e| e.to_string())) +} diff --git a/dt-connector/src/extractor/redis/redis_resp_types.rs b/dt-connector/src/extractor/redis/redis_resp_types.rs new file mode 100644 index 00000000..2d365852 --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_resp_types.rs @@ -0,0 +1,98 @@ +use thiserror::Error; + +/// Represents a redis RESP protcol response +/// https://redis.io/topics/protocol#resp-protocol-description +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Value { + /// A nil response from the server. + Nil, + /// A status response which represents the string "OK". + Okay, + /// An integer response. Note that there are a few situations + /// in which redis actually returns a string for an integer. + Int(i64), + /// A simple string response. + Status(String), + /// An arbitary binary data. + Data(Vec), + /// A bulk response of more data. This is generally used by redis + /// to express nested structures. + Bulk(Vec), +} + +pub type RedisResult = std::result::Result; + +#[derive(Error, Debug)] +#[error("RedisError (command: {command:?}, message: {message:?})")] +pub struct RedisError { + pub command: String, + pub message: String, +} + +type Result = std::result::Result; + +impl Value { + pub fn try_into>(self) -> Result { + T::parse_from(self) + } +} + +pub trait ParseFrom: Sized { + fn parse_from(value: T) -> Result; +} + +impl ParseFrom for () { + fn parse_from(value: Value) -> Result { + match value { + Value::Okay => Ok(()), + v => Err(format!("Failed parsing {:?}", v)), + } + } +} + +impl ParseFrom for i64 { + fn parse_from(value: Value) -> Result { + match value { + Value::Int(n) => Ok(n), + v => Err(format!("Failed parsing {:?}", v)), + } + } +} + +impl ParseFrom for Vec { + fn parse_from(value: Value) -> Result { + match value { + Value::Data(bytes) => Ok(bytes), + v => Err(format!("Failed parsing {:?}", v)), + } + } +} + +impl ParseFrom for String { + fn parse_from(value: Value) -> Result { + match value { + Value::Okay => Ok("Ok".to_owned()), + Value::Nil => Ok(String::new()), + Value::Int(n) => Ok(format!("{}", n)), + Value::Status(s) => Ok(s), + Value::Data(bytes) => String::from_utf8(bytes.to_vec()).map_err(|e| e.to_string()), + v => Err(format!("Failed parsing {:?}", v)), + } + } +} + +impl ParseFrom for Vec +where + T: ParseFrom, +{ + fn parse_from(v: Value) -> Result { + if let Value::Bulk(array) = v { + let mut result = Vec::with_capacity(array.len()); + for e in array { + result.push(T::parse_from(e)?); + } + return Ok(result); + } + Err(format!("Failed parsing {:?}", v)) + } +} diff --git a/dt-connector/src/extractor/redis/redis_snapshot_extractor.rs b/dt-connector/src/extractor/redis/redis_snapshot_extractor.rs new file mode 100644 index 00000000..025f2b3c --- /dev/null +++ b/dt-connector/src/extractor/redis/redis_snapshot_extractor.rs @@ -0,0 +1,37 @@ +use super::redis_client::RedisClient; +use super::redis_psync_extractor::RedisPsyncExtractor; +use crate::extractor::base_extractor::BaseExtractor; +use crate::Extractor; +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::error::Error; +use dt_meta::dt_data::DtData; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +pub struct RedisSnapshotExtractor { + pub conn: RedisClient, + pub repl_port: u64, + pub buffer: Arc>, + pub shut_down: Arc, +} + +#[async_trait] +impl Extractor for RedisSnapshotExtractor { + async fn extract(&mut self) -> Result<(), Error> { + let mut psync_extractor = RedisPsyncExtractor { + conn: &mut self.conn, + buffer: self.buffer.clone(), + run_id: String::new(), + repl_offset: 0, + repl_port: self.repl_port, + now_db_id: 0, + }; + psync_extractor.extract().await?; + BaseExtractor::wait_task_finish(self.buffer.as_ref(), self.shut_down.as_ref()).await + } + + async fn close(&mut self) -> Result<(), Error> { + self.conn.close().await + } +} diff --git a/dt-connector/src/lib.rs b/dt-connector/src/lib.rs index d7d42eb5..28d48e9c 100644 --- a/dt-connector/src/lib.rs +++ b/dt-connector/src/lib.rs @@ -7,22 +7,38 @@ pub mod sinker; use async_trait::async_trait; use check_log::check_log::CheckLog; use dt_common::error::Error; -use dt_meta::{ddl_data::DdlData, row_data::RowData}; +use dt_meta::{ddl_data::DdlData, dt_data::DtData, row_data::RowData}; #[async_trait] pub trait Sinker { - async fn sink_dml(&mut self, mut data: Vec, batch: bool) -> Result<(), Error>; + async fn sink_dml(&mut self, mut _data: Vec, _batch: bool) -> Result<(), Error> { + Ok(()) + } - async fn sink_ddl(&mut self, mut data: Vec, batch: bool) -> Result<(), Error>; + async fn sink_ddl(&mut self, mut _data: Vec, _batch: bool) -> Result<(), Error> { + Ok(()) + } - async fn close(&mut self) -> Result<(), Error>; + async fn close(&mut self) -> Result<(), Error> { + Ok(()) + } + + async fn sink_raw(&mut self, mut _data: Vec, _batch: bool) -> Result<(), Error> { + Ok(()) + } + + async fn refresh_meta(&mut self, _data: Vec) -> Result<(), Error> { + Ok(()) + } } #[async_trait] pub trait Extractor { async fn extract(&mut self) -> Result<(), Error>; - async fn close(&mut self) -> Result<(), Error>; + async fn close(&mut self) -> Result<(), Error> { + Ok(()) + } } #[async_trait] diff --git a/dt-connector/src/rdb_query_builder.rs b/dt-connector/src/rdb_query_builder.rs index b788a4a3..0e3a5c03 100644 --- a/dt-connector/src/rdb_query_builder.rs +++ b/dt-connector/src/rdb_query_builder.rs @@ -125,12 +125,10 @@ impl RdbQueryBuilder<'_> { cols.push(col.clone()); let col_value = before.get(col); if *col_value.unwrap() == ColValue::None { - return Err(Error::Unexpected { - error: format!( + return Err(Error::Unexpected(format!( "db: {}, tb: {}, where col: {} is NULL, which should not happen in batch delete", self.rdb_tb_meta.schema, self.rdb_tb_meta.tb, col - ), - }); + ))); } binds.push(col_value); } @@ -252,12 +250,10 @@ impl RdbQueryBuilder<'_> { } if set_pairs.is_empty() { - return Err(Error::Unexpected { - error: format!( - "db: {}, tb: {}, no cols in after, which should not happen in update", - self.rdb_tb_meta.schema, self.rdb_tb_meta.tb - ), - }); + return Err(Error::Unexpected(format!( + "db: {}, tb: {}, no cols in after, which should not happen in update", + self.rdb_tb_meta.schema, self.rdb_tb_meta.tb + ))); } let (where_sql, not_null_cols) = self.get_where_info(placeholder_index, before)?; @@ -336,12 +332,10 @@ impl RdbQueryBuilder<'_> { cols.push(col.clone()); let col_value = after.get(col); if *col_value.unwrap() == ColValue::None { - return Err(Error::Unexpected { - error: format!( + return Err(Error::Unexpected(format!( "db: {}, tb: {}, where col: {} is NULL, which should not happen in batch select", self.rdb_tb_meta.schema, self.rdb_tb_meta.tb, col - ), - }); + ))); } binds.push(col_value); } diff --git a/dt-connector/src/sinker/foxlake_sinker.rs b/dt-connector/src/sinker/foxlake_sinker.rs index 42959e9c..ffb6f67d 100644 --- a/dt-connector/src/sinker/foxlake_sinker.rs +++ b/dt-connector/src/sinker/foxlake_sinker.rs @@ -56,10 +56,6 @@ impl Sinker for FoxlakeSinker { self.put_to_file(&key, &content).await.unwrap(); Ok(()) } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } impl FoxlakeSinker { diff --git a/dt-connector/src/sinker/kafka/kafka_router.rs b/dt-connector/src/sinker/kafka/kafka_router.rs index bd0d4436..01ecd4c4 100644 --- a/dt-connector/src/sinker/kafka/kafka_router.rs +++ b/dt-connector/src/sinker/kafka/kafka_router.rs @@ -51,9 +51,10 @@ impl KafkaRouter { let tokens: Vec<&str> = name.split(':').collect(); if tokens.len() != 2 { - return Err(Error::ConfigError { - error: format!("invalid router config, check error near: {}", name), - }); + return Err(Error::ConfigError(format!( + "invalid router config, check error near: {}", + name + ))); } map.insert( tokens.first().unwrap().to_string(), diff --git a/dt-connector/src/sinker/kafka/kafka_sinker.rs b/dt-connector/src/sinker/kafka/kafka_sinker.rs index bee01860..60a210de 100644 --- a/dt-connector/src/sinker/kafka/kafka_sinker.rs +++ b/dt-connector/src/sinker/kafka/kafka_sinker.rs @@ -4,7 +4,7 @@ use crate::{call_batch_fn, Sinker}; use dt_common::error::Error; -use dt_meta::{ddl_data::DdlData, row_data::RowData}; +use dt_meta::dt_data::DtData; use kafka::producer::{Producer, Record}; @@ -18,39 +18,33 @@ pub struct KafkaSinker { #[async_trait] impl Sinker for KafkaSinker { - async fn sink_dml(&mut self, mut data: Vec, _batch: bool) -> Result<(), Error> { + async fn sink_raw(&mut self, mut data: Vec, _batch: bool) -> Result<(), Error> { call_batch_fn!(self, data, Self::send); Ok(()) } - - async fn sink_ddl(&mut self, _data: Vec, _batch: bool) -> Result<(), Error> { - Ok(()) - } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } impl KafkaSinker { async fn send( &mut self, - data: &mut [RowData], + data: &mut [DtData], sinked_count: usize, batch_size: usize, ) -> Result<(), Error> { - let mut topics = Vec::new(); - for rd in data.iter().skip(sinked_count).take(batch_size) { - let topic = self.kafka_router.get_route(&rd.schema, &rd.tb); - topics.push(topic); - } + // let mut topics = Vec::new(); + // for rd in data.iter().skip(sinked_count).take(batch_size) { + // let topic = self.kafka_router.get_route(&rd.schema, &rd.tb); + // topics.push(topic); + // } + + let topic = self.kafka_router.get_route("", ""); let mut messages = Vec::new(); - for (i, rd) in data.iter().skip(sinked_count).take(batch_size).enumerate() { + for (_, dt_data) in data.iter().skip(sinked_count).take(batch_size).enumerate() { messages.push(Record { key: (), - value: rd.to_string(), - topic: &topics[i - sinked_count], + value: dt_data.to_string(), + topic: &topic, partition: -1, }); } diff --git a/dt-connector/src/sinker/mod.rs b/dt-connector/src/sinker/mod.rs index 427f83b5..2dc0f842 100644 --- a/dt-connector/src/sinker/mod.rs +++ b/dt-connector/src/sinker/mod.rs @@ -7,3 +7,4 @@ pub mod mysql; pub mod open_faas_sinker; pub mod pg; pub mod rdb_router; +pub mod redis; diff --git a/dt-connector/src/sinker/mongo/mongo_sinker.rs b/dt-connector/src/sinker/mongo/mongo_sinker.rs index 6ccadd03..7fb99b9f 100644 --- a/dt-connector/src/sinker/mongo/mongo_sinker.rs +++ b/dt-connector/src/sinker/mongo/mongo_sinker.rs @@ -5,9 +5,12 @@ use mongodb::{ Client, Collection, }; -use dt_common::{constants::MongoConstants, error::Error, log_error}; +use dt_common::{error::Error, log_error}; -use dt_meta::{col_value::ColValue, ddl_data::DdlData, row_data::RowData, row_type::RowType}; +use dt_meta::{ + col_value::ColValue, mongo::mongo_constant::MongoConstants, row_data::RowData, + row_type::RowType, +}; use crate::{call_batch_fn, sinker::rdb_router::RdbRouter, Sinker}; @@ -40,14 +43,6 @@ impl Sinker for MongoSinker { } Ok(()) } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } - - async fn sink_ddl(&mut self, _data: Vec, _batch: bool) -> Result<(), Error> { - Ok(()) - } } impl MongoSinker { @@ -60,7 +55,7 @@ impl MongoSinker { RowType::Insert => { let after = row_data.after.as_mut().unwrap(); if let Some(ColValue::MongoDoc(doc)) = after.remove(MongoConstants::DOC) { - self.upsert(&collection, &doc, &doc).await.unwrap(); + self.upsert(&collection, doc.clone(), doc).await.unwrap(); } } @@ -85,12 +80,17 @@ impl MongoSinker { let update_doc = if let Some(ColValue::MongoDoc(doc)) = after.remove(MongoConstants::DOC) { Some(doc) + } else if let Some(ColValue::MongoDoc(doc)) = + after.remove(MongoConstants::DIFF_DOC) + { + // for Update row_data from oplog (NOT change stream), after contains diff_doc instead of doc + Some(doc) } else { None }; if query_doc.is_some() && query_doc.is_some() { - self.upsert(&collection, &query_doc.unwrap(), &update_doc.unwrap()) + self.upsert(&collection, query_doc.unwrap(), update_doc.unwrap()) .await .unwrap(); } @@ -113,7 +113,7 @@ impl MongoSinker { for rd in data.iter().skip(start_index).take(batch_size) { let before = rd.before.as_ref().unwrap(); if let Some(ColValue::MongoDoc(doc)) = before.get(MongoConstants::DOC) { - ids.push(doc.get_object_id(MongoConstants::ID).unwrap()); + ids.push(doc.get(MongoConstants::ID).unwrap()); } } @@ -139,7 +139,6 @@ impl MongoSinker { let mut docs = Vec::new(); for rd in data.iter().skip(start_index).take(batch_size) { let after = rd.after.as_ref().unwrap(); - // TODO, support mysql / pg -> mongo if let Some(ColValue::MongoDoc(doc)) = after.get(MongoConstants::DOC) { docs.push(doc); } @@ -161,15 +160,13 @@ impl MongoSinker { async fn upsert( &mut self, collection: &Collection, - query_doc: &Document, - update_doc: &Document, + query_doc: Document, + update_doc: Document, ) -> Result<(), Error> { - let query = - doc! {MongoConstants::ID : query_doc.get_object_id(MongoConstants::ID).unwrap()}; let update = doc! {MongoConstants::SET: update_doc}; let options = UpdateOptions::builder().upsert(true).build(); collection - .update_one(query, update, Some(options)) + .update_one(query_doc, update, Some(options)) .await .unwrap(); Ok(()) diff --git a/dt-connector/src/sinker/mysql/mysql_sinker.rs b/dt-connector/src/sinker/mysql/mysql_sinker.rs index c2cd11be..c42c3ebe 100644 --- a/dt-connector/src/sinker/mysql/mysql_sinker.rs +++ b/dt-connector/src/sinker/mysql/mysql_sinker.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::{ call_batch_fn, close_conn_pool, rdb_query_builder::RdbQueryBuilder, @@ -5,21 +7,26 @@ use crate::{ Sinker, }; -use dt_common::{error::Error, log_error}; +use dt_common::{error::Error, log_error, log_info}; use dt_meta::{ ddl_data::DdlData, + ddl_type::DdlType, mysql::{mysql_meta_manager::MysqlMetaManager, mysql_tb_meta::MysqlTbMeta}, row_data::RowData, row_type::RowType, }; -use sqlx::{MySql, Pool, Transaction}; +use sqlx::{ + mysql::{MySqlConnectOptions, MySqlPoolOptions}, + MySql, Pool, Transaction, +}; use async_trait::async_trait; #[derive(Clone)] pub struct MysqlSinker { + pub url: String, pub conn_pool: Pool, pub meta_manager: MysqlMetaManager, pub router: RdbRouter, @@ -56,7 +63,32 @@ impl Sinker for MysqlSinker { return close_conn_pool!(self); } - async fn sink_ddl(&mut self, _data: Vec, _batch: bool) -> Result<(), Error> { + async fn sink_ddl(&mut self, data: Vec, _batch: bool) -> Result<(), Error> { + for ddl_data in data.iter() { + log_info!("sink ddl: {}", ddl_data.query); + let query = sqlx::query(&ddl_data.query); + + // create a tmp connection with databse since sqlx conn pool does NOT support `USE db` + let mut conn_options = MySqlConnectOptions::from_str(&self.url).unwrap(); + if !ddl_data.schema.is_empty() && ddl_data.ddl_type != DdlType::CreateDatabase { + conn_options = conn_options.database(&ddl_data.schema); + } + + let conn_pool = MySqlPoolOptions::new() + .max_connections(1) + .connect_with(conn_options) + .await + .unwrap(); + query.execute(&conn_pool).await.unwrap(); + } + Ok(()) + } + + async fn refresh_meta(&mut self, data: Vec) -> Result<(), Error> { + for ddl_data in data.iter() { + self.meta_manager + .invalidate_cache(&ddl_data.schema, &ddl_data.tb); + } Ok(()) } } diff --git a/dt-connector/src/sinker/open_faas_sinker.rs b/dt-connector/src/sinker/open_faas_sinker.rs index 977d2dd1..db6df6d7 100644 --- a/dt-connector/src/sinker/open_faas_sinker.rs +++ b/dt-connector/src/sinker/open_faas_sinker.rs @@ -1,7 +1,7 @@ use crate::{call_batch_fn, Sinker}; use async_trait::async_trait; use dt_common::{error::Error, log_error}; -use dt_meta::{ddl_data::DdlData, row_data::RowData}; +use dt_meta::row_data::RowData; use reqwest::Client; use serde_json::json; @@ -17,14 +17,6 @@ impl Sinker for OpenFaasSinker { call_batch_fn!(self, data, Self::invoke); Ok(()) } - - async fn sink_ddl(&mut self, _data: Vec, _batch: bool) -> Result<(), Error> { - Ok(()) - } - - async fn close(&mut self) -> Result<(), Error> { - Ok(()) - } } impl OpenFaasSinker { diff --git a/dt-connector/src/sinker/pg/pg_sinker.rs b/dt-connector/src/sinker/pg/pg_sinker.rs index d2d0b501..503c3656 100644 --- a/dt-connector/src/sinker/pg/pg_sinker.rs +++ b/dt-connector/src/sinker/pg/pg_sinker.rs @@ -10,7 +10,6 @@ use sqlx::{Pool, Postgres}; use dt_meta::{ col_value::ColValue, - ddl_data::DdlData, pg::{pg_meta_manager::PgMetaManager, pg_tb_meta::PgTbMeta}, row_data::RowData, row_type::RowType, @@ -52,10 +51,6 @@ impl Sinker for PgSinker { async fn close(&mut self) -> Result<(), Error> { return close_conn_pool!(self); } - - async fn sink_ddl(&mut self, _data: Vec, _batch: bool) -> Result<(), Error> { - Ok(()) - } } impl PgSinker { diff --git a/dt-connector/src/sinker/pg/pg_struct_sinker.rs b/dt-connector/src/sinker/pg/pg_struct_sinker.rs index 93a32a23..523dcc28 100644 --- a/dt-connector/src/sinker/pg/pg_struct_sinker.rs +++ b/dt-connector/src/sinker/pg/pg_struct_sinker.rs @@ -110,7 +110,7 @@ impl PgStructSinker { if constraint_type == &ConstraintTypeEnum::Foregin.to_charval().unwrap() { let msg = format!("foreign key is not supported yet, schema:[{}], table:[{}], constraint_name:[{}]", schema_name, table_name, constraint_name); println!("{}", msg); - return Err(Error::StructError { error: msg }); + return Err(Error::StructError(msg)); } let sql = format!( "ALTER TABLE \"{}\".\"{}\" ADD CONSTRAINT \"{}\" {}", diff --git a/dt-connector/src/sinker/rdb_router.rs b/dt-connector/src/sinker/rdb_router.rs index a72190f5..8f89f3d2 100644 --- a/dt-connector/src/sinker/rdb_router.rs +++ b/dt-connector/src/sinker/rdb_router.rs @@ -62,9 +62,10 @@ impl RdbRouter { || !Self::is_valid_name(tokens[0], &name_type) || !Self::is_valid_name(tokens[1], &name_type) { - return Err(Error::ConfigError { - error: format!("invalid router config, check error near: {}", name), - }); + return Err(Error::ConfigError(format!( + "invalid router config, check error near: {}", + name + ))); } map.insert( tokens.first().unwrap().to_string(), diff --git a/dt-connector/src/sinker/redis/cmd_encoder.rs b/dt-connector/src/sinker/redis/cmd_encoder.rs new file mode 100644 index 00000000..949adbd0 --- /dev/null +++ b/dt-connector/src/sinker/redis/cmd_encoder.rs @@ -0,0 +1,39 @@ +use byteorder::WriteBytesExt; +use dt_meta::redis::redis_object::RedisCmd; +use std::io::Write; + +pub struct CmdEncoder {} + +impl CmdEncoder { + pub fn encode(cmd: &RedisCmd) -> Vec { + let mut buf = Vec::new(); + + buf.write_u8(super::RESP_ARRAY).unwrap(); + // write array length + Self::write_length(&mut buf, cmd.args.len()); + + for arg in cmd.args.iter() { + Self::write_arg(&mut buf, arg); + } + buf + } + + pub fn write_arg(buf: &mut Vec, arg: &[u8]) { + buf.write_u8(super::RESP_STRING).unwrap(); + // write arg length + Self::write_length(buf, arg.len()); + // write arg data + buf.write(arg).unwrap(); + // write crlf + Self::write_crlf(buf); + } + + fn write_length(buf: &mut Vec, len: usize) { + buf.write(len.to_string().as_bytes()).unwrap(); + Self::write_crlf(buf); + } + + fn write_crlf(buf: &mut Vec) { + buf.write(b"\r\n").unwrap(); + } +} diff --git a/dt-connector/src/sinker/redis/entry_rewriter.rs b/dt-connector/src/sinker/redis/entry_rewriter.rs new file mode 100644 index 00000000..94a6d281 --- /dev/null +++ b/dt-connector/src/sinker/redis/entry_rewriter.rs @@ -0,0 +1,375 @@ +use dt_common::error::Error; +use dt_meta::redis::{ + redis_entry::RedisEntry, + redis_object::{ + HashObject, ListObject, ModuleObject, RedisCmd, SetObject, StringObject, ZsetObject, + }, +}; + +const CRC64_TABLE: [u64; 256] = [ + 0x0000000000000000, + 0x7ad870c830358979, + 0xf5b0e190606b12f2, + 0x8f689158505e9b8b, + 0xc038e5739841b68f, + 0xbae095bba8743ff6, + 0x358804e3f82aa47d, + 0x4f50742bc81f2d04, + 0xab28ecb46814fe75, + 0xd1f09c7c5821770c, + 0x5e980d24087fec87, + 0x24407dec384a65fe, + 0x6b1009c7f05548fa, + 0x11c8790fc060c183, + 0x9ea0e857903e5a08, + 0xe478989fa00bd371, + 0x7d08ff3b88be6f81, + 0x07d08ff3b88be6f8, + 0x88b81eabe8d57d73, + 0xf2606e63d8e0f40a, + 0xbd301a4810ffd90e, + 0xc7e86a8020ca5077, + 0x4880fbd87094cbfc, + 0x32588b1040a14285, + 0xd620138fe0aa91f4, + 0xacf86347d09f188d, + 0x2390f21f80c18306, + 0x594882d7b0f40a7f, + 0x1618f6fc78eb277b, + 0x6cc0863448deae02, + 0xe3a8176c18803589, + 0x997067a428b5bcf0, + 0xfa11fe77117cdf02, + 0x80c98ebf2149567b, + 0x0fa11fe77117cdf0, + 0x75796f2f41224489, + 0x3a291b04893d698d, + 0x40f16bccb908e0f4, + 0xcf99fa94e9567b7f, + 0xb5418a5cd963f206, + 0x513912c379682177, + 0x2be1620b495da80e, + 0xa489f35319033385, + 0xde51839b2936bafc, + 0x9101f7b0e12997f8, + 0xebd98778d11c1e81, + 0x64b116208142850a, + 0x1e6966e8b1770c73, + 0x8719014c99c2b083, + 0xfdc17184a9f739fa, + 0x72a9e0dcf9a9a271, + 0x08719014c99c2b08, + 0x4721e43f0183060c, + 0x3df994f731b68f75, + 0xb29105af61e814fe, + 0xc849756751dd9d87, + 0x2c31edf8f1d64ef6, + 0x56e99d30c1e3c78f, + 0xd9810c6891bd5c04, + 0xa3597ca0a188d57d, + 0xec09088b6997f879, + 0x96d1784359a27100, + 0x19b9e91b09fcea8b, + 0x636199d339c963f2, + 0xdf7adabd7a6e2d6f, + 0xa5a2aa754a5ba416, + 0x2aca3b2d1a053f9d, + 0x50124be52a30b6e4, + 0x1f423fcee22f9be0, + 0x659a4f06d21a1299, + 0xeaf2de5e82448912, + 0x902aae96b271006b, + 0x74523609127ad31a, + 0x0e8a46c1224f5a63, + 0x81e2d7997211c1e8, + 0xfb3aa75142244891, + 0xb46ad37a8a3b6595, + 0xceb2a3b2ba0eecec, + 0x41da32eaea507767, + 0x3b024222da65fe1e, + 0xa2722586f2d042ee, + 0xd8aa554ec2e5cb97, + 0x57c2c41692bb501c, + 0x2d1ab4dea28ed965, + 0x624ac0f56a91f461, + 0x1892b03d5aa47d18, + 0x97fa21650afae693, + 0xed2251ad3acf6fea, + 0x095ac9329ac4bc9b, + 0x7382b9faaaf135e2, + 0xfcea28a2faafae69, + 0x8632586aca9a2710, + 0xc9622c4102850a14, + 0xb3ba5c8932b0836d, + 0x3cd2cdd162ee18e6, + 0x460abd1952db919f, + 0x256b24ca6b12f26d, + 0x5fb354025b277b14, + 0xd0dbc55a0b79e09f, + 0xaa03b5923b4c69e6, + 0xe553c1b9f35344e2, + 0x9f8bb171c366cd9b, + 0x10e3202993385610, + 0x6a3b50e1a30ddf69, + 0x8e43c87e03060c18, + 0xf49bb8b633338561, + 0x7bf329ee636d1eea, + 0x012b592653589793, + 0x4e7b2d0d9b47ba97, + 0x34a35dc5ab7233ee, + 0xbbcbcc9dfb2ca865, + 0xc113bc55cb19211c, + 0x5863dbf1e3ac9dec, + 0x22bbab39d3991495, + 0xadd33a6183c78f1e, + 0xd70b4aa9b3f20667, + 0x985b3e827bed2b63, + 0xe2834e4a4bd8a21a, + 0x6debdf121b863991, + 0x1733afda2bb3b0e8, + 0xf34b37458bb86399, + 0x8993478dbb8deae0, + 0x06fbd6d5ebd3716b, + 0x7c23a61ddbe6f812, + 0x3373d23613f9d516, + 0x49aba2fe23cc5c6f, + 0xc6c333a67392c7e4, + 0xbc1b436e43a74e9d, + 0x95ac9329ac4bc9b5, + 0xef74e3e19c7e40cc, + 0x601c72b9cc20db47, + 0x1ac40271fc15523e, + 0x5594765a340a7f3a, + 0x2f4c0692043ff643, + 0xa02497ca54616dc8, + 0xdafce7026454e4b1, + 0x3e847f9dc45f37c0, + 0x445c0f55f46abeb9, + 0xcb349e0da4342532, + 0xb1eceec59401ac4b, + 0xfebc9aee5c1e814f, + 0x8464ea266c2b0836, + 0x0b0c7b7e3c7593bd, + 0x71d40bb60c401ac4, + 0xe8a46c1224f5a634, + 0x927c1cda14c02f4d, + 0x1d148d82449eb4c6, + 0x67ccfd4a74ab3dbf, + 0x289c8961bcb410bb, + 0x5244f9a98c8199c2, + 0xdd2c68f1dcdf0249, + 0xa7f41839ecea8b30, + 0x438c80a64ce15841, + 0x3954f06e7cd4d138, + 0xb63c61362c8a4ab3, + 0xcce411fe1cbfc3ca, + 0x83b465d5d4a0eece, + 0xf96c151de49567b7, + 0x76048445b4cbfc3c, + 0x0cdcf48d84fe7545, + 0x6fbd6d5ebd3716b7, + 0x15651d968d029fce, + 0x9a0d8ccedd5c0445, + 0xe0d5fc06ed698d3c, + 0xaf85882d2576a038, + 0xd55df8e515432941, + 0x5a3569bd451db2ca, + 0x20ed197575283bb3, + 0xc49581ead523e8c2, + 0xbe4df122e51661bb, + 0x3125607ab548fa30, + 0x4bfd10b2857d7349, + 0x04ad64994d625e4d, + 0x7e7514517d57d734, + 0xf11d85092d094cbf, + 0x8bc5f5c11d3cc5c6, + 0x12b5926535897936, + 0x686de2ad05bcf04f, + 0xe70573f555e26bc4, + 0x9ddd033d65d7e2bd, + 0xd28d7716adc8cfb9, + 0xa85507de9dfd46c0, + 0x273d9686cda3dd4b, + 0x5de5e64efd965432, + 0xb99d7ed15d9d8743, + 0xc3450e196da80e3a, + 0x4c2d9f413df695b1, + 0x36f5ef890dc31cc8, + 0x79a59ba2c5dc31cc, + 0x037deb6af5e9b8b5, + 0x8c157a32a5b7233e, + 0xf6cd0afa9582aa47, + 0x4ad64994d625e4da, + 0x300e395ce6106da3, + 0xbf66a804b64ef628, + 0xc5bed8cc867b7f51, + 0x8aeeace74e645255, + 0xf036dc2f7e51db2c, + 0x7f5e4d772e0f40a7, + 0x05863dbf1e3ac9de, + 0xe1fea520be311aaf, + 0x9b26d5e88e0493d6, + 0x144e44b0de5a085d, + 0x6e963478ee6f8124, + 0x21c640532670ac20, + 0x5b1e309b16452559, + 0xd476a1c3461bbed2, + 0xaeaed10b762e37ab, + 0x37deb6af5e9b8b5b, + 0x4d06c6676eae0222, + 0xc26e573f3ef099a9, + 0xb8b627f70ec510d0, + 0xf7e653dcc6da3dd4, + 0x8d3e2314f6efb4ad, + 0x0256b24ca6b12f26, + 0x788ec2849684a65f, + 0x9cf65a1b368f752e, + 0xe62e2ad306bafc57, + 0x6946bb8b56e467dc, + 0x139ecb4366d1eea5, + 0x5ccebf68aecec3a1, + 0x2616cfa09efb4ad8, + 0xa97e5ef8cea5d153, + 0xd3a62e30fe90582a, + 0xb0c7b7e3c7593bd8, + 0xca1fc72bf76cb2a1, + 0x45775673a732292a, + 0x3faf26bb9707a053, + 0x70ff52905f188d57, + 0x0a2722586f2d042e, + 0x854fb3003f739fa5, + 0xff97c3c80f4616dc, + 0x1bef5b57af4dc5ad, + 0x61372b9f9f784cd4, + 0xee5fbac7cf26d75f, + 0x9487ca0fff135e26, + 0xdbd7be24370c7322, + 0xa10fceec0739fa5b, + 0x2e675fb4576761d0, + 0x54bf2f7c6752e8a9, + 0xcdcf48d84fe75459, + 0xb71738107fd2dd20, + 0x387fa9482f8c46ab, + 0x42a7d9801fb9cfd2, + 0x0df7adabd7a6e2d6, + 0x772fdd63e7936baf, + 0xf8474c3bb7cdf024, + 0x829f3cf387f8795d, + 0x66e7a46c27f3aa2c, + 0x1c3fd4a417c62355, + 0x935745fc4798b8de, + 0xe98f353477ad31a7, + 0xa6df411fbfb21ca3, + 0xdc0731d78f8795da, + 0x536fa08fdfd90e51, + 0x29b7d047efec8728, +]; + +pub struct EntryRewriter {} + +impl EntryRewriter { + pub fn rewrite_hash(obj: &mut HashObject) -> Result, Error> { + let mut cmds = vec![]; + for (k, v) in &obj.value { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("hset"); + cmd.add_redis_arg(&obj.key); + cmd.add_redis_arg(k); + cmd.add_redis_arg(v); + cmds.push(cmd); + } + Ok(cmds) + } + + pub fn rewrite_list(obj: &mut ListObject) -> Result, Error> { + let mut cmds = vec![]; + for ele in &obj.elements { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("rpush"); + cmd.add_redis_arg(&obj.key); + cmd.add_redis_arg(ele); + cmds.push(cmd); + } + Ok(cmds) + } + + pub fn rewrite_module(_obj: &mut ModuleObject) -> Result, Error> { + Err(Error::Unexpected("module rewrite not implemented".into())) + } + + pub fn rewrite_set(obj: &mut SetObject) -> Result, Error> { + let mut cmds = vec![]; + for ele in &obj.elements { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("sadd"); + cmd.add_redis_arg(&obj.key); + cmd.add_redis_arg(ele); + cmds.push(cmd); + } + Ok(cmds) + } + + pub fn rewrite_string(obj: &mut StringObject) -> Result, Error> { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("set"); + cmd.add_redis_arg(&obj.key); + cmd.add_redis_arg(&obj.value); + Ok(vec![cmd]) + } + + pub fn rewrite_zset(obj: &mut ZsetObject) -> Result, Error> { + let mut cmds = vec![]; + for ele in obj.elements.iter() { + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("zadd"); + cmd.add_redis_arg(&obj.key); + cmd.add_redis_arg(&ele.score); + cmd.add_redis_arg(&ele.member); + cmds.push(cmd); + } + Ok(cmds) + } + + pub fn rewrite_as_restore(entry: &RedisEntry, version: f32) -> Result { + let value = Self::create_value_dump(entry.value_type_byte, &entry.raw_bytes); + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("restore"); + cmd.add_redis_arg(&entry.key); + cmd.add_str_arg(&entry.expire_ms.to_string()); + cmd.add_arg(value); + if version >= 3.0 { + cmd.add_str_arg("replace"); + } + Ok(cmd) + } + + pub fn rewrite_expire(entry: &RedisEntry) -> Result, Error> { + if entry.expire_ms == 0 { + return Ok(None); + } + let mut cmd = RedisCmd::new(); + cmd.add_str_arg("pexpire"); + cmd.add_redis_arg(&entry.key); + cmd.add_str_arg(&entry.expire_ms.to_string()); + Ok(Some(cmd)) + } + + fn create_value_dump(type_byte: u8, val: &[u8]) -> Vec { + let mut buf: Vec = Vec::new(); + buf.push(type_byte); + buf.extend_from_slice(val); + buf.extend_from_slice(&6u16.to_le_bytes()); + let sum64 = Self::calc_crc64(&buf); + buf.extend_from_slice(&sum64.to_le_bytes()); + buf + } + + fn calc_crc64(p: &[u8]) -> u64 { + let mut crc: u64 = 0; + for b in p { + let inx = (crc as u8) ^ *b; + crc = CRC64_TABLE[inx as usize] ^ (crc >> 8); + } + crc + } +} diff --git a/dt-connector/src/sinker/redis/mod.rs b/dt-connector/src/sinker/redis/mod.rs new file mode 100644 index 00000000..c8bc82cd --- /dev/null +++ b/dt-connector/src/sinker/redis/mod.rs @@ -0,0 +1,20 @@ +pub mod cmd_encoder; +pub mod entry_rewriter; +pub mod redis_sinker; + +/// redis resp protocol data type +pub const RESP_STATUS: u8 = b'+'; // +\r\n +pub const RESP_ERROR: u8 = b'-'; // -\r\n +pub const RESP_STRING: u8 = b'$'; // $\r\n\r\n +pub const RESP_INT: u8 = b':'; // :\r\n +pub const RESP_NIL: u8 = b'_'; // _\r\n +pub const RESP_FLOAT: u8 = b','; // ,\r\n (golang float) +pub const RESP_BOOL: u8 = b'#'; // true: #t\r\n false: #f\r\n +pub const RESP_BLOB_ERROR: u8 = b'!'; // !\r\n\r\n +pub const RESP_VERBATIM: u8 = b'='; // =\r\nFORMAT:\r\n +pub const RESP_BIG_INT: u8 = b'('; // (\r\n +pub const RESP_ARRAY: u8 = b'*'; // *\r\n... (same as resp2) +pub const RESP_MAP: u8 = b'%'; // %\r\n(key)\r\n(value)\r\n... (golang map) +pub const RESP_SET: u8 = b'~'; // ~\r\n... (same as Array) +pub const RESP_ATTR: u8 = b'|'; // |\r\n(key)\r\n(value)\r\n... + command reply +pub const RESP_PUSH: u8 = b'>'; // >\r\n... (same as Array) diff --git a/dt-connector/src/sinker/redis/redis_sinker.rs b/dt-connector/src/sinker/redis/redis_sinker.rs new file mode 100644 index 00000000..11b528c4 --- /dev/null +++ b/dt-connector/src/sinker/redis/redis_sinker.rs @@ -0,0 +1,127 @@ +use async_trait::async_trait; +use dt_common::error::Error; +use dt_meta::dt_data::DtData; +use dt_meta::redis::redis_object::RedisCmd; +use dt_meta::redis::redis_object::RedisObject; +use dt_meta::redis::redis_write_method::RedisWriteMethod; +use redis::Connection; +use redis::ConnectionLike; + +use crate::call_batch_fn; +use crate::Sinker; + +use super::cmd_encoder::CmdEncoder; +use super::entry_rewriter::EntryRewriter; + +pub struct RedisSinker { + pub batch_size: usize, + pub conn: Connection, + pub now_db_id: i64, + pub version: f32, + pub method: RedisWriteMethod, +} + +#[async_trait] +impl Sinker for RedisSinker { + async fn sink_raw(&mut self, mut data: Vec, _batch: bool) -> Result<(), Error> { + if self.batch_size > 1 { + call_batch_fn!(self, data, Self::batch_sink); + } else { + self.serial_sink(&mut data).await?; + } + Ok(()) + } +} + +impl RedisSinker { + async fn batch_sink( + &mut self, + data: &mut [DtData], + start_index: usize, + batch_size: usize, + ) -> Result<(), Error> { + let mut cmds = Vec::new(); + for dt_data in data.iter_mut().skip(start_index).take(batch_size) { + cmds.extend_from_slice(&self.rewrite_entry(dt_data)?); + } + + let mut packed_cmds = Vec::new(); + for cmd in cmds { + packed_cmds.extend_from_slice(&CmdEncoder::encode(&cmd)); + } + + let result = self.conn.req_packed_commands(&packed_cmds, 0, batch_size); + if let Err(error) = result { + return Err(Error::SinkerError(format!( + "batch sink failed, error: {:?}", + error + ))); + } + Ok(()) + } + + async fn serial_sink(&mut self, data: &mut [DtData]) -> Result<(), Error> { + for dt_data in data.iter_mut() { + let cmds = self.rewrite_entry(dt_data)?; + for cmd in cmds { + let result = self.conn.req_packed_command(&CmdEncoder::encode(&cmd)); + if let Err(error) = result { + return Err(Error::SinkerError(format!( + "serial sink failed, error: {:?}, cmd: {}", + error, + cmd.to_string() + ))); + } + } + } + Ok(()) + } + + fn rewrite_entry(&mut self, dt_data: &mut DtData) -> Result, Error> { + let mut cmds = Vec::new(); + match dt_data { + DtData::Redis { ref mut entry } => { + if entry.db_id != self.now_db_id { + let db_id = &entry.db_id.to_string(); + let args = vec!["SELECT", db_id]; + let cmd = RedisCmd::from_str_args(&args); + cmds.push(cmd); + self.now_db_id = entry.db_id; + } + + match self.method { + RedisWriteMethod::Restore => { + if entry.is_raw() { + let cmd = EntryRewriter::rewrite_as_restore(&entry, self.version)?; + cmds.push(cmd); + } else { + cmds.push(entry.cmd.clone()); + } + } + + RedisWriteMethod::Rewrite => { + let mut rewrite_cmds = match entry.value { + RedisObject::String(ref mut obj) => EntryRewriter::rewrite_string(obj), + RedisObject::List(ref mut obj) => EntryRewriter::rewrite_list(obj), + RedisObject::Set(ref mut obj) => EntryRewriter::rewrite_set(obj), + RedisObject::Hash(ref mut obj) => EntryRewriter::rewrite_hash(obj), + RedisObject::Zset(ref mut obj) => EntryRewriter::rewrite_zset(obj), + RedisObject::Stream(ref mut obj) => Ok(obj.cmds.drain(..).collect()), + RedisObject::Module(_) => { + let cmd = EntryRewriter::rewrite_as_restore(&entry, self.version)?; + Ok(vec![cmd]) + } + _ => return Err(Error::SinkerError("rewrite not implemented".into())), + }?; + if let Some(expire_cmd) = EntryRewriter::rewrite_expire(&entry)? { + rewrite_cmds.push(expire_cmd) + } + cmds.extend(rewrite_cmds); + } + } + } + _ => {} + } + Ok(cmds) + } +} diff --git a/dt-connector/src/sinker/sinker_meta.rs b/dt-connector/src/sinker/sinker_meta.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dt-connector/src/sinker/sinker_meta.rs @@ -0,0 +1 @@ + diff --git a/dt-meta/src/adaptor/mysql_col_value_convertor.rs b/dt-meta/src/adaptor/mysql_col_value_convertor.rs index c79cc083..1dd07417 100644 --- a/dt-meta/src/adaptor/mysql_col_value_convertor.rs +++ b/dt-meta/src/adaptor/mysql_col_value_convertor.rs @@ -3,7 +3,9 @@ use std::io::{Cursor, Seek, SeekFrom}; use byteorder::{LittleEndian, ReadBytesExt}; use chrono::{TimeZone, Utc}; use dt_common::error::Error; -use mysql_binlog_connector_rust::column::column_value::ColumnValue; +use mysql_binlog_connector_rust::column::{ + column_value::ColumnValue, json::json_binary::JsonBinary, +}; use sqlx::{mysql::MySqlRow, Row}; use crate::{col_value::ColValue, mysql::mysql_col_type::MysqlColType}; @@ -69,8 +71,8 @@ impl MysqlColValueConvertor { } } - pub fn from_binlog(col_type: &MysqlColType, value: ColumnValue) -> ColValue { - match value { + pub fn from_binlog(col_type: &MysqlColType, value: ColumnValue) -> Result { + let col_value = match value { ColumnValue::Tiny(v) => { if *col_type == MysqlColType::UnsignedTiny { ColValue::UnsignedTiny(v as u8) @@ -133,7 +135,7 @@ impl MysqlColValueConvertor { if length as usize > v.len() { let pad_v: Vec = vec![0; length as usize - v.len()]; let final_v = [v, pad_v].concat(); - return ColValue::Blob(final_v); + return Ok(ColValue::Blob(final_v)); } } ColValue::Blob(v) @@ -143,10 +145,15 @@ impl MysqlColValueConvertor { ColumnValue::Bit(v) => ColValue::Bit(v), ColumnValue::Set(v) => ColValue::Set(v), ColumnValue::Enum(v) => ColValue::Enum(v), - ColumnValue::Json(v) => ColValue::Json(v), + ColumnValue::Json(v) => { + let v = JsonBinary::parse_as_string(&v)?; + ColValue::Json2(v) + } _ => ColValue::None, - } + }; + + Ok(col_value) } pub fn from_str(col_type: &MysqlColType, value_str: &str) -> Result { @@ -218,10 +225,13 @@ impl MysqlColValueConvertor { MysqlColType::Set => ColValue::String(value_str), MysqlColType::Enum => ColValue::String(value_str), + MysqlColType::Json => ColValue::Json2(value_str), + _ => { - return Err(Error::Unexpected { - error: format!("unsupported column type, column type: {:?}", col_type), - }) + return Err(Error::Unexpected(format!( + "unsupported column type: {:?}", + col_type + ))) } }; @@ -335,8 +345,13 @@ impl MysqlColValueConvertor { return Ok(ColValue::Enum2(value)); } MysqlColType::Json => { - let value: Vec = row.get_unchecked(col); - return Ok(ColValue::Json(value)); + let value: serde_json::Value = row.try_get(col)?; + // TODO, decimal will lose precision when insert into target mysql as string. + // insert into json_table(id, json_col) values(1, "212765.700000000010000"); the result will be: + // +-----+--------------------------+ + // | id | json_col | + // | 1 | 212765.7 | + return Ok(ColValue::Json2(value.to_string())); } _ => {} } diff --git a/dt-meta/src/adaptor/sqlx_ext.rs b/dt-meta/src/adaptor/sqlx_ext.rs index 9c98fc30..1679acdd 100644 --- a/dt-meta/src/adaptor/sqlx_ext.rs +++ b/dt-meta/src/adaptor/sqlx_ext.rs @@ -111,6 +111,7 @@ impl<'q> SqlxMysqlExt<'q> for Query<'q, MySql, MySqlArguments> { ColValue::Enum(v) => self.bind(v), ColValue::Enum2(v) => self.bind(v), ColValue::Json(v) => self.bind(v), + ColValue::Json2(v) => self.bind(v), _ => { let none: Option = Option::None; self.bind(none) diff --git a/dt-meta/src/col_value.rs b/dt-meta/src/col_value.rs index 81db22e7..d2f687d1 100644 --- a/dt-meta/src/col_value.rs +++ b/dt-meta/src/col_value.rs @@ -4,9 +4,10 @@ use std::{ }; use mongodb::bson::Document; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] #[allow(dead_code)] pub enum ColValue { None, @@ -35,6 +36,7 @@ pub enum ColValue { Set2(String), Enum2(String), Json(Vec), + Json2(String), MongoDoc(Document), } @@ -77,43 +79,45 @@ impl ColValue { ColValue::Enum2(v) => Some(v.to_string()), // TODO: support JSON ColValue::Json(v) => Some(format!("{:?}", v)), + ColValue::Json2(v) => Some(v.to_string()), _ => Option::None, } } } -impl Serialize for ColValue { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - ColValue::Tiny(v) => serializer.serialize_i8(*v), - ColValue::UnsignedTiny(v) => serializer.serialize_u8(*v), - ColValue::Short(v) => serializer.serialize_i16(*v), - ColValue::UnsignedShort(v) => serializer.serialize_u16(*v), - ColValue::Long(v) => serializer.serialize_i32(*v), - ColValue::UnsignedLong(v) => serializer.serialize_u32(*v), - ColValue::LongLong(v) => serializer.serialize_i64(*v), - ColValue::UnsignedLongLong(v) => serializer.serialize_u64(*v), - ColValue::Float(v) => serializer.serialize_f32(*v), - ColValue::Double(v) => serializer.serialize_f64(*v), - ColValue::Decimal(v) => serializer.serialize_str(v), - ColValue::Time(v) => serializer.serialize_str(v), - ColValue::Date(v) => serializer.serialize_str(v), - ColValue::DateTime(v) => serializer.serialize_str(v), - ColValue::Timestamp(v) => serializer.serialize_str(v), - ColValue::Year(v) => serializer.serialize_u16(*v), - ColValue::String(v) => serializer.serialize_str(v), - ColValue::Blob(v) => serializer.serialize_bytes(v), - ColValue::Bit(v) => serializer.serialize_u64(*v), - ColValue::Set(v) => serializer.serialize_u64(*v), - ColValue::Set2(v) => serializer.serialize_str(v), - ColValue::Enum(v) => serializer.serialize_u32(*v), - ColValue::Enum2(v) => serializer.serialize_str(v), - // TODO: support JSON - ColValue::Json(v) => serializer.serialize_bytes(v), - _ => serializer.serialize_none(), - } - } -} +// impl Serialize for ColValue { +// fn serialize(&self, serializer: S) -> Result +// where +// S: Serializer, +// { +// match self { +// ColValue::Tiny(v) => serializer.serialize_i8(*v), +// ColValue::UnsignedTiny(v) => serializer.serialize_u8(*v), +// ColValue::Short(v) => serializer.serialize_i16(*v), +// ColValue::UnsignedShort(v) => serializer.serialize_u16(*v), +// ColValue::Long(v) => serializer.serialize_i32(*v), +// ColValue::UnsignedLong(v) => serializer.serialize_u32(*v), +// ColValue::LongLong(v) => serializer.serialize_i64(*v), +// ColValue::UnsignedLongLong(v) => serializer.serialize_u64(*v), +// ColValue::Float(v) => serializer.serialize_f32(*v), +// ColValue::Double(v) => serializer.serialize_f64(*v), +// ColValue::Decimal(v) => serializer.serialize_str(v), +// ColValue::Time(v) => serializer.serialize_str(v), +// ColValue::Date(v) => serializer.serialize_str(v), +// ColValue::DateTime(v) => serializer.serialize_str(v), +// ColValue::Timestamp(v) => serializer.serialize_str(v), +// ColValue::Year(v) => serializer.serialize_u16(*v), +// ColValue::String(v) => serializer.serialize_str(v), +// ColValue::Blob(v) => serializer.serialize_bytes(v), +// ColValue::Bit(v) => serializer.serialize_u64(*v), +// ColValue::Set(v) => serializer.serialize_u64(*v), +// ColValue::Set2(v) => serializer.serialize_str(v), +// ColValue::Enum(v) => serializer.serialize_u32(*v), +// ColValue::Enum2(v) => serializer.serialize_str(v), +// // TODO: support JSON +// ColValue::Json(v) => serializer.serialize_bytes(v), +// ColValue::Json2(v) => serializer.serialize_str(v), +// _ => serializer.serialize_none(), +// } +// } +// } diff --git a/dt-meta/src/ddl_data.rs b/dt-meta/src/ddl_data.rs index 9671e042..bb01b545 100644 --- a/dt-meta/src/ddl_data.rs +++ b/dt-meta/src/ddl_data.rs @@ -6,6 +6,7 @@ use super::{ddl_type::DdlType, struct_meta::database_model::StructModel}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DdlData { pub schema: String, + pub tb: String, pub query: String, #[serde(skip)] pub meta: Option, diff --git a/dt-meta/src/ddl_type.rs b/dt-meta/src/ddl_type.rs index 444f44de..4c6decfe 100644 --- a/dt-meta/src/ddl_type.rs +++ b/dt-meta/src/ddl_type.rs @@ -19,6 +19,8 @@ pub enum DdlType { AlterDatabase, #[strum(serialize = "alter_table")] AlterTable, + #[strum(serialize = "create_index")] + CreateIndex, #[strum(serialize = "unknown")] Unknown, } diff --git a/dt-meta/src/dt_data.rs b/dt-meta/src/dt_data.rs index 0484747b..3175b805 100644 --- a/dt-meta/src/dt_data.rs +++ b/dt-meta/src/dt_data.rs @@ -1,14 +1,38 @@ +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::{kafka::kafka_message::KafkaMessage, redis::redis_entry::RedisEntry}; + use super::{ddl_data::DdlData, row_data::RowData}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum DtData { - Ddl { ddl_data: DdlData }, - Dml { row_data: RowData }, - Commit { xid: String, position: String }, + Ddl { + ddl_data: DdlData, + }, + Dml { + row_data: RowData, + }, + Commit { + xid: String, + position: String, + }, + #[serde(skip)] + Redis { + entry: RedisEntry, + }, + #[serde(skip)] + Kafka { + message: KafkaMessage, + }, } impl DtData { pub fn is_ddl(&self) -> bool { matches!(self, DtData::Ddl { .. }) } + + pub fn to_string(&self) -> String { + json!(self).to_string() + } } diff --git a/dt-meta/src/kafka/kafka_message.rs b/dt-meta/src/kafka/kafka_message.rs new file mode 100644 index 00000000..58a88cb7 --- /dev/null +++ b/dt-meta/src/kafka/kafka_message.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Clone)] +pub struct KafkaMessage { + pub topic: String, + pub partition: i32, + pub offset: i64, + pub key: Vec, + pub payload: Vec, + pub position: String, +} diff --git a/dt-meta/src/kafka/mod.rs b/dt-meta/src/kafka/mod.rs new file mode 100644 index 00000000..a781768f --- /dev/null +++ b/dt-meta/src/kafka/mod.rs @@ -0,0 +1 @@ +pub mod kafka_message; diff --git a/dt-meta/src/lib.rs b/dt-meta/src/lib.rs index b4d083d8..40417782 100644 --- a/dt-meta/src/lib.rs +++ b/dt-meta/src/lib.rs @@ -4,10 +4,13 @@ pub mod db_enums; pub mod ddl_data; pub mod ddl_type; pub mod dt_data; +pub mod kafka; +pub mod mongo; pub mod mysql; pub mod pg; pub mod rdb_meta_manager; pub mod rdb_tb_meta; +pub mod redis; pub mod row_data; pub mod row_type; pub mod sql_parser; diff --git a/dt-meta/src/mongo/mod.rs b/dt-meta/src/mongo/mod.rs new file mode 100644 index 00000000..b07efc0e --- /dev/null +++ b/dt-meta/src/mongo/mod.rs @@ -0,0 +1,3 @@ +pub mod mongo_cdc_source; +pub mod mongo_constant; +pub mod mongo_key; diff --git a/dt-meta/src/mongo/mongo_cdc_source.rs b/dt-meta/src/mongo/mongo_cdc_source.rs new file mode 100644 index 00000000..d832fa44 --- /dev/null +++ b/dt-meta/src/mongo/mongo_cdc_source.rs @@ -0,0 +1,23 @@ +use std::str::FromStr; + +use dt_common::error::Error; +use strum::IntoStaticStr; + +#[derive(Clone, IntoStaticStr, Debug)] +pub enum MongoCdcSource { + #[strum(serialize = "op_log")] + OpLog, + + #[strum(serialize = "change_stream")] + ChangeStream, +} + +impl FromStr for MongoCdcSource { + type Err = Error; + fn from_str(str: &str) -> Result { + match str { + "op_log" => Ok(Self::OpLog), + _ => Ok(Self::ChangeStream), + } + } +} diff --git a/dt-common/src/constants.rs b/dt-meta/src/mongo/mongo_constant.rs similarity index 81% rename from dt-common/src/constants.rs rename to dt-meta/src/mongo/mongo_constant.rs index 984d7c7e..f5fd5688 100644 --- a/dt-common/src/constants.rs +++ b/dt-meta/src/mongo/mongo_constant.rs @@ -4,5 +4,6 @@ impl MongoConstants { pub const APP_NAME: &str = "APE_DTS"; pub const ID: &str = "_id"; pub const DOC: &str = "doc"; + pub const DIFF_DOC: &str = "diff_doc"; pub const SET: &str = "$set"; } diff --git a/dt-meta/src/mongo/mongo_key.rs b/dt-meta/src/mongo/mongo_key.rs new file mode 100644 index 00000000..791dec6e --- /dev/null +++ b/dt-meta/src/mongo/mongo_key.rs @@ -0,0 +1,37 @@ +use mongodb::bson::{oid::ObjectId, Bson, DateTime, Document, Timestamp}; +use serde::{Deserialize, Serialize}; + +use super::mongo_constant::MongoConstants; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum MongoKey { + ObjectId(ObjectId), + String(String), + Int32(i32), + Int64(i64), + JavaScriptCode(String), + Timestamp(Timestamp), + DateTime(DateTime), + Symbol(String), +} + +impl MongoKey { + pub fn from_doc(doc: &Document) -> Option { + if let Some(id) = doc.get(MongoConstants::ID) { + let value = match id { + Bson::ObjectId(v) => Some(MongoKey::ObjectId(v.clone())), + Bson::String(v) => Some(MongoKey::String(v.clone())), + Bson::Int32(v) => Some(MongoKey::Int32(v.clone())), + Bson::Int64(v) => Some(MongoKey::Int64(v.clone())), + Bson::JavaScriptCode(v) => Some(MongoKey::JavaScriptCode(v.clone())), + Bson::Timestamp(v) => Some(MongoKey::Timestamp(v.clone())), + Bson::DateTime(v) => Some(MongoKey::DateTime(v.clone())), + Bson::Symbol(v) => Some(MongoKey::Symbol(v.clone())), + // other types don't derive Hash and Eq + _ => None, + }; + return value; + } + None + } +} diff --git a/dt-meta/src/mysql/mysql_meta_manager.rs b/dt-meta/src/mysql/mysql_meta_manager.rs index b58d5b5b..93d4d1f0 100644 --- a/dt-meta/src/mysql/mysql_meta_manager.rs +++ b/dt-meta/src/mysql/mysql_meta_manager.rs @@ -36,6 +36,16 @@ impl<'a> MysqlMetaManager { Ok(self) } + pub fn invalidate_cache(&mut self, schema: &str, tb: &str) { + if !schema.is_empty() && !tb.is_empty() { + let full_name = format!("{}.{}", schema, tb); + self.cache.remove(&full_name); + } else { + // clear all cache is always safe + self.cache.clear(); + } + } + pub async fn get_tb_meta(&mut self, schema: &str, tb: &str) -> Result { let full_name = format!("{}.{}", schema, tb); if let Some(tb_meta) = self.cache.get(&full_name) { @@ -92,9 +102,10 @@ impl<'a> MysqlMetaManager { } if cols.is_empty() { - return Err(Error::MetadataError { - error: format!("failed to get table metadata for: `{}`.`{}`", schema, tb), - }); + return Err(Error::MetadataError(format!( + "failed to get table metadata for: `{}`.`{}`", + schema, tb + ))); } Ok((cols, col_type_map)) } @@ -234,9 +245,6 @@ impl<'a> MysqlMetaManager { self.version = row.try_get(0)?; return Ok(()); } - - Err(Error::MetadataError { - error: "failed to init mysql version".to_string(), - }) + Err(Error::MetadataError("failed to init mysql version".into())) } } diff --git a/dt-meta/src/pg/pg_meta_manager.rs b/dt-meta/src/pg/pg_meta_manager.rs index ca1507df..574f2154 100644 --- a/dt-meta/src/pg/pg_meta_manager.rs +++ b/dt-meta/src/pg/pg_meta_manager.rs @@ -185,8 +185,9 @@ impl PgMetaManager { return Ok(oid); } - Err(Error::MetadataError { - error: format!("failed in get_oid for {}", tb), - }) + Err(Error::MetadataError(format!( + "failed to get oid for: {} by query: {}", + tb, sql + ))) } } diff --git a/dt-meta/src/rdb_meta_manager.rs b/dt-meta/src/rdb_meta_manager.rs index 3e9b076a..c4801d1f 100644 --- a/dt-meta/src/rdb_meta_manager.rs +++ b/dt-meta/src/rdb_meta_manager.rs @@ -38,9 +38,9 @@ impl RdbMetaManager { return Ok(tb_meta.basic); } - Err(Error::Unexpected { - error: "no available meta_manager in partitioner".to_string(), - }) + Err(Error::Unexpected( + "no available meta_manager in partitioner".into(), + )) } pub fn parse_rdb_cols( diff --git a/dt-meta/src/redis/mod.rs b/dt-meta/src/redis/mod.rs new file mode 100644 index 00000000..eeef73a9 --- /dev/null +++ b/dt-meta/src/redis/mod.rs @@ -0,0 +1,3 @@ +pub mod redis_entry; +pub mod redis_object; +pub mod redis_write_method; diff --git a/dt-meta/src/redis/redis_entry.rs b/dt-meta/src/redis/redis_entry.rs new file mode 100644 index 00000000..ef93283b --- /dev/null +++ b/dt-meta/src/redis/redis_entry.rs @@ -0,0 +1,59 @@ +use super::redis_object::{RedisCmd, RedisObject, RedisString}; + +#[derive(Debug, Clone)] +pub struct RedisEntry { + pub id: u64, + // whether the command is decoded from dump.rdb file + pub is_base: bool, + pub db_id: i64, + pub timestamp_ms: u64, + + pub group: String, + pub keys: Vec, + pub slots: Vec, + + pub offset: i64, + pub encoded_size: u64, + + pub expire_ms: i64, + pub key: RedisString, + pub value: RedisObject, + pub value_type_byte: u8, + pub raw_bytes: Vec, + + pub cmd: RedisCmd, + + pub position: String, +} + +impl RedisEntry { + pub fn new() -> Self { + Self { + id: 0, + is_base: false, + db_id: 0, + timestamp_ms: 0, + + group: String::new(), + keys: Vec::new(), + slots: Vec::new(), + + offset: 0, + encoded_size: 0, + + expire_ms: 0, + key: RedisString::new(), + value: RedisObject::Unknown, + raw_bytes: Vec::new(), + value_type_byte: 0, + + cmd: RedisCmd::new(), + + position: String::new(), + } + } + + pub fn is_raw(&self) -> bool { + self.is_base && !self.raw_bytes.is_empty() + } +} diff --git a/dt-meta/src/redis/redis_object.rs b/dt-meta/src/redis/redis_object.rs new file mode 100644 index 00000000..04fc3a49 --- /dev/null +++ b/dt-meta/src/redis/redis_object.rs @@ -0,0 +1,209 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum RedisObject { + String(StringObject), + List(ListObject), + Hash(HashObject), + Set(SetObject), + Zset(ZsetObject), + Module(ModuleObject), + Stream(StreamObject), + Unknown, +} + +#[derive(Debug, Clone)] +pub struct HashObject { + pub key: RedisString, + pub value: HashMap, +} + +impl HashObject { + pub fn new() -> Self { + HashObject { + key: RedisString::new(), + value: HashMap::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct ListObject { + pub key: RedisString, + pub elements: Vec, +} + +impl ListObject { + pub fn new() -> Self { + ListObject { + key: RedisString::new(), + elements: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct ModuleObject {} + +impl ModuleObject { + pub fn new() -> Self { + ModuleObject {} + } +} + +#[derive(Debug, Clone)] +pub struct SetObject { + pub key: RedisString, + pub elements: Vec, +} + +impl SetObject { + pub fn new() -> Self { + SetObject { + key: RedisString::new(), + elements: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct StreamObject { + pub key: RedisString, + pub cmds: Vec, +} + +impl StreamObject { + pub fn new() -> Self { + StreamObject { + key: RedisString::new(), + cmds: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct StringObject { + pub key: RedisString, + pub value: RedisString, +} + +impl StringObject { + pub fn new() -> Self { + StringObject { + key: RedisString::new(), + value: RedisString::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct ZSetEntry { + pub member: RedisString, + pub score: RedisString, +} + +#[derive(Debug, Clone)] +pub struct ZsetObject { + pub key: RedisString, + pub elements: Vec, +} + +impl ZsetObject { + pub fn new() -> Self { + ZsetObject { + key: RedisString::new(), + elements: vec![], + } + } +} + +/// raw bytes +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct RedisString { + pub bytes: Vec, +} + +impl RedisString { + pub fn new() -> Self { + Self { bytes: Vec::new() } + } + + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } +} + +impl From> for RedisString { + fn from(bytes: Vec) -> Self { + Self { bytes } + } +} + +impl From for RedisString { + fn from(str: String) -> Self { + Self { + bytes: str.as_bytes().to_vec(), + } + } +} + +impl From for String { + fn from(redis_string: RedisString) -> Self { + String::from_utf8(redis_string.bytes).unwrap() + } +} + +#[derive(Debug, Clone)] +pub struct RedisCmd { + pub args: Vec>, +} + +impl RedisCmd { + pub fn new() -> Self { + Self { args: Vec::new() } + } + + pub fn from_args(args: Vec>) -> Self { + let mut me = Self::new(); + for arg in args { + me.args.push(arg); + } + me + } + + pub fn from_str_args(args: &[&str]) -> Self { + let mut me = Self::new(); + for arg in args.iter() { + me.args.push(arg.as_bytes().to_vec()); + } + me + } + + pub fn add_arg(&mut self, arg: Vec) { + self.args.push(arg); + } + + pub fn add_str_arg(&mut self, arg: &str) { + self.args.push(arg.as_bytes().to_vec()); + } + + pub fn add_redis_arg(&mut self, arg: &RedisString) { + self.args.push(arg.as_bytes().to_vec()); + } + + pub fn get_name(&self) -> String { + if self.args.is_empty() { + String::new() + } else { + String::from_utf8(self.args[0].clone()).unwrap() + } + } + + pub fn to_string(&self) -> String { + let mut str_args = Vec::new(); + for arg in self.args.iter() { + str_args.push(String::from_utf8_lossy(arg)); + } + str_args.join(" ") + } +} diff --git a/dt-meta/src/redis/redis_write_method.rs b/dt-meta/src/redis/redis_write_method.rs new file mode 100644 index 00000000..a2aad18e --- /dev/null +++ b/dt-meta/src/redis/redis_write_method.rs @@ -0,0 +1,23 @@ +use std::str::FromStr; + +use dt_common::error::Error; +use strum::IntoStaticStr; + +#[derive(Clone, IntoStaticStr, Debug)] +pub enum RedisWriteMethod { + #[strum(serialize = "restore")] + Restore, + + #[strum(serialize = "rewrite")] + Rewrite, +} + +impl FromStr for RedisWriteMethod { + type Err = Error; + fn from_str(str: &str) -> Result { + match str { + "rewrite" => Ok(Self::Rewrite), + _ => Ok(Self::Restore), + } + } +} diff --git a/dt-meta/src/sql_parser/ddl_parser.rs b/dt-meta/src/sql_parser/ddl_parser.rs index 3205eb30..426aa650 100644 --- a/dt-meta/src/sql_parser/ddl_parser.rs +++ b/dt-meta/src/sql_parser/ddl_parser.rs @@ -12,7 +12,10 @@ use nom::{ }; use regex::Regex; -use std::{borrow::Cow, str}; +use std::{ + borrow::Cow, + str::{self}, +}; use crate::ddl_type::DdlType; @@ -30,10 +33,20 @@ impl DdlParser { let sql = Self::remove_comments(sql); let input = sql.trim().as_bytes(); match sql_query(input) { - Ok((_, o)) => Ok(o), - Err(_) => Err(Error::Unexpected { - error: format!("failed to parse sql: {}", sql), - }), + Ok((_, o)) => { + let database = if let Some(db) = o.1 { + Some(String::from_utf8(db)?) + } else { + None + }; + let table = if let Some(tb) = o.2 { + Some(String::from_utf8(tb)?) + } else { + None + }; + Ok((o.0, database, table)) + } + Err(_) => Err(Error::Unexpected(format!("failed to parse sql: {}", sql))), } } @@ -46,7 +59,7 @@ impl DdlParser { /// parse ddl sql and return: (ddl_type, schema, table) #[allow(clippy::type_complexity)] -pub fn sql_query(i: &[u8]) -> IResult<&[u8], (DdlType, Option, Option)> { +pub fn sql_query(i: &[u8]) -> IResult<&[u8], (DdlType, Option>, Option>)> { alt(( map(create_database, |r| { (DdlType::CreateDatabase, Some(r), None) @@ -61,7 +74,7 @@ pub fn sql_query(i: &[u8]) -> IResult<&[u8], (DdlType, Option, Option IResult<&[u8], String> { +pub fn create_database(i: &[u8]) -> IResult<&[u8], Vec> { let (remaining_input, (_, _, _, _, _, database, _)) = tuple(( tag_no_case("create"), multispace1, @@ -71,11 +84,10 @@ pub fn create_database(i: &[u8]) -> IResult<&[u8], String> { sql_identifier, multispace0, ))(i)?; - let database = String::from(str::from_utf8(database).unwrap()); - Ok((remaining_input, database)) + Ok((remaining_input, database.to_vec())) } -pub fn drop_database(i: &[u8]) -> IResult<&[u8], String> { +pub fn drop_database(i: &[u8]) -> IResult<&[u8], Vec> { let (remaining_input, (_, _, _, _, _, database, _)) = tuple(( tag_no_case("drop"), multispace1, @@ -85,11 +97,10 @@ pub fn drop_database(i: &[u8]) -> IResult<&[u8], String> { sql_identifier, multispace0, ))(i)?; - let database = String::from(str::from_utf8(database).unwrap()); - Ok((remaining_input, database)) + Ok((remaining_input, database.to_vec())) } -pub fn alter_database(i: &[u8]) -> IResult<&[u8], String> { +pub fn alter_database(i: &[u8]) -> IResult<&[u8], Vec> { let (remaining_input, (_, _, _, _, database, _)) = tuple(( tag_no_case("alter"), multispace1, @@ -98,11 +109,10 @@ pub fn alter_database(i: &[u8]) -> IResult<&[u8], String> { sql_identifier, multispace1, ))(i)?; - let database = String::from(str::from_utf8(database).unwrap()); - Ok((remaining_input, database)) + Ok((remaining_input, database.to_vec())) } -pub fn create_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn create_table(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { let (remaining_input, (_, _, _, _, _, table, _)) = tuple(( tag_no_case("create"), multispace1, @@ -115,7 +125,7 @@ pub fn create_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { Ok((remaining_input, table)) } -pub fn drop_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn drop_table(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { let (remaining_input, (_, _, _, _, _, table, _)) = tuple(( tag_no_case("drop"), multispace1, @@ -128,7 +138,7 @@ pub fn drop_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { Ok((remaining_input, table)) } -pub fn alter_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn alter_table(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { let (remaining_input, (_, _, _, _, table, _)) = tuple(( tag_no_case("alter"), multispace1, @@ -140,10 +150,22 @@ pub fn alter_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { Ok((remaining_input, table)) } -pub fn truncate_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn truncate_table(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { let (remaining_input, (_, _, _, _, table, _)) = tuple(( tag_no_case("truncate"), multispace1, + opt(tag_no_case("table")), + opt(multispace1), + schema_table_reference, + multispace0, + ))(i)?; + Ok((remaining_input, table)) +} + +pub fn rename_table(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { + let (remaining_input, (_, _, _, _, table, _)) = tuple(( + tag_no_case("rename"), + multispace1, tag_no_case("table"), multispace1, schema_table_reference, @@ -152,7 +174,7 @@ pub fn truncate_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { Ok((remaining_input, table)) } -pub fn rename_table(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn create_index(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { let (remaining_input, (_, _, _, _, table, _)) = tuple(( tag_no_case("rename"), multispace1, @@ -187,7 +209,7 @@ pub fn if_exists(i: &[u8]) -> IResult<&[u8], ()> { } // Parse a reference to a named schema.table, with an optional alias -pub fn schema_table_reference(i: &[u8]) -> IResult<&[u8], (Option, String)> { +pub fn schema_table_reference(i: &[u8]) -> IResult<&[u8], (Option>, Vec)> { map( tuple(( opt(pair(sql_identifier, pair(multispace0, tag(".")))), @@ -195,10 +217,8 @@ pub fn schema_table_reference(i: &[u8]) -> IResult<&[u8], (Option, Strin sql_identifier, )), |tup| { - let name = String::from(str::from_utf8(tup.2).unwrap()); - let schema = tup - .0 - .map(|(schema, _)| String::from(str::from_utf8(schema).unwrap())); + let name = tup.2.to_vec(); + let schema = tup.0.map(|(schema, _)| schema.to_vec()); (schema, name) }, )(i) @@ -206,14 +226,28 @@ pub fn schema_table_reference(i: &[u8]) -> IResult<&[u8], (Option, Strin #[inline] pub fn is_sql_identifier(chr: u8) -> bool { - is_alphanumeric(chr) || chr == b'_' || chr == b'@' + is_alphanumeric(chr) || chr == b'_' +} + +#[inline] +pub fn is_escaped_sql_identifier_1(chr: u8) -> bool { + chr != b'`' +} + +#[inline] +pub fn is_escaped_sql_identifier_2(chr: u8) -> bool { + chr != b'"' } pub fn sql_identifier(i: &[u8]) -> IResult<&[u8], &[u8]> { alt(( preceded(not(peek(sql_keyword)), take_while1(is_sql_identifier)), - delimited(tag("`"), take_while1(is_sql_identifier), tag("`")), - delimited(tag("["), take_while1(is_sql_identifier), tag("]")), + delimited(tag("`"), take_while1(is_escaped_sql_identifier_1), tag("`")), + delimited( + tag("\""), + take_while1(is_escaped_sql_identifier_2), + tag("\""), + ), ))(i) } @@ -262,6 +296,33 @@ mod test { } } + #[test] + fn test_create_table_with_schema_with_special_characters() { + let sqls = vec![ + // mysql + "CREATE TABLE IF NOT EXISTS `test_db_*.*`.bbb(id int);", + "CREATE TABLE IF NOT EXISTS `δΈ­ζ–‡.others*&^%$#@!+_)(&^%#`.`δΈ­ζ–‡!@$#$%^&*&(_+)`(id int);", + // pg + r#"CREATE TABLE IF NOT EXISTS "test_db_*.*".bbb(id int);"#, + r#"CREATE TABLE IF NOT EXISTS "δΈ­ζ–‡.others*&^%$#@!+_)(&^%#"."δΈ­ζ–‡!@$#$%^&*&(_+)"(id int);"#, + ]; + let dbs = vec![ + "test_db_*.*", + "δΈ­ζ–‡.others*&^%$#@!+_)(&^%#", + "test_db_*.*", + "δΈ­ζ–‡.others*&^%$#@!+_)(&^%#", + ]; + let tbs = vec!["bbb", "δΈ­ζ–‡!@$#$%^&*&(_+)", "bbb", "δΈ­ζ–‡!@$#$%^&*&(_+)"]; + + for i in 0..sqls.len() { + let sql = sqls[i]; + let r = DdlParser::parse(sql).unwrap(); + assert_eq!(r.0, DdlType::CreateTable); + assert_eq!(r.1, Some(dbs[i].to_string())); + assert_eq!(r.2, Some(tbs[i].to_string())); + } + } + #[test] fn test_create_table_without_schema() { let sqls = vec![ @@ -421,6 +482,23 @@ mod test { } } + #[test] + fn test_create_database_with_special_characters() { + let sqls = vec![ + "CREATE DATABASE IF NOT EXISTS `test_db_*.*`;", + "CREATE DATABASE IF NOT EXISTS `δΈ­ζ–‡.others*&^%$#@!+_)(&^%#`;", + ]; + let dbs = vec!["test_db_*.*", "δΈ­ζ–‡.others*&^%$#@!+_)(&^%#"]; + + for i in 0..sqls.len() { + let sql = sqls[i]; + let r = DdlParser::parse(sql).unwrap(); + assert_eq!(r.0, DdlType::CreateDatabase); + assert_eq!(r.1, Some(dbs[i].to_string())); + assert_eq!(r.2, None); + } + } + #[test] fn test_drop_database() { let sqls = vec![ @@ -486,6 +564,9 @@ mod test { "truncate /*some comments,*/table/*some comments*/ `aaa`.`bbb`", // escapes + spaces + comments "truncate /*some comments,*/table/*some comments*/ `aaa` . `bbb` ", + // without keyword `table` + "truncate `aaa`.`bbb`", + "truncate /*some comments,*/table/*some comments*/ `aaa`.`bbb`", ]; for sql in sqls { diff --git a/dt-parallelizer/src/base_parallelizer.rs b/dt-parallelizer/src/base_parallelizer.rs index 1fd20fe9..fb48db96 100644 --- a/dt-parallelizer/src/base_parallelizer.rs +++ b/dt-parallelizer/src/base_parallelizer.rs @@ -77,4 +77,28 @@ impl BaseParallelizer { } Ok(()) } + + pub async fn sink_raw( + &self, + mut sub_datas: Vec>, + sinkers: &[Arc>>], + parallel_size: usize, + batch: bool, + ) -> Result<(), Error> { + let mut futures = Vec::new(); + for i in 0..sub_datas.len() { + let data = sub_datas.remove(0); + let sinker = sinkers[i % parallel_size].clone(); + let future = + tokio::spawn( + async move { sinker.lock().await.sink_raw(data, batch).await.unwrap() }, + ); + futures.push(future); + } + + for future in futures { + future.await.unwrap(); + } + Ok(()) + } } diff --git a/dt-parallelizer/src/check_parallelizer.rs b/dt-parallelizer/src/check_parallelizer.rs index 3994414f..3bd1e140 100644 --- a/dt-parallelizer/src/check_parallelizer.rs +++ b/dt-parallelizer/src/check_parallelizer.rs @@ -6,16 +6,13 @@ use dt_common::error::Error; use dt_connector::Sinker; use dt_meta::{ddl_data::DdlData, dt_data::DtData, row_data::RowData}; -use crate::Parallelizer; +use crate::{Merger, Parallelizer}; -use super::{ - base_parallelizer::BaseParallelizer, rdb_merger::RdbMerger, - snapshot_parallelizer::SnapshotParallelizer, -}; +use super::{base_parallelizer::BaseParallelizer, snapshot_parallelizer::SnapshotParallelizer}; pub struct CheckParallelizer { pub base_parallelizer: BaseParallelizer, - pub merger: RdbMerger, + pub merger: Box, pub parallel_size: usize, } @@ -35,15 +32,15 @@ impl Parallelizer for CheckParallelizer { sinkers: &[Arc>>], ) -> Result<(), Error> { let mut merged_datas = self.merger.merge(data).await?; - for (_full_tb, tb_merged_data) in merged_datas.iter_mut() { - let batch_data = tb_merged_data.get_insert_rows(); + for tb_merged_data in merged_datas.drain(..) { + let batch_data = tb_merged_data.insert_rows; let batch_sub_datas = SnapshotParallelizer::partition(batch_data, self.parallel_size)?; self.base_parallelizer .sink_dml(batch_sub_datas, sinkers, self.parallel_size, true) .await .unwrap(); - let serial_data = tb_merged_data.get_unmerged_rows(); + let serial_data = tb_merged_data.unmerged_rows; let serial_sub_datas = SnapshotParallelizer::partition(serial_data, self.parallel_size)?; self.base_parallelizer diff --git a/dt-parallelizer/src/merge_parallelizer.rs b/dt-parallelizer/src/merge_parallelizer.rs index a194716b..d8310c79 100644 --- a/dt-parallelizer/src/merge_parallelizer.rs +++ b/dt-parallelizer/src/merge_parallelizer.rs @@ -1,27 +1,34 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{cmp, sync::Arc}; use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; -use dt_common::error::Error; +use dt_common::{config::sinker_config::SinkerBasicConfig, error::Error}; use dt_connector::Sinker; use dt_meta::{ddl_data::DdlData, dt_data::DtData, row_data::RowData, row_type::RowType}; -use crate::Parallelizer; +use crate::{Merger, Parallelizer}; -use super::{ - base_parallelizer::BaseParallelizer, - rdb_merger::{RdbMerger, TbMergedData}, -}; +use super::base_parallelizer::BaseParallelizer; pub struct MergeParallelizer { pub base_parallelizer: BaseParallelizer, - pub merger: RdbMerger, + pub merger: Box, pub parallel_size: usize, + pub sinker_basic_config: SinkerBasicConfig, } -const INSERT: &str = "insert"; -const DELETE: &str = "delete"; -const UNMERGED: &str = "unmerged"; +enum MergeType { + Insert, + Delete, + Unmerged, +} + +pub struct TbMergedData { + pub tb: String, + pub delete_rows: Vec, + pub insert_rows: Vec, + pub unmerged_rows: Vec, +} #[async_trait] impl Parallelizer for MergeParallelizer { @@ -38,14 +45,13 @@ impl Parallelizer for MergeParallelizer { data: Vec, sinkers: &[Arc>>], ) -> Result<(), Error> { - let mut merged_datas = self.merger.merge(data).await?; - self.sink_internal(&mut merged_datas, sinkers, DELETE) + let mut tb_merged_datas = self.merger.merge(data).await?; + self.sink_dml_internal(&mut tb_merged_datas, sinkers, MergeType::Delete) .await?; - self.sink_internal(&mut merged_datas, sinkers, INSERT) + self.sink_dml_internal(&mut tb_merged_datas, sinkers, MergeType::Insert) .await?; - self.sink_internal(&mut merged_datas, sinkers, UNMERGED) - .await?; - Ok(()) + self.sink_dml_internal(&mut tb_merged_datas, sinkers, MergeType::Unmerged) + .await } async fn sink_ddl( @@ -61,36 +67,52 @@ impl Parallelizer for MergeParallelizer { } impl MergeParallelizer { - #[inline(always)] - async fn sink_internal( + async fn sink_dml_internal( &self, - merged_datas: &mut HashMap, + tb_merged_datas: &mut Vec, sinkers: &[Arc>>], - sink_type: &str, + merge_type: MergeType, ) -> Result<(), Error> { - let parallel_size = sinkers.len(); - let mut i = 0; let mut futures = Vec::new(); - for (_full_tb, tb_merged_data) in merged_datas.iter_mut() { - let data = match sink_type { - DELETE => tb_merged_data.get_delete_rows(), - INSERT => tb_merged_data.get_insert_rows(), - _ => tb_merged_data.get_unmerged_rows(), + for tb_merged_data in tb_merged_datas.iter_mut() { + let data: Vec = match merge_type { + MergeType::Delete => tb_merged_data.delete_rows.drain(..).collect(), + MergeType::Insert => tb_merged_data.insert_rows.drain(..).collect(), + MergeType::Unmerged => tb_merged_data.unmerged_rows.drain(..).collect(), }; if data.is_empty() { continue; } - let sinker_type_clone = sink_type.to_string(); - let sinker = sinkers[i % parallel_size].clone(); - let future = tokio::spawn(async move { - match sinker_type_clone.as_str() { - DELETE | INSERT => sinker.lock().await.sink_dml(data, true).await.unwrap(), - _ => Self::sink_unmerged_rows(sinker, data).await.unwrap(), - }; - }); - futures.push(future); - i += 1; + // make sure NO too much threads generated + let batch_size = cmp::max( + data.len() / self.parallel_size, + cmp::max(self.sinker_basic_config.batch_size, 1), + ); + + match merge_type { + MergeType::Insert | MergeType::Delete => { + let mut i = 0; + while i < data.len() { + let sub_size = cmp::min(batch_size, data.len() - i); + let sub_data = data[i..i + sub_size].to_vec(); + let sinker = sinkers[futures.len() % self.parallel_size].clone(); + let future = tokio::spawn(async move { + sinker.lock().await.sink_dml(sub_data, true).await.unwrap(); + }); + futures.push(future); + i += batch_size; + } + } + + MergeType::Unmerged => { + let sinker = sinkers[futures.len() % self.parallel_size].clone(); + let future = tokio::spawn(async move { + Self::sink_unmerged_rows(sinker, data).await.unwrap(); + }); + futures.push(future); + } + } } // wait for sub sinkers to finish and unwrap errors @@ -111,6 +133,7 @@ impl MergeParallelizer { if data[start].row_type == RowType::Insert { sinker.lock().await.sink_dml(sub_data, true).await?; } else { + // for Delete / Update, the safest way is serial sinker.lock().await.sink_dml(sub_data, false).await?; } start = i; diff --git a/dt-parallelizer/src/rdb_merger.rs b/dt-parallelizer/src/rdb_merger.rs index cd179235..0dc50b11 100644 --- a/dt-parallelizer/src/rdb_merger.rs +++ b/dt-parallelizer/src/rdb_merger.rs @@ -1,34 +1,48 @@ use std::collections::HashMap; -use dt_common::error::Error; +use async_trait::async_trait; +use dt_common::{error::Error, log_debug}; use dt_meta::{rdb_meta_manager::RdbMetaManager, row_data::RowData, row_type::RowType}; +use crate::{merge_parallelizer::TbMergedData, Merger}; + pub struct RdbMerger { pub meta_manager: RdbMetaManager, } -impl RdbMerger { - pub async fn merge( - &mut self, - data: Vec, - ) -> Result, Error> { - let mut sub_datas = HashMap::::new(); +#[async_trait] +impl Merger for RdbMerger { + async fn merge(&mut self, data: Vec) -> Result, Error> { + let mut tb_data_map = HashMap::::new(); for row_data in data { let full_tb = format!("{}.{}", row_data.schema, row_data.tb); - if let Some(merged) = sub_datas.get_mut(&full_tb) { + if let Some(merged) = tb_data_map.get_mut(&full_tb) { self.merge_row_data(merged, row_data).await?; } else { - let mut merged = TbMergedData::new(); + let mut merged = RdbTbMergedData::new(); self.merge_row_data(&mut merged, row_data).await?; - sub_datas.insert(full_tb, merged); + tb_data_map.insert(full_tb, merged); } } - Ok(sub_datas) + + let mut results = Vec::new(); + for (tb, mut rdb_tb_merged) in tb_data_map.drain() { + let tb_merged = TbMergedData { + tb, + insert_rows: rdb_tb_merged.get_insert_rows(), + delete_rows: rdb_tb_merged.get_delete_rows(), + unmerged_rows: rdb_tb_merged.get_unmerged_rows(), + }; + results.push(tb_merged); + } + Ok(results) } +} +impl RdbMerger { async fn merge_row_data( &mut self, - merged: &mut TbMergedData, + merged: &mut RdbTbMergedData, row_data: RowData, ) -> Result<(), Error> { // if the table already has some rows unmerged, then following rows also need to be unmerged. @@ -68,6 +82,12 @@ impl RdbMerger { } RowType::Update => { + // if uk change found in any row_data, for safety, all following row_datas won't be merged + if self.check_uk_changed(&tb_meta.id_cols, &row_data) { + merged.unmerged_rows.push(row_data); + return Ok(()); + } + let (delete, insert) = self.split_update_row_data(row_data).await?; let insert_hash_code = self.get_hash_code(&insert).await?; @@ -109,6 +129,18 @@ impl RdbMerger { Ok(()) } + fn check_uk_changed(&mut self, id_cols: &[String], row_data: &RowData) -> bool { + let before = row_data.before.as_ref().unwrap(); + let after = row_data.after.as_ref().unwrap(); + for col in id_cols.iter() { + if before.get(col) != after.get(col) { + log_debug!("rdb_merger, uk change found, row_data: {:?}", row_data); + return true; + } + } + false + } + fn check_collision( &mut self, buffer: &HashMap, @@ -129,6 +161,7 @@ impl RdbMerger { for col in id_cols.iter() { if col_values.get(col) != exist_col_values.get(col) { + log_debug!("rdb_merger, collision found, row_data: {:?}", row_data); return true; } } @@ -173,14 +206,14 @@ impl RdbMerger { } } -pub struct TbMergedData { +struct RdbTbMergedData { // HashMap delete_rows: HashMap, insert_rows: HashMap, unmerged_rows: Vec, } -impl TbMergedData { +impl RdbTbMergedData { pub fn new() -> Self { Self { delete_rows: HashMap::new(), @@ -198,12 +231,6 @@ impl TbMergedData { } pub fn get_unmerged_rows(&mut self) -> Vec { - self.unmerged_rows.as_slice().to_vec() - } -} - -impl Default for TbMergedData { - fn default() -> Self { - Self::new() + self.unmerged_rows.drain(..).collect::>() } } diff --git a/dt-parallelizer/src/serial_parallelizer.rs b/dt-parallelizer/src/serial_parallelizer.rs index de0b3cc0..38d0dd15 100644 --- a/dt-parallelizer/src/serial_parallelizer.rs +++ b/dt-parallelizer/src/serial_parallelizer.rs @@ -43,4 +43,14 @@ impl Parallelizer for SerialParallelizer { .sink_ddl(vec![data], sinkers, 1, false) .await } + + async fn sink_raw( + &mut self, + data: Vec, + sinkers: &[Arc>>], + ) -> Result<(), Error> { + self.base_parallelizer + .sink_raw(vec![data], sinkers, 1, false) + .await + } } diff --git a/dt-pipeline/src/base_pipeline.rs b/dt-pipeline/src/base_pipeline.rs index b3ed9948..ecd0008e 100644 --- a/dt-pipeline/src/base_pipeline.rs +++ b/dt-pipeline/src/base_pipeline.rs @@ -6,7 +6,6 @@ use std::{ time::Instant, }; -use async_trait::async_trait; use concurrent_queue::ConcurrentQueue; use dt_common::{ error::Error, @@ -17,11 +16,10 @@ use dt_common::{ }; use dt_connector::Sinker; use dt_meta::{ddl_data::DdlData, dt_data::DtData, row_data::RowData}; -use dt_parallelizer::Parallelizer; -use crate::Pipeline; +use crate::Parallelizer; -pub struct BasicPipeline<'a> { +pub struct Pipeline<'a> { pub buffer: &'a ConcurrentQueue, pub parallelizer: Box, pub sinkers: Vec>>>, @@ -31,16 +29,15 @@ pub struct BasicPipeline<'a> { pub syncer: Arc>, } -#[async_trait] -impl Pipeline for BasicPipeline<'_> { - async fn stop(&mut self) -> Result<(), Error> { +impl Pipeline<'_> { + pub async fn stop(&mut self) -> Result<(), Error> { for sinker in self.sinkers.iter_mut() { sinker.lock().await.close().await.unwrap(); } Ok(()) } - async fn start(&mut self) -> Result<(), Error> { + pub async fn start(&mut self) -> Result<(), Error> { log_info!( "{} starts, parallel_size: {}, checkpoint_interval_secs: {}", self.parallelizer.get_name(), @@ -98,9 +95,7 @@ impl Pipeline for BasicPipeline<'_> { Ok(()) } -} -impl BasicPipeline<'_> { async fn sink_dml( &mut self, all_data: Vec, diff --git a/dt-pipeline/src/lib.rs b/dt-pipeline/src/lib.rs index 54fd4c08..5f1c5372 100644 --- a/dt-pipeline/src/lib.rs +++ b/dt-pipeline/src/lib.rs @@ -1,14 +1,54 @@ +pub mod base_parallelizer; +pub mod check_parallelizer; +pub mod merge_parallelizer; +pub mod mongo_parallelizer; +pub mod partition_parallelizer; +pub mod pipeline; +pub mod rdb_merger; +pub mod rdb_partitioner; +pub mod serial_parallelizer; +pub mod snapshot_parallelizer; +pub mod table_parallelizer; + +// new: +// pub mod base_pipeline; +// pub mod filters; +// pub mod transaction_pipeline; +// pub mod utils; + +use std::sync::Arc; + use async_trait::async_trait; use dt_common::error::Error; - -pub mod base_pipeline; -pub mod filters; -pub mod transaction_pipeline; -pub mod utils; +use dt_connector::Sinker; +use dt_meta::{ddl_data::DdlData, dt_data::DtData, row_data::RowData}; +use merge_parallelizer::TbMergedData; #[async_trait] pub trait Pipeline { + async fn start(&mut self) -> Result<(), Error>; + async fn stop(&mut self) -> Result<(), Error>; - async fn start(&mut self) -> Result<(), Error>; + // merge methods: + + async fn drain(&mut self, buffer: &ConcurrentQueue) -> Result, Error> { + Ok(Vec::new()) + } + + async fn sink_ddl( + &mut self, + data: Vec, + sinkers: &[Arc>>], + ) -> Result<(), Error> { + Ok(()) + } + + async fn sink_dml( + &mut self, + data: Vec, + sinkers: &[Arc>>], + ) -> Result<(), Error> { + Ok(()) + } } diff --git a/dt-pipeline/src/mongo_merger.rs b/dt-pipeline/src/mongo_merger.rs new file mode 100644 index 00000000..5f245e25 --- /dev/null +++ b/dt-pipeline/src/mongo_merger.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use dt_common::error::Error; +use dt_meta::{ + col_value::ColValue, + mongo::{mongo_constant::MongoConstants, mongo_key::MongoKey}, + row_data::RowData, + row_type::RowType, +}; + +use crate::{merge_parallelizer::TbMergedData, Merger}; + +pub struct MongoMerger {} + +#[async_trait] +impl Merger for MongoMerger { + async fn merge(&mut self, data: Vec) -> Result, Error> { + let mut tb_data_map: HashMap> = HashMap::new(); + for row_data in data { + let full_tb = format!("{}.{}", row_data.schema, row_data.tb); + if let Some(tb_data) = tb_data_map.get_mut(&full_tb) { + tb_data.push(row_data); + } else { + tb_data_map.insert(full_tb, vec![row_data]); + } + } + + let mut results = Vec::new(); + for (tb, tb_data) in tb_data_map.drain() { + let (insert_rows, delete_rows, unmerged_rows) = Self::merge_row_data(tb_data)?; + let tb_merged = TbMergedData { + tb, + insert_rows, + delete_rows, + unmerged_rows, + }; + results.push(tb_merged); + } + Ok(results) + } +} + +impl MongoMerger { + /// partition dmls of the same table into insert vec and delete vec + pub fn merge_row_data( + mut data: Vec, + ) -> Result<(Vec, Vec, Vec), Error> { + let mut insert_map = HashMap::new(); + let mut delete_map = HashMap::new(); + + while !data.is_empty() { + let hash_key = Self::get_hash_key(&data[0]); + if hash_key.is_none() { + break; + } + + let id = hash_key.unwrap(); + let row_data = data.remove(0); + match row_data.row_type { + RowType::Insert => { + insert_map.insert(id, row_data); + } + + RowType::Delete => { + insert_map.remove(&id); + delete_map.insert(id, row_data); + } + + RowType::Update => { + let before = row_data.before.unwrap(); + let after: HashMap = row_data.after.unwrap(); + let delete_row = RowData { + row_type: RowType::Delete, + schema: row_data.schema.clone(), + tb: row_data.tb.clone(), + before: Some(before), + after: Option::None, + position: row_data.position.clone(), + }; + delete_map.insert(id.clone(), delete_row); + + let insert_row = RowData { + row_type: RowType::Insert, + schema: row_data.schema, + tb: row_data.tb, + before: Option::None, + after: Some(after), + position: row_data.position, + }; + insert_map.insert(id, insert_row); + } + } + } + + let inserts = insert_map.drain().map(|i| i.1).collect::>(); + let deletes = delete_map.drain().map(|i| i.1).collect::>(); + Ok((inserts, deletes, data)) + } + + fn get_hash_key(row_data: &RowData) -> Option { + match row_data.row_type { + RowType::Insert => { + let after = row_data.after.as_ref().unwrap(); + if let Some(ColValue::MongoDoc(doc)) = after.get(MongoConstants::DOC) { + return MongoKey::from_doc(doc); + } + } + + RowType::Delete => { + let before = row_data.before.as_ref().unwrap(); + if let Some(ColValue::MongoDoc(doc)) = before.get(MongoConstants::DOC) { + return MongoKey::from_doc(doc); + } + } + + RowType::Update => { + let before = row_data.before.as_ref().unwrap(); + let after = row_data.after.as_ref().unwrap(); + // for Update row_data from oplog (NOT change stream), after contains diff_doc instead of doc, + // in which case we can NOT transfer Update into Delete + Insert + if after.get(MongoConstants::DOC).is_none() { + return None; + } else if let Some(ColValue::MongoDoc(doc)) = before.get(MongoConstants::DOC) { + return MongoKey::from_doc(doc); + } + } + } + return None; + } +} diff --git a/dt-pipeline/src/redis_parallelizer.rs b/dt-pipeline/src/redis_parallelizer.rs new file mode 100644 index 00000000..105fa577 --- /dev/null +++ b/dt-pipeline/src/redis_parallelizer.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::error::Error; +use dt_connector::Sinker; +use dt_meta::dt_data::DtData; + +use crate::Parallelizer; + +use super::base_parallelizer::BaseParallelizer; + +pub struct RedisParallelizer { + pub base_parallelizer: BaseParallelizer, + pub parallel_size: usize, +} + +#[async_trait] +impl Parallelizer for RedisParallelizer { + fn get_name(&self) -> String { + "RedisParallelizer".to_string() + } + + async fn drain(&mut self, buffer: &ConcurrentQueue) -> Result, Error> { + self.base_parallelizer.drain(buffer) + } + + async fn sink_raw( + &mut self, + data: Vec, + sinkers: &[Arc>>], + ) -> Result<(), Error> { + self.base_parallelizer + .sink_raw(vec![data], sinkers, 1, false) + .await + } +} diff --git a/dt-precheck/Cargo.toml b/dt-precheck/Cargo.toml index 99f831d5..839de967 100644 --- a/dt-precheck/Cargo.toml +++ b/dt-precheck/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] dt-common = { path = "../dt-common", version = "0.1.0"} dt-meta = { path = "../dt-meta", version = "0.1.0"} +dt-task = { path = "../dt-task", version = "0.1.0" } +dt-connector = { path = "../dt-connector", version = "0.1.0" } sqlx = {workspace = true, features = ["runtime-async-std-rustls", "mysql", "postgres", "decimal", "bigdecimal", "ipnetwork", "mac_address", "bit-vec", "time", "chrono", "json", "uuid"]} strum = { workspace = true } @@ -17,3 +19,5 @@ configparser = { workspace = true } futures = { workspace = true } regex = { workspace = true } mongodb = { workspace = true } +concurrent-queue = { workspace = true } +redis = { workspace = true } \ No newline at end of file diff --git a/dt-precheck/src/builder/prechecker_builder.rs b/dt-precheck/src/builder/prechecker_builder.rs index e6f8848a..8c3bf4af 100644 --- a/dt-precheck/src/builder/prechecker_builder.rs +++ b/dt-precheck/src/builder/prechecker_builder.rs @@ -1,24 +1,21 @@ use std::vec; use dt_common::{ - config::{ - config_enums::DbType, extractor_config::ExtractorConfig, sinker_config::SinkerConfig, - task_config::TaskConfig, - }, + config::{config_enums::DbType, task_config::TaskConfig}, + error::Error, utils::rdb_filter::RdbFilter, }; use crate::{ config::precheck_config::PrecheckConfig, - error::Error, fetcher::{ mongo::mongo_fetcher::MongoFetcher, mysql::mysql_fetcher::MysqlFetcher, - postgresql::pg_fetcher::PgFetcher, + postgresql::pg_fetcher::PgFetcher, redis::redis_fetcher::RedisFetcher, }, meta::check_result::CheckResult, prechecker::{ mongo_prechecker::MongoPrechecker, mysql_prechecker::MySqlPrechecker, - pg_prechecker::PostgresqlPrechecker, traits::Prechecker, + pg_prechecker::PostgresqlPrechecker, redis_prechecker::RedisPrechecker, traits::Prechecker, }, }; @@ -35,49 +32,33 @@ impl PrecheckerBuilder { } } - pub fn valid_config(&self) -> Result { - if let ExtractorConfig::Basic { url, .. } = &self.task_config.extractor { - if url.is_empty() { - return Ok(false); - } - } - if let SinkerConfig::Basic { url, .. } = &self.task_config.sinker { - if url.is_empty() { - return Ok(false); - } - } - Ok(true) + pub fn valid_config(&self) -> bool { + !self.task_config.extractor.get_url().is_empty() + && !self.task_config.sinker.get_url().is_empty() } pub fn build_checker(&self, is_source: bool) -> Option> { - let mut db_type_option: Option<&DbType> = None; - if is_source { - if let ExtractorConfig::Basic { db_type, .. } = &self.task_config.extractor { - db_type_option = Some(db_type) - } - } else if let SinkerConfig::Basic { db_type, .. } = &self.task_config.sinker { - db_type_option = Some(db_type) - } - if db_type_option.is_none() { - println!("build checker failed, maybe config is wrong"); - return None; - } - let filter = - RdbFilter::from_config(&self.task_config.filter, db_type_option.unwrap().clone()) - .unwrap(); - let checker: Option> = match db_type_option.unwrap() { + let (db_type, url) = if is_source { + ( + self.task_config.extractor.get_db_type(), + self.task_config.extractor.get_url(), + ) + } else { + ( + self.task_config.sinker.get_db_type(), + self.task_config.sinker.get_url(), + ) + }; + + let filter = RdbFilter::from_config(&self.task_config.filter, db_type.clone()).unwrap(); + let checker: Option> = match db_type { DbType::Mysql => Some(Box::new(MySqlPrechecker { filter_config: self.task_config.filter.clone(), precheck_config: self.precheck_config.clone(), - db_type_option: Some(db_type_option.unwrap().clone()), is_source, fetcher: MysqlFetcher { pool: None, - source_config: self.task_config.extractor.clone(), - filter_config: self.task_config.filter.clone(), - sinker_config: self.task_config.sinker.clone(), - router_config: self.task_config.router.clone(), - db_type_option: Some(db_type_option.unwrap().clone()), + url: url.clone(), is_source, filter, }, @@ -85,15 +66,10 @@ impl PrecheckerBuilder { DbType::Pg => Some(Box::new(PostgresqlPrechecker { filter_config: self.task_config.filter.clone(), precheck_config: self.precheck_config.clone(), - db_type_option: Some(db_type_option.unwrap().clone()), is_source, fetcher: PgFetcher { pool: None, - source_config: self.task_config.extractor.clone(), - filter_config: self.task_config.filter.clone(), - sinker_config: self.task_config.sinker.clone(), - router_config: self.task_config.router.clone(), - db_type_option: Some(db_type_option.unwrap().clone()), + url: url.clone(), is_source, filter, }, @@ -101,18 +77,24 @@ impl PrecheckerBuilder { DbType::Mongo => Some(Box::new(MongoPrechecker { fetcher: MongoFetcher { pool: None, - source_config: self.task_config.extractor.clone(), - filter_config: self.task_config.filter.clone(), - sinker_config: self.task_config.sinker.clone(), - router_config: self.task_config.router.clone(), + url: url.clone(), is_source, - db_type_option: Some(db_type_option.unwrap().clone()), filter, }, filter_config: self.task_config.filter.clone(), precheck_config: self.precheck_config.clone(), is_source, - db_type_option: Some(db_type_option.unwrap().clone()), + })), + DbType::Redis => Some(Box::new(RedisPrechecker { + fetcher: RedisFetcher { + conn: None, + url: url.clone(), + is_source, + filter, + }, + task_config: self.task_config.clone(), + precheck_config: self.precheck_config.clone(), + is_source, })), _ => None, }; @@ -120,46 +102,36 @@ impl PrecheckerBuilder { } pub async fn check(&self) -> Result>, Error> { - if !self.valid_config().unwrap() { - return Err(Error::PreCheckError { - error: "config is invalid.".to_string(), - }); + if !self.valid_config() { + return Err(Error::PreCheckError("config is invalid.".into())); } let (source_checker_option, sink_checker_option) = (self.build_checker(true), self.build_checker(false)); if source_checker_option.is_none() || sink_checker_option.is_none() { - return Err(Error::PreCheckError { - error: "config is invalid when build checker.maybe db_type is wrong.".to_string(), - }); + return Err(Error::PreCheckError( + "config is invalid when build checker.maybe db_type is wrong.".into(), + )); } let (mut source_checker, mut sink_checker) = (source_checker_option.unwrap(), sink_checker_option.unwrap()); - let mut check_results: Vec> = vec![]; println!("[*]begin to check the connection"); - let check_source_connection = source_checker.build_connection().await; - let check_sink_connection = sink_checker.build_connection().await; + let check_source_connection = source_checker.build_connection().await?; + let check_sink_connection = sink_checker.build_connection().await?; + // if connection failed, no need to do other check - if check_source_connection.is_err() { - return Err(check_source_connection.err().unwrap()); - } - if check_sink_connection.is_err() { - return Err(check_sink_connection.err().unwrap()); - } - check_results.push(check_source_connection.clone()); - check_results.push(check_sink_connection.clone()); - if !&check_source_connection.unwrap().is_validate - || !&check_sink_connection.unwrap().is_validate - { - for connection_check in check_results { - let result_tmp = connection_check.unwrap(); - result_tmp.log(); - } - return Err(Error::PreCheckError { - error: "connection failed, precheck not passed.".to_string(), - }); + if !check_source_connection.is_validate || !check_sink_connection.is_validate { + check_source_connection.log(); + check_sink_connection.log(); + return Err(Error::PreCheckError( + "connection failed, precheck not passed.".into(), + )); } + let mut check_results: Vec> = vec![]; + check_results.push(Ok(check_source_connection)); + check_results.push(Ok(check_sink_connection)); + println!("[*]begin to check the database version"); check_results.push(source_checker.check_database_version().await); check_results.push(sink_checker.check_database_version().await); @@ -197,9 +169,7 @@ impl PrecheckerBuilder { } } if error_count > 0 { - Err(Error::PreCheckError { - error: "precheck not passed.".to_string(), - }) + Err(Error::PreCheckError("precheck not passed.".into())) } else { Ok(()) } diff --git a/dt-precheck/src/config/task_config.rs b/dt-precheck/src/config/task_config.rs index d2bd2545..1d8cc0ba 100644 --- a/dt-precheck/src/config/task_config.rs +++ b/dt-precheck/src/config/task_config.rs @@ -1,8 +1,7 @@ use std::{fs::File, io::Read}; use configparser::ini::Ini; - -use crate::error::Error; +use dt_common::error::Error; use super::precheck_config::PrecheckConfig; @@ -39,9 +38,9 @@ impl PrecheckTaskConfig { do_cdc: do_cdc.parse().unwrap(), }) } else { - Err(Error::Unexpected { - error: String::from("config is not valid for precheck."), - }) + Err(Error::ConfigError( + "config is not valid for precheck.".into(), + )) } } } diff --git a/dt-precheck/src/error.rs b/dt-precheck/src/error.rs deleted file mode 100644 index 957b1ff4..00000000 --- a/dt-precheck/src/error.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum Error { - Unexpected { error: String }, - - PreCheckError { error: String }, - - IoError { error: String }, - - EnvVarError { error: String }, - - SqlxError { error: String }, - - MongoError { error: String }, -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { - Self::IoError { - error: err.to_string(), - } - } -} - -impl From for Error { - fn from(err: std::env::VarError) -> Self { - Self::EnvVarError { - error: err.to_string(), - } - } -} - -impl From for Error { - fn from(err: sqlx::Error) -> Self { - Self::SqlxError { - error: err.to_string(), - } - } -} - -impl From for Error { - fn from(err: mongodb::error::Error) -> Self { - Self::MongoError { - error: err.to_string(), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::Unexpected { error } - | Error::PreCheckError { error } - | Error::IoError { error } - | Error::EnvVarError { error } - | Error::SqlxError { error } - | Error::MongoError { error } => { - let msg: String = if !error.is_empty() { - error.clone() - } else { - String::from("unknown error") - }; - msg.fmt(f) - } - } - } -} diff --git a/dt-precheck/src/fetcher/mod.rs b/dt-precheck/src/fetcher/mod.rs index 4b8e6522..4ba1ebb6 100644 --- a/dt-precheck/src/fetcher/mod.rs +++ b/dt-precheck/src/fetcher/mod.rs @@ -1,4 +1,5 @@ pub mod mongo; pub mod mysql; pub mod postgresql; +pub mod redis; pub mod traits; diff --git a/dt-precheck/src/fetcher/mongo/mongo_fetcher.rs b/dt-precheck/src/fetcher/mongo/mongo_fetcher.rs index 3c16a30c..0cac5ffa 100644 --- a/dt-precheck/src/fetcher/mongo/mongo_fetcher.rs +++ b/dt-precheck/src/fetcher/mongo/mongo_fetcher.rs @@ -1,69 +1,34 @@ use std::collections::HashMap; use async_trait::async_trait; -use dt_common::{ - config::{ - config_enums::DbType, extractor_config::ExtractorConfig, filter_config::FilterConfig, - router_config::RouterConfig, sinker_config::SinkerConfig, - }, - constants::MongoConstants, - utils::rdb_filter::RdbFilter, -}; +use dt_common::{error::Error, utils::rdb_filter::RdbFilter}; +use dt_task::task_util::TaskUtil; use mongodb::{ bson::{doc, Bson, Document}, - options::ClientOptions, Client, }; use crate::{ - error::Error, fetcher::traits::Fetcher, meta::database_mode::{Constraint, Database, Schema, Table}, }; pub struct MongoFetcher { pub pool: Option, - pub source_config: ExtractorConfig, - pub filter_config: FilterConfig, - pub sinker_config: SinkerConfig, - pub router_config: RouterConfig, + pub url: String, pub is_source: bool, - pub db_type_option: Option, pub filter: RdbFilter, } #[async_trait] impl Fetcher for MongoFetcher { async fn build_connection(&mut self) -> Result<(), Error> { - let mut connection_url = String::from(""); - - if self.is_source { - if let ExtractorConfig::Basic { url, db_type } = &self.source_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - } else if let SinkerConfig::Basic { url, db_type } = &self.sinker_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - - let mut client_options = ClientOptions::parse_async(connection_url).await.unwrap(); - client_options.app_name = Some(MongoConstants::APP_NAME.to_string()); - self.pool = match mongodb::Client::with_options(client_options) { - Ok(pool) => Some(pool), - Err(e) => return Err(Error::from(e)), - }; - + self.pool = Some(TaskUtil::create_mongo_client(&self.url).await?); Ok(()) } async fn fetch_version(&mut self) -> Result { - let db = match self.get_random_db() { - Ok(db_name) => db_name, - Err(e) => return Err(e), - }; - - let document = self.execute_for_db(db, "buildInfo").await?; + let document = self.execute_for_db("buildInfo").await?; Ok(String::from( document .get("version") @@ -97,139 +62,22 @@ impl Fetcher for MongoFetcher { } impl MongoFetcher { - pub async fn execute_for_db(&self, db: String, command: &str) -> Result { - if db.is_empty() || command.is_empty() { - return Ok(Document::default()); - } - + pub async fn execute_for_db(&self, command: &str) -> Result { let client = match &self.pool { Some(pool) => pool, - None => { - return Err(Error::Unexpected { - error: String::from("client is closed."), - }) - } - }; - - let doc_command = doc! {command: 1}; - let command_result = client - .database(db.as_str()) - .run_command(doc_command, None) - .await; - - match command_result { - Ok(rs) => Ok(rs), - Err(e) => Err(Error::from(e)), - } - } - - pub fn get_random_db(&mut self) -> Result { - let mut db = String::from(""); - - match &self.filter_config { - FilterConfig::Rdb { do_dbs, do_tbs, .. } => { - if !do_dbs.is_empty() { - for do_db in do_dbs.split(',') { - if !self.filter.filter_db(do_db) { - db = String::from(do_db); - break; - } - } - } - if db.is_empty() && !do_tbs.is_empty() { - for do_tb in do_tbs.split(',') { - let do_tb_string = do_tb.to_string(); - let db_tb_vec_tmp: Vec<&str> = do_tb_string.split('.').collect(); - if db_tb_vec_tmp.len() == 2 - && !self.filter.filter_tb(db_tb_vec_tmp[0], db_tb_vec_tmp[1]) - { - db = String::from(db_tb_vec_tmp[0]); - break; - } - } - }; - } - } - if db == "*" { - db = String::from("admin"); - } - - Ok(db) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn generate_mongo_fetcher( - do_dbs: &str, - do_tbs: &str, - ignore_dbs: &str, - ignore_tbs: &str, - ) -> MongoFetcher { - let filter_config = FilterConfig::Rdb { - do_dbs: String::from(do_dbs), - ignore_dbs: String::from(ignore_dbs), - do_tbs: String::from(do_tbs), - ignore_tbs: String::from(ignore_tbs), - do_events: String::from("insert,update,delete"), + None => return Err(Error::PreCheckError("client is closed.".into())), }; - MongoFetcher { - pool: None, - source_config: ExtractorConfig::Basic { - url: String::from(""), - db_type: DbType::Mongo, - }, - filter_config: filter_config.clone(), - sinker_config: SinkerConfig::Basic { - url: String::from(""), - db_type: DbType::Mongo, - }, - router_config: RouterConfig::Rdb { - db_map: String::from(""), - tb_map: String::from(""), - field_map: String::from(""), - }, - is_source: true, - db_type_option: Some(DbType::Mongo), - filter: RdbFilter::from_config(&filter_config, DbType::Mongo).unwrap(), + let dbs = client.list_databases(None, None).await?; + if dbs.is_empty() { + return Err(Error::PreCheckError("no db exists in mongo.".into())); } - } - - #[test] - fn get_random_db_test() { - let mut target_db: String; - - target_db = generate_mongo_fetcher("db1,db2,db3", "", "", "") - .get_random_db() - .unwrap(); - assert_eq!(target_db, "db1"); - - target_db = generate_mongo_fetcher("db1,db2,db3", "", "db1,db2", "") - .get_random_db() - .unwrap(); - assert_eq!(target_db, "db3"); - target_db = generate_mongo_fetcher( - "db1,db2", - "db1.table1,db3.table1,db4.table2", - "db1,db2", - "db3.table1", - ) - .get_random_db() - .unwrap(); - assert_eq!(target_db, "db4"); - - target_db = generate_mongo_fetcher("*", "", "db1,db2", "") - .get_random_db() - .unwrap(); - assert_eq!(target_db, "admin"); - - target_db = generate_mongo_fetcher("", "db1.table1,*.*", "db1", "") - .get_random_db() - .unwrap(); - assert_eq!(target_db, "admin"); + let doc_command = doc! {command: 1}; + let doc = client + .database(&dbs[0].name) + .run_command(doc_command, None) + .await?; + Ok(doc) } } diff --git a/dt-precheck/src/fetcher/mysql/mysql_fetcher.rs b/dt-precheck/src/fetcher/mysql/mysql_fetcher.rs index 55115ca9..18fe1f37 100644 --- a/dt-precheck/src/fetcher/mysql/mysql_fetcher.rs +++ b/dt-precheck/src/fetcher/mysql/mysql_fetcher.rs @@ -1,61 +1,27 @@ -use std::{collections::HashMap, time::Duration}; +use std::collections::HashMap; use async_trait::async_trait; -use dt_common::{ - config::{ - config_enums::DbType, extractor_config::ExtractorConfig, filter_config::FilterConfig, - router_config::RouterConfig, sinker_config::SinkerConfig, - }, - utils::rdb_filter::RdbFilter, -}; +use dt_common::{error::Error, utils::rdb_filter::RdbFilter}; +use dt_task::task_util::TaskUtil; use futures::{Stream, TryStreamExt}; -use sqlx::{ - mysql::{MySqlPoolOptions, MySqlRow}, - query, MySql, Pool, Row, -}; +use sqlx::{mysql::MySqlRow, query, MySql, Pool, Row}; use crate::{ - error::Error, fetcher::traits::Fetcher, meta::database_mode::{Constraint, Database, Schema, Table}, }; pub struct MysqlFetcher { pub pool: Option>, - pub source_config: ExtractorConfig, - pub filter_config: FilterConfig, - pub sinker_config: SinkerConfig, - pub router_config: RouterConfig, + pub url: String, pub is_source: bool, - pub db_type_option: Option, pub filter: RdbFilter, } #[async_trait] impl Fetcher for MysqlFetcher { async fn build_connection(&mut self) -> Result<(), Error> { - let mut connection_url = String::from(""); - - if self.is_source { - if let ExtractorConfig::Basic { url, db_type } = &self.source_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - } else if let SinkerConfig::Basic { url, db_type } = &self.sinker_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - - let db_pool_result = MySqlPoolOptions::new() - .max_connections(8) - .acquire_timeout(Duration::from_secs(5)) - .connect(connection_url.as_str()) - .await; - match db_pool_result { - Ok(pool) => self.pool = Option::Some(pool), - Err(error) => return Err(Error::from(error)), - } - + self.pool = Some(TaskUtil::create_mysql_conn_pool(&self.url, 1, true).await?); Ok(()) } diff --git a/dt-precheck/src/fetcher/postgresql/pg_fetcher.rs b/dt-precheck/src/fetcher/postgresql/pg_fetcher.rs index b4601e2e..a6601e5f 100644 --- a/dt-precheck/src/fetcher/postgresql/pg_fetcher.rs +++ b/dt-precheck/src/fetcher/postgresql/pg_fetcher.rs @@ -1,61 +1,27 @@ -use std::{collections::HashMap, time::Duration}; +use std::collections::HashMap; use async_trait::async_trait; -use dt_common::{ - config::{ - config_enums::DbType, extractor_config::ExtractorConfig, filter_config::FilterConfig, - router_config::RouterConfig, sinker_config::SinkerConfig, - }, - utils::rdb_filter::RdbFilter, -}; +use dt_common::{error::Error, utils::rdb_filter::RdbFilter}; +use dt_task::task_util::TaskUtil; use futures::{Stream, TryStreamExt}; -use sqlx::{ - postgres::{PgPoolOptions, PgRow}, - query, Pool, Postgres, Row, -}; +use sqlx::{postgres::PgRow, query, Pool, Postgres, Row}; use crate::{ - error::Error, fetcher::traits::Fetcher, meta::database_mode::{Constraint, Database, Schema, Table}, }; pub struct PgFetcher { pub pool: Option>, - pub source_config: ExtractorConfig, - pub filter_config: FilterConfig, - pub sinker_config: SinkerConfig, - pub router_config: RouterConfig, + pub url: String, pub is_source: bool, - pub db_type_option: Option, pub filter: RdbFilter, } #[async_trait] impl Fetcher for PgFetcher { async fn build_connection(&mut self) -> Result<(), Error> { - let mut connection_url = String::from(""); - - if self.is_source { - if let ExtractorConfig::Basic { url, db_type } = &self.source_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - } else if let SinkerConfig::Basic { url, db_type } = &self.sinker_config { - connection_url = String::from(url); - self.db_type_option = Some(db_type.to_owned()); - } - - let db_pool_result = PgPoolOptions::new() - .max_connections(8) - .acquire_timeout(Duration::from_secs(5)) - .connect(connection_url.as_str()) - .await; - match db_pool_result { - Ok(pool) => self.pool = Option::Some(pool), - Err(error) => return Err(Error::from(error)), - } - + self.pool = Some(TaskUtil::create_pg_conn_pool(&self.url, 1, true).await?); Ok(()) } diff --git a/dt-precheck/src/fetcher/redis/mod.rs b/dt-precheck/src/fetcher/redis/mod.rs new file mode 100644 index 00000000..1a02cbf8 --- /dev/null +++ b/dt-precheck/src/fetcher/redis/mod.rs @@ -0,0 +1 @@ +pub mod redis_fetcher; diff --git a/dt-precheck/src/fetcher/redis/redis_fetcher.rs b/dt-precheck/src/fetcher/redis/redis_fetcher.rs new file mode 100644 index 00000000..73970157 --- /dev/null +++ b/dt-precheck/src/fetcher/redis/redis_fetcher.rs @@ -0,0 +1,28 @@ +use async_trait::async_trait; +use dt_common::{error::Error, utils::rdb_filter::RdbFilter}; +use dt_task::task_util::TaskUtil; + +use crate::fetcher::traits::Fetcher; + +pub struct RedisFetcher { + pub url: String, + pub conn: Option, + pub is_source: bool, + pub filter: RdbFilter, +} + +#[async_trait] +impl Fetcher for RedisFetcher { + async fn build_connection(&mut self) -> Result<(), Error> { + self.conn = Some(TaskUtil::create_redis_conn(&self.url).await?); + Ok(()) + } + + async fn fetch_version(&mut self) -> Result { + let mut conn = self.conn.as_mut().unwrap(); + let version = TaskUtil::get_redis_version(&mut conn)?; + Ok(version.to_string()) + } +} + +impl RedisFetcher {} diff --git a/dt-precheck/src/fetcher/traits.rs b/dt-precheck/src/fetcher/traits.rs index a3ce2ad7..dccba62a 100644 --- a/dt-precheck/src/fetcher/traits.rs +++ b/dt-precheck/src/fetcher/traits.rs @@ -1,11 +1,9 @@ use std::collections::HashMap; use async_trait::async_trait; +use dt_common::error::Error; -use crate::{ - error::Error, - meta::database_mode::{Constraint, Database, Schema, Table}, -}; +use crate::meta::database_mode::{Constraint, Database, Schema, Table}; #[async_trait] pub trait Fetcher { @@ -15,14 +13,24 @@ pub trait Fetcher { async fn fetch_configuration( &mut self, - config_keys: Vec, - ) -> Result, Error>; - - async fn fetch_databases(&mut self) -> Result, Error>; - - async fn fetch_schemas(&mut self) -> Result, Error>; - - async fn fetch_tables(&mut self) -> Result, Error>; - - async fn fetch_constraints(&mut self) -> Result, Error>; + _config_keys: Vec, + ) -> Result, Error> { + Ok(HashMap::new()) + } + + async fn fetch_databases(&mut self) -> Result, Error> { + Ok(vec![]) + } + + async fn fetch_schemas(&mut self) -> Result, Error> { + Ok(vec![]) + } + + async fn fetch_tables(&mut self) -> Result, Error> { + Ok(vec![]) + } + + async fn fetch_constraints(&mut self) -> Result, Error> { + Ok(vec![]) + } } diff --git a/dt-precheck/src/lib.rs b/dt-precheck/src/lib.rs index 08748807..d3bc1c96 100644 --- a/dt-precheck/src/lib.rs +++ b/dt-precheck/src/lib.rs @@ -6,7 +6,6 @@ use crate::{ pub mod builder; pub mod config; -pub mod error; pub mod fetcher; pub mod meta; pub mod prechecker; diff --git a/dt-precheck/src/meta/check_result.rs b/dt-precheck/src/meta/check_result.rs index 25f0778f..82cdfbc1 100644 --- a/dt-precheck/src/meta/check_result.rs +++ b/dt-precheck/src/meta/check_result.rs @@ -1,6 +1,4 @@ -use dt_common::config::config_enums::DbType; - -use crate::error::Error; +use dt_common::{config::config_enums::DbType, error::Error}; use super::check_item::CheckItem; @@ -29,7 +27,7 @@ impl CheckResult { pub fn build_with_err( check_item: CheckItem, is_source: bool, - db_type_option: Option, + db_type: DbType, err_option: Option, ) -> Self { let check_desc; @@ -38,7 +36,7 @@ impl CheckResult { if !is_source { source_or_sink = String::from("sink"); } - let db_type = db_type_option.unwrap(); + match check_item { CheckItem::CheckDatabaseConnection => { check_desc = format!("check if the {} database can be connected.", source_or_sink); diff --git a/dt-precheck/src/prechecker/mod.rs b/dt-precheck/src/prechecker/mod.rs index 9623ace1..6d4abfd6 100644 --- a/dt-precheck/src/prechecker/mod.rs +++ b/dt-precheck/src/prechecker/mod.rs @@ -1,4 +1,5 @@ pub mod mongo_prechecker; pub mod mysql_prechecker; pub mod pg_prechecker; +pub mod redis_prechecker; pub mod traits; diff --git a/dt-precheck/src/prechecker/mongo_prechecker.rs b/dt-precheck/src/prechecker/mongo_prechecker.rs index 3cbf81a7..6d0c12c2 100644 --- a/dt-precheck/src/prechecker/mongo_prechecker.rs +++ b/dt-precheck/src/prechecker/mongo_prechecker.rs @@ -1,11 +1,13 @@ use async_trait::async_trait; -use dt_common::config::{config_enums::DbType, filter_config::FilterConfig}; +use dt_common::{ + config::{config_enums::DbType, filter_config::FilterConfig}, + error::Error, +}; use mongodb::bson::Bson; use regex::Regex; use crate::{ config::precheck_config::PrecheckConfig, - error::Error, fetcher::{mongo::mongo_fetcher::MongoFetcher, traits::Fetcher}, meta::{check_item::CheckItem, check_result::CheckResult}, }; @@ -19,22 +21,16 @@ pub struct MongoPrechecker { pub filter_config: FilterConfig, pub precheck_config: PrecheckConfig, pub is_source: bool, - pub db_type_option: Option, } #[async_trait] impl Prechecker for MongoPrechecker { async fn build_connection(&mut self) -> Result { - let result = self.fetcher.build_connection().await; - match result { - Ok(_) => {} - Err(e) => return Err(e), - } - + self.fetcher.build_connection().await?; Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseConnection, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, None, )) } @@ -45,15 +41,16 @@ impl Prechecker for MongoPrechecker { let version = self.fetcher.fetch_version().await?; let reg = Regex::new(MONGO_SUPPORTED_VERSION_REGEX).unwrap(); if !reg.is_match(version.as_str()) { - check_error = Some(Error::PreCheckError { - error: format!("mongo version:[{}] is invalid.", version), - }); + check_error = Some(Error::PreCheckError(format!( + "mongo version:[{}] is invalid.", + version + ))); } Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseVersionSupported, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, check_error, )) } @@ -73,18 +70,15 @@ impl Prechecker for MongoPrechecker { return Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, check_error, )); } // 1. replSet used // 2. the specify url is the master - let random_db = self.fetcher.get_random_db()?; - let rs_status = self - .fetcher - .execute_for_db(random_db.clone(), "hello") - .await?; + // let random_db = self.fetcher.get_random_db()?; + let rs_status = self.fetcher.execute_for_db("hello").await?; let (ok, primary, me): (bool, &str, &str) = ( rs_status.get("ok").and_then(Bson::as_f64).unwrap_or(0.0) >= 1.0, @@ -105,15 +99,13 @@ impl Prechecker for MongoPrechecker { } if !err_msg.is_empty() { - check_error = Some(Error::PreCheckError { - error: String::from(err_msg), - }); + check_error = Some(Error::PreCheckError(err_msg.into())); } Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, check_error, )) } @@ -122,7 +114,7 @@ impl Prechecker for MongoPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckIfStructExisted, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, None, )) } @@ -133,11 +125,9 @@ impl Prechecker for MongoPrechecker { let invalid_dbs = vec!["admin", "local"]; for db in invalid_dbs { if !self.fetcher.filter.filter_db(db) { - check_error = Some(Error::PreCheckError { - error: String::from( - "database 'admin' and 'local' are not supported as source and target.", - ), - }); + check_error = Some(Error::PreCheckError( + "database 'admin' and 'local' are not supported as source and target.".into(), + )); break; } } @@ -145,7 +135,7 @@ impl Prechecker for MongoPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckIfTableStructSupported, self.is_source, - self.db_type_option.clone(), + DbType::Mongo, check_error, )) } diff --git a/dt-precheck/src/prechecker/mysql_prechecker.rs b/dt-precheck/src/prechecker/mysql_prechecker.rs index ddd8a06e..cb88d09b 100644 --- a/dt-precheck/src/prechecker/mysql_prechecker.rs +++ b/dt-precheck/src/prechecker/mysql_prechecker.rs @@ -1,13 +1,15 @@ use std::collections::HashSet; use async_trait::async_trait; -use dt_common::config::{config_enums::DbType, filter_config::FilterConfig}; +use dt_common::{ + config::{config_enums::DbType, filter_config::FilterConfig}, + error::Error, +}; use dt_meta::struct_meta::db_table_model::DbTable; use regex::Regex; use crate::{ config::precheck_config::PrecheckConfig, - error::Error, fetcher::{mysql::mysql_fetcher::MysqlFetcher, traits::Fetcher}, meta::{check_item::CheckItem, check_result::CheckResult}, }; @@ -21,22 +23,16 @@ pub struct MySqlPrechecker { pub filter_config: FilterConfig, pub precheck_config: PrecheckConfig, pub is_source: bool, - pub db_type_option: Option, } #[async_trait] impl Prechecker for MySqlPrechecker { async fn build_connection(&mut self) -> Result { - let result = self.fetcher.build_connection().await; - match result { - Ok(_) => {} - Err(e) => return Err(e), - } - + self.fetcher.build_connection().await?; Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseConnection, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, None, )) } @@ -49,15 +45,14 @@ impl Prechecker for MySqlPrechecker { match result { Ok(version) => { if version.is_empty() { - check_error = Some(Error::PreCheckError { - error: "found no version info.".to_string(), - }); + check_error = Some(Error::PreCheckError("found no version info.".into())); } else { let re = Regex::new(MYSQL_SUPPORT_DB_VERSION_REGEX).unwrap(); if !re.is_match(version.as_str()) { - check_error = Some(Error::PreCheckError { - error: format!("mysql version:[{}] is invalid.", version), - }); + check_error = Some(Error::PreCheckError(format!( + "mysql version:[{}] is invalid.", + version + ))); } } } @@ -67,7 +62,7 @@ impl Prechecker for MySqlPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseVersionSupported, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )) } @@ -87,7 +82,7 @@ impl Prechecker for MySqlPrechecker { return Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )); } @@ -127,9 +122,9 @@ impl Prechecker for MySqlPrechecker { } } _ => { - return Err(Error::PreCheckError { - error: "find database cdc settings meet unknown error".to_string(), - }) + return Err(Error::PreCheckError( + "find database cdc settings meet unknown error".into(), + )) } } } @@ -137,15 +132,13 @@ impl Prechecker for MySqlPrechecker { Err(e) => return Err(e), } if !errs.is_empty() { - check_error = Some(Error::PreCheckError { - error: errs.join(";"), - }) + check_error = Some(Error::PreCheckError(errs.join(";"))) } Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )) } @@ -230,15 +223,13 @@ impl Prechecker for MySqlPrechecker { } } if !err_msgs.is_empty() { - check_error = Some(Error::PreCheckError { - error: err_msgs.join("."), - }) + check_error = Some(Error::PreCheckError(err_msgs.join(".").into())) } Ok(CheckResult::build_with_err( CheckItem::CheckIfStructExisted, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )) } @@ -251,7 +242,7 @@ impl Prechecker for MySqlPrechecker { return Ok(CheckResult::build_with_err( CheckItem::CheckIfTableStructSupported, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )); } @@ -334,15 +325,13 @@ impl Prechecker for MySqlPrechecker { )) } if !err_msgs.is_empty() { - check_error = Some(Error::PreCheckError { - error: err_msgs.join(";"), - }) + check_error = Some(Error::PreCheckError(err_msgs.join(";").into())) } Ok(CheckResult::build_with_err( CheckItem::CheckIfTableStructSupported, self.is_source, - self.db_type_option.clone(), + DbType::Mysql, check_error, )) } diff --git a/dt-precheck/src/prechecker/pg_prechecker.rs b/dt-precheck/src/prechecker/pg_prechecker.rs index 0590de2c..0fa37988 100644 --- a/dt-precheck/src/prechecker/pg_prechecker.rs +++ b/dt-precheck/src/prechecker/pg_prechecker.rs @@ -1,12 +1,14 @@ use std::collections::HashSet; use async_trait::async_trait; -use dt_common::config::{config_enums::DbType, filter_config::FilterConfig}; +use dt_common::{ + config::{config_enums::DbType, filter_config::FilterConfig}, + error::Error, +}; use dt_meta::struct_meta::{db_table_model::DbTable, pg_enums::ConstraintTypeEnum}; use crate::{ config::precheck_config::PrecheckConfig, - error::Error, fetcher::{postgresql::pg_fetcher::PgFetcher, traits::Fetcher}, meta::check_item::CheckItem, meta::check_result::CheckResult, @@ -22,7 +24,6 @@ pub struct PostgresqlPrechecker { pub filter_config: FilterConfig, pub precheck_config: PrecheckConfig, pub is_source: bool, - pub db_type_option: Option, } #[async_trait] @@ -38,7 +39,7 @@ impl Prechecker for PostgresqlPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseConnection, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )) } @@ -51,17 +52,16 @@ impl Prechecker for PostgresqlPrechecker { match result { Ok(version) => { if version.is_empty() { - check_error = Some(Error::PreCheckError { - error: "found no version info".to_string(), - }); + check_error = Some(Error::PreCheckError("found no version info".into())); } else { let version_i32: i32 = version.parse().unwrap(); if !(PG_SUPPORT_DB_VERSION_NUM_MIN..=PG_SUPPORT_DB_VERSION_NUM_MAX) .contains(&version_i32) { - check_error = Some(Error::PreCheckError { - error: format!("version:{} is not supported yet", version_i32), - }); + check_error = Some(Error::PreCheckError(format!( + "version:{} is not supported yet", + version_i32 + ))); } } } @@ -71,7 +71,7 @@ impl Prechecker for PostgresqlPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckDatabaseVersionSupported, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )) } @@ -91,7 +91,7 @@ impl Prechecker for PostgresqlPrechecker { return Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )); } @@ -140,9 +140,7 @@ impl Prechecker for PostgresqlPrechecker { Err(e) => return Err(e), } if !err_msgs.is_empty() { - check_error = Some(Error::PreCheckError { - error: err_msgs.join(";"), - }); + check_error = Some(Error::PreCheckError(err_msgs.join(";").into())); } if check_error.is_none() { @@ -151,7 +149,7 @@ impl Prechecker for PostgresqlPrechecker { match slot_result { Ok(slots) => { if max_replication_slots_i32 == (slots.len() as i32) { - check_error = Some(Error::PreCheckError { error: format!("the current number of slots:[{}] has reached max_replication_slots, and new slots cannot be created", max_replication_slots_i32) }); + check_error = Some(Error::PreCheckError ( format!("the current number of slots:[{}] has reached max_replication_slots, and new slots cannot be created", max_replication_slots_i32) )); } } Err(e) => check_error = Some(e), @@ -161,7 +159,7 @@ impl Prechecker for PostgresqlPrechecker { Ok(CheckResult::build_with_err( CheckItem::CheckIfDatabaseSupportCdc, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )) } @@ -191,9 +189,9 @@ impl Prechecker for PostgresqlPrechecker { all_schemas.extend(&tb_schemas); if all_schemas.is_empty() { println!("found no schema need to do migrate, very strange"); - return Err(Error::PreCheckError { - error: String::from("found no schema need to do migrate"), - }); + return Err(Error::PreCheckError( + "found no schema need to do migrate".into(), + )); } if (self.is_source || !self.precheck_config.do_struct_init) && !tbs.is_empty() { @@ -252,15 +250,13 @@ impl Prechecker for PostgresqlPrechecker { } } if !err_msgs.is_empty() { - check_error = Some(Error::PreCheckError { - error: err_msgs.join("."), - }) + check_error = Some(Error::PreCheckError(err_msgs.join(".").into())) } Ok(CheckResult::build_with_err( CheckItem::CheckIfStructExisted, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )) } @@ -274,7 +270,7 @@ impl Prechecker for PostgresqlPrechecker { return Ok(CheckResult::build_with_err( CheckItem::CheckIfTableStructSupported, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )); } @@ -301,9 +297,9 @@ impl Prechecker for PostgresqlPrechecker { all_schemas.extend(&tb_schemas); if all_schemas.is_empty() { println!("found no schema need to do migrate, very strange"); - return Err(Error::PreCheckError { - error: String::from("found no schema need to do migrate"), - }); + return Err(Error::PreCheckError( + "found no schema need to do migrate".into(), + )); } let (mut has_pk_tables, mut has_fk_tables): (HashSet, HashSet) = @@ -360,15 +356,13 @@ impl Prechecker for PostgresqlPrechecker { )); } if !err_msgs.is_empty() { - check_error = Some(Error::PreCheckError { - error: err_msgs.join(";"), - }) + check_error = Some(Error::PreCheckError(err_msgs.join(";").into())) } Ok(CheckResult::build_with_err( CheckItem::CheckIfTableStructSupported, self.is_source, - self.db_type_option.clone(), + DbType::Pg, check_error, )) } diff --git a/dt-precheck/src/prechecker/redis_prechecker.rs b/dt-precheck/src/prechecker/redis_prechecker.rs new file mode 100644 index 00000000..8d98fdea --- /dev/null +++ b/dt-precheck/src/prechecker/redis_prechecker.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use concurrent_queue::ConcurrentQueue; +use dt_common::{ + config::{config_enums::DbType, extractor_config::ExtractorConfig, task_config::TaskConfig}, + error::Error, +}; +use dt_connector::extractor::redis::{ + redis_client::RedisClient, redis_psync_extractor::RedisPsyncExtractor, +}; + +use crate::{ + config::precheck_config::PrecheckConfig, + fetcher::{redis::redis_fetcher::RedisFetcher, traits::Fetcher}, + meta::{check_item::CheckItem, check_result::CheckResult}, +}; + +use super::traits::Prechecker; + +pub struct RedisPrechecker { + pub fetcher: RedisFetcher, + pub task_config: TaskConfig, + pub precheck_config: PrecheckConfig, + pub is_source: bool, +} + +const MIN_SUPPORTED_VERSION: f32 = 2.8; + +#[async_trait] +impl Prechecker for RedisPrechecker { + async fn build_connection(&mut self) -> Result { + self.fetcher.build_connection().await?; + Ok(CheckResult::build_with_err( + CheckItem::CheckDatabaseConnection, + self.is_source, + DbType::Redis, + None, + )) + } + + async fn check_database_version(&mut self) -> Result { + let version = self.fetcher.fetch_version().await?; + let version: f32 = version.parse().unwrap(); + let check_error = if version < MIN_SUPPORTED_VERSION { + Some(Error::PreCheckError(format!( + "redis version:[{}] is NOT supported, the minimum supported version is {}.", + version, MIN_SUPPORTED_VERSION + ))) + } else { + None + }; + + Ok(CheckResult::build_with_err( + CheckItem::CheckDatabaseVersionSupported, + self.is_source, + DbType::Redis, + check_error, + )) + } + + async fn check_cdc_supported(&mut self) -> Result { + let repl_port = match self.task_config.extractor { + ExtractorConfig::RedisCdc { repl_port, .. } + | ExtractorConfig::RedisSnapshot { repl_port, .. } => repl_port, + // should never happen since we've already checked the extractor type before into this function + _ => 0, + }; + let mut conn = RedisClient::new(&self.fetcher.url).await?; + let buffer = Arc::new(ConcurrentQueue::bounded(1)); + + let mut psyncer = RedisPsyncExtractor { + conn: &mut conn, + run_id: String::new(), + repl_offset: 0, + now_db_id: 0, + repl_port, + buffer, + }; + + if let Err(error) = psyncer.start_psync().await { + conn.close().await?; + return Ok(CheckResult::build_with_err( + CheckItem::CheckAccountPermission, + self.is_source, + DbType::Redis, + Some(error), + )); + } else { + conn.close().await?; + Ok(CheckResult::build( + CheckItem::CheckAccountPermission, + self.is_source, + )) + } + } + + async fn check_permission(&mut self) -> Result { + Ok(CheckResult::build( + CheckItem::CheckAccountPermission, + self.is_source, + )) + } + + async fn check_struct_existed_or_not(&mut self) -> Result { + Ok(CheckResult::build_with_err( + CheckItem::CheckIfStructExisted, + self.is_source, + DbType::Redis, + None, + )) + } + + async fn check_table_structs(&mut self) -> Result { + Ok(CheckResult::build_with_err( + CheckItem::CheckIfTableStructSupported, + self.is_source, + DbType::Redis, + None, + )) + } +} diff --git a/dt-precheck/src/prechecker/traits.rs b/dt-precheck/src/prechecker/traits.rs index 182077df..c30f01a1 100644 --- a/dt-precheck/src/prechecker/traits.rs +++ b/dt-precheck/src/prechecker/traits.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; +use dt_common::error::Error; -use crate::{error::Error, meta::check_result::CheckResult}; +use crate::meta::check_result::CheckResult; #[async_trait] pub trait Prechecker { diff --git a/dt-task/Cargo.toml b/dt-task/Cargo.toml index 609beea8..9c2e16fd 100644 --- a/dt-task/Cargo.toml +++ b/dt-task/Cargo.toml @@ -32,4 +32,5 @@ configparser = { workspace = true } project-root = { workspace = true } regex = { workspace = true } strum = { workspace = true } -serde_json = { workspace = true } \ No newline at end of file +serde_json = { workspace = true } +redis = { workspace = true } \ No newline at end of file diff --git a/dt-task/src/extractor_util.rs b/dt-task/src/extractor_util.rs index 65e26c45..2a234717 100644 --- a/dt-task/src/extractor_util.rs +++ b/dt-task/src/extractor_util.rs @@ -1,10 +1,14 @@ -use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::{ + str::FromStr, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; use concurrent_queue::ConcurrentQueue; use dt_common::{ config::config_enums::DbType, error::Error, syncer::Syncer, utils::rdb_filter::RdbFilter, }; use dt_connector::extractor::{ + kafka::kafka_extractor::KafkaExtractor, mongo::{ mongo_cdc_extractor::MongoCdcExtractor, mongo_snapshot_extractor::MongoSnapshotExtractor, }, @@ -17,11 +21,15 @@ use dt_connector::extractor::{ pg_cdc_extractor::PgCdcExtractor, pg_check_extractor::PgCheckExtractor, pg_snapshot_extractor::PgSnapshotExtractor, pg_struct_extractor::PgStructExtractor, }, + redis::{ + redis_cdc_extractor::RedisCdcExtractor, redis_client::RedisClient, + redis_snapshot_extractor::RedisSnapshotExtractor, + }, snapshot_resumer::SnapshotResumer, }; use dt_meta::{ - dt_data::DtData, mysql::mysql_meta_manager::MysqlMetaManager, - pg::pg_meta_manager::PgMetaManager, + dt_data::DtData, mongo::mongo_cdc_source::MongoCdcSource, + mysql::mysql_meta_manager::MysqlMetaManager, pg::pg_meta_manager::PgMetaManager, }; use futures::TryStreamExt; use sqlx::Row; @@ -139,16 +147,16 @@ impl ExtractorUtil { } #[allow(clippy::too_many_arguments)] - pub async fn create_mysql_cdc_extractor<'a>( + pub async fn create_mysql_cdc_extractor( url: &str, binlog_filename: &str, binlog_position: u32, server_id: u64, - buffer: &'a ConcurrentQueue, + buffer: Arc>, filter: RdbFilter, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); let conn_pool = TaskUtil::create_mysql_conn_pool(url, 2, enable_sqlx_log).await?; let meta_manager = MysqlMetaManager::new(conn_pool).init().await?; @@ -166,17 +174,17 @@ impl ExtractorUtil { } #[allow(clippy::too_many_arguments)] - pub async fn create_pg_cdc_extractor<'a>( + pub async fn create_pg_cdc_extractor( url: &str, slot_name: &str, start_lsn: &str, heartbeat_interval_secs: u64, - buffer: &'a ConcurrentQueue, + buffer: Arc>, filter: RdbFilter, log_level: &str, - shut_down: &'a AtomicBool, + shut_down: Arc, syncer: Arc>, - ) -> Result, Error> { + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); let conn_pool = TaskUtil::create_pg_conn_pool(url, 2, enable_sqlx_log).await?; let meta_manager = PgMetaManager::new(conn_pool.clone()).init().await?; @@ -195,16 +203,16 @@ impl ExtractorUtil { } #[allow(clippy::too_many_arguments)] - pub async fn create_mysql_snapshot_extractor<'a>( + pub async fn create_mysql_snapshot_extractor( url: &str, db: &str, tb: &str, slice_size: usize, resumer: SnapshotResumer, - buffer: &'a ConcurrentQueue, + buffer: Arc>, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); // max_connections: 1 for extracting data from table, 1 for db-meta-manager let conn_pool = TaskUtil::create_mysql_conn_pool(url, 2, enable_sqlx_log).await?; @@ -222,14 +230,14 @@ impl ExtractorUtil { }) } - pub async fn create_mysql_check_extractor<'a>( + pub async fn create_mysql_check_extractor( url: &str, check_log_dir: &str, batch_size: usize, - buffer: &'a ConcurrentQueue, + buffer: Arc>, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); let conn_pool = TaskUtil::create_mysql_conn_pool(url, 2, enable_sqlx_log).await?; let meta_manager = MysqlMetaManager::new(conn_pool.clone()).init().await?; @@ -244,14 +252,14 @@ impl ExtractorUtil { }) } - pub async fn create_pg_check_extractor<'a>( + pub async fn create_pg_check_extractor( url: &str, check_log_dir: &str, batch_size: usize, - buffer: &'a ConcurrentQueue, + buffer: Arc>, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); let conn_pool = TaskUtil::create_pg_conn_pool(url, 2, enable_sqlx_log).await?; let meta_manager = PgMetaManager::new(conn_pool.clone()).init().await?; @@ -267,16 +275,16 @@ impl ExtractorUtil { } #[allow(clippy::too_many_arguments)] - pub async fn create_pg_snapshot_extractor<'a>( + pub async fn create_pg_snapshot_extractor( url: &str, db: &str, tb: &str, slice_size: usize, resumer: SnapshotResumer, - buffer: &'a ConcurrentQueue, + buffer: Arc>, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); let conn_pool = TaskUtil::create_pg_conn_pool(url, 2, enable_sqlx_log).await?; let meta_manager = PgMetaManager::new(conn_pool.clone()).init().await?; @@ -293,14 +301,14 @@ impl ExtractorUtil { }) } - pub async fn create_mongo_snapshot_extractor<'a>( + pub async fn create_mongo_snapshot_extractor( url: &str, db: &str, tb: &str, resumer: SnapshotResumer, - buffer: &'a ConcurrentQueue, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + buffer: Arc>, + shut_down: Arc, + ) -> Result { let mongo_client = TaskUtil::create_mongo_client(url).await.unwrap(); Ok(MongoSnapshotExtractor { buffer, @@ -312,33 +320,35 @@ impl ExtractorUtil { }) } - pub async fn create_mongo_cdc_extractor<'a>( + pub async fn create_mongo_cdc_extractor( url: &str, resume_token: &str, - start_timestamp: &i64, - buffer: &'a ConcurrentQueue, + start_timestamp: &u32, + source: &str, + buffer: Arc>, filter: RdbFilter, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let mongo_client = TaskUtil::create_mongo_client(url).await.unwrap(); Ok(MongoCdcExtractor { buffer, filter, resume_token: resume_token.to_string(), start_timestamp: *start_timestamp, + source: MongoCdcSource::from_str(source)?, shut_down, mongo_client, }) } - pub async fn create_mysql_struct_extractor<'a>( + pub async fn create_mysql_struct_extractor( url: &str, db: &str, - buffer: &'a ConcurrentQueue, + buffer: Arc>, filter: RdbFilter, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); // TODO, pass max_connections as parameter let conn_pool = TaskUtil::create_mysql_conn_pool(url, 2, enable_sqlx_log).await?; @@ -352,14 +362,14 @@ impl ExtractorUtil { }) } - pub async fn create_pg_struct_extractor<'a>( + pub async fn create_pg_struct_extractor( url: &str, db: &str, - buffer: &'a ConcurrentQueue, + buffer: Arc>, filter: RdbFilter, log_level: &str, - shut_down: &'a AtomicBool, - ) -> Result, Error> { + shut_down: Arc, + ) -> Result { let enable_sqlx_log = TaskUtil::check_enable_sqlx_log(log_level); // TODO, pass max_connections as parameter let conn_pool = TaskUtil::create_pg_conn_pool(url, 2, enable_sqlx_log).await?; @@ -372,4 +382,70 @@ impl ExtractorUtil { shut_down, }) } + + pub async fn create_redis_snapshot_extractor( + url: &str, + repl_port: u64, + buffer: Arc>, + shut_down: Arc, + ) -> Result { + // let conn = TaskUtil::create_redis_conn(url).await?; + let conn = RedisClient::new(url).await.unwrap(); + Ok(RedisSnapshotExtractor { + conn, + buffer, + shut_down, + repl_port, + }) + } + + pub async fn create_redis_cdc_extractor( + url: &str, + run_id: &str, + repl_offset: u64, + repl_port: u64, + now_db_id: i64, + heartbeat_interval_secs: u64, + buffer: Arc>, + shut_down: Arc, + syncer: Arc>, + ) -> Result { + // let conn = TaskUtil::create_redis_conn(url).await?; + let conn = RedisClient::new(url).await.unwrap(); + Ok(RedisCdcExtractor { + conn, + buffer, + run_id: run_id.to_string(), + repl_offset, + heartbeat_interval_secs, + shut_down, + syncer, + repl_port, + now_db_id: now_db_id, + }) + } + + pub async fn create_kafka_extractor( + url: &str, + group: &str, + topic: &str, + partition: i32, + offset: i64, + ack_interval_secs: u64, + buffer: Arc>, + shut_down: Arc, + syncer: Arc>, + ) -> Result { + Ok(KafkaExtractor { + url: url.into(), + group: group.into(), + topic: topic.into(), + partition, + offset, + ack_interval_secs, + buffer, + shut_down, + syncer, + }) + } } diff --git a/dt-task/src/parallelizer_util.rs b/dt-task/src/parallelizer_util.rs index 09e21831..20eb841d 100644 --- a/dt-task/src/parallelizer_util.rs +++ b/dt-task/src/parallelizer_util.rs @@ -6,11 +6,11 @@ use dt_common::{ }; use dt_parallelizer::{ base_parallelizer::BaseParallelizer, check_parallelizer::CheckParallelizer, - merge_parallelizer::MergeParallelizer, mongo_parallelizer::MongoParallelizer, + merge_parallelizer::MergeParallelizer, mongo_merger::MongoMerger, partition_parallelizer::PartitionParallelizer, rdb_merger::RdbMerger, - rdb_partitioner::RdbPartitioner, serial_parallelizer::SerialParallelizer, - snapshot_parallelizer::SnapshotParallelizer, table_parallelizer::TableParallelizer, - Parallelizer, + rdb_partitioner::RdbPartitioner, redis_parallelizer::RedisParallelizer, + serial_parallelizer::SerialParallelizer, snapshot_parallelizer::SnapshotParallelizer, + table_parallelizer::TableParallelizer, Merger, Parallelizer, }; use super::task_util::TaskUtil; @@ -48,6 +48,7 @@ impl ParallelizerUtil { base_parallelizer, merger, parallel_size, + sinker_basic_config: config.sinker_basic.clone(), }) } @@ -67,7 +68,17 @@ impl ParallelizerUtil { parallel_size, }), - ParallelType::Mongo => Box::new(MongoParallelizer { + ParallelType::Mongo => { + let merger = Box::new(MongoMerger {}); + Box::new(MergeParallelizer { + base_parallelizer, + merger, + parallel_size, + sinker_basic_config: config.sinker_basic.clone(), + }) + } + + ParallelType::Redis => Box::new(RedisParallelizer { base_parallelizer, parallel_size, }), @@ -75,9 +86,17 @@ impl ParallelizerUtil { Ok(parallelizer) } - pub async fn create_rdb_merger(config: &TaskConfig) -> Result { + pub async fn create_rdb_merger( + config: &TaskConfig, + ) -> Result, Error> { let meta_manager = TaskUtil::create_rdb_meta_manager(config).await?; - Ok(RdbMerger { meta_manager }) + let rdb_merger = RdbMerger { meta_manager }; + Ok(Box::new(rdb_merger)) + } + + pub async fn create_mongo_merger() -> Result, Error> { + let mongo_merger = MongoMerger {}; + Ok(Box::new(mongo_merger)) } pub async fn create_rdb_partitioner(config: &TaskConfig) -> Result { diff --git a/dt-task/src/sinker_util.rs b/dt-task/src/sinker_util.rs index da68e35d..31b16644 100644 --- a/dt-task/src/sinker_util.rs +++ b/dt-task/src/sinker_util.rs @@ -18,10 +18,14 @@ use dt_connector::{ open_faas_sinker::OpenFaasSinker, pg::{pg_checker::PgChecker, pg_sinker::PgSinker, pg_struct_sinker::PgStructSinker}, rdb_router::RdbRouter, + redis::redis_sinker::RedisSinker, }, Sinker, }; -use dt_meta::{mysql::mysql_meta_manager::MysqlMetaManager, pg::pg_meta_manager::PgMetaManager}; +use dt_meta::{ + mysql::mysql_meta_manager::MysqlMetaManager, pg::pg_meta_manager::PgMetaManager, + redis::redis_write_method::RedisWriteMethod, +}; use kafka::producer::{Producer, RequiredAcks}; use reqwest::Client; use rusoto_core::Region; @@ -179,7 +183,19 @@ impl SinkerUtil { .await? } - _ => vec![], + SinkerConfig::Redis { + url, + batch_size, + method, + } => { + SinkerUtil::create_redis_sinker( + url, + task_config.pipeline.parallel_size, + *batch_size, + method, + ) + .await? + } }; Ok(sinkers) } @@ -201,6 +217,7 @@ impl SinkerUtil { let mut sub_sinkers: Vec>>> = Vec::new(); for _ in 0..parallel_size { let sinker = MysqlSinker { + url: url.to_string(), conn_pool: conn_pool.clone(), meta_manager: meta_manager.clone(), router: router.clone(), @@ -434,4 +451,27 @@ impl SinkerUtil { } Ok(sub_sinkers) } + + async fn create_redis_sinker<'a>( + url: &str, + parallel_size: usize, + batch_size: usize, + method: &str, + ) -> Result>>>, Error> { + let mut sub_sinkers: Vec>>> = Vec::new(); + for _ in 0..parallel_size { + let mut conn = TaskUtil::create_redis_conn(url).await?; + let version = TaskUtil::get_redis_version(&mut conn)?; + let method = RedisWriteMethod::from_str(method).unwrap(); + let sinker = RedisSinker { + conn, + batch_size, + now_db_id: -1, + version, + method, + }; + sub_sinkers.push(Arc::new(async_mutex::Mutex::new(Box::new(sinker)))); + } + Ok(sub_sinkers) + } } diff --git a/dt-task/src/task_runner.rs b/dt-task/src/task_runner.rs index 722aefe2..21317dc2 100644 --- a/dt-task/src/task_runner.rs +++ b/dt-task/src/task_runner.rs @@ -24,6 +24,7 @@ use dt_pipeline::{ }; use futures::future::join; use log4rs::config::RawConfig; +use tokio::try_join; use super::{ extractor_util::ExtractorUtil, parallelizer_util::ParallelizerUtil, sinker_util::SinkerUtil, @@ -119,10 +120,7 @@ impl TaskRunner { }, _ => { - return Err(Error::Unexpected { - error: "unexpected extractor config type for rdb snapshot task" - .to_string(), - }); + return Err(Error::ConfigError("unsupported extractor config".into())); } }; @@ -133,25 +131,33 @@ impl TaskRunner { } async fn start_single_task(&self, extractor_config: &ExtractorConfig) -> Result<(), Error> { - let buffer = ConcurrentQueue::bounded(self.config.pipeline.buffer_size); - let shut_down = AtomicBool::new(false); + let buffer = Arc::new(ConcurrentQueue::bounded(self.config.pipeline.buffer_size)); + let shut_down = Arc::new(AtomicBool::new(false)); let syncer = Arc::new(Mutex::new(Syncer { checkpoint_position: String::new(), })); let mut extractor = self - .create_extractor(extractor_config, &buffer, &shut_down, syncer.clone()) + .create_extractor( + extractor_config, + buffer.clone(), + shut_down.clone(), + syncer.clone(), + ) .await?; let mut pipeline = self.create_pipeline(&buffer, &shut_down, &syncer).await?; - let result = join(extractor.extract(), pipeline.start()).await; - pipeline.stop().await?; - extractor.close().await?; - if result.0.is_err() { - return result.0; - } - result.1 + let f1 = tokio::spawn(async move { + extractor.extract().await.unwrap(); + extractor.close().await.unwrap(); + }); + let f2 = tokio::spawn(async move { + pipeline.start().await.unwrap(); + pipeline.stop().await.unwrap(); + }); + let _ = try_join!(f1, f2); + Ok(()) } async fn create_pipeline<'a>( @@ -170,6 +176,7 @@ impl TaskRunner { let obj = BasicPipeline { buffer, parallelizer, + sinker_basic_config: self.config.sinker_basic.clone(), sinkers, shut_down, checkpoint_interval_secs: self.config.pipeline.checkpoint_interval_secs, @@ -196,13 +203,13 @@ impl TaskRunner { Ok(pipeline) } - async fn create_extractor<'a>( + async fn create_extractor( &self, extractor_config: &ExtractorConfig, - buffer: &'a ConcurrentQueue, - shut_down: &'a AtomicBool, + buffer: Arc>, + shut_down: Arc, syncer: Arc>, - ) -> Result, Error> { + ) -> Result, Error> { let resumer = SnapshotResumer { resumer_values: self.config.resumer.resume_values.clone(), db_type: extractor_config.get_db_type(), @@ -337,12 +344,14 @@ impl TaskRunner { url, resume_token, start_timestamp, + source, } => { let filter = RdbFilter::from_config(&self.config.filter, DbType::Mongo)?; let extractor = ExtractorUtil::create_mongo_cdc_extractor( url, resume_token, start_timestamp, + source, buffer, filter, shut_down, @@ -378,10 +387,59 @@ impl TaskRunner { .await?; Box::new(extractor) } - _ => { - return Err(Error::ConfigError { - error: String::from("extractor_config type is not supported."), - }) + + ExtractorConfig::RedisSnapshot { url, repl_port } => { + let extractor = ExtractorUtil::create_redis_snapshot_extractor( + url, *repl_port, buffer, shut_down, + ) + .await?; + Box::new(extractor) + } + + ExtractorConfig::RedisCdc { + url, + run_id, + repl_offset, + now_db_id, + repl_port, + heartbeat_interval_secs, + } => { + let extractor = ExtractorUtil::create_redis_cdc_extractor( + url, + run_id, + *repl_offset, + *repl_port, + *now_db_id, + *heartbeat_interval_secs, + buffer, + shut_down, + syncer, + ) + .await?; + Box::new(extractor) + } + + ExtractorConfig::Kafka { + url, + group, + topic, + partition, + offset, + ack_interval_secs, + } => { + let extractor = ExtractorUtil::create_kafka_extractor( + url, + group, + topic, + *partition, + *offset, + *ack_interval_secs, + buffer, + shut_down, + syncer, + ) + .await?; + Box::new(extractor) } }; Ok(extractor) diff --git a/dt-task/src/task_util.rs b/dt-task/src/task_util.rs index 09c9b353..d819cf3d 100644 --- a/dt-task/src/task_util.rs +++ b/dt-task/src/task_util.rs @@ -2,14 +2,17 @@ use std::{str::FromStr, time::Duration}; use dt_common::{ config::{sinker_config::SinkerConfig, task_config::TaskConfig}, - constants::MongoConstants, error::Error, }; +use dt_connector::sinker::redis::cmd_encoder::CmdEncoder; use dt_meta::{ - mysql::mysql_meta_manager::MysqlMetaManager, pg::pg_meta_manager::PgMetaManager, - rdb_meta_manager::RdbMetaManager, + mongo::mongo_constant::MongoConstants, mysql::mysql_meta_manager::MysqlMetaManager, + pg::pg_meta_manager::PgMetaManager, rdb_meta_manager::RdbMetaManager, + redis::redis_object::RedisCmd, }; use mongodb::options::ClientOptions; +use redis::ConnectionLike; +use regex::Regex; use sqlx::{ mysql::{MySqlConnectOptions, MySqlPoolOptions}, postgres::{PgConnectOptions, PgPoolOptions}, @@ -62,6 +65,38 @@ impl TaskUtil { Ok(conn_pool) } + pub async fn create_redis_conn(url: &str) -> Result { + let conn = redis::Client::open(url).unwrap().get_connection().unwrap(); + Ok(conn) + } + + pub fn get_redis_version(conn: &mut redis::Connection) -> Result { + let cmd = RedisCmd::from_str_args(&vec!["INFO"]); + let value = conn.req_packed_command(&CmdEncoder::encode(&cmd)).unwrap(); + if let redis::Value::Data(data) = value { + let info = String::from_utf8(data).unwrap(); + let re = Regex::new(r"redis_version:(\S+)").unwrap(); + let cap = re.captures(&info).unwrap(); + + let version_str = cap[1].to_string(); + let tokens: Vec<&str> = version_str.split(".").collect(); + if tokens.is_empty() { + return Err(Error::Unexpected( + "can not get redis version by INFO".into(), + )); + } + + let mut version = tokens[0].to_string(); + if tokens.len() > 1 { + version = format!("{}.{}", tokens[0], tokens[1]); + } + return Ok(f32::from_str(&version).unwrap()); + } + Err(Error::Unexpected( + "can not get redis version by INFO".into(), + )) + } + pub async fn create_rdb_meta_manager(config: &TaskConfig) -> Result { let log_level = &config.runtime.log_level; let meta_manager = match &config.sinker { @@ -76,9 +111,7 @@ impl TaskUtil { } _ => { - return Err(Error::Unexpected { - error: "unexpected sinker type".to_string(), - }); + return Err(Error::ConfigError("unsupported sinker config".into())); } }; Ok(meta_manager) @@ -104,7 +137,9 @@ impl TaskUtil { pub async fn create_mongo_client(url: &str) -> Result { let mut client_options = ClientOptions::parse_async(url).await.unwrap(); + // app_name only for debug usage client_options.app_name = Some(MongoConstants::APP_NAME.to_string()); + client_options.direct_connection = Some(true); Ok(mongodb::Client::with_options(client_options).unwrap()) } diff --git a/dt-tests/Cargo.toml b/dt-tests/Cargo.toml index 375a52a1..27aff568 100644 --- a/dt-tests/Cargo.toml +++ b/dt-tests/Cargo.toml @@ -26,4 +26,7 @@ configparser = { workspace = true } project-root = { workspace = true } regex = { workspace = true } serde_json = { workspace = true } -concurrent-queue = { workspace = true } \ No newline at end of file +concurrent-queue = { workspace = true } +redis = { workspace = true } +rdkafka = { workspace = true } +rand = "0.8.5" \ No newline at end of file diff --git a/dt-tests/README.md b/dt-tests/README.md index a7eb4261..f0fef00a 100644 --- a/dt-tests/README.md +++ b/dt-tests/README.md @@ -130,6 +130,37 @@ mongo "mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0" docker run -d --name dst-mongo \ -e MONGO_INITDB_ROOT_USERNAME=ape_dts \ -e MONGO_INITDB_ROOT_PASSWORD=123456 \ - -p 27113:27017 \ + -p 27018:27017 \ mongo ``` + +## redis +### images of redis versions +- redis:7.0 +- redis:6.0 +- redis:6.2 +- redis:5.0 +- redis:4.0 +- redis:2.8 + +### source + +``` +docker run --name some-redis-1 \ +-p 6380:6379 \ +-d redis redis-server \ +--requirepass 123456 \ +--save 60 1 \ +--loglevel warning +``` + +### target + +``` +docker run --name some-redis-2 \ +-p 6381:6379 \ +-d redis redis-server \ +--requirepass 123456 \ +--save 60 1 \ +--loglevel warning +``` \ No newline at end of file diff --git a/dt-tests/k8s/redis/2-8/pod-redis-2-8-dst.yaml b/dt-tests/k8s/redis/2-8/pod-redis-2-8-dst.yaml new file mode 100644 index 00000000..71d33f67 --- /dev/null +++ b/dt-tests/k8s/redis/2-8/pod-redis-2-8-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-2-8-dst + namespace: dts + labels: + app: redis + version: "2-8" + use: dst +spec: + containers: + - name: pod-redis-2-8-dst + image: redis:2.8.22 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/2-8/pod-redis-2-8-src.yaml b/dt-tests/k8s/redis/2-8/pod-redis-2-8-src.yaml new file mode 100644 index 00000000..ef435a75 --- /dev/null +++ b/dt-tests/k8s/redis/2-8/pod-redis-2-8-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-2-8-src + namespace: dts + labels: + app: redis + version: "2-8" + use: src +spec: + containers: + - name: pod-redis-2-8-src + image: redis:2.8.22 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/2-8/service-redis-2-8-dst.yaml b/dt-tests/k8s/redis/2-8/service-redis-2-8-dst.yaml new file mode 100644 index 00000000..ce8659e5 --- /dev/null +++ b/dt-tests/k8s/redis/2-8/service-redis-2-8-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-2-8-dst + namespace: dts +spec: + selector: + app: redis + version: "2-8" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/2-8/service-redis-2-8-src.yaml b/dt-tests/k8s/redis/2-8/service-redis-2-8-src.yaml new file mode 100644 index 00000000..87baaa48 --- /dev/null +++ b/dt-tests/k8s/redis/2-8/service-redis-2-8-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-2-8-src + namespace: dts +spec: + selector: + app: redis + version: "2-8" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/4-0/pod-redis-4-0-dst.yaml b/dt-tests/k8s/redis/4-0/pod-redis-4-0-dst.yaml new file mode 100644 index 00000000..39995e01 --- /dev/null +++ b/dt-tests/k8s/redis/4-0/pod-redis-4-0-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-4-0-dst + namespace: dts + labels: + app: redis + version: "4-0" + use: dst +spec: + containers: + - name: pod-redis-4-0-dst + image: redis:4.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/4-0/pod-redis-4-0-src.yaml b/dt-tests/k8s/redis/4-0/pod-redis-4-0-src.yaml new file mode 100644 index 00000000..c2416a06 --- /dev/null +++ b/dt-tests/k8s/redis/4-0/pod-redis-4-0-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-4-0-src + namespace: dts + labels: + app: redis + version: "4-0" + use: src +spec: + containers: + - name: pod-redis-4-0-src + image: redis:4.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/4-0/service-redis-4-0-dst.yaml b/dt-tests/k8s/redis/4-0/service-redis-4-0-dst.yaml new file mode 100644 index 00000000..ab01646f --- /dev/null +++ b/dt-tests/k8s/redis/4-0/service-redis-4-0-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-4-0-dst + namespace: dts +spec: + selector: + app: redis + version: "4-0" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/4-0/service-redis-4-0-src.yaml b/dt-tests/k8s/redis/4-0/service-redis-4-0-src.yaml new file mode 100644 index 00000000..35938528 --- /dev/null +++ b/dt-tests/k8s/redis/4-0/service-redis-4-0-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-4-0-src + namespace: dts +spec: + selector: + app: redis + version: "4-0" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/5-0/pod-redis-5-0-dst.yaml b/dt-tests/k8s/redis/5-0/pod-redis-5-0-dst.yaml new file mode 100644 index 00000000..805d5504 --- /dev/null +++ b/dt-tests/k8s/redis/5-0/pod-redis-5-0-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-5-0-dst + namespace: dts + labels: + app: redis + version: "5-0" + use: dst +spec: + containers: + - name: pod-redis-5-0-dst + image: redis:5.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/5-0/pod-redis-5-0-src.yaml b/dt-tests/k8s/redis/5-0/pod-redis-5-0-src.yaml new file mode 100644 index 00000000..e027952f --- /dev/null +++ b/dt-tests/k8s/redis/5-0/pod-redis-5-0-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-5-0-src + namespace: dts + labels: + app: redis + version: "5-0" + use: src +spec: + containers: + - name: pod-redis-5-0-src + image: redis:5.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/5-0/service-redis-5-0-dst.yaml b/dt-tests/k8s/redis/5-0/service-redis-5-0-dst.yaml new file mode 100644 index 00000000..31d7ea33 --- /dev/null +++ b/dt-tests/k8s/redis/5-0/service-redis-5-0-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-5-0-dst + namespace: dts +spec: + selector: + app: redis + version: "5-0" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/5-0/service-redis-5-0-src.yaml b/dt-tests/k8s/redis/5-0/service-redis-5-0-src.yaml new file mode 100644 index 00000000..66352ff5 --- /dev/null +++ b/dt-tests/k8s/redis/5-0/service-redis-5-0-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-5-0-src + namespace: dts +spec: + selector: + app: redis + version: "5-0" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-0/pod-redis-6-0-dst.yaml b/dt-tests/k8s/redis/6-0/pod-redis-6-0-dst.yaml new file mode 100644 index 00000000..c952a181 --- /dev/null +++ b/dt-tests/k8s/redis/6-0/pod-redis-6-0-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-6-0-dst + namespace: dts + labels: + app: redis + version: "6-0" + use: dst +spec: + containers: + - name: pod-redis-6-0-dst + image: redis:6.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-0/pod-redis-6-0-src.yaml b/dt-tests/k8s/redis/6-0/pod-redis-6-0-src.yaml new file mode 100644 index 00000000..99ddd416 --- /dev/null +++ b/dt-tests/k8s/redis/6-0/pod-redis-6-0-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-6-0-src + namespace: dts + labels: + app: redis + version: "6-0" + use: src +spec: + containers: + - name: pod-redis-6-0-src + image: redis:6.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-0/service-redis-6-0-dst.yaml b/dt-tests/k8s/redis/6-0/service-redis-6-0-dst.yaml new file mode 100644 index 00000000..38624b80 --- /dev/null +++ b/dt-tests/k8s/redis/6-0/service-redis-6-0-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-6-0-dst + namespace: dts +spec: + selector: + app: redis + version: "6-0" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-0/service-redis-6-0-src.yaml b/dt-tests/k8s/redis/6-0/service-redis-6-0-src.yaml new file mode 100644 index 00000000..c7119826 --- /dev/null +++ b/dt-tests/k8s/redis/6-0/service-redis-6-0-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-6-0-src + namespace: dts +spec: + selector: + app: redis + version: "6-0" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-2/pod-redis-6-2-dst.yaml b/dt-tests/k8s/redis/6-2/pod-redis-6-2-dst.yaml new file mode 100644 index 00000000..ff3e7fa7 --- /dev/null +++ b/dt-tests/k8s/redis/6-2/pod-redis-6-2-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-6-2-dst + namespace: dts + labels: + app: redis + version: "6-2" + use: dst +spec: + containers: + - name: pod-redis-6-2-dst + image: redis:6.2 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-2/pod-redis-6-2-src.yaml b/dt-tests/k8s/redis/6-2/pod-redis-6-2-src.yaml new file mode 100644 index 00000000..27f3ec35 --- /dev/null +++ b/dt-tests/k8s/redis/6-2/pod-redis-6-2-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-6-2-src + namespace: dts + labels: + app: redis + version: "6-2" + use: src +spec: + containers: + - name: pod-redis-6-2-src + image: redis:6.2 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-2/service-redis-6-2-dst.yaml b/dt-tests/k8s/redis/6-2/service-redis-6-2-dst.yaml new file mode 100644 index 00000000..a23f7328 --- /dev/null +++ b/dt-tests/k8s/redis/6-2/service-redis-6-2-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-6-2-dst + namespace: dts +spec: + selector: + app: redis + version: "6-2" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/6-2/service-redis-6-2-src.yaml b/dt-tests/k8s/redis/6-2/service-redis-6-2-src.yaml new file mode 100644 index 00000000..54870ce1 --- /dev/null +++ b/dt-tests/k8s/redis/6-2/service-redis-6-2-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-6-2-src + namespace: dts +spec: + selector: + app: redis + version: "6-2" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/configmap-redis-7-0-src.yaml b/dt-tests/k8s/redis/7-0/configmap-redis-7-0-src.yaml new file mode 100644 index 00000000..d2d96b22 --- /dev/null +++ b/dt-tests/k8s/redis/7-0/configmap-redis-7-0-src.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-redis-7-0-src +data: + redis.conf: | + enable-module-command yes \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/pod-redis-7-0-dst.yaml b/dt-tests/k8s/redis/7-0/pod-redis-7-0-dst.yaml new file mode 100644 index 00000000..6502daad --- /dev/null +++ b/dt-tests/k8s/redis/7-0/pod-redis-7-0-dst.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-7-0-dst + namespace: dts + labels: + app: redis + version: "7-0" + use: dst +spec: + containers: + - name: pod-redis-7-0-dst + image: redis:7.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/pod-redis-7-0-src.yaml b/dt-tests/k8s/redis/7-0/pod-redis-7-0-src.yaml new file mode 100644 index 00000000..6a54213e --- /dev/null +++ b/dt-tests/k8s/redis/7-0/pod-redis-7-0-src.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-7-0-src + namespace: dts + labels: + app: redis + version: "7-0" + use: src +spec: + containers: + - name: pod-redis-7-0-src + image: redis:7.0 + ports: + - containerPort: 6379 + protocol: TCP + command: ["redis-server"] \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/service-redis-7-0-dst.yaml b/dt-tests/k8s/redis/7-0/service-redis-7-0-dst.yaml new file mode 100644 index 00000000..3119afc0 --- /dev/null +++ b/dt-tests/k8s/redis/7-0/service-redis-7-0-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-7-0-dst + namespace: dts +spec: + selector: + app: redis + version: "7-0" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/service-redis-7-0-src.yaml b/dt-tests/k8s/redis/7-0/service-redis-7-0-src.yaml new file mode 100644 index 00000000..4dfac76b --- /dev/null +++ b/dt-tests/k8s/redis/7-0/service-redis-7-0-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-7-0-src + namespace: dts +spec: + selector: + app: redis + version: "7-0" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/7-0/tmp.yaml b/dt-tests/k8s/redis/7-0/tmp.yaml new file mode 100644 index 00000000..f4c39269 --- /dev/null +++ b/dt-tests/k8s/redis/7-0/tmp.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-7-0-src + namespace: dts + labels: + app: redis + version: "7-0" + use: src +spec: + containers: + - name: pod-redis-7-0-src + image: redis:7.0 + ports: + - containerPort: 6379 + protocol: TCP + volumeMounts: + - name: config + mountPath: /redis-config + command: + - sh + - -c + - | + redis-server /redis-config/redis.conf & + sleep 5 + redis-cli -h localhost MODULE LOAD redisjson + volumes: + - name: config + configMap: + name: configmap-redis-7-0-src \ No newline at end of file diff --git a/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-dst.yaml b/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-dst.yaml new file mode 100644 index 00000000..78435621 --- /dev/null +++ b/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-dst.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-rebloom-dst + namespace: dts + labels: + app: redis + version: "rebloom" + use: dst +spec: + containers: + - name: pod-redis-rebloom-dst + image: redislabs/rebloom:2.6.3 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-src.yaml b/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-src.yaml new file mode 100644 index 00000000..24127d16 --- /dev/null +++ b/dt-tests/k8s/redis/rebloom/pod-redis-rebloom-src.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-rebloom-src + namespace: dts + labels: + app: redis + version: "rebloom" + use: src +spec: + containers: + - name: pod-redis-rebloom-src + image: redislabs/rebloom:2.6.3 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/rebloom/service-redis-rebloom-dst.yaml b/dt-tests/k8s/redis/rebloom/service-redis-rebloom-dst.yaml new file mode 100644 index 00000000..5c629e8c --- /dev/null +++ b/dt-tests/k8s/redis/rebloom/service-redis-rebloom-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-rebloom-dst + namespace: dts +spec: + selector: + app: redis + version: "rebloom" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/rebloom/service-redis-rebloom-src.yaml b/dt-tests/k8s/redis/rebloom/service-redis-rebloom-src.yaml new file mode 100644 index 00000000..ab79bea5 --- /dev/null +++ b/dt-tests/k8s/redis/rebloom/service-redis-rebloom-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-rebloom-src + namespace: dts +spec: + selector: + app: redis + version: "rebloom" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-dst.yaml b/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-dst.yaml new file mode 100644 index 00000000..a48daca8 --- /dev/null +++ b/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-dst.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-redisearch-dst + namespace: dts + labels: + app: redis + version: "redisearch" + use: dst +spec: + containers: + - name: pod-redis-redisearch-dst + image: redislabs/redisearch:2.8.4 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-src.yaml b/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-src.yaml new file mode 100644 index 00000000..c2c77b62 --- /dev/null +++ b/dt-tests/k8s/redis/redisearch/pod-redis-redisearch-src.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-redisearch-src + namespace: dts + labels: + app: redis + version: "redisearch" + use: src +spec: + containers: + - name: pod-redis-redisearch-src + image: redislabs/redisearch:2.8.4 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/redisearch/service-redis-redisearch-dst.yaml b/dt-tests/k8s/redis/redisearch/service-redis-redisearch-dst.yaml new file mode 100644 index 00000000..4f7921af --- /dev/null +++ b/dt-tests/k8s/redis/redisearch/service-redis-redisearch-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-redisearch-dst + namespace: dts +spec: + selector: + app: redis + version: "redisearch" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/redisearch/service-redis-redisearch-src.yaml b/dt-tests/k8s/redis/redisearch/service-redis-redisearch-src.yaml new file mode 100644 index 00000000..5a6fbcc1 --- /dev/null +++ b/dt-tests/k8s/redis/redisearch/service-redis-redisearch-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-redisearch-src + namespace: dts +spec: + selector: + app: redis + version: "redisearch" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/rejson/pod-redis-rejson-dst.yaml b/dt-tests/k8s/redis/rejson/pod-redis-rejson-dst.yaml new file mode 100644 index 00000000..c9020e15 --- /dev/null +++ b/dt-tests/k8s/redis/rejson/pod-redis-rejson-dst.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-rejson-dst + namespace: dts + labels: + app: redis + version: "rejson" + use: dst +spec: + containers: + - name: pod-redis-rejson-dst + image: redislabs/rejson:2.6.4 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/rejson/pod-redis-rejson-src.yaml b/dt-tests/k8s/redis/rejson/pod-redis-rejson-src.yaml new file mode 100644 index 00000000..b487e8d3 --- /dev/null +++ b/dt-tests/k8s/redis/rejson/pod-redis-rejson-src.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-redis-rejson-src + namespace: dts + labels: + app: redis + version: "rejson" + use: src +spec: + containers: + - name: pod-redis-rejson-src + image: redislabs/rejson:2.6.4 + ports: + - containerPort: 6379 + protocol: TCP \ No newline at end of file diff --git a/dt-tests/k8s/redis/rejson/service-redis-rejson-dst.yaml b/dt-tests/k8s/redis/rejson/service-redis-rejson-dst.yaml new file mode 100644 index 00000000..6588054b --- /dev/null +++ b/dt-tests/k8s/redis/rejson/service-redis-rejson-dst.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-rejson-dst + namespace: dts +spec: + selector: + app: redis + version: "rejson" + use: dst + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/k8s/redis/rejson/service-redis-rejson-src.yaml b/dt-tests/k8s/redis/rejson/service-redis-rejson-src.yaml new file mode 100644 index 00000000..84704de7 --- /dev/null +++ b/dt-tests/k8s/redis/rejson/service-redis-rejson-src.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-redis-rejson-src + namespace: dts +spec: + selector: + app: redis + version: "rejson" + use: src + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: LoadBalancer \ No newline at end of file diff --git a/dt-tests/tests/.env b/dt-tests/tests/.env index 39d72851..974bde90 100644 --- a/dt-tests/tests/.env +++ b/dt-tests/tests/.env @@ -1,14 +1,20 @@ -mysql_extractor_url=mysql://root:123456@127.0.0.1:3306 -mysql_sinker_url=mysql://root:123456@127.0.0.1:3307 +mysql_extractor_url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled +mysql_sinker_url=mysql://root:123456@127.0.0.1:3308 mysql_cycle_node1_url=mysql://root:123456@127.0.0.1:3306 mysql_cycle_node2_url=mysql://root:123456@127.0.0.1:3307 mysql_cycle_node3_url=mysql://root:123456@127.0.0.1:3308 -pg_extractor_url=postgres://postgres:123456@127.0.0.1:5431/postgres?options[statement_timeout]=10s -pg_sinker_url=postgres://postgres:123456@127.0.0.1:5430/postgres?options[statement_timeout]=10s +pg_extractor_url=postgres://postgres:postgres@127.0.0.1:5433/postgres?options[statement_timeout]=10s +pg_sinker_url=postgres://postgres:postgres@127.0.0.1:5434/postgres?options[statement_timeout]=10s -mongo_extractor_url=mongodb://localhost:27017/?directConnection=true -mongo_sinker_url=mongodb://localhost:27018/?directConnection=true +mongo_extractor_url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +mongo_sinker_url=mongodb://ape_dts:123456@localhost:27018 + +redis_extractor_url=redis://aa2b463deaac14c8585714786ffe0d8c-543369047.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://af3c50366d01d4c03943a47cf146b0b2-15030835.cn-northwest-1.elb.amazonaws.com.cn:6379 + +kafka_extractor_url=localhost:9093 +kafka_sinker_url=localhost:9093 do_clean_after_test=true \ No newline at end of file diff --git a/dt-tests/tests/integration_test.rs b/dt-tests/tests/integration_test.rs index 93dc6573..1c783982 100644 --- a/dt-tests/tests/integration_test.rs +++ b/dt-tests/tests/integration_test.rs @@ -1,8 +1,9 @@ mod log_reader; mod mongo_to_mongo; mod mysql_to_foxlake; +mod mysql_to_kafka_to_mysql; mod mysql_to_mysql; mod pg_to_pg; -mod rdb_to_kafka; +mod redis_to_redis; mod test_config_util; mod test_runner; diff --git a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/basic_test/dst_ddl.sql deleted file mode 100644 index c26de772..00000000 --- a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/dst_ddl.sql +++ /dev/null @@ -1,7 +0,0 @@ -use test_db_1 - -db.tb_1.drop() -db.tb_2.drop() - -db.createCollection("tb_1"); -db.createCollection("tb_2"); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_ddl.sql deleted file mode 100644 index c26de772..00000000 --- a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_ddl.sql +++ /dev/null @@ -1,7 +0,0 @@ -use test_db_1 - -db.tb_1.drop() -db.tb_2.drop() - -db.createCollection("tb_1"); -db.createCollection("tb_2"); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/dst_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/dst_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_dml.sql b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_dml.sql similarity index 71% rename from dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_dml.sql rename to dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_dml.sql index f3123c1a..1b0cc44e 100644 --- a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/src_dml.sql +++ b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/src_dml.sql @@ -22,4 +22,13 @@ db.tb_1.deleteOne({ "name": "a", "age": "1" }); db.tb_1.deleteOne({ "name": "b", "age": "2" }); db.tb_2.deleteOne({ "name": "d", "age": "4" }); -db.tb_2.deleteOne({ "name": "e", "age": "5" }); \ No newline at end of file +db.tb_2.deleteOne({ "name": "e", "age": "5" }); + +use test_db_2 + +-- insert records with custom defined _id and object_id +db.tb_1.insertMany([{ "name": "a", "age": "1", "_id": "1" }, { "name": "b", "age": "1", "_id": "2" }, { "name": "c", "age": "1" }]); + +db.tb_1.updateMany({ "age": "1" }, { "$set": { "age" : "1000" } }); + +db.tb_1.deleteMany({ "age": "1000" }); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/task_config.ini new file mode 100644 index 00000000..de7ee9d8 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/change_stream_test/task_config.ini @@ -0,0 +1,35 @@ +[extractor] +db_type=mongo +extract_type=cdc +; resume_token={"_data":"8264819327000000022B022C0100296E5A100429B60CE1B0544AFABB16199CDB4222A946645F69640064648193279AA9CADD41A9DCB60004"} +url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +source=change_stream + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mongo +sink_type=write +batch_size=2 +url=mongodb://ape_dts:123456@localhost:27018 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=mongo +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/dst_ddl.sql new file mode 100644 index 00000000..e382243a --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/dst_ddl.sql @@ -0,0 +1,20 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_1 + +db.tb_1.insertOne({ "name": "a", "age": "1", "_id": "1" }); +db.tb_1.insertOne({ "name": "b", "age": "2", "_id": "2" }); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_dml.sql b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_dml.sql new file mode 100644 index 00000000..702305fe --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/src_dml.sql @@ -0,0 +1,10 @@ +use test_db_1 + +db.tb_1.insertOne({ "name": "a", "age": "1", "_id": "1" }); +db.tb_1.insertOne({ "name": "b", "age": "2", "_id": "2" }); +db.tb_1.insertOne({ "name": "c", "age": "3" }); +db.tb_1.insertOne({ "name": "d", "age": "4" }); +db.tb_1.insertOne({ "name": "e", "age": "5" }); + +db.tb_1.deleteOne({ "name": "a", "age": "1" }); +db.tb_1.updateOne({ "age" : "2" }, { "$set": { "name" : "d_1" } }); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/task_config.ini new file mode 100644 index 00000000..de7ee9d8 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/idempotent_test/task_config.ini @@ -0,0 +1,35 @@ +[extractor] +db_type=mongo +extract_type=cdc +; resume_token={"_data":"8264819327000000022B022C0100296E5A100429B60CE1B0544AFABB16199CDB4222A946645F69640064648193279AA9CADD41A9DCB60004"} +url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +source=change_stream + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mongo +sink_type=write +batch_size=2 +url=mongodb://ape_dts:123456@localhost:27018 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=mongo +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/dst_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/dst_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_dml.sql b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_dml.sql new file mode 100644 index 00000000..1b0cc44e --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/src_dml.sql @@ -0,0 +1,34 @@ +use test_db_1 + +db.tb_1.insertOne({ "name": "a", "age": "1" }); +db.tb_1.insertOne({ "name": "b", "age": "2" }); +db.tb_1.insertOne({ "name": "c", "age": "3" }); +db.tb_1.insertOne({ "name": "d", "age": "4" }); +db.tb_1.insertOne({ "name": "e", "age": "5" }); + +db.tb_2.insertOne({ "name": "a", "age": "1" }); +db.tb_2.insertOne({ "name": "b", "age": "2" }); +db.tb_2.insertOne({ "name": "c", "age": "3" }); +db.tb_2.insertOne({ "name": "d", "age": "4" }); +db.tb_2.insertOne({ "name": "e", "age": "5" }); + +db.tb_1.updateOne({ "age" : "4" }, { "$set": { "name" : "d_1" } }); +db.tb_1.updateOne({ "age" : "5" }, { "$set": { "name" : "e_1" } }); + +db.tb_2.updateOne({ "age" : "1" }, { "$set": { "name" : "a_1" } }); +db.tb_2.updateOne({ "age" : "2" }, { "$set": { "name" : "b_1" } }); + +db.tb_1.deleteOne({ "name": "a", "age": "1" }); +db.tb_1.deleteOne({ "name": "b", "age": "2" }); + +db.tb_2.deleteOne({ "name": "d", "age": "4" }); +db.tb_2.deleteOne({ "name": "e", "age": "5" }); + +use test_db_2 + +-- insert records with custom defined _id and object_id +db.tb_1.insertMany([{ "name": "a", "age": "1", "_id": "1" }, { "name": "b", "age": "1", "_id": "2" }, { "name": "c", "age": "1" }]); + +db.tb_1.updateMany({ "age": "1" }, { "$set": { "age" : "1000" } }); + +db.tb_1.deleteMany({ "age": "1000" }); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/task_config.ini similarity index 86% rename from dt-tests/tests/mongo_to_mongo/cdc/basic_test/task_config.ini rename to dt-tests/tests/mongo_to_mongo/cdc/op_log_test/task_config.ini index 4ebff85d..fa790b6d 100644 --- a/dt-tests/tests/mongo_to_mongo/cdc/basic_test/task_config.ini +++ b/dt-tests/tests/mongo_to_mongo/cdc/op_log_test/task_config.ini @@ -3,11 +3,12 @@ db_type=mongo extract_type=cdc # resume_token={"_data":"8264819327000000022B022C0100296E5A100429B60CE1B0544AFABB16199CDB4222A946645F69640064648193279AA9CADD41A9DCB60004"} url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +source=op_log [filter] ignore_dbs= do_dbs= -do_tbs=test_db_1.tb_1,test_db_1.tb_2 +do_tbs=test_db_1.*,test_db_2.* ignore_tbs= do_events=insert,update,delete @@ -15,7 +16,7 @@ do_events=insert,update,delete db_type=mongo sink_type=write batch_size=2 -url=mongodb://ape_dts:123456@localhost:27113 +url=mongodb://ape_dts:123456@localhost:27018 [router] tb_map= diff --git a/dt-tests/tests/mongo_to_mongo/cdc/resume_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/dst_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/dst_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_ddl.sql new file mode 100644 index 00000000..53f511c7 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_ddl.sql @@ -0,0 +1,15 @@ +use test_db_1 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_dml.sql b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_dml.sql new file mode 100644 index 00000000..1b0cc44e --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/src_dml.sql @@ -0,0 +1,34 @@ +use test_db_1 + +db.tb_1.insertOne({ "name": "a", "age": "1" }); +db.tb_1.insertOne({ "name": "b", "age": "2" }); +db.tb_1.insertOne({ "name": "c", "age": "3" }); +db.tb_1.insertOne({ "name": "d", "age": "4" }); +db.tb_1.insertOne({ "name": "e", "age": "5" }); + +db.tb_2.insertOne({ "name": "a", "age": "1" }); +db.tb_2.insertOne({ "name": "b", "age": "2" }); +db.tb_2.insertOne({ "name": "c", "age": "3" }); +db.tb_2.insertOne({ "name": "d", "age": "4" }); +db.tb_2.insertOne({ "name": "e", "age": "5" }); + +db.tb_1.updateOne({ "age" : "4" }, { "$set": { "name" : "d_1" } }); +db.tb_1.updateOne({ "age" : "5" }, { "$set": { "name" : "e_1" } }); + +db.tb_2.updateOne({ "age" : "1" }, { "$set": { "name" : "a_1" } }); +db.tb_2.updateOne({ "age" : "2" }, { "$set": { "name" : "b_1" } }); + +db.tb_1.deleteOne({ "name": "a", "age": "1" }); +db.tb_1.deleteOne({ "name": "b", "age": "2" }); + +db.tb_2.deleteOne({ "name": "d", "age": "4" }); +db.tb_2.deleteOne({ "name": "e", "age": "5" }); + +use test_db_2 + +-- insert records with custom defined _id and object_id +db.tb_1.insertMany([{ "name": "a", "age": "1", "_id": "1" }, { "name": "b", "age": "1", "_id": "2" }, { "name": "c", "age": "1" }]); + +db.tb_1.updateMany({ "age": "1" }, { "$set": { "age" : "1000" } }); + +db.tb_1.deleteMany({ "age": "1000" }); \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc/resume_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/task_config.ini new file mode 100644 index 00000000..de7ee9d8 --- /dev/null +++ b/dt-tests/tests/mongo_to_mongo/cdc/resume_test/task_config.ini @@ -0,0 +1,35 @@ +[extractor] +db_type=mongo +extract_type=cdc +; resume_token={"_data":"8264819327000000022B022C0100296E5A100429B60CE1B0544AFABB16199CDB4222A946645F69640064648193279AA9CADD41A9DCB60004"} +url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +source=change_stream + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mongo +sink_type=write +batch_size=2 +url=mongodb://ape_dts:123456@localhost:27018 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=mongo +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mongo_to_mongo/cdc_tests.rs b/dt-tests/tests/mongo_to_mongo/cdc_tests.rs index 022d4e7a..71fe1859 100644 --- a/dt-tests/tests/mongo_to_mongo/cdc_tests.rs +++ b/dt-tests/tests/mongo_to_mongo/cdc_tests.rs @@ -6,7 +6,25 @@ mod test { #[tokio::test] #[serial] - async fn cdc_basic_test() { - TestBase::run_mongo_cdc_test("mongo_to_mongo/cdc/basic_test", 3000, 10000).await; + async fn cdc_op_log_test() { + TestBase::run_mongo_cdc_test("mongo_to_mongo/cdc/op_log_test", 3000, 3000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_change_steram_test() { + TestBase::run_mongo_cdc_test("mongo_to_mongo/cdc/change_stream_test", 3000, 3000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_resume_test() { + TestBase::run_mongo_cdc_resume_test("mongo_to_mongo/cdc/resume_test", 3000, 3000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_idempotent_test() { + TestBase::run_mongo_cdc_resume_test("mongo_to_mongo/cdc/idempotent_test", 3000, 3000).await; } } diff --git a/dt-tests/tests/mongo_to_mongo/precheck/struct_supported_basic_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/precheck/struct_supported_basic_test/task_config.ini index 81ec9690..bc5c0633 100644 --- a/dt-tests/tests/mongo_to_mongo/precheck/struct_supported_basic_test/task_config.ini +++ b/dt-tests/tests/mongo_to_mongo/precheck/struct_supported_basic_test/task_config.ini @@ -1,13 +1,13 @@ [extractor] -extract_type=basic db_type=mongo -url=mongodb://ape_dts:123456@mongo1:9042/?replicaSet=rs0 +extract_type=cdc +url= [sinker] -sink_type=basic db_type=mongo -url=mongodb://ape_dts:123456@localhost:27113 -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] #do_dbs=source_db diff --git a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/dst_ddl.sql b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/dst_ddl.sql index c26de772..53f511c7 100644 --- a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/dst_ddl.sql +++ b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/dst_ddl.sql @@ -4,4 +4,12 @@ db.tb_1.drop() db.tb_2.drop() db.createCollection("tb_1"); -db.createCollection("tb_2"); \ No newline at end of file +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_ddl.sql b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_ddl.sql index d0e1e67b..53f511c7 100644 --- a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_ddl.sql +++ b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_ddl.sql @@ -3,5 +3,13 @@ use test_db_1 db.tb_1.drop() db.tb_2.drop() -db.createCollection("tb_1") -db.createCollection("tb_2") \ No newline at end of file +db.createCollection("tb_1"); +db.createCollection("tb_2"); + +use test_db_2 + +db.tb_1.drop() +db.tb_2.drop() + +db.createCollection("tb_1"); +db.createCollection("tb_2"); diff --git a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_dml.sql b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_dml.sql index 82f07d29..2510c3b3 100644 --- a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_dml.sql +++ b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/src_dml.sql @@ -1,13 +1,18 @@ use test_db_1 -db.tb_1.insertOne({ "name": "a", "age": "1" }) -db.tb_1.insertOne({ "name": "b", "age": "2" }) -db.tb_1.insertOne({ "name": "c", "age": "3" }) -db.tb_1.insertOne({ "name": "d", "age": "4" }) -db.tb_1.insertOne({ "name": "d", "age": "5" }) - -db.tb_2.insertOne({ "name": "a", "age": "1" }) -db.tb_2.insertOne({ "name": "b", "age": "2" }) -db.tb_2.insertOne({ "name": "c", "age": "3" }) -db.tb_2.insertOne({ "name": "d", "age": "4" }) -db.tb_2.insertOne({ "name": "d", "age": "5" }) \ No newline at end of file +db.tb_1.insertOne({ "name": "a", "age": "1" }); +db.tb_1.insertOne({ "name": "b", "age": "2" }); +db.tb_1.insertOne({ "name": "c", "age": "3" }); +db.tb_1.insertOne({ "name": "d", "age": "4" }); +db.tb_1.insertOne({ "name": "e", "age": "5" }); + +db.tb_2.insertOne({ "name": "a", "age": "1" }); +db.tb_2.insertOne({ "name": "b", "age": "2" }); +db.tb_2.insertOne({ "name": "c", "age": "3" }); +db.tb_2.insertOne({ "name": "d", "age": "4" }); +db.tb_2.insertOne({ "name": "e", "age": "5" }); + +use test_db_2 + +-- insert records with custom defined _id and object_id +db.tb_1.insertMany([{ "name": "a", "age": "1", "_id": "1" }, { "name": "b", "age": "1", "_id": "2" }, { "name": "c", "age": "1" }]); diff --git a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/task_config.ini b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/task_config.ini index e9938eaa..0c64a1c1 100644 --- a/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/task_config.ini +++ b/dt-tests/tests/mongo_to_mongo/snapshot/basic_test/task_config.ini @@ -12,7 +12,7 @@ batch_size=2 [filter] do_dbs= ignore_dbs= -do_tbs=test_db_1.tb_1,test_db_1.tb_2 +do_tbs=test_db_1.*,test_db_2.* ignore_tbs= do_events=insert diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/kafka_to_dst/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/kafka_to_dst/task_config.ini new file mode 100644 index 00000000..609600a2 --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/kafka_to_dst/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=kafka +extract_type=cdc +url=localhost:9093 +group=ape_test +topic=test +partition=0 +offset=0 +ack_interval_secs=5 + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mysql +sink_type=write +batch_size=2 +url=mysql://root:123456@127.0.0.1:3308 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=rdb_merge +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/dst_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/dst_ddl.sql new file mode 100644 index 00000000..675724ba --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/dst_ddl.sql @@ -0,0 +1,21 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_no_uk ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_one_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_multi_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_multi_uk ( f_0 tinyint, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0), UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.col_has_special_character_table (`p:k` tinyint, `col"1` text, `col,2` text, `col\3` text, PRIMARY KEY(`p:k`)); + +DROP DATABASE IF EXISTS test_db_2; + +CREATE DATABASE test_db_2; + +CREATE TABLE test_db_2.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_ddl.sql new file mode 100644 index 00000000..675724ba --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_ddl.sql @@ -0,0 +1,21 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_no_uk ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_one_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_multi_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_multi_uk ( f_0 tinyint, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0), UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.col_has_special_character_table (`p:k` tinyint, `col"1` text, `col,2` text, `col\3` text, PRIMARY KEY(`p:k`)); + +DROP DATABASE IF EXISTS test_db_2; + +CREATE DATABASE test_db_2; + +CREATE TABLE test_db_2.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_dml.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_dml.sql new file mode 100644 index 00000000..9b8be13c --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/src_dml.sql @@ -0,0 +1,54 @@ +INSERT INTO test_db_1.no_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_no_uk VALUES (2,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_no_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.one_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.one_pk_no_uk VALUES (2,20,30,40,50,654321.4321,4321.21,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.one_pk_no_uk VALUES (3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.no_pk_one_uk VALUES (1,1,1,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_one_uk VALUES (2,2,1,40,50,654321.4321,4321.21,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.no_pk_one_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.no_pk_multi_uk VALUES (1,1,1,1,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_multi_uk VALUES (2,2,1,2,50,654321.4321,4321.23,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.no_pk_multi_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.one_pk_multi_uk VALUES (1,1,1,1,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.one_pk_multi_uk VALUES (2,2,1,2,50,654321.4321,4321.23,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.one_pk_multi_uk VALUES (9, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.col_has_special_character_table VALUES(1, 'col:1:value', 'col&2:value', 'col\3:value'); +INSERT INTO test_db_1.col_has_special_character_table VALUES(2, NULL, NULL, NULL); + +UPDATE test_db_1.no_pk_no_uk SET f_1=20, f_2=30, f_3=40, f_4=50, f_5=654321.4321, f_6=4321.21, f_7=54321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; + +UPDATE test_db_1.one_pk_no_uk SET f_1=20, f_2=30, f_3=40, f_4=50, f_5=654321.4321, f_6=4321.21, f_7=54321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; +UPDATE test_db_1.one_pk_no_uk SET f_1=2, f_2=3, f_3=4, f_4=5, f_5=123456.1234, f_6=1234.12, f_7=12345.123, f_8=1893, f_9='2022-01-02 03:04:05.123456', f_10='03:04:05.123456', f_11='2022-01-02', f_12=2022, f_13='2022-01-02 03:04:05.123456', f_14='ab', f_15='cd', f_16='ef', f_17='gh', f_18='ij', f_19='kl', f_20='mn', f_21='op', f_22=x'0123456789ABCDEF', f_23=x'0123456789ABCDEF', f_24=x'0123456789ABCDEF', f_25=x'0123456789ABCDEF', f_26='x-small', f_27='c', f_28=NULL WHERE f_0=2; + +UPDATE test_db_1.no_pk_one_uk SET f_1=20, f_2=300, f_3=400, f_4=50, f_5=654321.4321, f_6=4321.21, f_7=54321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; +UPDATE test_db_1.no_pk_one_uk SET f_1=2, f_2=30, f_3=40, f_4=5, f_5=123456.1234, f_6=1234.12, f_7=12345.123, f_8=1893, f_9='2022-01-02 03:04:05.123456', f_10='03:04:05.123456', f_11='2022-01-02', f_12=2022, f_13='2022-01-02 03:04:05.123456', f_14='ab', f_15='cd', f_16='ef', f_17='gh', f_18='ij', f_19='kl', f_20='mn', f_21='op', f_22=x'0123456789ABCDEF', f_23=x'0123456789ABCDEF', f_24=x'0123456789ABCDEF', f_25=x'0123456789ABCDEF', f_26='x-small', f_27='c', f_28=NULL WHERE f_0=2; + +UPDATE test_db_1.no_pk_multi_uk SET f_1=200, f_2=300, f_3=400, f_4=500, f_5=54321.4321, f_6=321.21, f_7=4321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; +UPDATE test_db_1.no_pk_multi_uk SET f_1=20, f_2=30, f_3=40, f_4=50, f_5=23456.1234, f_6=234.12, f_7=2345.123, f_8=1893, f_9='2022-01-02 03:04:05.123456', f_10='03:04:05.123456', f_11='2022-01-02', f_12=2022, f_13='2022-01-02 03:04:05.123456', f_14='ab', f_15='cd', f_16='ef', f_17='gh', f_18='ij', f_19='kl', f_20='mn', f_21='op', f_22=x'0123456789ABCDEF', f_23=x'0123456789ABCDEF', f_24=x'0123456789ABCDEF', f_25=x'0123456789ABCDEF', f_26='x-small', f_27='c', f_28=NULL WHERE f_0=2; + +UPDATE test_db_1.one_pk_multi_uk SET f_1=200, f_2=300, f_3=400, f_4=500, f_5=54321.4321, f_6=321.21, f_7=4321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; +UPDATE test_db_1.one_pk_multi_uk SET f_1=20, f_2=30, f_3=40, f_4=50, f_5=23456.1234, f_6=234.12, f_7=2345.123, f_8=1893, f_9='2022-01-02 03:04:05.123456', f_10='03:04:05.123456', f_11='2022-01-02', f_12=2022, f_13='2022-01-02 03:04:05.123456', f_14='ab', f_15='cd', f_16='ef', f_17='gh', f_18='ij', f_19='kl', f_20='mn', f_21='op', f_22=x'0123456789ABCDEF', f_23=x'0123456789ABCDEF', f_24=x'0123456789ABCDEF', f_25=x'0123456789ABCDEF', f_26='x-small', f_27='c', f_28=NULL WHERE f_0=2; + +UPDATE test_db_1.col_has_special_character_table SET `col"1`=NULL, `col,2`=NULL, `col\3`=NULL WHERE `p:k`=1; +UPDATE test_db_1.col_has_special_character_table SET `col"1`='col:1:value', `col,2`='col&2:value', `col\3`='col\3:value' WHERE `p:k`=2; + +DELETE FROM test_db_1.no_pk_no_uk; +DELETE FROM test_db_1.one_pk_no_uk; +DELETE FROM test_db_1.no_pk_one_uk; +DELETE FROM test_db_1.no_pk_multi_uk; +DELETE FROM test_db_1.one_pk_multi_uk; +DELETE FROM test_db_1.col_has_special_character_table; + +INSERT INTO test_db_2.no_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_2.no_pk_no_uk VALUES (2,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_2.no_pk_no_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +UPDATE test_db_2.no_pk_no_uk SET f_1=20, f_2=30, f_3=40, f_4=50, f_5=654321.4321, f_6=4321.21, f_7=54321.321, f_8=3045, f_9='2021-02-01 04:05:06.654321', f_10='04:05:06.654321', f_11='2012-02-01', f_12=2021, f_13='2021-02-01 04:05:06.654321', f_14='1', f_15='2', f_16='3', f_17='4', f_18='5', f_19='6', f_20='7', f_21='8', f_22=x'ABCDEF0123456789', f_23=x'ABCDEF0123456789', f_24=x'ABCDEF0123456789', f_25=x'ABCDEF0123456789', f_26='small', f_27='b', f_28=NULL WHERE f_0=1; + +DELETE FROM test_db_2.no_pk_no_uk; \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/task_config.ini new file mode 100644 index 00000000..7161b8c3 --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_dst/task_config.ini @@ -0,0 +1,36 @@ +[extractor] +db_type=mysql +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mysql +sink_type=write +batch_size=2 +url=mysql://root:123456@127.0.0.1:3308 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=rdb_merge +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/dst_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/dst_ddl.sql new file mode 100644 index 00000000..3984ca7e --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/dst_ddl.sql @@ -0,0 +1 @@ +create topic test \ No newline at end of file diff --git a/dt-tests/tests/rdb_to_kafka/cdc_mysql_kafka_test/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/task_config.ini similarity index 90% rename from dt-tests/tests/rdb_to_kafka/cdc_mysql_kafka_test/task_config.ini rename to dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/task_config.ini index 75674209..59081383 100644 --- a/dt-tests/tests/rdb_to_kafka/cdc_mysql_kafka_test/task_config.ini +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc/basic_test/src_to_kafka/task_config.ini @@ -9,21 +9,21 @@ url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled [filter] ignore_dbs= do_dbs= -do_tbs=test_db_1.* +do_tbs=test_db_1.*,test_db_2.* ignore_tbs= do_events=insert,update,delete [sinker] db_type=kafka sink_type=write -batch_size=200 +batch_size=2 url=localhost:9093 ack_timeout_secs=1 required_acks=one [router] db_map=*:test -tb_map=test_db_1.c:test2 +tb_map= field_map= [parallelizer] diff --git a/dt-tests/tests/rdb_to_kafka/cdc_tests.rs b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc_tests.rs similarity index 53% rename from dt-tests/tests/rdb_to_kafka/cdc_tests.rs rename to dt-tests/tests/mysql_to_kafka_to_mysql/cdc_tests.rs index 720cb2d9..d3b20b14 100644 --- a/dt-tests/tests/rdb_to_kafka/cdc_tests.rs +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/cdc_tests.rs @@ -5,9 +5,10 @@ mod test { use crate::test_runner::test_base::TestBase; - // #[tokio::test] + #[tokio::test] #[serial] async fn cdc_basic_test() { - TestBase::run_cdc_test("pg_to_pg/cdc_basic_test", 7000, 5000).await; + TestBase::run_rdb_kafka_rdb_cdc_test("mysql_to_kafka_to_mysql/cdc/basic_test", 5000, 5000) + .await; } } diff --git a/dt-tests/tests/rdb_to_kafka/mod.rs b/dt-tests/tests/mysql_to_kafka_to_mysql/mod.rs similarity index 100% rename from dt-tests/tests/rdb_to_kafka/mod.rs rename to dt-tests/tests/mysql_to_kafka_to_mysql/mod.rs diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/kafka_to_dst/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/kafka_to_dst/task_config.ini new file mode 100644 index 00000000..609600a2 --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/kafka_to_dst/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=kafka +extract_type=cdc +url=localhost:9093 +group=ape_test +topic=test +partition=0 +offset=0 +ack_interval_secs=5 + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mysql +sink_type=write +batch_size=2 +url=mysql://root:123456@127.0.0.1:3308 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=rdb_merge +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/dst_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/dst_ddl.sql new file mode 100644 index 00000000..675724ba --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/dst_ddl.sql @@ -0,0 +1,21 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_no_uk ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_one_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_multi_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_multi_uk ( f_0 tinyint, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0), UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.col_has_special_character_table (`p:k` tinyint, `col"1` text, `col,2` text, `col\3` text, PRIMARY KEY(`p:k`)); + +DROP DATABASE IF EXISTS test_db_2; + +CREATE DATABASE test_db_2; + +CREATE TABLE test_db_2.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_ddl.sql new file mode 100644 index 00000000..675724ba --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_ddl.sql @@ -0,0 +1,21 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_no_uk ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_one_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.no_pk_multi_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.one_pk_multi_uk ( f_0 tinyint, f_1 smallint, f_2 mediumint, f_3 int, f_4 bigint, f_5 decimal(10,4), f_6 float(6,2), f_7 double(8,3), f_8 bit(64), f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL, PRIMARY KEY (f_0), UNIQUE KEY uk_1 (f_1,f_2), UNIQUE KEY uk_2 (f_3,f_4,f_5), UNIQUE KEY uk_3 (f_6,f_7,f_8) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.col_has_special_character_table (`p:k` tinyint, `col"1` text, `col,2` text, `col\3` text, PRIMARY KEY(`p:k`)); + +DROP DATABASE IF EXISTS test_db_2; + +CREATE DATABASE test_db_2; + +CREATE TABLE test_db_2.no_pk_no_uk ( f_0 tinyint DEFAULT NULL, f_1 smallint DEFAULT NULL, f_2 mediumint DEFAULT NULL, f_3 int DEFAULT NULL, f_4 bigint DEFAULT NULL, f_5 decimal(10,4) DEFAULT NULL, f_6 float(6,2) DEFAULT NULL, f_7 double(8,3) DEFAULT NULL, f_8 bit(64) DEFAULT NULL, f_9 datetime(6) DEFAULT NULL, f_10 time(6) DEFAULT NULL, f_11 date DEFAULT NULL, f_12 year DEFAULT NULL, f_13 timestamp(6) NULL DEFAULT NULL, f_14 char(255) DEFAULT NULL, f_15 varchar(255) DEFAULT NULL, f_16 binary(255) DEFAULT NULL, f_17 varbinary(255) DEFAULT NULL, f_18 tinytext, f_19 text, f_20 mediumtext, f_21 longtext, f_22 tinyblob, f_23 blob, f_24 mediumblob, f_25 longblob, f_26 enum('x-small','small','medium','large','x-large') DEFAULT NULL, f_27 set('a','b','c','d','e') DEFAULT NULL, f_28 json DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_dml.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_dml.sql new file mode 100644 index 00000000..deb1d78e --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/src_dml.sql @@ -0,0 +1,27 @@ +INSERT INTO test_db_1.no_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_no_uk VALUES (2,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_no_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.one_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.one_pk_no_uk VALUES (2,20,30,40,50,654321.4321,4321.21,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.one_pk_no_uk VALUES (3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.no_pk_one_uk VALUES (1,1,1,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_one_uk VALUES (2,2,1,40,50,654321.4321,4321.21,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.no_pk_one_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.no_pk_multi_uk VALUES (1,1,1,1,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.no_pk_multi_uk VALUES (2,2,1,2,50,654321.4321,4321.23,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.no_pk_multi_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.one_pk_multi_uk VALUES (1,1,1,1,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_1.one_pk_multi_uk VALUES (2,2,1,2,50,654321.4321,4321.23,54321.321,3045,'2021-02-01 04:05:06.654321','04:05:06.654321','2012-02-01',2021,'2021-02-01 04:05:06.654321','1','2','3','4','5','6','7','8',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789',x'ABCDEF0123456789','small','b', NULL); +INSERT INTO test_db_1.one_pk_multi_uk VALUES (9, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO test_db_1.col_has_special_character_table VALUES(1, 'col:1:value', 'col&2:value', 'col\3:value'); +INSERT INTO test_db_1.col_has_special_character_table VALUES(2, NULL, NULL, NULL); + + +INSERT INTO test_db_2.no_pk_no_uk VALUES (1,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_2.no_pk_no_uk VALUES (2,2,3,4,5,123456.1234,1234.12,12345.123,1893,'2022-01-02 03:04:05.123456','03:04:05.123456','2022-01-02',2022,'2022-01-02 03:04:05.123456','ab','cd','ef','gh','ij','kl','mn','op',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF',x'0123456789ABCDEF','x-small','c', NULL); +INSERT INTO test_db_2.no_pk_no_uk VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/task_config.ini new file mode 100644 index 00000000..f76a199a --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_dst/task_config.ini @@ -0,0 +1,36 @@ +[extractor] +db_type=mysql +extract_type=snapshot +binlog_position=0 +binlog_filename= +server_id=2000 +url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled + +[filter] +ignore_dbs= +do_dbs= +do_tbs=test_db_1.*,test_db_2.* +ignore_tbs= +do_events=insert,update,delete + +[sinker] +db_type=mysql +sink_type=write +batch_size=2 +url=mysql://root:123456@127.0.0.1:3308 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=rdb_merge +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/dst_ddl.sql b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/dst_ddl.sql new file mode 100644 index 00000000..3984ca7e --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/dst_ddl.sql @@ -0,0 +1 @@ +create topic test \ No newline at end of file diff --git a/dt-tests/tests/rdb_to_kafka/snapshot_mysql_kafka_est/task_config.ini b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/task_config.ini similarity index 68% rename from dt-tests/tests/rdb_to_kafka/snapshot_mysql_kafka_est/task_config.ini rename to dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/task_config.ini index 1dc2788c..db763175 100644 --- a/dt-tests/tests/rdb_to_kafka/snapshot_mysql_kafka_est/task_config.ini +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot/basic_test/src_to_kafka/task_config.ini @@ -1,26 +1,29 @@ [extractor] db_type=mysql extract_type=snapshot -url=mysql://root:123456@127.0.0.1:3307 +binlog_position=0 +binlog_filename= +server_id=2000 +url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled [filter] -do_dbs= ignore_dbs= -do_tbs=tpcc.* +do_dbs= +do_tbs=test_db_1.*,test_db_2.* ignore_tbs= -do_events=insert +do_events=insert,update,delete [sinker] db_type=kafka sink_type=write -batch_size=500 +batch_size=2 url=localhost:9093 ack_timeout_secs=1 required_acks=one [router] db_map=*:test -tb_map=test_db_1.c:test2 +tb_map= field_map= [parallelizer] diff --git a/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot_tests.rs b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot_tests.rs new file mode 100644 index 00000000..b2d62fd6 --- /dev/null +++ b/dt-tests/tests/mysql_to_kafka_to_mysql/snapshot_tests.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +mod test { + + use serial_test::serial; + + use crate::test_runner::test_base::TestBase; + + #[tokio::test] + #[serial] + async fn snapshot_basic_test() { + TestBase::run_rdb_kafka_rdb_snapshot_test( + "mysql_to_kafka_to_mysql/snapshot/basic_test", + 5000, + 5000, + ) + .await; + } +} diff --git a/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/dst_ddl.sql b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/dst_ddl.sql new file mode 100644 index 00000000..ef6ad3bd --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/dst_ddl.sql @@ -0,0 +1,20 @@ +DROP DATABASE IF EXISTS test_db_1; +DROP DATABASE IF EXISTS test_db_2; +DROP DATABASE IF EXISTS test_db_3; +DROP DATABASE IF EXISTS test_db_4; +DROP DATABASE IF EXISTS `δΈ­ζ–‡database!@#$%^&*()_+`; +CREATE DATABASE test_db_1; +CREATE DATABASE test_db_2; +CREATE DATABASE test_db_3; + +CREATE TABLE test_db_1.tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.drop_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.truncate_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +INSERT INTO test_db_1.truncate_tb_1 VALUES (1, 1); + +CREATE TABLE test_db_1.truncate_tb_2 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +INSERT INTO test_db_1.truncate_tb_2 VALUES (1, 1); + +CREATE TABLE test_db_2.truncate_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_ddl.sql b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_ddl.sql new file mode 100644 index 00000000..ef6ad3bd --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_ddl.sql @@ -0,0 +1,20 @@ +DROP DATABASE IF EXISTS test_db_1; +DROP DATABASE IF EXISTS test_db_2; +DROP DATABASE IF EXISTS test_db_3; +DROP DATABASE IF EXISTS test_db_4; +DROP DATABASE IF EXISTS `δΈ­ζ–‡database!@#$%^&*()_+`; +CREATE DATABASE test_db_1; +CREATE DATABASE test_db_2; +CREATE DATABASE test_db_3; + +CREATE TABLE test_db_1.tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.drop_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE test_db_1.truncate_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +INSERT INTO test_db_1.truncate_tb_1 VALUES (1, 1); + +CREATE TABLE test_db_1.truncate_tb_2 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +INSERT INTO test_db_1.truncate_tb_2 VALUES (1, 1); + +CREATE TABLE test_db_2.truncate_tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_dml.sql b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_dml.sql new file mode 100644 index 00000000..261bcd84 --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/src_dml.sql @@ -0,0 +1,51 @@ + +INSERT INTO test_db_1.tb_1 VALUES (1,1); + +-- add column +ALTER TABLE test_db_1.tb_1 ADD COLUMN f_2 smallint DEFAULT NULL; +ALTER TABLE test_db_1.tb_1 ADD COLUMN f_3 smallint DEFAULT NULL; + +INSERT INTO test_db_1.tb_1 VALUES (2,2,2,2); + +-- drop column +ALTER TABLE test_db_1.tb_1 DROP COLUMN f_2; + +INSERT INTO test_db_1.tb_1 VALUES (3,3,3); + +-- truncate table +TRUNCATE test_db_1.truncate_tb_1; +TRUNCATE TABLE test_db_1.truncate_tb_2; + +-- rename table +ALTER TABLE test_db_1.tb_1 RENAME test_db_1.tb_2; +RENAME TABLE test_db_1.tb_2 TO test_db_1.tb_3; + +-- drop table +DROP TABLE test_db_1.drop_tb_1; + +-- drop database +DROP DATABASE test_db_3; + +-- create database +CREATE DATABASE test_db_4; + +-- create table +CREATE TABLE test_db_2.tb_1 ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO test_db_2.tb_1 VALUES (1,1,1); + +-- add index +ALTER TABLE test_db_2.tb_1 ADD INDEX idx_f_1 (f_1); + +-- NOT supported ddl +CREATE INDEX idx_f_2 ON test_db_2.tb_1 (f_2); + +-- RENAME TABLE products TO products_old, products_new TO products; + +-- create database with special character +CREATE DATABASE `δΈ­ζ–‡database!@#$%^&*()_+`; + +-- create table with chinese character +CREATE TABLE `δΈ­ζ–‡database!@#$%^&*()_+`.`δΈ­ζ–‡` ( f_0 tinyint, f_1 smallint DEFAULT NULL, f_2 smallint DEFAULT NULL, PRIMARY KEY (f_0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `δΈ­ζ–‡database!@#$%^&*()_+`.`δΈ­ζ–‡` VALUES(1, 1, 1); diff --git a/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/task_config.ini new file mode 100644 index 00000000..eef2e54c --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/ddl_test/task_config.ini @@ -0,0 +1,36 @@ +[extractor] +db_type=mysql +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled + +[filter] +ignore_dbs= +do_dbs= +do_tbs=*.* +ignore_tbs= +do_events=insert,update,delete,ddl + +[sinker] +db_type=mysql +sink_type=write +batch_size=4 +url=mysql://root:123456@127.0.0.1:3308 + +[router] +tb_map= +field_map= +db_map= + +[pipeline] +parallel_type=rdb_merge +buffer_size=4 +checkpoint_interval_secs=1 +parallel_size=2 + +[runtime] +log_dir=./logs +log_level=info +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_mysql/cdc/json_test/dst_ddl.sql b/dt-tests/tests/mysql_to_mysql/cdc/json_test/dst_ddl.sql new file mode 100644 index 00000000..a2239ca7 --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/json_test/dst_ddl.sql @@ -0,0 +1,5 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.json_test(f_0 INT AUTO_INCREMENT, f_1 JSON, PRIMARY KEY(f_0)); \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_ddl.sql b/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_ddl.sql new file mode 100644 index 00000000..a2239ca7 --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_ddl.sql @@ -0,0 +1,5 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.json_test(f_0 INT AUTO_INCREMENT, f_1 JSON, PRIMARY KEY(f_0)); \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_dml.sql b/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_dml.sql new file mode 100644 index 00000000..04e9ddab --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/cdc/json_test/src_dml.sql @@ -0,0 +1,134 @@ +-- basic json object +INSERT INTO test_db_1.json_test VALUES (NULL, '{"k.1":1,"k.0":0,"k.-1":-1,"k.true":true,"k.false":false,"k.null":null,"k.string":"string","k.true_false":[true,false],"k.32767":32767,"k.32768":32768,"k.-32768":-32768,"k.-32769":-32769,"k.2147483647":2147483647,"k.2147483648":2147483648,"k.-2147483648":-2147483648,"k.-2147483649":-2147483649,"k.18446744073709551615":18446744073709551615,"k.18446744073709551616":18446744073709551616,"k.3.14":3.14,"k.{}":{},"k.[]":[]}') + +-- unicode support +INSERT INTO test_db_1.json_test VALUES (NULL, '{"key":"éééàààà"}') +INSERT INTO test_db_1.json_test VALUES (NULL, '{"δΈ­ζ–‡":"πŸ˜€"}') + +-- multiple nested json object +INSERT INTO test_db_1.json_test VALUES (NULL, '{"literal1":true,"i16":4,"i32":2147483647,"int64":4294967295,"double":1.0001,"string":"abc","time":"2022-01-01 12:34:56.000000","array":[1,2,{"i16":4,"array":[false,true,"abcd"]}],"small_document":{"i16":4,"array":[false,true,3],"small_document":{"i16":4,"i32":2147483647,"int64":4294967295}}}'),(5, '[{"i16":4,"small_document":{"i16":4,"i32":2147483647,"int64":4294967295}},{"i16":4,"array":[false,true,"abcd"]},"abc",10,null,true,false]'); + +-- null +INSERT INTO test_db_1.json_test VALUES (NULL, null) + +-- json with empty key +INSERT INTO test_db_1.json_test VALUES (NULL, '{"bitrate":{"":0}}') + +-- json array +INSERT INTO test_db_1.json_test VALUES (NULL, '[-1,0,1,true,false,null,"string",[true,false],32767,32768,-32768,-32769,2147483647,2147483648,-2147483648,-2147483649,18446744073709551615,18446744073709551616,3.14,{},[]]') + +-- json array nested +INSERT INTO test_db_1.json_test VALUES (NULL, '[-1,["b",["c"]],1]') + +-- scalar string +INSERT INTO test_db_1.json_test VALUES (NULL, '"scalar string"'),(11, '"LONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONG"') + +-- scalar boolean: true +INSERT INTO test_db_1.json_test VALUES (NULL, 'true') + +-- scalar boolean: false +INSERT INTO test_db_1.json_test VALUES (NULL, 'false') + +-- scalar null +INSERT INTO test_db_1.json_test VALUES (NULL, 'null') + +-- scalar negative integer +INSERT INTO test_db_1.json_test VALUES (NULL, '-1') + +-- scalar positive integer +INSERT INTO test_db_1.json_test VALUES (NULL, '1') + +-- scalar max positive int16 +INSERT INTO test_db_1.json_test VALUES (NULL, '32767') + +-- scalar int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '32768') + +-- scalar min negative int16 +INSERT INTO test_db_1.json_test VALUES (NULL, '-32768') + +-- scalar negative int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '-32769') + +-- scalar max_positive int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '2147483647') + +-- scalar positive int64 +INSERT INTO test_db_1.json_test VALUES (NULL, '2147483648') + +-- scalar min negative int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '-2147483648') + +-- scalar negative int64 +INSERT INTO test_db_1.json_test VALUES (NULL, '-2147483649') + +-- scalar uint64 +INSERT INTO test_db_1.json_test VALUES (NULL, '18446744073709551615') + +-- scalar uint64 overflow +INSERT INTO test_db_1.json_test VALUES (NULL, '18446744073709551616') + +-- scalar float +INSERT INTO test_db_1.json_test VALUES (NULL, '3.14') + +-- empty object +INSERT INTO test_db_1.json_test VALUES (NULL, '{}') + +-- empty array +INSERT INTO test_db_1.json_test VALUES (NULL, '[]') + +-- TODO, scalar json objects may lose precision in binlog. +-- for example, INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25' AS TIME) AS JSON)), the result will be: +-- +-----+--------------------------+ +-- | f_0 | f_1 | +-- | 1 | "23:24:25.000000" | +-- but in binlog, we get f_1: 23:24:25 + +-- scalar datetime +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('2015-01-15 23:24:25' AS DATETIME) AS JSON)) + +-- scalar time +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25' AS TIME) AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25.12' AS TIME(3)) AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25.0237' AS TIME(3)) AS JSON)) + +-- scalar timestamp +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25' AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25.12' AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25.0237' AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, (CAST(UNIX_TIMESTAMP(CONVERT_TZ('2015-01-15 23:24:25','GMT',@@session.time_zone)) AS JSON))) + +-- scalar geometry +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(ST_GeomFromText('POINT(1 1)') AS JSON)) + +-- scalar string with charset conversion +INSERT INTO test_db_1.json_test VALUES (NULL, CAST('[]' AS CHAR CHARACTER SET 'ascii')) + +-- scalar binary as base64 +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(x'cafe' AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(x'cafebabe' AS JSON)) + +-- scalar decimal +-- TODO, decimal will lose precision when insert into target mysql as string +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST("212765.700000000010000" AS DECIMAL(21,15)) AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST("111111.11111110000001" AS DECIMAL(24,17)) AS JSON)) + +-- set partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_SET(f_1, '$.addr.detail.ab', '970785C8') + +-- remove partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$.addr.detail.ab') + +-- remove partial update with holes and sparse keys +INSERT INTO test_db_1.json_test VALUES (NULL, '{"17fc9889474028063990914001f6854f6b8b5784":"test_field_for_remove_fields_behaviour_2","1f3a2ea5bc1f60258df20521bee9ac636df69a3a":{"currency":"USD"},"4f4d99a438f334d7dbf83a1816015b361b848b3b":{"currency":"USD"},"9021162291be72f5a8025480f44bf44d5d81d07c":"test_field_for_remove_fields_behaviour_3_will_be_removed","9b0ed11532efea688fdf12b28f142b9eb08a80c5":{"currency":"USD"},"e65ad0762c259b05b4866f7249eabecabadbe577":"test_field_for_remove_fields_behaviour_1_updated","ff2c07edcaa3e987c23fb5cc4fe860bb52becf00":{"currency":"USD"}}') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$."17fc9889474028063990914001f6854f6b8b5784"') + +-- replace partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_REPLACE(f_1, '$.addr.detail.ab', '9707') + +-- remove array value +INSERT INTO test_db_1.json_test VALUES (NULL, '["foo","bar","baz"]') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$[1]') \ No newline at end of file diff --git a/dt-common/src/test/config/check_config.ini b/dt-tests/tests/mysql_to_mysql/cdc/json_test/task_config.ini similarity index 66% rename from dt-common/src/test/config/check_config.ini rename to dt-tests/tests/mysql_to_mysql/cdc/json_test/task_config.ini index b92b7051..dc986260 100644 --- a/dt-common/src/test/config/check_config.ini +++ b/dt-tests/tests/mysql_to_mysql/cdc/json_test/task_config.ini @@ -1,36 +1,36 @@ [extractor] db_type=mysql -extract_type=snapshot +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 url=mysql://root:123456@127.0.0.1:3307?ssl-mode=disabled -[sinker] -db_type=mysql -sink_type=check -url=mysql://root:123456@127.0.0.1:3308 -batch_size=2 - [filter] -do_dbs= ignore_dbs= +do_dbs= do_tbs=test_db_1.* ignore_tbs= -do_events=insert +do_events=insert,update,delete + +[sinker] +db_type=mysql +sink_type=write +batch_size=2 +url=mysql://root:123456@127.0.0.1:3308 [router] -db_map= tb_map= field_map= - -[parallelizer] -parallel_type=rdb_check -parallel_size=2 +db_map= [pipeline] -type=basic +parallel_type=rdb_merge buffer_size=4 checkpoint_interval_secs=1 +parallel_size=2 [runtime] +log_dir=./logs log_level=info -log4rs_file=./log4rs.yaml -log_dir=./logs \ No newline at end of file +log4rs_file=./log4rs.yaml \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_mysql/cdc/uk_changed_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/cdc/uk_changed_test/task_config.ini index ff2210e3..6177ae6b 100644 --- a/dt-tests/tests/mysql_to_mysql/cdc/uk_changed_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/cdc/uk_changed_test/task_config.ini @@ -24,16 +24,13 @@ db_map= tb_map= field_map= -[parallelizer] -parallel_type=rdb_merge -parallel_size=2 - [pipeline] -type=basic buffer_size=1000 checkpoint_interval_secs=1 +parallel_size=2 +parallel_type=rdb_merge [runtime] -log_level=info +log_level=debug log4rs_file=./log4rs.yaml log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/mysql_to_mysql/cdc_tests.rs b/dt-tests/tests/mysql_to_mysql/cdc_tests.rs index 62b77b36..cecb2dad 100644 --- a/dt-tests/tests/mysql_to_mysql/cdc_tests.rs +++ b/dt-tests/tests/mysql_to_mysql/cdc_tests.rs @@ -24,6 +24,18 @@ mod test { TestBase::run_cdc_test("mysql_to_mysql/cdc/charset_test", 3000, 2000).await; } + #[tokio::test] + #[serial] + async fn cdc_json_test() { + TestBase::run_cdc_test("mysql_to_mysql/cdc/json_test", 3000, 2000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_ddl_test() { + TestBase::run_ddl_test("mysql_to_mysql/cdc/ddl_test", 3000, 5000).await; + } + #[tokio::test] #[serial] async fn cdc_timezone_test() { diff --git a/dt-tests/tests/mysql_to_mysql/precheck/db_not_exists_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/precheck/db_not_exists_test/task_config.ini index 1d2c7dc5..a1c21160 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck/db_not_exists_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/precheck/db_not_exists_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_source_user}:{precheck_mysql2mysql_source_password}@{precheck_mysql2mysql_source_url}?ssl-mode=disabled +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url= [sinker] -sink_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_sink_user}:{precheck_mysql2mysql_sink_password}@{precheck_mysql2mysql_sink_url} -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] do_dbs= diff --git a/dt-tests/tests/mysql_to_mysql/precheck/struct_existed_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/precheck/struct_existed_test/task_config.ini index 1d2c7dc5..a1c21160 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck/struct_existed_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/precheck/struct_existed_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_source_user}:{precheck_mysql2mysql_source_password}@{precheck_mysql2mysql_source_url}?ssl-mode=disabled +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url= [sinker] -sink_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_sink_user}:{precheck_mysql2mysql_sink_password}@{precheck_mysql2mysql_sink_url} -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] do_dbs= diff --git a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_basic_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_basic_test/task_config.ini index c55300bc..74a95119 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_basic_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_basic_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_source_user}:{precheck_mysql2mysql_source_password}@{precheck_mysql2mysql_source_url}?ssl-mode=disabled +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url= [sinker] -sink_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_sink_user}:{precheck_mysql2mysql_sink_password}@{precheck_mysql2mysql_sink_url} -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] #do_dbs=source_db diff --git a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_have_fk_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_have_fk_test/task_config.ini index 187bb5ca..ea6680b5 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_have_fk_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_have_fk_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_source_user}:{precheck_mysql2mysql_source_password}@{precheck_mysql2mysql_source_url}?ssl-mode=disabled +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url= [sinker] -sink_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_sink_user}:{precheck_mysql2mysql_sink_password}@{precheck_mysql2mysql_sink_url} -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] #do_dbs=source_db diff --git a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_no_pk_test/task_config.ini b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_no_pk_test/task_config.ini index c55300bc..74a95119 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_no_pk_test/task_config.ini +++ b/dt-tests/tests/mysql_to_mysql/precheck/struct_supported_no_pk_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_source_user}:{precheck_mysql2mysql_source_password}@{precheck_mysql2mysql_source_url}?ssl-mode=disabled +extract_type=cdc +binlog_position=0 +binlog_filename= +server_id=2000 +url= [sinker] -sink_type=basic db_type=mysql -url=mysql://{precheck_mysql2mysql_sink_user}:{precheck_mysql2mysql_sink_password}@{precheck_mysql2mysql_sink_url} -batch_size=1 +sink_type=write +batch_size=2 +url= [filter] #do_dbs=source_db diff --git a/dt-tests/tests/mysql_to_mysql/precheck_tests.rs b/dt-tests/tests/mysql_to_mysql/precheck_tests.rs index 955d89a6..ac635983 100644 --- a/dt-tests/tests/mysql_to_mysql/precheck_tests.rs +++ b/dt-tests/tests/mysql_to_mysql/precheck_tests.rs @@ -32,40 +32,16 @@ mod test { #[serial] async fn struct_existed_test() { let test_dir = "mysql_to_mysql/precheck/struct_existed_test"; - - let mut src_expected_results = HashMap::new(); - src_expected_results.insert(CheckItem::CheckIfStructExisted.to_string(), true); - - let mut dst_expected_results = HashMap::new(); - dst_expected_results.insert(CheckItem::CheckIfStructExisted.to_string(), true); - - TestBase::run_precheck_test( - test_dir, - &HashSet::new(), - &src_expected_results, - &dst_expected_results, - ) - .await + TestBase::run_precheck_test(test_dir, &HashSet::new(), &HashMap::new(), &HashMap::new()) + .await } #[tokio::test] #[serial] async fn struct_supported_basic_test() { let test_dir = "mysql_to_mysql/precheck/struct_supported_basic_test"; - - let mut src_expected_results = HashMap::new(); - src_expected_results.insert(CheckItem::CheckIfTableStructSupported.to_string(), true); - - let mut dst_expected_results = HashMap::new(); - dst_expected_results.insert(CheckItem::CheckIfTableStructSupported.to_string(), true); - - TestBase::run_precheck_test( - test_dir, - &HashSet::new(), - &src_expected_results, - &dst_expected_results, - ) - .await + TestBase::run_precheck_test(test_dir, &HashSet::new(), &HashMap::new(), &HashMap::new()) + .await } #[tokio::test] diff --git a/dt-tests/tests/mysql_to_mysql/snapshot/json_test/dst_ddl.sql b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/dst_ddl.sql new file mode 100644 index 00000000..3dc35718 --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/dst_ddl.sql @@ -0,0 +1,5 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.json_test(f_0 INT AUTO_INCREMENT, f_1 JSON, PRIMARY KEY(f_0)); diff --git a/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_ddl.sql b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_ddl.sql new file mode 100644 index 00000000..765f37c7 --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_ddl.sql @@ -0,0 +1,6 @@ +DROP DATABASE IF EXISTS test_db_1; + +CREATE DATABASE test_db_1; + +CREATE TABLE test_db_1.json_test(f_0 INT AUTO_INCREMENT, f_1 JSON, PRIMARY KEY(f_0)); + diff --git a/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_dml.sql b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_dml.sql new file mode 100644 index 00000000..2c55b56b --- /dev/null +++ b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/src_dml.sql @@ -0,0 +1,127 @@ +-- basic json object +INSERT INTO test_db_1.json_test VALUES (NULL, '{"k.1":1,"k.0":0,"k.-1":-1,"k.true":true,"k.false":false,"k.null":null,"k.string":"string","k.true_false":[true,false],"k.32767":32767,"k.32768":32768,"k.-32768":-32768,"k.-32769":-32769,"k.2147483647":2147483647,"k.2147483648":2147483648,"k.-2147483648":-2147483648,"k.-2147483649":-2147483649,"k.18446744073709551615":18446744073709551615,"k.18446744073709551616":18446744073709551616,"k.3.14":3.14,"k.{}":{},"k.[]":[]}') + +-- unicode support +INSERT INTO test_db_1.json_test VALUES (NULL, '{"key":"éééàààà"}') +INSERT INTO test_db_1.json_test VALUES (NULL, '{"δΈ­ζ–‡":"πŸ˜€"}') + +-- multiple nested json object +INSERT INTO test_db_1.json_test VALUES (NULL, '{"literal1":true,"i16":4,"i32":2147483647,"int64":4294967295,"double":1.0001,"string":"abc","time":"2022-01-01 12:34:56.000000","array":[1,2,{"i16":4,"array":[false,true,"abcd"]}],"small_document":{"i16":4,"array":[false,true,3],"small_document":{"i16":4,"i32":2147483647,"int64":4294967295}}}'),(5, '[{"i16":4,"small_document":{"i16":4,"i32":2147483647,"int64":4294967295}},{"i16":4,"array":[false,true,"abcd"]},"abc",10,null,true,false]'); + +-- null +INSERT INTO test_db_1.json_test VALUES (NULL, null) + +-- json with empty key +INSERT INTO test_db_1.json_test VALUES (NULL, '{"bitrate":{"":0}}') + +-- json array +INSERT INTO test_db_1.json_test VALUES (NULL, '[-1,0,1,true,false,null,"string",[true,false],32767,32768,-32768,-32769,2147483647,2147483648,-2147483648,-2147483649,18446744073709551615,18446744073709551616,3.14,{},[]]') + +-- json array nested +INSERT INTO test_db_1.json_test VALUES (NULL, '[-1,["b",["c"]],1]') + +-- scalar string +INSERT INTO test_db_1.json_test VALUES (NULL, '"scalar string"'),(11, '"LONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONGLONG"') + +-- scalar boolean: true +INSERT INTO test_db_1.json_test VALUES (NULL, 'true') + +-- scalar boolean: false +INSERT INTO test_db_1.json_test VALUES (NULL, 'false') + +-- scalar null +INSERT INTO test_db_1.json_test VALUES (NULL, 'null') + +-- scalar negative integer +INSERT INTO test_db_1.json_test VALUES (NULL, '-1') + +-- scalar positive integer +INSERT INTO test_db_1.json_test VALUES (NULL, '1') + +-- scalar max positive int16 +INSERT INTO test_db_1.json_test VALUES (NULL, '32767') + +-- scalar int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '32768') + +-- scalar min negative int16 +INSERT INTO test_db_1.json_test VALUES (NULL, '-32768') + +-- scalar negative int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '-32769') + +-- scalar max_positive int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '2147483647') + +-- scalar positive int64 +INSERT INTO test_db_1.json_test VALUES (NULL, '2147483648') + +-- scalar min negative int32 +INSERT INTO test_db_1.json_test VALUES (NULL, '-2147483648') + +-- scalar negative int64 +INSERT INTO test_db_1.json_test VALUES (NULL, '-2147483649') + +-- scalar uint64 +INSERT INTO test_db_1.json_test VALUES (NULL, '18446744073709551615') + +-- scalar uint64 overflow +INSERT INTO test_db_1.json_test VALUES (NULL, '18446744073709551616') + +-- scalar float +INSERT INTO test_db_1.json_test VALUES (NULL, '3.14') + +-- scalar datetime +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('2015-01-15 23:24:25' AS DATETIME) AS JSON)) + +-- scalar time +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25' AS TIME) AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25.12' AS TIME(3)) AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST('23:24:25.0237' AS TIME(3)) AS JSON)) + +-- scalar timestamp +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25' AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25.12' AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(TIMESTAMP'2015-01-15 23:24:25.0237' AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, (CAST(UNIX_TIMESTAMP(CONVERT_TZ('2015-01-15 23:24:25','GMT',@@session.time_zone)) AS JSON))) + +-- scalar geometry +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(ST_GeomFromText('POINT(1 1)') AS JSON)) + +-- scalar string with charset conversion +INSERT INTO test_db_1.json_test VALUES (NULL, CAST('[]' AS CHAR CHARACTER SET 'ascii')) + +-- scalar binary as base64 +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(x'cafe' AS JSON)) +INSERT INTO test_db_1.json_test VALUES (NULL, CAST(x'cafebabe' AS JSON)) + +-- scalar decimal +-- TODO, decimal will lose precision when insert into target mysql as string +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST("212765.700000000010000" AS DECIMAL(21,15)) AS JSON)) +-- INSERT INTO test_db_1.json_test VALUES (NULL, CAST(CAST("111111.11111110000001" AS DECIMAL(24,17)) AS JSON)) + +-- empty object +INSERT INTO test_db_1.json_test VALUES (NULL, '{}') + +-- empty array +INSERT INTO test_db_1.json_test VALUES (NULL, '[]') + +-- set partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_SET(f_1, '$.addr.detail.ab', '970785C8') + +-- remove partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$.addr.detail.ab') + +-- remove partial update with holes and sparse keys +INSERT INTO test_db_1.json_test VALUES (NULL, '{"17fc9889474028063990914001f6854f6b8b5784":"test_field_for_remove_fields_behaviour_2","1f3a2ea5bc1f60258df20521bee9ac636df69a3a":{"currency":"USD"},"4f4d99a438f334d7dbf83a1816015b361b848b3b":{"currency":"USD"},"9021162291be72f5a8025480f44bf44d5d81d07c":"test_field_for_remove_fields_behaviour_3_will_be_removed","9b0ed11532efea688fdf12b28f142b9eb08a80c5":{"currency":"USD"},"e65ad0762c259b05b4866f7249eabecabadbe577":"test_field_for_remove_fields_behaviour_1_updated","ff2c07edcaa3e987c23fb5cc4fe860bb52becf00":{"currency":"USD"}}') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$."17fc9889474028063990914001f6854f6b8b5784"') + +-- replace partial update with holes +INSERT INTO test_db_1.json_test VALUES (NULL, '{"age":22,"addr":{"code":100,"detail":{"ab":"970785C8-C299"}},"name":"Alice"}') +UPDATE test_db_1.json_test SET f_1 = JSON_REPLACE(f_1, '$.addr.detail.ab', '9707') + +-- remove array value +INSERT INTO test_db_1.json_test VALUES (NULL, '["foo","bar","baz"]') +UPDATE test_db_1.json_test SET f_1 = JSON_REMOVE(f_1, '$[1]') \ No newline at end of file diff --git a/dt-common/src/test/config/snapshot_config.ini b/dt-tests/tests/mysql_to_mysql/snapshot/json_test/task_config.ini similarity index 100% rename from dt-common/src/test/config/snapshot_config.ini rename to dt-tests/tests/mysql_to_mysql/snapshot/json_test/task_config.ini diff --git a/dt-tests/tests/mysql_to_mysql/snapshot_tests.rs b/dt-tests/tests/mysql_to_mysql/snapshot_tests.rs index 477d44ca..7ce0b124 100644 --- a/dt-tests/tests/mysql_to_mysql/snapshot_tests.rs +++ b/dt-tests/tests/mysql_to_mysql/snapshot_tests.rs @@ -3,6 +3,7 @@ mod test { use std::collections::HashMap; + use dt_common::config::config_enums::DbType; use serial_test::serial; use crate::test_runner::test_base::TestBase; @@ -54,6 +55,7 @@ mod test { TestBase::run_snapshot_test_and_check_dst_count( "mysql_to_mysql/snapshot/special_character_in_name_test", + &DbType::Mysql, dst_expected_counts, ) .await; @@ -74,11 +76,18 @@ mod test { TestBase::run_snapshot_test_and_check_dst_count( "mysql_to_mysql/snapshot/resume_test", + &DbType::Mysql, dst_expected_counts, ) .await; } + #[tokio::test] + #[serial] + async fn snapshot_json_test() { + TestBase::run_snapshot_test("mysql_to_mysql/snapshot/json_test").await; + } + #[tokio::test] #[serial] async fn snapshot_timezone_test() { diff --git a/dt-tests/tests/pg_to_pg/precheck/db_not_exists_test/task_config.ini b/dt-tests/tests/pg_to_pg/precheck/db_not_exists_test/task_config.ini index 62197eac..25d25bfa 100644 --- a/dt-tests/tests/pg_to_pg/precheck/db_not_exists_test/task_config.ini +++ b/dt-tests/tests/pg_to_pg/precheck/db_not_exists_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=pg -url=postgres://{precheck_pg2pg_source_user}:{precheck_pg2pg_source_password}@{precheck_pg2pg_source_url}/postgres?options[statement_timeout]=10s +extract_type=cdc +url= +heartbeat_interval_secs=10 +start_lsn= +slot_name=ape_test [sinker] -sink_type=basic db_type=pg -url=postgres://{precheck_pg2pg_sink_user}:{precheck_pg2pg_sink_password}@{precheck_pg2pg_sink_url}/postgres -batch_size=1 +sink_type=write +url= +batch_size=2 [filter] #do_dbs=source_db diff --git a/dt-tests/tests/pg_to_pg/precheck/struct_existed_test/task_config.ini b/dt-tests/tests/pg_to_pg/precheck/struct_existed_test/task_config.ini index 62197eac..25d25bfa 100644 --- a/dt-tests/tests/pg_to_pg/precheck/struct_existed_test/task_config.ini +++ b/dt-tests/tests/pg_to_pg/precheck/struct_existed_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=pg -url=postgres://{precheck_pg2pg_source_user}:{precheck_pg2pg_source_password}@{precheck_pg2pg_source_url}/postgres?options[statement_timeout]=10s +extract_type=cdc +url= +heartbeat_interval_secs=10 +start_lsn= +slot_name=ape_test [sinker] -sink_type=basic db_type=pg -url=postgres://{precheck_pg2pg_sink_user}:{precheck_pg2pg_sink_password}@{precheck_pg2pg_sink_url}/postgres -batch_size=1 +sink_type=write +url= +batch_size=2 [filter] #do_dbs=source_db diff --git a/dt-tests/tests/pg_to_pg/precheck/struct_supported_basic_test/task_config.ini b/dt-tests/tests/pg_to_pg/precheck/struct_supported_basic_test/task_config.ini index 291c56a7..af72bd8a 100644 --- a/dt-tests/tests/pg_to_pg/precheck/struct_supported_basic_test/task_config.ini +++ b/dt-tests/tests/pg_to_pg/precheck/struct_supported_basic_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=pg -url=postgres://{precheck_pg2pg_source_user}:{precheck_pg2pg_source_password}@{precheck_pg2pg_source_url}/postgres?options[statement_timeout]=10s +extract_type=cdc +url= +heartbeat_interval_secs=10 +start_lsn= +slot_name=ape_test [sinker] -sink_type=basic db_type=pg -url=postgres://{precheck_pg2pg_sink_user}:{precheck_pg2pg_sink_password}@{precheck_pg2pg_sink_url}/postgres -batch_size=1 +sink_type=write +url= +batch_size=2 [filter] do_dbs=precheck_it_pg2pg_2 diff --git a/dt-tests/tests/pg_to_pg/precheck/struct_supported_have_fk_test/task_config.ini b/dt-tests/tests/pg_to_pg/precheck/struct_supported_have_fk_test/task_config.ini index c05b4951..568e226f 100644 --- a/dt-tests/tests/pg_to_pg/precheck/struct_supported_have_fk_test/task_config.ini +++ b/dt-tests/tests/pg_to_pg/precheck/struct_supported_have_fk_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=pg -url=postgres://{precheck_pg2pg_source_user}:{precheck_pg2pg_source_password}@{precheck_pg2pg_source_url}/postgres?options[statement_timeout]=10s +extract_type=cdc +url= +heartbeat_interval_secs=10 +start_lsn= +slot_name=ape_test [sinker] -sink_type=basic db_type=pg -url=postgres://{precheck_pg2pg_sink_user}:{precheck_pg2pg_sink_password}@{precheck_pg2pg_sink_url}/postgres -batch_size=1 +sink_type=write +url= +batch_size=2 [filter] do_dbs=precheck_it_pg2pg_2_1 diff --git a/dt-tests/tests/pg_to_pg/precheck/struct_supported_no_pk_test/task_config.ini b/dt-tests/tests/pg_to_pg/precheck/struct_supported_no_pk_test/task_config.ini index bd4bb84e..c2108e53 100644 --- a/dt-tests/tests/pg_to_pg/precheck/struct_supported_no_pk_test/task_config.ini +++ b/dt-tests/tests/pg_to_pg/precheck/struct_supported_no_pk_test/task_config.ini @@ -1,13 +1,16 @@ [extractor] -extract_type=basic db_type=pg -url=postgres://{precheck_pg2pg_source_user}:{precheck_pg2pg_source_password}@{precheck_pg2pg_source_url}/postgres?options[statement_timeout]=10s +extract_type=cdc +url= +heartbeat_interval_secs=10 +start_lsn= +slot_name=ape_test [sinker] -sink_type=basic db_type=pg -url=postgres://{precheck_pg2pg_sink_user}:{precheck_pg2pg_sink_password}@{precheck_pg2pg_sink_url}/postgres -batch_size=1 +sink_type=write +url= +batch_size=2 [filter] do_dbs=precheck_it_pg2pg_2_2 diff --git a/dt-tests/tests/pg_to_pg/precheck_tests.rs b/dt-tests/tests/pg_to_pg/precheck_tests.rs index 32211811..d3910a68 100644 --- a/dt-tests/tests/pg_to_pg/precheck_tests.rs +++ b/dt-tests/tests/pg_to_pg/precheck_tests.rs @@ -25,28 +25,14 @@ mod test { #[serial] async fn struct_existed_test() { let test_dir = "pg_to_pg/precheck/struct_existed_test"; - - let mut src_expected_results = HashMap::new(); - src_expected_results.insert(CheckItem::CheckIfStructExisted.to_string(), true); - - let mut dst_expected_results = HashMap::new(); - dst_expected_results.insert(CheckItem::CheckIfStructExisted.to_string(), true); - - run_precheck_test(test_dir, &src_expected_results, &dst_expected_results).await + run_precheck_test(test_dir, &HashMap::new(), &HashMap::new()).await } #[tokio::test] #[serial] async fn struct_supported_basic_test() { let test_dir = "pg_to_pg/precheck/struct_supported_basic_test"; - - let mut src_expected_results = HashMap::new(); - src_expected_results.insert(CheckItem::CheckIfTableStructSupported.to_string(), true); - - let mut dst_expected_results = HashMap::new(); - dst_expected_results.insert(CheckItem::CheckIfTableStructSupported.to_string(), true); - - run_precheck_test(test_dir, &src_expected_results, &dst_expected_results).await + run_precheck_test(test_dir, &HashMap::new(), &HashMap::new()).await } #[tokio::test] diff --git a/dt-tests/tests/pg_to_pg/snapshot_tests.rs b/dt-tests/tests/pg_to_pg/snapshot_tests.rs index 03a42579..f3d9ecc7 100644 --- a/dt-tests/tests/pg_to_pg/snapshot_tests.rs +++ b/dt-tests/tests/pg_to_pg/snapshot_tests.rs @@ -3,6 +3,7 @@ mod test { use std::collections::HashMap; + use dt_common::config::config_enums::DbType; use serial_test::serial; use crate::test_runner::test_base::TestBase; @@ -63,6 +64,7 @@ mod test { TestBase::run_snapshot_test_and_check_dst_count( "pg_to_pg/snapshot/resume_test", + &DbType::Pg, dst_expected_counts, ) .await; @@ -91,6 +93,7 @@ mod test { TestBase::run_snapshot_test_and_check_dst_count( "pg_to_pg/snapshot/special_character_in_name_test", + &DbType::Pg, dst_expected_counts, ) .await; diff --git a/dt-tests/tests/rdb_to_kafka/snapshot_tests.rs b/dt-tests/tests/rdb_to_kafka/snapshot_tests.rs deleted file mode 100644 index 1a46ac5c..00000000 --- a/dt-tests/tests/rdb_to_kafka/snapshot_tests.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[allow(dead_code)] -#[cfg(test)] -mod test { - fn snapshot_test() { - // Todo: add snapshot test below. this code is just to pass CI check - assert_eq!(1, 1) - } -} diff --git a/dt-tests/tests/redis_to_redis/cdc/2_8/.env b/dt-tests/tests/redis_to_redis/cdc/2_8/.env new file mode 100644 index 00000000..bd20b47e --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/2_8/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a9e3574b134014e3b805a948d2a706d8-495199463.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a8723a1a8ec4146cbad902184b7984cb-1161380606.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_dml.sql new file mode 100644 index 00000000..2c06242f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/src_dml.sql @@ -0,0 +1,531 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD -- version: 3.2.0 +-- -- SET +-- BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- -- INCRBY +-- BITFIELD 2-2 incrby i5 100 1 +-- BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- -- OVERFLOW +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX -- version: 5.0.0 +-- ZADD 10-1 0 a 1 b 2 c +-- BZPOPMAX 10-1 23 0 + +-- BZPOPMIN -- version: 5.0.0 +-- ZADD 11-1 0 a 1 b 2 c +-- BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +EXPIREAT 16-2 4102416000 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD -- version: 3.2.0 +-- GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +-- Starting with Redis version 4.0.0: Accepts multiple field and value arguments. +-- HSET 21-1 field1 "hello" field2 "world" +HSET 21-1 field1 "hello" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +-- Starting with Redis version 4.0.0: Accepts multiple field and value arguments. +-- HSET 24-1 field2 "Hi" field3 "World" +HSET 24-1 field2 "Hi" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +-- Starting with Redis version 3.2.0: Added the count argument. +-- SPOP 61-1 3 +SPOP 61-1 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB -- version: 4.0.0 +-- SWAPDB 0 1 + +-- UNLINK --version: 4.0.0 +-- SET 64-1 "Hello" +-- SET 64-2 "World" +-- UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD -- version: 5.0.0 +-- XADD 65-1 1526919030474-55 message "Hello," +-- -- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- -- XADD 65-1 1526919030474-* message " World!" +-- XADD 65-1 * name Sara surname OConnor +-- XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- -- XLEN 65-1 +-- -- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL -- version: 5.0.0 +-- XADD 66-1 1538561700640-0 a 1 +-- XADD 66-1 * b 2 +-- XADD 66-1 * c 3 +-- XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM -- version: 5.0.0 +-- XTRIM 67-1 MAXLEN 1000 +-- XADD 67-1 * field1 A field2 B field3 C field4 D +-- XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX -- version: 5.0.0 +-- ZADD 73-1 1 "one" +-- ZADD 73-1 2 "two" +-- ZADD 73-1 3 "three" +-- ZPOPMAX 73-1 + +-- ZPOPMIN -- version: 5.0.0 +-- ZADD 74-1 1 "one" +-- ZADD 74-1 2 "two" +-- ZADD 74-1 3 "three" +-- ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/2_8/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/4_0/.env b/dt-tests/tests/redis_to_redis/cdc/4_0/.env new file mode 100644 index 00000000..090f77f6 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/4_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a4f1c63fdbafc4bb384eee07f1b79c24-849495829.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a0413811336244ae3ab068cfb018f1d7-863213277.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..fd8018a3 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/src_dml.sql @@ -0,0 +1,523 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX -- version: 5.0.0 +-- ZADD 10-1 0 a 1 b 2 c +-- BZPOPMAX 10-1 23 0 + +-- BZPOPMIN -- version: 5.0.0 +-- ZADD 11-1 0 a 1 b 2 c +-- BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD -- version: 5.0.0 +-- XADD 65-1 1526919030474-55 message "Hello," +-- -- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- -- XADD 65-1 1526919030474-* message " World!" +-- XADD 65-1 * name Sara surname OConnor +-- XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- -- XLEN 65-1 +-- -- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL -- version: 5.0.0 +-- XADD 66-1 1538561700640-0 a 1 +-- XADD 66-1 * b 2 +-- XADD 66-1 * c 3 +-- XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM -- version: 5.0.0 +-- XTRIM 67-1 MAXLEN 1000 +-- XADD 67-1 * field1 A field2 B field3 C field4 D +-- XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX -- version: 5.0.0 +-- ZADD 73-1 1 "one" +-- ZADD 73-1 2 "two" +-- ZADD 73-1 3 "three" +-- ZPOPMAX 73-1 + +-- ZPOPMIN -- version: 5.0.0 +-- ZADD 74-1 1 "one" +-- ZADD 74-1 2 "two" +-- ZADD 74-1 3 "three" +-- ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/4_0/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/5_0/.env b/dt-tests/tests/redis_to_redis/cdc/5_0/.env new file mode 100644 index 00000000..34c7b930 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/5_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ab5200748cfca4828bb48ec74ee77423-345824646.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a2f28b604af384750809f2216a681264-1920935663.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..0b09e33a --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/src_dml.sql @@ -0,0 +1,523 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/5_0/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_0/.env b/dt-tests/tests/redis_to_redis/cdc/6_0/.env new file mode 100644 index 00000000..0f7c2056 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://abdf6c45849c54a2a857d3ecfb2c466b-981085820.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a542c20654bd8457e97213a0db0b32a0-1382178154.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..efb3f3af --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/src_dml.sql @@ -0,0 +1,524 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_0/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_2/.env b/dt-tests/tests/redis_to_redis/cdc/6_2/.env new file mode 100644 index 00000000..3852a764 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_2/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ac2cd119b7d51454a857a9ea1368c792-168326526.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a5068abaa38d549d291b81e5f8ec4d34-685795292.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_dml.sql new file mode 100644 index 00000000..bc34ba14 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/src_dml.sql @@ -0,0 +1,528 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +RPUSH 4-1 a b c +RPUSH 4-2 x y z +BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +SET 12-1 "sheep" +COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +EXPIREAT 16-2 4102416000 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +SET 18-1 "Hello" +GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +SET 19-1 "Hello" +GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +SET 20-1 "Hello" +GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +RPUSH 30-1 "one" +RPUSH 30-1 "two" +RPUSH 30-1 "three" +LMOVE 30-1 30-2 RIGHT LEFT +LMOVE 30-1 30-2 LEFT RIGHT +LRANGE 30-1 0 -1 +LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +LPOP 32-1 2 +LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 + +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" + +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +RPOP 49-1 2 +LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +ZADD 69-1 1 "one" +ZADD 69-1 2 "two" +ZADD 69-1 3 "three" +ZADD 69-2 1 "one" +ZADD 69-2 2 "two" +ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/6_2/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/.env b/dt-tests/tests/redis_to_redis/cdc/7_0/.env new file mode 100644 index 00000000..81bfddb2 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://aa2b463deaac14c8585714786ffe0d8c-543369047.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://af3c50366d01d4c03943a47cf146b0b2-15030835.cn-northwest-1.elb.amazonaws.com.cn:6379 diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_dml.sql new file mode 100644 index 00000000..ea523736 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/src_dml.sql @@ -0,0 +1,84 @@ +-------------------- add entries -------------------- + +-------------------- string entries +-- SET +SET set_key_1 val_1 +SET set_key_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ +SET "set_key_3_ πŸ˜€" "val_2_ πŸ˜€" + +-- MSET +MSET mset_key_1 val_1 mset_key_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "mset_key_3_ πŸ˜€" "val_3_ πŸ˜€" + +-------------------- hash entries +-- HSET +HSET hset_key_1 field_1 val_1 +HSET hset_key_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ +HSET hset_key_1 "field_3_ πŸ˜€" "val_3_ πŸ˜€" + +-- HMSET +HMSET hmset_key_1 field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" + +-------------------- list entries +-- LPUSH +LPUSH list_key_1 val_1 +LPUSH list_key_1 val_2_δΈ­ζ–‡ +LPUSH list_key_1 "val_3_ πŸ˜€" + +-- RPUSH +RPUSH list_key_1 val_5 val_6 + +-- LINSERT +LINSERT list_key_1 BEFORE val_1 val_7 + +-------------------- sets entries +-- SADD +SADD sets_key_1 val_1 val_2_δΈ­ζ–‡ "val_3_ πŸ˜€" val_5 + +-- SREM +SREM sets_key_1 val_5 + +-------------------- zset entries +-- ZADD +ZADD zset_key_1 1 val_1 2 val_2_δΈ­ζ–‡ 3 "val_3_ πŸ˜€" +ZINCRBY zset_key_1 5 val_1 + +-------------------- stream entries +-- XADD +XADD stream_key_1 * field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" +XADD "stream_key_2 δΈ­ζ–‡πŸ˜€" * field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" + + +-------------------- remove entries -------------------- + +-------------------- string entries +-- DEL +DEL "set_key_3_ πŸ˜€" + +DEL mset_key_2_δΈ­ζ–‡ "mset_key_3_ πŸ˜€" + +-------------------- hash entries +-- HDEL +HDEL hset_key_1 "field_3_ πŸ˜€" + +-- HMDEL +HDEL hmset_key_1 field_2_δΈ­ζ–‡ "field_3_ πŸ˜€" + +-------------------- list entries +-- LPOP +LPOP list_key_1 + +-- LTRIM +LTRIM list_key_1 0 2 + +-- RPOP +RPOP list_key_1 + +-------------------- sets entries +SREM sets_key_1 val_2_δΈ­ζ–‡ "val_3_ πŸ˜€" + +-------------------- zset entries +ZREM zset_key_1 val_1 + +-------------------- stream entries +XTRIM stream_key_1 MAXLEN 0 +DEL "stream_key_2 δΈ­ζ–‡πŸ˜€" \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/task_config.ini new file mode 100644 index 00000000..afa0c42f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/basic_test/task_config.ini @@ -0,0 +1,39 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6379 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +method=restore +url=redis://:123456@127.0.0.1:6380 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..10c4a2a4 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/src_dml.sql @@ -0,0 +1,518 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +RPUSH 4-1 a b c +RPUSH 4-2 x y z +BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +LPUSH 5-1 a b c d +LPUSH 5-2 1 2 3 4 +BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP +ZADD 9-1 1 a 2 b 3 c +ZADD 9-2 1 d 2 e 3 f +BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY +SET 12-1 "sheep" +COPY 12-1 12-2 +GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +EXPIRE 15-1 1 XX +EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL +SET 18-1 "Hello" +GETDEL 18-1 + +-- GETEX +SET 19-1 "Hello" +GETEX 19-1 EX 1 + +-- GETSET +SET 20-1 "Hello" +GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE +RPUSH 30-1 "one" +RPUSH 30-1 "two" +RPUSH 30-1 "three" +LMOVE 30-1 30-2 RIGHT LEFT +LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP +LPUSH 31-1 "one" "two" "three" "four" "five" +LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +SET 42-2 "Hello" +PEXPIRE 42-2 1000 XX +SET 42-3 "Hello" +PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE +ZADD 69-1 1 "one" +ZADD 69-1 2 "two" +ZADD 69-1 3 "three" +ZADD 69-2 1 "one" +ZADD 69-2 2 "two" +ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP +ZADD 72-1 1 "one" 2 "two" 3 "three" +ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE +ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_dml.sql new file mode 100644 index 00000000..7ee23f34 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/src_dml.sql @@ -0,0 +1,18 @@ +SET set_key_0 val_0 + +SELECT 1 + +SET set_key_1 val_1 + +SELECT 2 + +SET set_key_2_0 val_2_0 +SET set_key_2_1 val_2_1 + +SELECT 3 + +SET set_key_3 val_3 + +SELECT 2 + +DEL set_key_2_0 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_dbs_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_dml.sql new file mode 100644 index 00000000..d9f180cb --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/src_dml.sql @@ -0,0 +1,20 @@ +SET set_key_0 val_0 + +SELECT 1 + +SET set_key_1 val_1 + +MULTI +SELECT 2 + +SET set_key_2_0 val_2_0 +SET set_key_2_1 val_2_1 + +SELECT 3 + +SET set_key_3 val_3 + +SELECT 2 + +DEL set_key_2_0 +EXEC \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/7_0/multi_exec_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rebloom/.env b/dt-tests/tests/redis_to_redis/cdc/rebloom/.env new file mode 100644 index 00000000..d1f445cd --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rebloom/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a8835528242004c93a70d11ccbccfbe1-2001293504.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a3aae6c850439452bbcb3982b3a58eeb-137133407.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_dml.sql new file mode 100644 index 00000000..43cb429a --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/src_dml.sql @@ -0,0 +1,55 @@ + +-- BF.ADD +BF.ADD 1-1 item1 +-- BF.EXISTS 1-1 item1 +-- BF.DEBUG 1-1 + +-- BF.INSERT +-- Add three items to a filter, then create the filter with default parameters if it does not already exist. +BF.INSERT 2-1 ITEMS item1 item2 item3 +-- Add one item to a filter, then create the filter with a capacity of 10000 if it does not already exist. +BF.INSERT 2-2 CAPACITY 10000 ITEMS item1 +-- Add two items to a filter, then return error if the filter does not already exist. +BF.ADD 2-3 item1 +BF.INSERT 2-3 NOCREATE ITEMS item2 item3 + +-- BF.SCANDUMP + +-- BF.LOADCHUNK + +-- BF.MADD +BF.MADD 3-1 item1 item2 item3 + +-- BF.RESERVE +BF.RESERVE 4-1 0.01 1000 +BF.RESERVE 4-2 0.01 1000 EXPANSION 2 +BF.RESERVE 4-3 0.01 1000 NONSCALING + +-- CF.ADD +CF.ADD 5-1 item1 +-- CF.DEBUG 5-1 + +-- CF.ADDNX +CF.ADDNX 6-1 item1 + +-- CF.INSERT +CF.INSERT 7-1 ITEMS item1 item2 item2 +CF.INSERT 7-2 CAPACITY 1000 ITEMS item1 item2 +CF.ADD 7-3 item3 +CF.INSERT 7-3 CAPACITY 1000 NOCREATE ITEMS item1 item2 +CF.RESERVE 7-4 2 BUCKETSIZE 1 EXPANSION 0 +CF.INSERT 7-4 ITEMS 1 1 1 1 + +-- CF.INSERTNX +CF.INSERTNX 8-1 CAPACITY 1000 ITEMS item1 item2 +CF.INSERTNX 8-2 CAPACITY 1000 ITEMS item1 item2 item3 +CF.ADD 8-3 item3 +CF.INSERTNX 8-3 CAPACITY 1000 NOCREATE ITEMS item1 item2 + +-- CF.RESERVE +CF.RESERVE 9-1 1000 +CF.RESERVE 9-2 1000 BUCKETSIZE 8 MAXITERATIONS 20 EXPANSION 2 + +-- CF.SCANDUMP + +-- CF.LOADCHUNK \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rebloom/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/redisearch/.env b/dt-tests/tests/redis_to_redis/cdc/redisearch/.env new file mode 100644 index 00000000..9ffa6c3b --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/redisearch/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ad821353ffc3743ab863116d537009d6-1349279413.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://adc59f71fd1824ad28b2aa6af5e30d40-808149200.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_dml.sql new file mode 100644 index 00000000..2e28eb54 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/src_dml.sql @@ -0,0 +1,56 @@ +-- FT.ALIASADD +FT.CREATE 1-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE +FT.ALIASADD alias-1-1 1-1 +-- FT._LIST +-- FT.INFO 1-1 + +-- FT.ALIASDEL +FT.CREATE 2-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALIASADD alias-2-1 2-1 +FT.ALIASDEL alias-2-1 + +-- FT.ALIASUPDATE +FT.CREATE 3-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALIASADD alias-3-1 3-1 +FT.ALIASUPDATE alias-3-2 3-1 + +-- FT.ALTER +FT.CREATE 4-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALTER 4-1 SCHEMA ADD id2 NUMERIC SORTABLE + +-- FT.CREATE +-- Create an index that stores the title, publication date, and categories of blog post hashes whose keys start with blog:post: (for example, blog:post:1). +FT.CREATE 5-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE +-- Index the sku attribute from a hash as both a TAG and as TEXT: +FT.CREATE 5-2 ON HASH PREFIX 1 blog:post: SCHEMA sku AS sku_text TEXT sku AS sku_tag TAG SORTABLE +-- Index two different hashes, one containing author data and one containing books, in the same index: +FT.CREATE 5-3 ON HASH PREFIX 2 author:details: book:details: SCHEMA author_id TAG SORTABLE author_ids TAG title TEXT name TEXT +-- Index authors whose names start with G. +FT.CREATE 5-4 ON HASH PREFIX 1 author:details FILTER 'startswith(@name, "G")' SCHEMA name TEXT +-- Index only books that have a subtitle. +FT.CREATE 5-5 ON HASH PREFIX 1 book:details FILTER '@subtitle != ""' SCHEMA title TEXT +-- Index books that have a "categories" attribute where each category is separated by a ; character. +FT.CREATE 5-6 ON HASH PREFIX 1 book:details FILTER '@subtitle != ""' SCHEMA title TEXT categories TAG SEPARATOR ; +-- Index a JSON document using a JSON Path expression. +FT.CREATE 5-7 ON JSON SCHEMA $.title AS title TEXT $.categories AS categories TAG + +-- FT.DROPINDEX +FT.CREATE 6-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.DROPINDEX 6-1 DD + +-- FT.SYNUPDATE +FT.CREATE 7-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.SYNUPDATE 7-1 synonym hello hi shalom + +-- -- FT.DICTADD +-- FT.DICTADD 7-1 foo bar "hello world" + +-- FT.PROFILE idx SEARCH QUERY "hello world" + +-- FT.SUGADD sug "hello world" 1 +-- FT.SUGGET + +-- FT.SUGDEL +-- FT.SUGDEL sug "hello" + + diff --git a/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/redisearch/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rejson/.env b/dt-tests/tests/redis_to_redis/cdc/rejson/.env new file mode 100644 index 00000000..b3336059 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rejson/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a2682297c8b264a4f8b1fb572aae8a72-1436023536.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a0b40abe73f5b45d78840e5f375974a9-87513695.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_dml.sql new file mode 100644 index 00000000..fb62c566 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/src_dml.sql @@ -0,0 +1,74 @@ +-- JSON.SET +JSON.SET 1-1 $ '{"a":2}' +JSON.SET 1-1 $.b '8' +-- JSON.GET 1-1 $ + +JSON.SET 2-1 $ '{"f1": {"a":1}, "f2":{"a":2}}' +JSON.SET 2-1 $..a 3 + +-- JSON.ARRAPPEND +JSON.SET 3-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRAPPEND 3-1 $.colors '"blue"' + +-- JSON.ARRINDEX +JSON.SET 4-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRINDEX 4-1 $..colors '"silver"' + +-- JSON.ARRINSERT +JSON.SET 5-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRINSERT 5-1 $.colors 2 '"yellow"' '"gold"' + +-- JSON.ARRPOP +JSON.SET 6-1 $ '[{"name":"Healthy headphones","description":"Wireless Bluetooth headphones with noise-cancelling technology","connection":{"wireless":true,"type":"Bluetooth"},"price":99.98,"stock":25,"colors":["black","silver"],"max_level":[60,70,80]},{"name":"Noisy headphones","description":"Wireless Bluetooth headphones with noise-cancelling technology","connection":{"wireless":true,"type":"Bluetooth"},"price":99.98,"stock":25,"colors":["black","silver"],"max_level":[80,90,100,120]}]' +JSON.ARRPOP 6-1 $.[1].max_level 0 + +-- -- JSON.ARRTRIM +JSON.SET 7-1 $ "[[{\"name\":\"Healthy-headphones\",\"description\":\"Wireless-Bluetooth-headphones-with-noise-cancelling-technology\",\"connection\":{\"wireless\":true,\"type\":\"Bluetooth\"},\"price\":99.98,\"stock\":25,\"colors\":[\"black\",\"silver\"],\"max_level\":[60,70,80]},{\"name\":\"Noisy-headphones\",\"description\":\"Wireless-Bluetooth-headphones-with-noise-cancelling-technology\",\"connection\":{\"wireless\":true,\"type\":\"Bluetooth\"},\"price\":99.98,\"stock\":25,\"colors\":[\"black\",\"silver\"],\"max_level\":[85,90,100,120]}]]" +JSON.ARRAPPEND 7-1 $.[1].max_level 140 160 180 200 220 240 260 280 +JSON.ARRTRIM 7-1 $.[1].max_level 4 8 + +-- JSON.CLEAR +JSON.SET 8-1 $ '{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14}' +JSON.CLEAR 8-1 $.* + +-- JSON.DEL +JSON.SET 9-1 $ '{"a": 1, "nested": {"a": 2, "b": 3}}' +JSON.DEL 9-1 $..a + +-- JSON.FORGET +JSON.SET 10-1 $ '{"a": 1, "nested": {"a": 2, "b": 3}}' +JSON.FORGET 10-1 $..a + +-- JSON.MERGE +-- Create a unexistent path-value +JSON.SET 11-1 $ '{"a":2}' +JSON.MERGE 11-1 $.b '8' +-- Delete on existing value +JSON.SET 11-2 $ '{"a":2}' +JSON.MERGE 11-2 $.a 'null' +-- Replace an Array +JSON.SET 11-3 $ '{"a":[2,4,6,8]}' +JSON.MERGE 11-3 $.a '[10,12]' + +-- JSON.MSET +JSON.MSET 12-2 $ '{"a":2}' +JSON.MSET 12-3 $ '{"a":2}' +JSON.MSET 12-1 $ '{"a":2}' 12-2 $.f.a '3' 12-3 $ '{"f1": {"a":1}, "f2":{"a":2}}' + +-- JSON.NUMINCRBY +JSON.SET 13-1 . '{"a":"b","b":[{"a":2}, {"a":5}, {"a":"c"}]}' +JSON.NUMINCRBY 13-1 $.a 2 +JSON.NUMINCRBY 13-1 $..a 2 + +-- JSON.NUMMULTBY +JSON.SET 14-1 . '{"a":"b","b":[{"a":2}, {"a":5}, {"a":"c"}]}' +JSON.NUMMULTBY 14-1 $.a 2 +JSON.NUMMULTBY 14-1 $..a 2 + +-- JSON.STRAPPEND +JSON.SET 15-1 $ '{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}' +JSON.STRAPPEND 15-1 $..a '"baz"' + +-- JSON.TOGGLE +JSON.SET 16-1 $ '{"bool": true}' +JSON.TOGGLE 16-1 $.bool diff --git a/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/task_config.ini new file mode 100644 index 00000000..8dc8c4a1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc/rejson/cmds_test/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/cdc_2_8_tests.rs b/dt-tests/tests/redis_to_redis/cdc_2_8_tests.rs new file mode 100644 index 00000000..0cc6f032 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_2_8_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/2_8/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_4_0_tests.rs b/dt-tests/tests/redis_to_redis/cdc_4_0_tests.rs new file mode 100644 index 00000000..03cad9a5 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_4_0_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/4_0/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_5_0_tests.rs b/dt-tests/tests/redis_to_redis/cdc_5_0_tests.rs new file mode 100644 index 00000000..16d52fe2 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_5_0_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/5_0/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_6_0_tests.rs b/dt-tests/tests/redis_to_redis/cdc_6_0_tests.rs new file mode 100644 index 00000000..57fee38f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_6_0_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/6_0/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_6_2_tests.rs b/dt-tests/tests/redis_to_redis/cdc_6_2_tests.rs new file mode 100644 index 00000000..37906615 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_6_2_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/6_2/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_7_0_tests.rs b/dt-tests/tests/redis_to_redis/cdc_7_0_tests.rs new file mode 100644 index 00000000..e2816d82 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_7_0_tests.rs @@ -0,0 +1,29 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_basic_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/7_0/basic_test", 2000, 10000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_multi_dbs_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/7_0/multi_dbs_test", 2000, 10000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_multi_exec_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/7_0/multi_exec_test", 2000, 10000).await; + } + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/7_0/cmds_test", 2000, 15000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_rebloom_tests.rs b/dt-tests/tests/redis_to_redis/cdc_rebloom_tests.rs new file mode 100644 index 00000000..3eacbb3d --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_rebloom_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/rebloom/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_redisearch_tests.rs b/dt-tests/tests/redis_to_redis/cdc_redisearch_tests.rs new file mode 100644 index 00000000..37ff8cad --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_redisearch_tests.rs @@ -0,0 +1,12 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + // TODO, fix psync for redisearch + // #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_cdc_test("redis_to_redis/cdc/redisearch/cmds_test", 2000, 10000).await; + } +} diff --git a/dt-tests/tests/redis_to_redis/cdc_rejson_tests.rs b/dt-tests/tests/redis_to_redis/cdc_rejson_tests.rs new file mode 100644 index 00000000..c9ac0010 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/cdc_rejson_tests.rs @@ -0,0 +1,12 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn cdc_cmds_test() { + TestBase::run_redis_rejson_cdc_test("redis_to_redis/cdc/rejson/cmds_test", 2000, 10000) + .await; + } +} diff --git a/dt-tests/tests/redis_to_redis/mod.rs b/dt-tests/tests/redis_to_redis/mod.rs new file mode 100644 index 00000000..9232b3f5 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/mod.rs @@ -0,0 +1,19 @@ +pub mod cdc_2_8_tests; +pub mod cdc_4_0_tests; +pub mod cdc_5_0_tests; +pub mod cdc_6_0_tests; +pub mod cdc_6_2_tests; +pub mod cdc_7_0_tests; +pub mod cdc_rebloom_tests; +pub mod cdc_redisearch_tests; +pub mod cdc_rejson_tests; +pub mod precheck_tests; +pub mod snapshot_2_8_tests; +pub mod snapshot_4_0_tests; +pub mod snapshot_5_0_tests; +pub mod snapshot_6_0_tests; +pub mod snapshot_6_2_tests; +pub mod snapshot_7_0_tests; +pub mod snapshot_rebloom_tests; +pub mod snapshot_redisearch_tests; +pub mod snapshot_rejson_tests; diff --git a/dt-tests/tests/redis_to_redis/precheck/basic_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/precheck/basic_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/precheck/basic_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/precheck/basic_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/precheck/basic_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/precheck/basic_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/precheck/basic_test/task_config.ini b/dt-tests/tests/redis_to_redis/precheck/basic_test/task_config.ini new file mode 100644 index 00000000..8e68d4e1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/precheck/basic_test/task_config.ini @@ -0,0 +1,42 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +now_db_id=0 +repl_port=10008 +repl_offset=0 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs + +[precheck] +do_struct_init=true +do_cdc=true \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/precheck_tests.rs b/dt-tests/tests/redis_to_redis/precheck_tests.rs new file mode 100644 index 00000000..b2de2082 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/precheck_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod test { + use std::collections::{HashMap, HashSet}; + + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn precheck_basic_test() { + TestBase::run_precheck_test( + "redis_to_redis/precheck/basic_test", + &HashSet::new(), + &HashMap::new(), + &HashMap::new(), + ) + .await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/.env b/dt-tests/tests/redis_to_redis/snapshot/2_8/.env new file mode 100644 index 00000000..bd20b47e --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a9e3574b134014e3b805a948d2a706d8-495199463.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a8723a1a8ec4146cbad902184b7984cb-1161380606.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_dml.sql new file mode 100644 index 00000000..2c06242f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/src_dml.sql @@ -0,0 +1,531 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD -- version: 3.2.0 +-- -- SET +-- BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- -- INCRBY +-- BITFIELD 2-2 incrby i5 100 1 +-- BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- -- OVERFLOW +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +-- BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX -- version: 5.0.0 +-- ZADD 10-1 0 a 1 b 2 c +-- BZPOPMAX 10-1 23 0 + +-- BZPOPMIN -- version: 5.0.0 +-- ZADD 11-1 0 a 1 b 2 c +-- BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +EXPIREAT 16-2 4102416000 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD -- version: 3.2.0 +-- GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +-- Starting with Redis version 4.0.0: Accepts multiple field and value arguments. +-- HSET 21-1 field1 "hello" field2 "world" +HSET 21-1 field1 "hello" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +-- Starting with Redis version 4.0.0: Accepts multiple field and value arguments. +-- HSET 24-1 field2 "Hi" field3 "World" +HSET 24-1 field2 "Hi" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +-- Starting with Redis version 3.2.0: Added the count argument. +-- SPOP 61-1 3 +SPOP 61-1 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB -- version: 4.0.0 +-- SWAPDB 0 1 + +-- UNLINK --version: 4.0.0 +-- SET 64-1 "Hello" +-- SET 64-2 "World" +-- UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD -- version: 5.0.0 +-- XADD 65-1 1526919030474-55 message "Hello," +-- -- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- -- XADD 65-1 1526919030474-* message " World!" +-- XADD 65-1 * name Sara surname OConnor +-- XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- -- XLEN 65-1 +-- -- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL -- version: 5.0.0 +-- XADD 66-1 1538561700640-0 a 1 +-- XADD 66-1 * b 2 +-- XADD 66-1 * c 3 +-- XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM -- version: 5.0.0 +-- XTRIM 67-1 MAXLEN 1000 +-- XADD 67-1 * field1 A field2 B field3 C field4 D +-- XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX -- version: 5.0.0 +-- ZADD 73-1 1 "one" +-- ZADD 73-1 2 "two" +-- ZADD 73-1 3 "three" +-- ZPOPMAX 73-1 + +-- ZPOPMIN -- version: 5.0.0 +-- ZADD 74-1 1 "one" +-- ZADD 74-1 2 "two" +-- ZADD 74-1 3 "three" +-- ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_dml.sql new file mode 100644 index 00000000..fd4e194a --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/src_dml.sql @@ -0,0 +1,35 @@ +-- RDB_TYPE_HASH (4) +HSET 1-1 f_1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + +-- RDB_TYPE_HASH (4) +HSET 1-2 f_1 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 + +-- in redis 2.8/4.0/5.0/6.0/6.2, RDB_TYPE_HASH_ZIP_LIST (13) +-- ZIP_STR_06B (0x00): Used for strings less than 64 bytes +-- ZIP_STR_14B (0x01): Used for strings less than 16KB +-- ZIP_STR_32B (0x02): Used for strings less than 4GB + +-- ZIP_INT_04B: 0 12 +-- ZIP_INT_08B: 13 -1 -16 15 -128 127 +-- ZIP_INT_16B: -32767 32767 +-- ZIP_INT_24B: βˆ’8388608 8388607 +-- ZIP_INT_32B: -2147483648 2147483647 +-- ZIP_INT_64B: -9223372036854775808 9223372036854775807 +-- ZIP_STR_06B: 9223372036854775808 +HSET 2-1 f_1 0 +HSET 2-1 f_2 12 +HSET 2-1 f_3 13 +HSET 2-1 f_4 -1 +HSET 2-1 f_5 -16 +HSET 2-1 f_6 15 +HSET 2-1 f_7 -128 +HSET 2-1 f_8 127 +HSET 2-1 f_9 -32768 +HSET 2-1 f_10 32767 +HSET 2-1 f_11 -8388607 +HSET 2-1 f_12 8388607 +HSET 2-1 f_13 -2147483648 +HSET 2-1 f_14 2147483647 +HSET 2-1 f_15 -9223372036854775808 +HSET 2-1 f_16 9223372036854775807 +HSET 2-1 f_17 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/hash_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_dml.sql new file mode 100644 index 00000000..3a6d9c9f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/src_dml.sql @@ -0,0 +1,10 @@ + +-- -- string length will be compressed as: RDB_6_BIT_LEN +SET 1-1 abc + +-- -- string length will be compressed as: RDB_14_BIT_LEN +SET 2-1 abcdefghijkadfdefewqldsfdaadfdsfewfdal[rtg4dfdsads]adsfddfdsefrevnnxcvcdsgryiuiuoirewqt587regfdhnhtguftgrwefgfbvghjgffsvcnbhgvdjhhgfdsgbngtjhgfdbhthkkkiulpoikujhvdazgbbtcdwewww2123tyjiolplokjhgfdcvbn,;p[pkjhgfdvgrddscfjjufwrw] + +-- string length will be compressed as: RDB_32_BIT_LEN +SET 3-1 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 + diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/length_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_dml.sql new file mode 100644 index 00000000..62ce838c --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/src_dml.sql @@ -0,0 +1,20 @@ +-- in redis 2.8, RDB_TYPE_LIST_ZIP_LIST (10) +-- ZIP_STR_06B (0x00): Used for strings less than 64 bytes +-- ZIP_STR_14B (0x01): Used for strings less than 16KB +-- ZIP_STR_32B (0x02): Used for strings less than 4GB + +-- RDB_TYPE_LIST (1) +RPUSH 1-1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- RDB_TYPE_LIST (1) +RPUSH 1-2 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 + +-- RDB_TYPE_LIST_ZIP_LIST (10) +-- ZIP_INT_04B: 0 12 +-- ZIP_INT_08B: 13 -1 -16 15 -128 127 +-- ZIP_INT_16B: -32767 32767 +-- ZIP_INT_24B: βˆ’8388608 8388607 +-- ZIP_INT_32B: -2147483648 2147483647 +-- ZIP_INT_64B: -9223372036854775808 9223372036854775807 +-- ZIP_STR_06B: 9223372036854775808 +RPUSH 2-1 0 12 13 -1 -16 15 -128 127 -32768 32767 -8388607 8388607 -2147483648 2147483647 -9223372036854775808 9223372036854775807 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/list_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_dml.sql new file mode 100644 index 00000000..f868b4f0 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/src_dml.sql @@ -0,0 +1,33 @@ +-- RDB_TYPE_SET(2) +SADD 1-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" + +-- in redis 2.8/4.0/5.0/6.0/6.2/7.0, RDB_TYPE_SET_INT_SET (11) +-- encoded as i16 +SADD 1-2 -1 +SADD 1-2 0 +SADD 1-2 1 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-4 -1 +SADD 1-4 -2147483648 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-5 1 +SADD 1-5 2147483647 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-6 -1 +SADD 1-6 -9223372036854775808 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-7 1 +SADD 1-7 9223372036854775807 + +-- RDB_TYPE_SET(2) +SADD 1-8 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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/set_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_dml.sql new file mode 100644 index 00000000..7e8b1c15 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/src_dml.sql @@ -0,0 +1,9 @@ +-- RDB_TYPE_STRING (0) +SET 1-1 val_0 +SET 2-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SET 3-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 4-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 5-1 0.1111110 +SET 6-1 0 +SET 7-1 10000 +SET 8-1 24YLsf3sJ0X7n3docweHrAiyn8OXtzs9BKRgabc\r\nabcδΈ­ζ–‡πŸ˜€\r\n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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/string_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_dml.sql new file mode 100644 index 00000000..9f77fc25 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/src_dml.sql @@ -0,0 +1,15 @@ +-- score will be stored as double +-- 1-1, RDB_TYPE_ZSET_2 (5) +ZADD 1-1 1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 2.1 "abc\r\nδΈ­ζ–‡πŸ˜€" 3.333333 c +-- 1-2, RDB_TYPE_ZSET_2 (5) +ZADD 1-2 1 aaaaaaaaaaaaaaaa 2.1 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 + +-- in redis 2.8/4.0/5.0/6.0/6.2, RDB_TYPE_ZSET_ZIP_LIST (12) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808, and other float numbers +ZADD 2-1 1.1 0 2.1 12 3.1 13 4.1 -1 5.1 -16 6.1 15 7.1 -128 8.1 127 9.1 -32768 10.1 32767 11.1 -8388607 12.1 8388607 13.1 -2147483648 14.1 2147483647 15.1 -9223372036854775808 16.1 9223372036854775807 17.1 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/2_8/zset_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/4_0/.env b/dt-tests/tests/redis_to_redis/snapshot/4_0/.env new file mode 100644 index 00000000..090f77f6 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/4_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a4f1c63fdbafc4bb384eee07f1b79c24-849495829.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a0413811336244ae3ab068cfb018f1d7-863213277.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..fd8018a3 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/src_dml.sql @@ -0,0 +1,523 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX -- version: 5.0.0 +-- ZADD 10-1 0 a 1 b 2 c +-- BZPOPMAX 10-1 23 0 + +-- BZPOPMIN -- version: 5.0.0 +-- ZADD 11-1 0 a 1 b 2 c +-- BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD -- version: 5.0.0 +-- XADD 65-1 1526919030474-55 message "Hello," +-- -- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- -- XADD 65-1 1526919030474-* message " World!" +-- XADD 65-1 * name Sara surname OConnor +-- XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- -- XLEN 65-1 +-- -- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL -- version: 5.0.0 +-- XADD 66-1 1538561700640-0 a 1 +-- XADD 66-1 * b 2 +-- XADD 66-1 * c 3 +-- XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM -- version: 5.0.0 +-- XTRIM 67-1 MAXLEN 1000 +-- XADD 67-1 * field1 A field2 B field3 C field4 D +-- XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX -- version: 5.0.0 +-- ZADD 73-1 1 "one" +-- ZADD 73-1 2 "two" +-- ZADD 73-1 3 "three" +-- ZPOPMAX 73-1 + +-- ZPOPMIN -- version: 5.0.0 +-- ZADD 74-1 1 "one" +-- ZADD 74-1 2 "two" +-- ZADD 74-1 3 "three" +-- ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/4_0/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/.env b/dt-tests/tests/redis_to_redis/snapshot/5_0/.env new file mode 100644 index 00000000..34c7b930 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ab5200748cfca4828bb48ec74ee77423-345824646.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a2f28b604af384750809f2216a681264-1920935663.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..0b09e33a --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/src_dml.sql @@ -0,0 +1,523 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_dml.sql new file mode 100644 index 00000000..63beadde --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/src_dml.sql @@ -0,0 +1,19 @@ +-- RDB_TYPE_HASH (4) +HSET 1-1 f_1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" f_3 c + +-- RDB_TYPE_HASH (4) +HSET 1-2 f_1 1 f_2 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 + +-- in redis 2.8/4.0/5.0/6.0/6.2, RDB_TYPE_HASH_ZIP_LIST (13) +-- ZIP_STR_06B (0x00): Used for strings less than 64 bytes +-- ZIP_STR_14B (0x01): Used for strings less than 16KB +-- ZIP_STR_32B (0x02): Used for strings less than 4GB + +-- ZIP_INT_04B: 0 12 +-- ZIP_INT_08B: 13 -1 -16 15 -128 127 +-- ZIP_INT_16B: -32767 32767 +-- ZIP_INT_24B: βˆ’8388608 8388607 +-- ZIP_INT_32B: -2147483648 2147483647 +-- ZIP_INT_64B: -9223372036854775808 9223372036854775807 +-- ZIP_STR_06B: 9223372036854775808 +HSET 2-1 f_1 0 f_2 12 f_3 13 f_4 -1 f_5 -16 f_6 15 f_7 -128 f_8 127 f_9 -32768 f_10 32767 f_11 -8388607 f_12 8388607 f_13 -2147483648 f_14 2147483647 f_15 -9223372036854775808 f_16 9223372036854775807 f_17 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/hash_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_dml.sql new file mode 100644 index 00000000..3a6d9c9f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/src_dml.sql @@ -0,0 +1,10 @@ + +-- -- string length will be compressed as: RDB_6_BIT_LEN +SET 1-1 abc + +-- -- string length will be compressed as: RDB_14_BIT_LEN +SET 2-1 abcdefghijkadfdefewqldsfdaadfdsfewfdal[rtg4dfdsads]adsfddfdsefrevnnxcvcdsgryiuiuoirewqt587regfdhnhtguftgrwefgfbvghjgffsvcnbhgvdjhhgfdsgbngtjhgfdbhthkkkiulpoikujhvdazgbbtcdwewww2123tyjiolplokjhgfdcvbn,;p[pkjhgfdvgrddscfjjufwrw] + +-- string length will be compressed as: RDB_32_BIT_LEN +SET 3-1 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 + diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/length_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_dml.sql new file mode 100644 index 00000000..77551f49 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/src_dml.sql @@ -0,0 +1,23 @@ +-- in redis 4.0/5.0/6.0/6.2, RDB_TYPE_LIST_QUICK_LIST (14) +-- ZIP_STR_06B (0x00): Used for strings less than 64 bytes +-- ZIP_STR_14B (0x01): Used for strings less than 16KB +-- ZIP_STR_32B (0x02): Used for strings less than 4GB + +-- RDB_TYPE_LIST_QUICK_LIST (14) +-- ZIP_STR_14B: 1-1 first element +-- ZIP_STR_06B: 1-1 second element +RPUSH 1-1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- RDB_TYPE_LIST_QUICK_LIST (14) +-- ZIP_STR_32B +RPUSH 1-2 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 + +-- RDB_TYPE_LIST_QUICK_LIST (14) +-- ZIP_INT_04B: 0 12 +-- ZIP_INT_08B: 13 -1 -16 15 -128 127 +-- ZIP_INT_16B: -32767 32767 +-- ZIP_INT_24B: βˆ’8388608 8388607 +-- ZIP_INT_32B: -2147483648 2147483647 +-- ZIP_INT_64B: -9223372036854775808 9223372036854775807 +-- ZIP_STR_06B: 9223372036854775808 +RPUSH 2-1 0 12 13 -1 -16 15 -128 127 -32768 32767 -8388607 8388607 -2147483648 2147483647 -9223372036854775808 9223372036854775807 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/list_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_dml.sql new file mode 100644 index 00000000..f868b4f0 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/src_dml.sql @@ -0,0 +1,33 @@ +-- RDB_TYPE_SET(2) +SADD 1-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" + +-- in redis 2.8/4.0/5.0/6.0/6.2/7.0, RDB_TYPE_SET_INT_SET (11) +-- encoded as i16 +SADD 1-2 -1 +SADD 1-2 0 +SADD 1-2 1 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-4 -1 +SADD 1-4 -2147483648 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-5 1 +SADD 1-5 2147483647 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-6 -1 +SADD 1-6 -9223372036854775808 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-7 1 +SADD 1-7 9223372036854775807 + +-- RDB_TYPE_SET(2) +SADD 1-8 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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/set_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_dml.sql new file mode 100644 index 00000000..2a1b05c8 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/src_dml.sql @@ -0,0 +1,29 @@ +-- in redis 5.0/6.0/6.2 (XADD starts from 5.0), RDB_TYPE_STREAM_LIST_PACKS (15): +-- LP_ENCODING_6BIT_STR: Used for strings less than 64 bytes +-- LP_ENCODING_12BIT_STR: Used for strings less than 4k bytes +-- LP_ENCODING_32BIT_STR: Used for strings less than 4G + +-- RDB_TYPE_STREAM_LIST_PACKS (15) +XADD 1-1 1526919030474-57 f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- RDB_TYPE_STREAM_LIST_PACKS (15) +XADD 2-1 * f_1 value_1 f_2 value_2 f_3 value_3 + +-- RDB_TYPE_STREAM_LIST_PACKS (15) +-- this will add 2 entries in the same stream +XADD 3-1 * name John age 30 name Lucy age 20 +XADD 3-1 * name Mike age 40 name Jack age 30 + +-- RDB_TYPE_STREAM_LIST_PACKS (15) +-- the longest string will be compressed in listpack by: LP_ENCODING_32BIT_STR +XADD 4-1 * f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" f_1 abcdefg f_2 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 + +-- RDB_TYPE_STREAM_LIST_PACKS (15) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808 +XADD 5-1 * f_1 0 f_1 -1 f_2 -128 f_3 127 f_4 -4096 f_5 4095 f_6 -32768 f_7 32767 f_8 -8388607 f_9 8388607 f_10 -2147483648 f_11 2147483647 f_12 -9223372036854775808 f_13 9223372036854775807 f_14 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/stream_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_dml.sql new file mode 100644 index 00000000..7e8b1c15 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/src_dml.sql @@ -0,0 +1,9 @@ +-- RDB_TYPE_STRING (0) +SET 1-1 val_0 +SET 2-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SET 3-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 4-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 5-1 0.1111110 +SET 6-1 0 +SET 7-1 10000 +SET 8-1 24YLsf3sJ0X7n3docweHrAiyn8OXtzs9BKRgabc\r\nabcδΈ­ζ–‡πŸ˜€\r\n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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/string_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_dml.sql new file mode 100644 index 00000000..9f77fc25 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/src_dml.sql @@ -0,0 +1,15 @@ +-- score will be stored as double +-- 1-1, RDB_TYPE_ZSET_2 (5) +ZADD 1-1 1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 2.1 "abc\r\nδΈ­ζ–‡πŸ˜€" 3.333333 c +-- 1-2, RDB_TYPE_ZSET_2 (5) +ZADD 1-2 1 aaaaaaaaaaaaaaaa 2.1 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 + +-- in redis 2.8/4.0/5.0/6.0/6.2, RDB_TYPE_ZSET_ZIP_LIST (12) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808, and other float numbers +ZADD 2-1 1.1 0 2.1 12 3.1 13 4.1 -1 5.1 -16 6.1 15 7.1 -128 8.1 127 9.1 -32768 10.1 32767 11.1 -8388607 12.1 8388607 13.1 -2147483648 14.1 2147483647 15.1 -9223372036854775808 16.1 9223372036854775807 17.1 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/5_0/zset_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_0/.env b/dt-tests/tests/redis_to_redis/snapshot/6_0/.env new file mode 100644 index 00000000..0f7c2056 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://abdf6c45849c54a2a857d3ecfb2c466b-981085820.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a542c20654bd8457e97213a0db0b32a0-1382178154.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..efb3f3af --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/src_dml.sql @@ -0,0 +1,524 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +-- RPUSH 4-1 a b c +-- RPUSH 4-2 x y z +-- BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +-- SET 12-1 "sheep" +-- COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +-- SET 18-1 "Hello" +-- GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +-- SET 19-1 "Hello" +-- GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +-- SET 20-1 "Hello" +-- GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +-- RPUSH 30-1 "one" +-- RPUSH 30-1 "two" +-- RPUSH 30-1 "three" +-- LMOVE 30-1 30-2 RIGHT LEFT +-- LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +-- RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +-- ZADD 69-1 1 "one" +-- ZADD 69-1 2 "two" +-- ZADD 69-1 3 "three" +-- ZADD 69-2 1 "one" +-- ZADD 69-2 2 "two" +-- ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +-- ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +-- ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_0/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_2/.env b/dt-tests/tests/redis_to_redis/snapshot/6_2/.env new file mode 100644 index 00000000..3852a764 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_2/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ac2cd119b7d51454a857a9ea1368c792-168326526.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a5068abaa38d549d291b81e5f8ec4d34-685795292.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_dml.sql new file mode 100644 index 00000000..bc34ba14 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/src_dml.sql @@ -0,0 +1,528 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +RPUSH 4-1 a b c +RPUSH 4-2 x y z +BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +-- LPUSH 5-1 a b c d +-- LPUSH 5-2 1 2 3 4 +-- BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP -- version: 7.0.0 +-- ZADD 9-1 1 a 2 b 3 c +-- ZADD 9-2 1 d 2 e 3 f +-- BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY -- version: 6.2.0 +SET 12-1 "sheep" +COPY 12-1 12-2 +-- GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- EXPIRE 15-1 1 XX +-- EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +EXPIREAT 16-2 4102416000 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL -- version: 6.2.0 +SET 18-1 "Hello" +GETDEL 18-1 + +-- GETEX -- version: 6.2.0 +SET 19-1 "Hello" +GETEX 19-1 EX 1 + +-- GETSET -- version: 6.2.0 +SET 20-1 "Hello" +GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE --version: 6.2.0 +RPUSH 30-1 "one" +RPUSH 30-1 "two" +RPUSH 30-1 "three" +LMOVE 30-1 30-2 RIGHT LEFT +LMOVE 30-1 30-2 LEFT RIGHT +LRANGE 30-1 0 -1 +LRANGE 30-2 0 -1 + +-- LMPOP --version: 7.0.0 +-- LPUSH 31-1 "one" "two" "three" "four" "five" +-- LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +-- Starting with Redis version 6.2.0: Added the count argument. +LPOP 32-1 2 +LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +-- Starting with Redis version 7.0.0: Added options: NX, XX, GT and LT. +-- SET 42-2 "Hello" +-- PEXPIRE 42-2 1000 XX +-- SET 42-3 "Hello" +-- PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 + +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" + +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +-- Starting with Redis version 6.2.0: Added the count argument. +RPOP 49-1 2 +LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +-- Starting with Redis version 7.0.0: Added support for the -* explicit ID form. +-- XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE -- version: 6.2.0 +ZADD 69-1 1 "one" +ZADD 69-1 2 "two" +ZADD 69-1 3 "three" +ZADD 69-2 1 "one" +ZADD 69-2 2 "two" +ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP -- version: 7.0.0 +-- ZADD 72-1 1 "one" 2 "two" 3 "three" +-- ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE -- version: 6.2.0 +ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/6_2/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/.env b/dt-tests/tests/redis_to_redis/snapshot/7_0/.env new file mode 100644 index 00000000..6ec321ae --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://aa2b463deaac14c8585714786ffe0d8c-543369047.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://af3c50366d01d4c03943a47cf146b0b2-15030835.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_dml.sql new file mode 100644 index 00000000..b04e8357 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/src_dml.sql @@ -0,0 +1,46 @@ +-------------------- string entries +-- SET +SET set_key_1 val_1 +SET set_key_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ +SET "set_key_3_ πŸ˜€" "val_2_ πŸ˜€" + +-- MSET +MSET mset_key_1 val_1 mset_key_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "mset_key_3_ πŸ˜€" "val_3_ πŸ˜€" + +-------------------- hash entries +-- HSET +HSET hset_key_1 field_1 val_1 +HSET hset_key_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ +HSET hset_key_1 "field_3_ πŸ˜€" "val_3_ πŸ˜€" + +-- HMSET +HMSET hmset_key_1 field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" + +-------------------- list entries +-- LPUSH +LPUSH list_key_1 val_1 +LPUSH list_key_1 val_2_δΈ­ζ–‡ +LPUSH list_key_1 "val_3_ πŸ˜€" + +-- RPUSH +RPUSH list_key_1 val_5 val_6 + +-- LINSERT +LINSERT list_key_1 BEFORE val_1 val_7 + +-------------------- sets entries +-- SADD +SADD sets_key_1 val_1 val_2_δΈ­ζ–‡ "val_3_ πŸ˜€" val_5 + +-- SREM +SREM sets_key_1 val_5 + +-------------------- zset entries +-- ZADD +ZADD zset_key_1 1 val_1 2 val_2_δΈ­ζ–‡ 3 "val_3_ πŸ˜€" +ZINCRBY zset_key_1 5 val_1 + +-------------------- stream entries +-- XADD +XADD stream_key_1 * field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" +XADD "stream_key_2 δΈ­ζ–‡πŸ˜€" * field_1 val_1 field_2_δΈ­ζ–‡ val_2_δΈ­ζ–‡ "field_3_ πŸ˜€" "val_3_ πŸ˜€" \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/basic_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_dml.sql new file mode 100644 index 00000000..10c4a2a4 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/src_dml.sql @@ -0,0 +1,518 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +RPUSH 4-1 a b c +RPUSH 4-2 x y z +BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +LPUSH 5-1 a b c d +LPUSH 5-2 1 2 3 4 +BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP +ZADD 9-1 1 a 2 b 3 c +ZADD 9-2 1 d 2 e 3 f +BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY +SET 12-1 "sheep" +COPY 12-1 12-2 +GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +EXPIRE 15-1 1 XX +EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL +SET 18-1 "Hello" +GETDEL 18-1 + +-- GETEX +SET 19-1 "Hello" +GETEX 19-1 EX 1 + +-- GETSET +SET 20-1 "Hello" +GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE +RPUSH 30-1 "one" +RPUSH 30-1 "two" +RPUSH 30-1 "three" +LMOVE 30-1 30-2 RIGHT LEFT +LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP +LPUSH 31-1 "one" "two" "three" "four" "five" +LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +SET 42-2 "Hello" +PEXPIRE 42-2 1000 XX +SET 42-3 "Hello" +PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE +ZADD 69-1 1 "one" +ZADD 69-1 2 "two" +ZADD 69-1 3 "three" +ZADD 69-2 1 "one" +ZADD 69-2 2 "two" +ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP +ZADD 72-1 1 "one" 2 "two" 3 "three" +ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE +ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_dml.sql new file mode 100644 index 00000000..5c906453 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/src_dml.sql @@ -0,0 +1,15 @@ +-- RDB_TYPE_HASH (4) +HSET 1-1 f_1 "abc\r\nδΈ­ζ–‡πŸ˜€" f_2 abcdefghijkadfdefewqldsfdaadfdsfewfdal[rtg4dfdsads] adsfddfdsefrevnnxcvcdsgryiuiuoirewqt587regfdhnhtguftgrwefgfbvghjgffsvcnbhgvdjhhgfdsgbngtjhgfdbhthkkkiulpoikujhvdazgbbtcdwewww2123tyjiolplokjhgfdcvbn,;p[pkjhgfdvgrddscfjjufwrw]m2Px5pO3osFosJ6ICjikrOSqsnqPVxJzz3O5Gf9sZNejHglx1D3pbndftHVyrdlEb24NAPQWOivUK0EVOMoOj78Zu5Ud1C26nbriJiU5DasWQmjtgyaPD2N9ijw69qBWsU5U0NFMfgtbl6XabxZW9GX24mBIcThgWsM4G7nUvJZqrc5uj4trHWliUPlYSZKBu4pjiMzOZXNSGoW3WdzN7LUFGD1SWw + +-- RDB_TYPE_HASH (4) +HSET 1-2 f_1 abcdefg f_2 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 + +-- in redis 7.0, RDB_TYPE_HASH_LIST_PACK (16) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808 +HSET 2-1 f_1 0 f_2 -1 f_3 -128 f_4 127 f_5 -4096 f_6 4095 f_7 -32768 f_8 32767 f_9 -8388607 f_10 8388607 f_11 -2147483648 f_12 2147483647 f_13 -9223372036854775808 f_14 9223372036854775807 f_15 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/hash_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_dml.sql new file mode 100644 index 00000000..3a6d9c9f --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/src_dml.sql @@ -0,0 +1,10 @@ + +-- -- string length will be compressed as: RDB_6_BIT_LEN +SET 1-1 abc + +-- -- string length will be compressed as: RDB_14_BIT_LEN +SET 2-1 abcdefghijkadfdefewqldsfdaadfdsfewfdal[rtg4dfdsads]adsfddfdsefrevnnxcvcdsgryiuiuoirewqt587regfdhnhtguftgrwefgfbvghjgffsvcnbhgvdjhhgfdsgbngtjhgfdbhthkkkiulpoikujhvdazgbbtcdwewww2123tyjiolplokjhgfdcvbn,;p[pkjhgfdvgrddscfjjufwrw] + +-- string length will be compressed as: RDB_32_BIT_LEN +SET 3-1 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 + diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/length_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_dml.sql new file mode 100644 index 00000000..f700b4bb --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/src_dml.sql @@ -0,0 +1,22 @@ +-- in redis 7.0, RDB_TYPE_LIST_QUICK_LIST_2 (18): +-- LP_ENCODING_6BIT_STR: Used for strings less than 64 bytes +-- LP_ENCODING_12BIT_STR: Used for strings less than 4k bytes +-- LP_ENCODING_32BIT_STR: Used for strings less than 4G + +-- LP_ENCODING_6BIT_STR +RPUSH 1-1 "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- LP_ENCODING_12BIT_STR +RPUSH 1-2 abcdefghijkadfdefewqldsfdaadfdsfewfdal[rtg4dfdsads]adsfddfdsefrevnnxcvcdsgryiuiuoirewqt587regfdhnhtguftgrwefgfbvghjgffsvcnbhgvdjhhgfdsgbngtjhgfdbhthkkkiulpoikujhvdazgbbtcdwewww2123tyjiolplokjhgfdcvbn,;p[pkjhgfdvgrddscfjjufwrw] + +-- LP_ENCODING_32BIT_STR +RPUSH 1-3 24YLsf3sJ0X7n3docweHrAiyn8OXtzs9BKRgyYx8XqiKQwNa26dCdmIWkXcvhPptBp5MpPYFdg02KZ0OJEjT30DZR9UNIom4WaJWDXONNRliujCFuvWnZTDlx17e9gsFeY6AmfWX1hsStv9DFGJMkxXf6JJNQZ04JWhDIdarm2klt6Y0g4WLt71J2YbJa0Me5g5z82G86wuYNtbSdzBiaKb2uNwXzzT079zk67rwWsgV02k6iJ9UQYILIvT02qKApD1uhq7WpQiTTEYFb7JV2qT4zfQORrl4KbBZ1XPQBUawY6BzGNNfPM4R5bSbxP16fY3IeLQQ87sOYlfmdkQRprwgNZniigKZTqAcqqFJsOiqQdrG1TAykoWAXvOitLVH56XKBXhQbX3Clos31XH7FxWgH9aOxzP5LGEm5q0GRvjOzQvgOCKR2QuDHZiikoihjZcqKtthmDTkifA0aRkv8jAZTvv1ovPCR05YM2y6uWOfirlzv0pxrdjVfKkIg3N5HiyenQzKSOWJrUBM2IGgVCEtkdSC6gV3vjqqh1FbYtldbGqWlT1Amoj41HC6LEjvZAV9eaDTWgZlPKXdXOStrrN8motzLsxK7lMiW8Y1UBe0E3scYcvsQQHIT6hYLfRRVh0Vv6qzxtH19wUzxmnLBSu58LgC5JuCFAkm1WsTCqQv3Zm3jfq8ce2zqGoqFrcmuN4mAfmOOBvPprIf2vhWZvm38befuAwhCNVhsYgbkt1HGF5eVFEBOWwCvZTp65tstM2gJeHEJmvqPPzd2XUy78kgP4Ho8dkP29kCd2gQGCT49QK7Ltc29cm2Px5pO3osFosJ6ICjikrOSqsnqPVxJzz3O5Gf9sZNejHglx1D3pbndftHVyrdlEb24NAPQWOivUK0EVOMoOj78Zu5Ud1C26nbriJiU5DasWQmjtgyaPD2N9ijw69qBWsU5U0NFMfgtbl6XabxZW9GX24mBIcThgWsM4G7nUvJZqrc5uj4trHWliUPlYSZKBu4pjiMzOZXNSGoW3WdzN7LUFGD1SWwbZkTyG7QxtvEioxppr0eBQkyLMOvCj425v82FKhikmEMqBZYJTcSdnuGxHApAfmRiDmZEqc43jD62QBIccQiHLpxMlIkmqobq3JPTwBcURWlW6MnIENWfr9tQWudUeaTNGvB4YqwcImgCZYCX8SBJ1rRvop6AMn5F9TZo2GBQkKKfxWQOZvfXejtOMfqbcUBvEWEuJ4vk8XKxOLy9avhX2MHDYE3sRWCxmYaBBbwfbflvubcLjeXzxJAIC5zApSEnve60WwaqwBBmE9oenBeJSLhW4N3z0SNtWb1KavFV5plWXi3CzkEBSf6zP4R4vwMchXrJ7mlufOfHV0mJRA8hjUYJ9X0212EEWEMxICzH4X7UkCrC5mv7okgZ8XpfPnTcvZpTMDWBWyMct9pVxS5Bx6t2w2a7NUDuz4NpA8Pyhi9L8kkCYawNN2fhqx6N19c24ChZutlNargHF7TvcFaFI3S5CORg87v7h2XuRXSPzrv8FE9RXugY59eI5Ip43qTftS7WtTIVCjVuTFC2SETWmBMrJIFgRZxKgHSgtPPs2CUhngk33XNpXqQF0AH6NPMmXCp2MkOqHOc8QYy4rSnqkOTQ5jFmVO0QNvTAWZ4xEWzx6gCACmPxOAPZUWtLD0QN7oXcGQweYYdwKmAYm1JD1DrJx7Z553VR3pZz5R2kYO786uyRvzK5ROspht1KBfx5iChSf4O1SYwy2gCFmgZs6De6Tx9nAP0TlEPsew7Q7hBdoVwggUhZpY42588YrArvwCuiEu41tRTPJVmu8tbUlFR9emNz24ghsrNeq2kkixT0ERqDErSZDI6MYdqtJd730939NGCi8PfQgsbdaxI0btD9p0sONYTzlPUf4mvwcLb9koPPfTTexQnWVaMhlGjzp9q5xOaPTk8uufnaWslM63UEVAQUhl6RzrFcOVHKOlNWJApezmTFYohVccASaOa9jXTPpuYbfdgeSKzFjDpdSenjv7jOt4AU1gCCiuBxJPBf80EKrjYZaC6o5AKj7B0okqAqOXD1f8nA1QooKWodvMFQ44jl7L1QoQGPd6Qq4mVs95eLju2QGtPOnXYNHY9AtKK9VSygDRGuArZJT4yqjET9G34tibC40bvHu4YopJu2xtFuQzcSZRwlhpYmvwM7tjhCoIHP0VPJ2p5gtxjZZlp7GhWVs6drIzIZl1r8rs0uN4jAQmn4XWDLQHErk0H99Ozd0Pk5xEO65Uovx50ZsxDncMGc3HhwNNh29pmiRT0ee3TcRMCdbq43nRJW1BQIHUzLOTPO5pD2GdpIAx5hyUccNhrAKwDIutfoC5LeBjqRPMEOCXpgYWJo8E8izBmAzzSnpzPSyrt1VLdeIGPe1zoDr8Ztjwx4PRF1KSWXc3SfnOnR24RaR4pFQ0rPzea6Z6Bd3Fiq6HEl80WrXv0U7PnZJqyYbiK2r006UswIaChP08PgdF4tZdBUTcjmblGlX1jks91mtz6r2tLlMSovh21uTuFSxeYQUYdXFSXn6sMY3m6JV6525LvBp2JJzPKGWyJ2rKxtmMQhCA69TmCBQpajWsl5Xuky29KugybABOcG1VUUgea6Wh2JAEbFh8jEECIUJCcaJlVPlmnzNVEd3nnM3aMveKqoGrwA7QrpyEggYNqvSRsYGlLcFRXM00J8kXd8VIY5wC96fNeE82UI1UcDWIBIFgokBFLMLITbCfJHZD6dJjFgye1RtvGDiWqF7E2xGA4qJxpMY06TaG5ySqp6lthtRgPEyODdI8yRdJlkaEQGxVDBkBFebN2EoEg6kfIpYE9LoM4K4zNWrlvfwAwTd5fT7vWBH1hYTU6mISVKrAwvPvhlG2Z482UEoTt6G103gy3qjUmUhSwwiTgb2La9pBqHRUlH8zNYUdFRieeArhWWGrAGbnQ4M4iUjXkLZSjSALSzWZvX3vxK6AbZRLVG9BU2AGGtvSO86jI08rtCbEP8S6Kx786czPZBrAn25uNrZark0FhCqhxI5KAaTlm5ul4TrHXen752g1XcTuKhiNfrurUqSyzb5bgORj82abPbNBTc4U4yYFmTnXsvuraxGv1Aa7gFje6FPta8AOWYRgY3GN06CFvHLYUPjp1EqYKHBUwXJoempJlApDQorKvOuaz1cG9Meblo5l1wnzMSsRCYEKy9VJuuMxzECixxPVh6Zs822rOXtoaR7dOk5bT7XPZFQ8GasSs8n4Wxvn0CwhaluoCBv0gwncXD1z5a08Ewdvvahag3aUV6SEI51zfB05PWL7VKG4H5UAdWawhw6YTor6pOp2A3brY8aCseaCvZ0HAlWCWRISI3k8bHWNuxM5Z8ZezaBal29NxC5TbvOP9ygqspj2mbYrxPfOuUWG0XIWLKd0iQBZYflYlAYvEnjEzDA0o3xEye8s19a9CHe7nLt2hoJWlJgsluiTAdgdMMAmj0jvEk75pqKYAHycBHKURS6omrQSdn0lS2tBPHMAovUaWZlK5d156Mhua9HfoGNbtUiARaXgIb23xnklIDDNZ7LKTNeKL1Uuny39uybzuN8IJAaQMidCmV18tzWQMI4aCiMSCMIuhmPZt0F1SlLsl0d38NNEmR8ninom1ZY7pgobAWUeGjXWtOiNh5b9JgTlluHOmVonuQzqouBqGZmvtjEjP6UrJSuVHFjqBnzrelAu5oRGwX5VTyvaAf2ni1QXVMVMln5XjmbZgcxN4cEVv4m3Jd9avF1YLsqc1zOWoHzBTuYNee4MypodZZmwvlxusHFRgFauwkSYMS4QEMnZJhWxr2v6saSTzBeYlfwUft8IVsshC0mOnAruEJpmiTIZrIkWqG3VHvOOMQLDedOp7hkdr6NrY64HYJzewoGEdDjSZQk2MmsdbWrczLwca7LBxHch16L01uvXJlzmHjwHSnsmdsoQWwhbDaQt4Wm6MTC8tMRmbw9EhvTVbXLsD4gRcwTBswFtmMxjxahgzd3e5GqoLQ48YocJUju4OsbyArkwtz6wXGqgaItgKIwyARPXvpMGInfqQAC8TlcXiAYqHPu3ma66B5jE99gtq9NcsPTPG0AFFJj2MHKXEjF0TwcI0cpDhwhD2Yy1rQBE18gDvkCYl4cmE3XSqyoAbxXiofPveQZYmq7kEZnMrtMXJEAM1qPmpyC7sok6bCWzE71eywctLP6pp1Rnx96uG7Ni4U6uR4GU3IYhPqWoT4LbgFaoDUBX1hjO2hfUJeFa5gcGyC1LGiH7f0OTTY0gYI6ZB7XWzd5US9pye1VpaA7zu2KXu5tbt6FAd621AVqmU9qESD3MlbWcJoQYMA3VxeRZwyePKx6iXRZBy7HZZBk7UYwZfoHVV1AcAIaGvK0qmRgCDTCDnBTY9oX9IcC0CBNqwTsH3h4P36I84XFv7guAPq9VWO3xHpqZoPhxZXZENedCi4a8B1vVivERDUS1lqP9HNwT27YAzUQxLqDXxuyyKHIG42pmE85cAtR0cxnQDZVDChyfvkA6w8Z0TzAHpbnFXLPQf8d1SIR8Ol6QMpjrmYBk3LVqDRwMESVTtOP9V5XKtsqHjDQ0LLT3Aq4zK6MPmHCZz7GvSVBJ1PIzOWAig7NcqPEEjTGhIVqOaEhPWWsvK8ek2z1rQdymM79LMjgAw6XiYT4lSXtMm6KjDLDH1KZHVqVfJmabcHtkJVIgHySuUTS9gKwPwis3yzpWIlozqE5YXVUt0vI0GP96EySjym5yQfvgdgIIYocG99S4XAxNXGpxlQViFhFpc3FqMGujx24fVAfV1BiFDkhN0ZWg6Jvr6nic7Q49yMggMeTMgA8UzRXGTRCTtcGUwPW7kObonYnOOOQ2NrMxbHuIJv9Hw9SWGKH5EAWIAshDIAeyqBxrIAApTGSjstrg3ZGyhVvitlEngsvqO2U4zceYtVu4UfKcUQW6NB3Z9k9GLpjeNU45acGlK5UG3rPMelXCxOxA4uX6JxbG2Rkq1Bcbe3aHZIDvMPxStRwsCn4Wl1ckWHF1P4CNoHohupuYTUyYkZVGyGZyVnANfO4lsLUxnitpMeLGswE3hhOaSjAHG9sXrV6WHRLPI3E67SxgosdUxMalWbCd93dQszkjeEr3bc2WnSKhy1cNPSEBPUzzKsRExO6Xk4MrETEn3EN1367Q3ZUkuhG5YW9DURmfkWukt2wJgxEdojXpaR4buLwQzwqtFNVSN4LOVYRLhFnUoQrxFzjhBLPuinjqrS3LKqpBLAJw7XwKbU93uB2ZKbTc5CvK1GfP4zsyULk5Wz9AdhMo9lM7lbbFVcbezl59vyiAfMwGyA2TCidgDv5rSVcdXbv6wnZvvZ0q7QSQ404C7zOU9plGeozbDrO56CncMSWFobN32hdiShBJssEExfWr5MyVMs5GG39KvPtoLHzUqGImYh96DjUPBlO8fDYV5rpmm3ADUxHIgyt6BNEBDwkXgahyYanDdyl3Uvj4wl2OofVqp7JXF3RrCRAWQsisDI6k9dTzd0XRFWjkG1hCImg9Ot1nsmIvLNA08e20tmOSgTFXw97o95x39DtUDpSTlNLFWw2JuwspkOXqxJl04AjtwnqcQDubLYFSF008NsAFLcQyhTv8ueuNOq1olOoWpqhB57RGDkXw1tRtG6ikvFMB0Hksn6bixHLbhGqldh9mMr1Z5X0OXiapu1rO49VsWNjqjJezYGeD8Y7ZyqBH3nnmJQwlbkNsRETcyIE7fhLCWsIGZkrJslzh5AGArdnE2xF1U82jU8BGv0rjbg3pTfXWYIUiF5xQpNNT53villfH26tB2y5h71Sj3LLFqbTzzDta96sYZBRgIrSQPoN8pLporUuNpq2xZNxa6HEBWAaUplqBkA59zcZGp8oOx7SVW20SbuJX8DCuSkjdK4FLlse1YmlhThxMliSdB8mWhj4nw7kkz1RJHbbaMzfroX2abWSyux6wy0YqvQxKvtMfiM5qrN8ixp73ii1R90l6H7v6gRjuvMo5iYh8kHqkKJ8KbJjr3jNWaMUOLJFgel06qdOqCDwh4YhWutbqD6DHDmONVkeFUbFX1GLQwyvteiXfubKEqs0uN6X7q2T67Yj3dll04Bwin6sGTDhFCkBXi7Mx8qc2OlVXCOeBfZuMIGRQaxPIY0RSWh4Iq6TfttTf3XXdIWOzaXhNpBplbRWFlhsTd9qL6chGaBGvcVdo4eF9congBltRib2mHlIi8q8dDCXmrTCPzS95gqTf6A0A1Kw8E3TPqewIGME6ntsH4BcFwBYaCCGrE6eZNbTDyLjtlGB4DApX0E7f3XL56c3ci0ev4SfpyCx0TERdrkKKKgWPMhUWl2mcQuXJg85xgGAJDDPwy4ApvBcVTmKyrK5rjdDsMRBCYrSmuK7qCAgC4kyNTWfqDgE9Adnfp993qNmGDco7z8eSQbO2V6Gic9220HH4WYVZS0bt1r0YpkAreRif3bQSARTVyGfbTX2kUIFwDfJHQTuMACvz42M5Mh7UIudSvmcBIm9l8UNOefTKh2QlfKMR5RI4KocWfXZpWOpSbESH7isaG0e12XnIxsipvlvjB9A50k4n2pIwCLbfFpBI7ewkT9RlOYfMcmucVG8YUV12z7wbTZAUdVpcLh5lDlUrDAeuyh9AlieBFEylBKILeiVl1cqA3Td6x78XQbVtMfYaUFS5e9Ti3NbssFckPV2HK7S8C31omSyMtnMD3MugVsZYntSxhpaQAIHO3VikZQZNx9UpqAyDZ6d9PVjea7V4j7wAtGJWvZU0nkarIUXbLTwYTBaem4kMJDvq2iEk0KfxeuuRdyN32G7xTGtI90h8PMEofnAZ7UnyxdIdFzY1DWgM3nUAHS55iCFXti0ZVSqvm0hlqre3iQFNagFu11lhkzI2qHiQ3RJCQf5snmPmDee6MsFLNi5UmtYpzO7pxIor29noV47hlijWHlJgayhVk9vbkgJ0WDmntn1ZpfcDpIKppz2g85qlczEa5sA3g3rYA3FK2Qth0QHjir81RmqKY7xdCkdTsOqJOmYq52h8c7cdUZZxylKYKoizKJ4R1dWRs2rqV0gqix5R0RU1OP3wrmozDCKJPygW6gvvMhTmEZh5IfZ6OXqO24FVS5Tz8A2xfFXCIVRQFif8ouCSzEoQJgumXot9zglcwdYuaNsbwp0ehnpV6jKRCYTMJAyf5vKb2NUU8hDtWWFcb9wzukresomR0Mp3JbMIoP8RXkPGx3eJD0JcI60Yzc5MyPr262zJkJBcugyeq9AdWAJKS0cDLkgu0VMMgGP1A7YLev89aEqeiNpMptV6se4JvaZuSFbmgnYJq5JuoHwBQuKQd5STqhlsn2v4oOfj7eR0s5EWjtbH4uaqfh19GvQOFPYrcmKX0wnzmp6D5Jge62uSftpUKHj8sYrKpy1hmaYdrAAWZWqSR6KL7RsV5WplYAYyiF86Ob6tnzSP9NN4hse36OSzmrBs0F8UJfJB5xeVGkSHh9hGDgNNAeXWxr6TvbDDMVdGyMDXd1a2UpY9e2OU68LcAVn9ZQItHt8v6DcG8V9fUPydUJWDjqTold8TptCoOhKVsLIG7nrVgCrtovXDbaZQ8XGSJB1pnRWGr4CxUoqv6W0GZiQ3O9KI9sZZxW9KE1Gs7meTfLsUyYw88JucMOQZmY6xfNLgmeUzldTd0NTxuumglxmMFtay4CZCOseJQSJCPnb1y0qjixlpwnBIB2U76EvPbhqt9GR6JD7HSpvXPz41PsO0we9bLqzwUCeDevlILMGeOymhWBtVIV73vthzDTSjjaySLWlgtARy4rOlLqRAkLdSnapm4S7iuW8sVNZn6hA50XUr4kP2qDVIje5mKH5QBhPzmIzset9Iauh2SXEHez0AVQ6qGhePnPnc80MpRhBMKdHGXQMdmlh9MNeApritizsMpQ5E4zUlshyPqNgcZNvZaPGV0B5J2rIX7r2yT9e3tDOZOtQyUlhkajHy2FkjmQT6f8760qdUM28Nno47THUwznrZ538Ev0h21Zelzo8ahSwn23sDsPqrJdyC3jaafrtI6deDc5bWxvXuQihtwJWuYBrXHWTSDl5WXkQrpuz52OfDt8HSUveO4Fey8wN1rFAiBBKrUxlPfPtDcbRhbOBZkLrRUub2JbkxfBUhMsrbXQhgTfVJWtV6beQXiCI93iTnogjmIkwEyzN6BvHCrNUTLWOmKVA43pAKpCqPpikZG5LMAEp5HfQD7zexr7qdWASecVL0Oq0vF2oCazv26jm0NqQvSIHMkwuL1n5ltDwMFjxhDjCv50xUYrZukhpxhCFk5NEafvRKtdwNc0bAx8Z9L6RX6q9HPpOZo0HX7pTsETgzMAPYkmwTFJYTndRv30wMbLhsL0kig8GO6CC4ooZkZUSV6xKXJIuWDl5LMTlQL1rQpFDNhUT0GNyhq0BaDzG2i1x9NBcF619OWnWTLuu6nFFfWo07OOisy8NrgCmjl82OOi5rshareYZWuy13zkROjHyP4aDRcOHhzlXve0QgpBIcwSHueSsAxbYq64XEyQkFhMksLvCYdXdtEaN2Yevpye0T2ulannI8hN8IT7cZUqKGb8jCCNw4rERrEbZCmC0Q9cetwi16OVqrVOSBEfEUoglDCttnrPqIGPG5GuiSyo0qBRV39rL2YqU7yQLeeOazvS13VAf67Hpj2cjEQKvfEMKZbhO4cRg2ETooyTKhBCGsjxa396uuM3mcQcrdmBcSf9FRJ5tTRYWpjmFXlNSDG5ONLr6DuEV9y26mHfTANqTj8qnasHR7btQb2ZrNQhWRdbRc88B78depK7Qelxe9vNhg1x1Nw9W6WKqRXzoDiM0czKmQunt3JbXcZcvQjBCgs0YOJn9d6NfjiATIjd1xSlAk8kdRZfT45T7TJ8ECevfv3i9vxxzVINsfeDCn8BOnOUE95GiLxUee8dO4ayxaSyzrPCMZ0SCvSCtFJGbt1qtTmFyFIJK5JHwayhpf1ijZgluwfUjh3mrDqmYxHkmfedadJMXM1KmgWCkGbi32v4me821aLGH3T10hMdywIQydAnZMp5P8dAbZMcIhzYKNJXmcQT8OV3NPvQ9Cpb215HcSgIVGwyCHkDtzt4AbDV3wdKxbjsf4K8a4d6Ksxbrm6LH57zeIoNTVTimSPJnArHuyD8eNeA5V8k6hJZxNwd51fEzGHIHTYGL7NtnYD7zlfpUF76YwTl5GpJjqvcRwc4tK9xHeLPo3N8fQjDIR4Oiy99se8uxXrs1z6u07uSBlfRdv5MfudMxEsqBwhBaQvb1Zy2GyZG24BMXmSM521r7TsIKWHmv0jCFMDY9mY6pMLyfraJQN2FchRky9unRJWe8ltSxObhOysqHx1BEWUX0E3CyKIOXRFC5Bs28R6prSuSG4mdkgBGbgwx6PcXhJ21PboNKySQrM7DfO7oTccPILwFMWJ7wVd53R5QdyLVG5rU0NEniHzcPIVxBRMLF0sTT0RplSx0RkZ8RG5B43PpyaodfIZQtc1r571FJlcmNaxexbUkdS2MdadUhaQyAQQbyJln7NIb3LxXeuLLFyQMbvv4RL5KRl15W5xODHv2Yd0rFmbC5mkLMeZUkyMrnPtFjvesyHRtIGWmRSu0p8dkbkaRfbsaOBRCGMUsQCR9JcKG4BzrxxsTiYG5mcoL8Qz2Js749Hzs8n5OyCgo64pcYJrEB0Y80VEpEiGY4hESpSV4IHjvmsyzvJRyKxXS5fcVpRHeSlXBv4GnSkUYpPUTvu7NLzv3lgwAbhYqANIMKAaYVvx0Fked37tFcMjDyUpogFe4pjmcHHvawE5ifMGJW43IDI86cfrM7xF8KcxM9tzTxd3x5I0JGh3wl8kU7KKD939LFNfPvADnMQTPUx5S44sonpKCrL7pyURkBU5dRcUogVrTFk63xZC3vIsOlWZpNyWUmZlmg6r1fSo7QuECt5e8cGOqF1DzySMGuZ9STWNIuA9GH2chXeE0pEn96iEUIB9JnV2Lm5B0Q4BB40HooDTzd9o4Sq2MpS6KBfaF9YKJa1uq6ke1gjUgnehkbaMG6lqNOez02TBFtFQWTW6do4NRmDRrWY8ZpxcdSeZcundxO758xxZWqAZmQF7yowbbCbxosmcEB2qReBBbNodS6jh5Rm38IaARTOPmYJc2XGnwzHWeeSVJqQPpNwL3sbsBITLBQ2C0z0NJBvRqv0GZyH2mExCZgy2KQz3RN5WxkJajk6aeovhGpssEXBw8x60M9OC7S9Mhexrkn98CtApAeoOv1uqkFUx87ctfXXnUwZM4pwyjrjespqLcUXuVuY4LR5bcQGslm8RJnyIUzgmPKRVs7fI2nyYq1iJQE4N0fPL2vR8QqA7eKq53WnL6Kog9vDUstJ7RJeRnmosCS8biwbQnuqL6UprOMFnSaEBm6zAOfAVHgNCdM2XfhGSPbL51CM2miu5xwSg6rz5jRV1pGM4b6rS1Xe6KlEhEw29wbtYtSv0suC4SVb2APUaP9qlgzlsGAkqVRJyVutKWNDfg4HEGymR0mPnHltHOZlz6kvz1xVKxkTcKAbdnBxpAJI68IoNIFvmLVQ4oksfph7b5puIQQmtigCa99GFEUq52bB7ht6saoKoh2ZhqWjjKWybggQdLItCDTxnEQLNJj5udtkdN5qCmEPlkfhQiVzosaBBityUX551crPUgLnm4SN7HmvjN4496Iogm1aywRZAEYvlvE18JhTkoSDRzZRSzBG13KHkIZPJTJt8zJIyMeGLjEimCdjxjUERkHUmV3zk0YQx9TlFtTeOkiHGVh2iBDHQzNltYGSGOQ9wHCfhh0qnXAlP5aam4W3PLovWWBLp7I30YosjUSaemtOaRpg5dhXPB64tr9cL0tDKF0NQepmtzyoJ5ELhNP1yXN2PaljD3Ixr12mtBAM0yeMpctWhTcKSNAm6jxePhRfBbFJYMkgvo2hF2oHC6jQBbLRZHcvo0S58wkjYmmqi7zNPHzjVI9Dptl7ls2dvDDi8FWEw2oQIdV5H0XJSHprDiyh4NHua0qbXRanNIKrQFc5jgHrafNwMdtaZWbiSYC8djrEdGmRD2oXYCF6NEm285QY5mlAaTo08Mh6LhGjwB8PJ5p7VVSnhPxouA3pgmCi3KmMKS0G7yEoabP0nhMCrnaSrAnko4SMmkM3yqKQtI71hToiu0ZA7OBS8VKAbjES87885uF2Qwgyi1gW9AawsIFBgkpUIKhFj9sGaOGSQD2bO7Z0QEE89C4lvNOD9v0XkaPKUnvxjQGUeuEl4p7TGTKnl93Jm9wzXXCEkaK4WUSCx2cNcBMDumVZ9hHqN5uDlILchDQZOBvbcsnXwh580aVYvKogUALU4OpAGLvHOgyFfSnbXniHuKZbyEegLPCdkM1ncQwxTMy9044LYg54xgmaEvXWraktYBqVJWNBCPdAbJA2t6IdUCiIyUMs0fJffE3WEU4XyfCdXgHl7ssLnWGJ1LdmXBg2eqkdQPwzGyfc0WhvPaL4Uupa1rN9WKU4c0iIguVUznATAs6LGLNNYp5HtH58mALopUFIZso5DSAkmDIBkKet7eP8OfxlBu1f9wfjRSJ2KZtCFQopL9hZqSn7C83rbx4om7DbWejAACuKJ4p5CzItA5mk4hbkJ1Sr1IqHwq2V3MlYhYZsexJ1PUazIRoN4oI8JosYoF9NvlXaojrN8F3byg03BjRZnGNdB7ynxEcabpI7LHZRUZKaOIRQhOCAL4kxAjfCanqCnxE6gML8C1HpfKMIEnui6wCDfJhfyo1FZCtdR2cKJVmvv3gZMrlIAHj8dCFM0RfHZ8raU9RFacLj2DbjPbY059vio3bAliazeclap5JRadjcsuaw8XicnxYFYpTYCuqaB0mFZA7zbcU2C8lN1eAzCCJYpqL3gwqnd0LdXtGwyqndbl4HaaKwrsG8G9eEwpQy1G17NncqcFjJbZ1elu6I5weVlkkbpj1KqdHpIyWfvMjf7CPAADMuQH1gZ60R3poCr4Lc3G0jX72mH6FdKLRJM5a0AsbJP7XjZmJOZRPNyCVifJOmpLea2aHHFSov7ytkvEt6U5kjpqG5qhYSmTii44yqE2KyXrAUYy34AGPLRmpLgVz77Tt9vwkgjvTYK1GLjbXlYG0ec5fd6RSwef2so6R9OH3l1JZVyn7DHi26h17MM6XZpDEOhalTQZYyXVU57ADvaNnbCuMW6DEnW6CMF0EbWKKsafoBY0NHcIjRnXaJyEmzT82p6JBKdcWnrJ37bUwulZ6ivgiNgoxVTBplKbJKl4yCNzSIqPeVTVoKTXPLfLe8l7EMIad5jVIRZAk0SXV7BuwF0E0j34kzJr20lp98IMK4MOCkCeJHYJRFiJBc9iSKAdEoIbZMvlpKuqktFXrih8A40U2hLk7eA1ShQwMM0YEfp0K5ovBOpJEDrZJY9FXoG0Sf8DxzEXqLxA6FqeYpTuGUhvWM1fn03RbW8HxZ3bDYE9EFc2VSbTNkVWlfmOfCcGJD6jQPDdsg7Mkh0AZCWve4RJsqCLOj5jqTenVsgpnPpOMR9N3X8WiK95Q035IJ09LNRhRB9RM8eKSmpR1ggmHgQkZJskSynbz0AII5wpkXUXDdfKlvrsHapIFlVLllZcvdZwCKc1ir72TIdHbshnwAf0IbYG6JSLivdr4HLDDkmVrAIKaxCLb5KQ0NTfla0HtqrhyEi5BsBWBowFAdU5m82uK5HMWZhmHPnkwR0qZFbjNb804K0cmhgdWXORxamJP7zTDqjxhEOkxmrlv3O4BnzinSd8AmsgZOVXyOxjfm7MLiLhB00UyjDuktL0A92aZVsgLJV8C390IuyRfdLnSd6Dv4UlgSRnONYhHBN1UWajbmKy8K8fzkwAt3kP1I0F3agxGHPhnF58xn1PvCIzQzSbNRsd5WzxSTxenZHb57QTSC3438yYaApTGtLkINSTa1NfpISXOsIneMKiyjAXP8HuB8nymvnqhcdBcbwWf9jytsX6wjvz26mw15SwF6dKZ5GiL5kvzfbWEW14vZ0zO5idmgbQVeJT42HjQ5uP0EuonxadLvFTJGqoXkQ1DiOyO1pqUWud0rnWknvEUOzXEtjlHvBwG0U9vBnJUdqf6fol6NNM85x4LB7WWOtcRDKykFOnTzpcDMeSPwwOyctFgwR5tpdvbhlqqIyjugsVcLf0uWcw5lRibXaYrxtgMXCc6ZMK9IdUwHZ5t6NYrR7X68N0d2k8j2ksd3BnIs2JrgiHdjGtOR1ePkifKTzfbejDznpX0eR3XdHW4l392I74ViqyPuCgNSxsDVKLXnSVH7WEStB1Gmg5PGhatPp6886N5aZZXwFUgOy8Lj2P4V86dw7yhAJWlhJZUXMryPkWBlleXLk4oqXfUCt49cHrFTT3aXsAHhKoKLYg9tpxE1Vtto4WHVAZjo3kcNXBnYWopOECvN0XemWNzVIfQYiCY17Ei2Vk57MmGTBWvZbV93bBSUEWJ0gKybZAdCJKlqBndMngtwvyUD2fcy2ZwAE36lBg8WWRXefmbALu0la0QGl5O2zDc9xyZuqJtjgXu0u3B5A2mTSPWWL6UOnFuePhzEOur8Nqt2ehTSJ4NpQAS70FuX9HX588u8jtvtBmtPofTjKshwCM6BtaO2LulemzcDDBUw5AlyglW91h8cx0q6Cp7Xt79OqCMO7fcebDh6C054Kg6Tox2FMWkxneGf1y30WuZxBWhavynaBstlxXzWm3pXvtwAWdMG2KhCx6vASAuoQ0MNnv8JMTLOEkRIlRjA2iLW9V1CYRdLPhbBZNujx34dfkWOKeX34QW5Nhg3QSF7DhbWoxpYV2ub6eopptGUALVsgZEAitgLoq2xedaLhDPI6pO4Y6rTleCuc6Vcn9UpByA5NtHChMUeyfHuX9b4jUwHvOtwuX5uOhlEeVosPi28YWxr2PYs9T2pyrrTVFpLcOqU9GkyoUnpi47dUH8LXeN1W4f5lia3jkkDJxbpfmod2ek3tjsZWH39kNuEdMgB9H0ZTjR2SSpqwz65McllQRoSbfcSvKkWeYSt8m9hnbR6XJKnxiNPTItr5WzG5c1FHBvKmPFU4T5IvpI8HfT09hWeR1lFprxK9urUiRH99WYBhuuPmn85uDxr5t65D21QTJVaX6d1RbFdeYpXZ0Dgude7I1TCjFfDlf5AJaxm1hc1cOlLJxRDAzV193vNkOwR0gogNvUzsIsjpe9v1hRP9MkiHiiqVZH6LYMT1NdxPJp3stbcP9EAIZCA6OAh43ZGnKSnQdW7kGPghoz0WPRvtkZDfDabxr6TqKH1vEK8252xTX0uiJ7dAk4O8AwZmfrH1QbV2zzSlckP6t47IhEuALfNd2Ya68WtvgiPF11mWbEijRR781IMzLJiHuqw81fQH7X6YDR6imdKpXc7hRDpXlFxrHA8W2lwWwiUBrdJuWud0P15CFOzwtGiLUlApF2sJZAZpdhGOPAzF4FDpEl9FjCs2j0Ml5KNEGHm84XNZgdOE0w5efc77uNFFT26CbOdrEZOgI0CwcZ5orTxpWLM59LWhOJWWVUxFAgvUVXyIRa0KPdoGYyte5DIZr47euZu4bfcEpcR9XE81cXhJn5wIVuLib3U7X0K5NxCA4C8k9GxXMDWGGmB5Y9OxYPlWEVJWXwyZcqd8ZppTDYyuzOZHXGLCsRyhKhgFT8kSduXIoXxm9JNYysYUuOtfgQVbWgH3vY6lrOtETCokphgJp82yNj3soUMFSAZPwXVdglKfD1GRjPr4TgKNkEP1LrGGHA5XB314K8DXTo9KUPTodXm4zY66scvyNQ5lNNDJw3bXJk497IVczkvq4swIfK3bNr1y6ekJ1TSX569vyEKyXCWdIxAVYWZS4aM6LeUlAZQYk6gCQEqPKyCb64X9S7b7CIBNmSJ4b9pQv0GDm46JMDJmBuOS4f2DxTBkNcMJJaXZVjjQJVOG0pSL0yN6vXRHLxgpKIMM3r1maMirI1zcjXXcd7kLujvnhLMD3SbeDOSS9fWjATkMVGQF0M5PEze7xI5XPSAzqBJx7eQyWvAcHL8acuF5ErG2Pq88qqrYMfR1qgGC9h74hpWS34OHaDO2RfiKatvOLD3no08FyCq06zoCJUDOFEDXjFtOO9ttnQaRJIlj3qwWNTA1xUMZwADkaz8XOUZ0NCPkzKSKy74EY20jihkv0KYkUtTGb8sV9BARGlaULlOuQqqiAttrptWwrwXPdJCzaOtNZxnGZJIZGcflzk0cUKBOJ9QreWbEUqGydhTl0xTxpvWtVie8W8moUGTbvy1R63xaGM7AVyKBBThM6JlMOdnU0rMrXEG70lJTAfsD2JmMMYqB3HMrYji9uX0dZ1tJ4qRfplnVjLPksrZ91sMwNiUiBmcXwtFw9gf4QsIAd2lKbwzZOuHizWDyKZMXTAemQhcsqf9sCJ9MjWK1Utdgiz2nyAFKbKTrli98nrrcRqVjjLRInGgleWY2FNwuiYZbF3yTGfd7rNS3DHinDXkUlqx14xizvo0jVeiV6Nl9uRg0OOWwD6528PTV47igFVIGcYLmB27YEkEAkiRfKpeWIQmEThnYGH8TVZ1Yg4FMUmVwErtKY8lghxdTvjlA45fXvwj7PEkHD0FAgciZwXp9GDX87ZB59JhChK41fiK5b30UcvjbttCsUrDGZxysjo8IxPcyz4npQSt1LUlLGvn3irDrNFDMm4ztrMJhBRubF2F4rXssIFDR3LWrlKrt7ExVTUcbTWfb76kL5mauzGcVrcJ4gXEtFrwbDUO0ZfyfpIrtvK8kH4C7M42EHZ0i3vR2v6WtsYSatZJV4bmLbO3rFjlRp8n1NRItfdFYIXthxvkZ2LBsn3j7TDv0gR5L9Q5Tihz06NtjQjgGrZEvuspYyYCem6c4AIMll9H66vsOzvEZlC5ZTqyTWeD2W1gnulABRo1ay8TqrmcaDRDW6rOpWJbBbo7G5eWa3YXiWCSNSGY9fj1Wjkoq1ymBgN7uE3xuOA2Ib7TVd2s75c1euRa18Uo7llcLygTggu3tj1UasPKZ98fUmSitvHZTp6xxYKPyDFPzNMPsbySEUGEGxa558t4T2gfPWnYAKf2nE6PaH82gGgmsM8g6EiuKOrhcxRGKhcb1FsduwwUstXoHNwZl7Gc9yjqHRnDXIfM9VBEq7uZD7WzQBa4Sb0VEZbl7d1H5eCDXRLNf1pH8J0nn7RjHqYhb4jiUKRjcuOA5vPMfntOWhFz9EqKbD6Nt5qJYqieo9WSH6SXS951MQqxTyPobKIZtWaS7mCDqnAiQ6fxGSCku78rgCUnymvqDJKfJeRNZwI7FLfgFnnkiXqOXDIKLZM5ZuNezxXPt2WBDWvx0Gcu7QlypcvUnmjpr1JtEIpkdKQI8tS6q0BSMLn9evOoBHsZa3QCQoOaqAmYyDW5xBQVTmHDXPHbPF7IE6D9hyYcSMF40SMhMiktfzH29FC3GXKeJSL6hXpjS4w7luk02ZNoA3kW0zvy0AaSQxxhcyQvf2Jva3FMY5lKAD0imTgfZZckpBazmWhbzyrAhgZ6kJ5rfCDhaFSP75gAn7VX0zLv5D9OBonHnfCWdNPWtakkAnFZBohWa0vJMejsFVmKP0PTF1p7sGFxzjutb4JSVu8IbDcyKI7iCtJ8TP3JyQ089XUTz5Tkm13zxAAsYbU6ev1y9kOY4ejuJg7w1gYHeUhz8U4dVW6ey0rCibn3SEvXlFdSRkJkBRE5EhLCCjRurNvHO4onYCgOXQW4I9zHtzqzGUcopDHVmXGOD3RpRDwUfVnl3oLPqeVO81ZDxrgLDdYaligO5trTSinj6GeLgQ9Rez57haCUAIXTugXKpQNkaywvuOHJ2U7VAqxip6Yc6hZMKl80L4ma3x8xw68xgqbRfpU9nu2Ycb8FQWR8RJFEXXs4Xr1Kg9A8Yhiceytn11wtuQ5exULSgiBYnAu0I4JdamC1cq1YbwpeYyrNgyd60QaSrTxQqJ2FyYN9OAeyJJC1EehORx9x1LOW2vGhxzuXCeIzOClDuP5enFdFn8TwJLbF2nsmBwwBhW2Yiq3R3hEVvbHSxC7xApcwx17d03sCquGdy3EU63mwzk69Jm3tl0Ao0WebAV34YSTPwu3LESKHAIrPY1Dkya1E7CAze4lyvYN11lfvFgZogRfI18cC0ee6UWhKNFmCOwKK9u96MoxDW1yOtF7gQ7neLvKdf51QXC0Xecu1CKJS6DGy82ZOGzFjkpQwc9Ae11Aebf6JKNkS0P4XxHCpgKH5ZIVk4vFMeZV8siWrr1Nn8xod3uUGS6BOKKtq4FICV4mlVGZmR6EJc0ttUZap8Mv1ZZY19pvx8eQZrCkklot39rtIr720ZJFxH2w7lrHfQ4fEf8KnGzqgCYgnc2boKQVMSRgHZpOFTOFH2v9ww4hbuuMHIIRBWQl2yejhUO3AJBHdFrH3mv1K3HzqaHAHiaucCQ7wKZxPNvIIJjT0IrvNR5jyuxOzAAIVQWLCWYbmXkvfJ7PAsBHXMvUGmrrHZbar98qowgMAkYRUtHL0dQahHfaefxFvG8jH1SwGKPHoGYIJroKhkwh3GNs0Xe8aprJTeOjhSd49RtoQ57kGJJQtnIQR5LNQjehWnTj7AVlnjBgpfEcniP4dHr2xmnyiAxEOSeX2x3McEmhPQQ44BE01T3r5wDEBM2p7qTbssIBAia0RZGaitSWlX4jlJKeemvtYXyVlsnacSTyWuYcKsmrF7O6OD8vu2DWFUXunzCi5qWILhPKKHkvgn3NM5bUovUM1Lv7QYvgz5gt3Ff5WvjVbFXvD3A7XpLc7EinHyUwnlHWxKYeBobgQRFyCLNH04WfqKn9vXSRMt3m4cZ9I3VM9JQ5kADzxEKoC44y76oOD2OjXeCuzcoMzmeKC9ol22GTIOyIOyEqYU6DXKO0kDp5rNvIiKu8GjcXdiDHwQHiKSlXbcnRJ9tvPKuwdiCqyvI29Tq2neXxPFR9TGM7GMBVJZGebZkzv0w45fV0jWCHUFeYw10GjepwECtkpJ6suZkf8zM4kUR3WJkBXQ6MmQMl55QieWT0QvJGuCvxUChv3gR4TXEFXuqv7wUfpXbxhGgninjdIvDT95KipyhRleIc0dymFgztdvQaLb1ROzmmcD2bpaAwPufzo3UlzSSZbKiixIjsc71yBAuU5N1Klf1TApItX7MDtIlN9DGVLWJQPZPwCg39cQawWuAGEKrWBxiJjOdpZMJUoOzCI1bf3MIlLvQkd6A9OURuXRQ73dsEAjLs37uf8wNHPZ5pm2lPXWKU1GDisBrVsGjpN2QrGZWRdpA73JL87zjxgiHGryLyNL1HxWdzJYtZz6OY7moG6u1gyMpH2D2dlLBmyJmpNzZ5AiVpxUehuECClToHw6dtZX1JihpdM8HSIzuEQmEaf6GGRKQMc88FTT0ujyaQdfSGH7ofg7MVHX0GGNxPKFEWTFNk2G0Wtp4PCGD8hnl3aX49Ct948mNIqMAkLuiQLu7EQAAQWRgS8mu7VvcfylYAEdCtwkB9WlQy55HgobZYYjOO2SuvFN7X2PCQQTX05FRpsJ6wLccUWQUbmroiIpwAecHgDW3VF0DJoNdg4grwUVbzYwquFOXoO17buJnZOi36i7HUIycppik4jRRZa8gbCVRQxm6LwWH2HTIWHKlRVAB3HuMNvhggRtYJK19ih2XQCPtOrVAPtOIesOZ0s0sWfTa2QW6RIRzDpqakAnfNYTP4NgfCkU2ip9HAfeyAIv5W2vBasdFAnowG3YcxP9HnlQDJrwFzUtHYVpWcH6y8d3BMqXQ46kfqcZM8YNfjD7vFeUlI1lAhE2caRxzTUTEtmT0m1NhcgLEMHDMX69KpGgalLpmaiXAvUmMMmUOZiC5gRqSC29xEeoLBCmmSpM8FCD21HNKvp8r16vioBbrQruwjIopVqEFv7wIuWWmgXOLk5o3hP6Q1CYdJENUUrKFNTukOYS4qoSILnld6J8wJVXguDAGDiEsJZ5mgtuXk4Rikug2IXtCDhykbL0sR695k5A1ehQira7cQU2FeGyzmwHUvb7D60rV6vgSR9M6Db2xbfyP52W7U0dfRmnHt4geTZVB3lZOofagHP0Bebqn7zzQlhPpiU0oMDFv7ZZFGs7lYuj1QQQnW93q0tBIactVgF11jBpgiDWsDXGHPToLlW7bIQs4wTco5zPqzewj7iTEzO1sXb0tzxAEiE5yrwip3Vs3SQHptznnti8aGiz6k4zXCjYEIQAHy4CthaAw5rHY8ZwABoiy0EDPkaPLg1dg4p90j8efYF5uv49NMvcZbNtvyXIVmFl6CHunq3pdGM9uWE5WlXUZek3ZcFsLbxioCxktZ5hEVBHWYKjXQ0mmnfTtpXC7LCa5hhF0He3yC5ifVfrDsOOoaU5UtMqrkMDOInmhwVeAtuoUSRKJ1jLITwz7KqvN3rwYfcyXwU3DZVlF3yS7KblSL2YydeRLpa07YeEVCzR16MPtZoZ54Q8E9gN9CsptIxnl4WC3mUbM9NC3Dxw2hPdWaXKRl0aUitysWmotnqHG1TYTja7txXiDLxYxS3LwCTCEKUCkgFVtI2Tw2rLHVlU51P8lqc4hSHnzxfG9wKW8fdniqLf8WRtwDRmOSaLjxlnJCFBYxekcF2rG5B2vstVDwsDHrsRdUeyM8GDmL80PAILTfJJPzZ9NfIN7p39O3uozDfMgLmPoXkW4j9H8gtZG1Vuv2YyadIkIPht8tMM02GSGsA1tVrrrl5HG2TGf4xXbCqrTZb3661id3fmZoyB8Hjmuj5tyYlg2OHjvmuMxhv8hnhiOnqV3sHishj47uS8UuYdrqYpFuqGDZ68xh7wo5CrtGbYoApkiZfO9x9iV26WeaYCon6IFZNQhkGscRHpzqNKPiRw7gbvc849ZeakF6x3fIzLNER7AHIEUUZ9TYkQFLTcSmcqg2qQcYYfB4VCOhOxIXcAQGuQQXdPDOIGv9dlGLIrSn4hmU7ZPXtKe4Th16s0vDZSvqBAUZmEguaoe3O96YrjAkNs6MlshIK2DBj7ljRMbASrVbND2yXonakWatP7bqRgPo6hGLjia6DeA5zfnq046Vhcrv4Km0OFbBNCRJiLhJb0wBZPuJoA4MBMcfEQBqX2y7NB9Folsj1GvwjepTrLSoujnF1ed6ILgGQDOrdxsDVTtWI7ZCY3YVgHfPKrQawc7uSqqdq4DIC2yj7KyukA8KUJ7RNEg9iX2lzs95TZkwLTXv0EFPyhPjCdY2Mp9a0AfObxwJxJrc87ohxtUQbqGKdlHiAeRyKPzbrD98Byvnoq1DnMHOemlJUJ0e9RTmRb514aveYWpkzvqqm0izSP1eXDlqc4ZgpbopkVxWuiY5YHsqFkkHUk1gv8KL58ma7d4URxAwngub05sM5bnXnZD119jsK2sMowgAdWDh5lMnv8bnNGS9Wzoa7WHuzaFlCaHDvd2YUgeJRtCpPXwOUhUTokhu4xettqBV2axp7nzE8E4AaXY7EUyL04uQynfGgY2q6GzdruwJ0ZkDakajNVaivXuKhRjr4PZU7XDaoS6Z5sENnz3aCK6dMJCqDr6ZeglnC3SZJHRWI2WvtQ6rDppgjel723sYSBuTmTjCBHQpvDqWpLWhMIvSaqgKf9PXm6ecZw3491H2EwPpozScLBVof641ZF8IGRH7y8NwZwIEhR3TutxjsuRrgWGLJpylNZJkDnvFXIx31DJdLNpoFYf2eYKFB4qaD4428Dp1ltVBPaSWh35FLZfbFhjiT9ZwdBYlbPPd3blkTb3aFHjWWtgqmXrqUmZvQ4fnQTIa0zJdn35o39r859Ow5GPGxGdGHKRIi390Dtiao3y3PNkpgZtgJ6NChf4YqFRXnXa1WHEe4LKE6LMJRrXYVURDy6iwCDQOnT3f0PO4ep2cZ1Dq4EyGDfOWyPZauLJTUuRkf5PU5oR8eifMJGOi3VXqZ9D5OLsjRH1GeRJzV9j0MMetTtdS0WE3OJj01LK1fiJLgCj93cGz1Nqed65bv4iPtJ1p9Ozcdhd8cBXiHYa2XzWtDpeKj3wZgp5qSeor3mvcD078CrcKVAv01a76M5E8gb2dhfJI9D6YIfQJN2gJWWJfleCTbu0nmYTpWXo3PGjyiAxoRy0u0IDVMHGXfSbDLEhmagvjGWYeVFqUgmqYWe2rbApRj38kaPsOYpdZ7MyqiEa7QOUbugYFhgosWIWoW2etYIw4KMRSVFTAe5XZFTWGIjRrDbE0eVeqPO6kSxS9jRMEcQk79c0x3X1yD7smfeEc89hoLhQ0PyNTCpSxY8M7EHKEEgvwDR9jIsAErmkTZXf0anQhbOQMjM2niGxbDq5R0sDlu1IHVpY5DteVEbPAojFq5PpiEMYAnaGVT2f4aWxN2Qaoe7jXggWcsQligOx6bO940pHBDUtrw3jpFq5C9xNHC5RpUTWlpX24hc8cSZkVxjQhO9Y441JO2heXEFmuiMJIu31sNSqXCJk2cjyyg9A6KerDTuePYMsVV524cZCVBU0I7P7zPQkMvYOiDa9NdR37D98K8GPyf33AH3ESkgIUUsKjnr1L9Z9FLFTJNALxZE4Dwc6lK2UUI4hWXMQdFdZU7DgFEEEnNyfW48Y13l1kSD9PqkvFdgw2LNQTm7F1mSdkraW5T1AhKOKvvsakwZpHc3SJsaBdHqyuM1nilonfkusSW8A6TPXvPRBBdspuCBA8fQmSKT2Gk2yQag34zY0MudPBHUZIEvRHOSTDA9ljjF8YqhPw10bms1crUbVHPr4rDFEQ7bheOMRXwvqbmMZ7jqeAiMKkI5m42uHOuu5sR16YZq1bQzxPMBaXdIexA2Lqh7ZNj0jCseoUcQHawIhwrGuXJSjTXBKW2qbql0paVtMiVjkzHYPEXmH4Xn6BmlY94mfaityHYochAn6HglP4EcHgiLdkgeicD5QtFWZXBJC8rg8I6XWaybZBaibKEdEpphWJUuBmlHHmCweCqsIyn0sznZTrVFVLmRgLzLEp29gguyXFTWgY7UjxUK2NZlI4PMrP7okyE1tpXPzK2aZZJLv1jhYy4MS8GPQRfweTRWdFymHXE4MQWlB3gltw4wgXmTQGhilSWN49WBkmBnyC9JCt9qvhDQzfY0PMWb6aj8nyjFSwzSZYvGZAcjE8gfF8eOkzJgQVdYaP9UtwM2xnARFNAxgRptWIP0LZuc2y4AEO9zZpxWxYQ3s4SGJsAj1wMUvFaM2pUlzmT5QQt9WFBsauQTnPtbbEQKnDMtqZanZZ6S4ubF3UUXEjhfxXkymAYoRiL9wT0jMlamjOV0m7GDnk5uFgZpBR7sNFs1nbfVUsMvvc78OKd2jMaljcgQPrv0f1o5vWsBACpmIS1pFWe43UxFbRYoMvNpGkSjO2rP86CUgoWL08x4kb8q844l0QXKVJLXVdiP3tPvBwRkORRvJW3DdUCYHCNWhbyBtDl44srQqlij3NKShVA5Jjbg9M8OnjoTvtUGUERfX6k17tS2ebrXtSjsLvgB7LTqM5DyjaCvYrHDBxjeG2NCNxi5B3b5cFvYv10yu6FizReSVzT2hPDmn25OEpDweMufP7nJwZguv5lijOuiwyiodcWgGnjkyV2FSBWmjhxeVMVqCO4tN0hTWTNauDgkMffsN31N0sK4wRtF2pE5hChkrqRFVsiwIUHgQVSQ7S8jKMIKEjqgZ7z0erFB99OP4CL8E0LXBAEtm0i4WPt6GefKWOWffV2dn0SZStZWj4cWfECI2WsR0yZBp0VodnvHxtEG7Us4zS5ak6kz0CvgqNiAqqDZYrb0aqSMSsOzCh5AtK7lMWG36JT3scb4x4caZhX16YdbN0wyhEFbI38s5ccyeGfiJKNWvWQQShkAtpFeY2l7dAQqFB4gJ82quCh5tT0tM7CW4ZnZdp3PD2bEx2AxhhO9jyO2cOCCnQGI2c1uuuhUZo7WfdIz8PNHE9GcFf6KJnKuSJU5yU5WKpiaA0W7pHgf2oFbTHC1yFEVEA2B7GiGfRzx9YUyfJ34sOhF3XtKhPrwkoWT2RntC3HyGvI5Z4km3djklKC4KXbKisb7EwortN8uNp20pQwJjArrCDiUrXUc6OnfLtMdlKzTEEQmTmwDjDRzFKP87ZKrBVMDFwYVAeKSVVqwQR6nkWj4Wmg9BPcEtECsMsklL7pywNBus8jH7fKaHIEcDJ9pAVUzaVaqN4GYmDn7UW5zzOuMYXQxXAiXFtaFT5nLEc7gmxhxFEaB8V6qy8mS9cuUECupd6WCodr3a567t9JZCHl3EFDHEqWFqEPQibMElzlaYRG9Om9pV0CUZYRIg1WG9KBFL10tBIXIDZ51YALQCWSro35KaUYBlSOOqBI1MRiXBkWztrcGgq0Ru56pvXrJta44Tl5BPs2yYivBHJMWdc5ivLMF40HUoezUWFd8qvQV6HUEcfNc6PHByIfdcQNdOg01qW1Am0CspkPobZupdhfMWunZMvvAqIvrktj6U4QIwoEjOZZdlxKMJefCq4cUv5WG5fOGWXslakgqvneiCd82yLxTU9kAgeD4HnHfwDQne1ziYjYA7LE98upgreh3Z4WFyxUHevwhDoK0koMJsNuQizOBpoSYxUZlaPNDRVglYv3NEMnw9sCv9BZR9XjRFdBddmmE0Uq7dPDdu4zmj1gtkM39GeEuwFR6w55dbCotbw7kL60HqHE8XrCFPYf9oCFxMRvrnwlGM66xDuF3PzSGUzZiUsSTd5aMLjcGgGJPb3q3DEDayDUiHlaEitbEIJoifqZdErNtWJOI9sNVWZpvvFNMjOOuTei72OlWHxwqHQmwePLWOlDzuboaJponXbekS1dQOjcB3ulbihVW5D6sTwW6XAosLP3eAFtQeUrKzqmwVnBxP3z9DQb3oN2QhDBptWpdV18CyG6KI6NxquiBu5MI2XFbzzQu3FGiyQYypRUCz6wzssK7SWpLggg5E0FRpGXTBPRaGQjeMBVCrXMTTEPNu0uLWAQIlzo8BTsmk6QDBjJWhdi8MJvdea9JSQL3fWfisDPxj37HVzF1pxHarYCkzOJu6JAYfIAWImNrVPlnNmRwF4VxGEwEXStYPydNBG3CLn23tbIsi2AX7vVsOW3wu2jHjuRcynDHqONtvf6rMrsDmLizPuqNqmkhep6B2d1XOJc0gBEP7EYRyi67P3oi0uCmBOXn8jg8mJh3klKB0qTzZKLDXQwLXYx6rNRxm2wof3fbs9DCqfiSuD1wcCB1qHcS7yKlWbRx6uhQKMHceOeg4DpgLIbqv6zWqf9JGIxQAehoUWd8JllG7iWBZd6jfJeoyDsHIDrxdGYGDSnsQlIoZVzvjMRhG48XkTBTJbUpaunVji06uevAjUKAE8VzgGonGFfoJ4A2SdP6xo6Fp4SkdMoo0hqriQbYAFlnNhwTnlmBEiFflIsIpmR3ez3XSQT81gazNFO01hOte60jaqpRbhTy6EE5hLqsctBnYLI64G7DbFMGOtkX1Tg6FlGTaHLL8MaaF2VN5h7j5LeOdEE1XxAMDGCATFtqesMOglNnGY2ODTxr46ofNbVT9QDtJqN7bjsUSPgnml3g9ZmyuGkF7Vp56RqWHGpV53MakRjeXJouDMXpYsTSBtcUxsfK3uaEQKCNQQQ7wzdF416r0lzaijKXoEYbe8untgX5HcVA8augIpl0gN37wnKntp3mUhKpxaUihor2Y065nONvXGsweTje3jnxO4azGXUPsVusNDpeA4BdBH5aSMFfszdhDlrQ1HtiDKP36Fev7ZieeJESQpHrln8lOCn5IbBQr2Wc62mc3qOwE4w8tjn7S3Rq9DETom615uUUx0RXl2z6u1eUZPMgKloc4ufX6psznLwFXc1QtsEafiHu4aTxVbXQxCe3xmPc1gzv0KlhWec3dBNRuWOn2l3CVKA3g9Vr1tOtfv31jihtnmF9lCxWP2CmSGxAlddNSESJfPHgIvYEffJ3Pa8ANiVHNoloJVMfveY7OgfwvJT3xWArMNahBpsvDBqe3qE2qzhwksWPFV8vCAcXKM9esIbzlscqGIRMM1FM22LSkTWxm20RiuuXpGkSXtTOimcU49cYnOLhdWs11Oxh8Wxo3MLTzP0WJji9Fl0WN09jWR4hPb0KoNyaWFAZuG502Rd96LZXgtNpRq8zp5zF2ElUyrDJOprkorJfkYfLo2SpsbGA7s8Pr7CfJheKrYXgpHUAO7P9FR02sixis1dJsn7QEdX9kyU1SrBNt0l9xdAY2aXfTQwq6IlEAgZLeTlPY9TRZdt9uxjCRxuf8yHiNFEFfTInsYPiGkqoMyXfzUTIRruYateTuKCEpNuwwXGxLz7wTJp9aJ6O5j4CNr2Uxwbx7jiQkiHldlykjlMDzWO6B2sqkK9D2TDUNY2OZljaC1gOM8y62ifBFPCCind1MqZjMQuhsTPti7e7aQ9ttuG2C6WbQP0hG20EzduuJyEyZm0d1wgLQz5wOlYAWUdt4g0SAM5GD7R9W95MMdAPgKSbYTMh1xiG8BgjlCX8lxxKaNc5oWto1L61fFhoP8KP9TqgBmgOlLaAFtsV1sKQFutT2YV1zhudjAtqCyQ5Ni5Vb1b93lL79eVaVlUNFTG40mjAvjomqREUHkAis8iC0KkXc6ZKl0zyU06TkYZVCPBQIerOKgYftx1L3ZSyktYUMDCYq5rX6LJUwHsNVzFCQTdKUf02XU770c6FO99vPwetnNpkoaSBf5B3A26lWT4nnZ35PLWIBj08NKM9x5cXhvQOz3elLpvmp03rJgjm5yvM7cIJDABzKxg4DcbB056wB7PqfE9aoqioyFd5rWiakW2ttYdPHcmZGRr2IIHpGJW21nhQIV2unZsKl9VTtKpUrLQvVKYiQ9DfQjbmWWAkbvcRRnWXfamoNmux0kpZfEANQfefTt0KPUtx9iH1mkM08uF7l08SvCaL3RIQZVONU395Qv0OcfHTzhhAUbO8NC8RVaIGaSNVo9hsjPcmNjTcQZmrrC5UcXGGzbx6qCnshGGGC0ukU0aEqkwhHfNO8QXJoELhzyY43dtMcyIRpT0pn2j5vehq37Pqrxyk2FEwEHBwaK0tD6VgRTCeRJBpaDgzIUUEWpLtkOteWmgGx3az2NONmhINxiAghlvqHqiZVrTltR5tC2xk2LtzNKIQBiJ202Px9mHqcF9a2BlR7equK7PYLfAhdefsxCX6j5IZnWI4yMaQWTrPclax9w4JxEir9FkP7qRqo9TL6DZafLhota7Iyu6DeJWXDpzH5c3SfE6oDHbtuxcTleOTO5eWHWgmgqbQRKzy08N5gzRnt71NakEEzMJpAJaxQTxJnXjBq1EfAZvjI7TT7IXQvVSyxLax1wXKJFJZU0O0wJxI7I2dgHu5NCehSTy1yEsEI83wFRJwV31bSLVvD14ZveObso06YrL0IDJ5qhSq4osbBBIbAN8IUle329vINPUqKRXUbBOJcc3LAyJdwAvq1r5Dg9MmWFYYB6TiDmnYdomd6eNNvV4PJUHkd4jX37Nq1QB8WmQb47boYF4xXbQ90cfL9ksMO0lNT0cIwRMZSt0vRh90B8laaLHw9QJTWR5O38Snp3s8myty9zHmF9MUyOC2xWu8tZNSpkzUUoNyHI5WlCZ18t8u5JQjavROEOrkfNHD0aeRV5zJVP0mu2LTh38Ru7tMVKuKrndSN5J6w5Zb0dNmnLP8d0vMiaUrqTKtSoavBrFvYsv0YscqbQNiTUptPvg1GxsnxgGkxoOZSjhs3Lf3SeBMbzjbX7IQ2w1n6KsOYUZruOqbv7zJItLZ17xPXYVeNzw0BqAATblXj55Eim2rKuJ3LjKj5zSWpN2clNhXGy0Sf1IWcTfDxH7IKeFAyjXrSk5A8MSLai5yResrsQbPAeYhfC0nUyW168xIIN0k04FcyRnanIZYIv8rYZJbyIEG8Ar2Wo2rRweXK8T8YxVBwUWSGXgrRGw4xSwA58scwX3deY2OEi0KJdwqylULQ1hmxR8sMJPF86P53fVwTNVy3av1aZllHhqliDb2A9RTz1Kzv4Vp5jm0pwok4ECGUVzgkLzBXzZMSeDNKAi6vUClqscIlIkuCNjjq2aSRjvylSM06SmM5t4HNtmmE2Mt4ay2mHnUvdZEVZlquS9e5pkOZzvDKfgBKzfVLidiCr1Wi3Ihiw7oB7Q6f0y88rfTSP5myKJEWTQmr0hzd9SvnN9TEXkzQh75hxdDcYRGrR3cLCqWQw9vHf5LNcnUc4DuPyZ1K8CnrLtUTQsY0WzMRW8a8D8kCqrz7FwepoCCy5uXn8dagiHwiwD9BJtHDXDAN38rNWvy0Wup172yOif72WCRdvlMqyTbmraotRE4MjEe4R9kVYDMCs9brXJhfB6xK56Cl7xFAIPm2pZTj2pWmwsCIk3nVCzxjJUAYNHOWJPDg0zQR5DRVSMTNswlRomjk5wjQgfnN57PAorTPW7F95whCqjHWxp6vApVUQxOjp2GfMYYdWA0y3JqEfrIsovuSWfBtcc6tVGAz2Feu0Te1pvb4ngpXKos57wccifAqNdneQmjP5cAOKxNddK6btFLqHJZiVLrlHjtb2pUJZqZdNWwEdM7vpijBHKAudieZX765QuTGi1rN0zaxZjoK8MPH7ClRwVeRS2Mu9IMTGC1tXlrRSWS9FSgLHuH7HX1hovo4kCTs9hdP00zFPkDRfGr9AI6zjYNPhEehRU5EMinMwYfIqEpU5ZtfkIJOmVYPvjk1Ru8uG1T5k8IOBuiQExHTbuXnstEt8TGcRsTxFK3tBqs62aF81hRNOEChgfsHXOk4yfM7DPMciiK4QsABK0ZJ7uYZmEIrl6eAnTz2YlkdOAhZJrEQIXS95jKCSUHDnBKwfu5ZH23exqTGNh6bF0850tWi9N3RFivzgmyI6Yn6wn3SBJj4ps3Fo8z0dPUpXjkOV1IXoXn4n8BsK8uIX2NRw3jbKQlwIQAY8k3KjSXPVc4yKRbYFlrB7RBwqPYIdO4193O5FIUDk87OJpNSAQugNZcRUHvA3Egg8yCZBYlnkHPQCX45GrmNWWFUeK4RCSsbdZSAPP25ijDe6bMGX5iKr2ufjllXuZm2ngtgVxfhGbN8dOgkeR2UXwcKVpHPcK2CkkFxyFwuqER2dmWUxAxFnltXRh3JjW9LC5WPNqO9vcjzWNZAJxRQfXMl0SdLltgq6vto9obI7WtQFivAidOiryEEkhZIGYPlEYjKXt7QiwUO5pzsAOt26hBgXrtuIBVSWsrtMKsTZAHORJBBRYW43uzkDlRkKp8imrQA75CRWRobRu4lPB2bRaHOP9dw1AlxDJErDde3pqb2iXibw7WDNdeVZ2ZamJhjvIAuSBo8Y5GnfDSnMnfEIrYl7loINEgGCLa5nJBEGFN5ff2zp7hVP6PTnSQAvKJaffYmmp2Ra1Mwxm17L1BmIyfjMasv4Kc2cAKQIBtLwPt68mw9laHG3eUVs9mEihSOgtjTWAQrOsQWzc5X2G2dZwafEvthLUyIvVs9JtXxVajMynP3dDE2t8eiiHydt3SeEbP71GF8NeZrphDZBibyzspRYMZ7MLDkNek3u6zPBuVNRvCfyV0e2WRoV2fFwFRdlL0GyPc5L2cdAo3vFg9FabPB8SAYcXfCWXAYXdUosGphy6TlaBRkBGYnNTPR4boEmNkszor7gQdOS93ZnSq47BBIH91W7BDEOiqSMU1gbKzJJADGhidl95jlOzYYhMGCAkAyZzguj0niIfdXEkHF7ILAQPn9WumGi4UDV2az4yZtTjIJNB9j4pG9JxPM75hC03BPNggc0J7BUHuXxZRwGCHvW4afzcmEL0bOcD7PQ0WQpey3MdmIdVRx84BssnAbODQ2urfKUTFfcdUfnyjMRp6DnLK9uFO8OJidp4Ib9GGqUuHxfUKtzjdHUvVaxAFXMO3wTdXm2P9y8s0sUcO89az97m4dyEeYncZZD6dXxiboO9zFcJl4Rm393aTBfDMVHhyUvc19yXNLeXuY8Z766V9zT52lJJgnKUGOmUTKArylH3anbgk6yoPxvc2dxIQNETAIfnvwIoCKtlcvYuJGLkPaj4cehHKblS70kbtJUYIkmLa3T3kFnLw1fPOOeTBLeqHSFk9HqhnpmCjH4WzBFGMJR1PS09Di7DABDty5caUzBXjNUGwHhxmIU1OzsiNVcO7QJiY6UmohHgw01cgNVPiGeWteGHreyzX89HaW5HNH4y1ZKkf0nMOwtgQxCg5RNzSJU2HLTSwwiQvrsofUxdH2gPIOfbF49xMYPEMkoFs3BzkMfUEvFBHizct27suTYyTaMI7sb2RzQPnAtNH37jwfHncgPsWpMeUCMZAArDYL9if4T6XaGj1ZRKImOyjcQUlY3EKN3mHSiHkbdpIKblYxeATtcDsOFfwPLJOCboQcz7NRg8O8Fk43BNfrFzUXe6LNUURWdM6Ak7xlvCVRvcPK895oXViKNEHmiGjfO3H3mb5hLr893jFbWvoPwLeiLYLv19PaeL0q3aiNeAYXIRhKOKfOWInteWtKzJ0EAtPleSQbAk0i2ViN2Tfp3eKzNNGKTTEL2QC5Txcm23S1JPjJNe6BVI1rLgQUUj2FZgPu9lauRcFX6fOVIvPJVswNYSd6Lc0n0LCnL4O7RMZ1dGhg3sDqJyXJg5dGNPxrXCPhvWMMi1bPIpyK1EiaGQ2Uiz41xZPRtEtWW5exh6z8B3ltKMX9cCFN0FkPcBxiyZsBGJHcbzSpAJCBdLN3zsS010vpUh3K5Qearx12NzSC5odLUCqrlnAy65XOIWWfYcrinKEsBoFYJVOuKHjBmfi4bJTWscftvYNaw340uNRFwuYZ7zumK4xo0hFx1arCwggrOnIHAWhCZLM7i5oklhLNYp8oARpQ0BwrM81JQ3cSG9lKH6R4nCBUWpWXuhNfyQL8sgHQtURdDUiQoyGprNGxXu7zU1z + +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808 +RPUSH 2-1 0 -1 -128 127 -4096 4095 -32768 32767 -8388607 8388607 -2147483648 2147483647 -9223372036854775808 9223372036854775807 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/list_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_dml.sql new file mode 100644 index 00000000..68208136 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/src_dml.sql @@ -0,0 +1,14 @@ +SET set_key_0 val_0 + +SELECT 1 + +SET set_key_1 val_1 + +SELECT 2 + +SET set_key_2_0 val_2_0 +SET set_key_2_1 val_2_1 + +SELECT 3 + +SET set_key_3 val_3 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/multi_dbs_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_dml.sql new file mode 100644 index 00000000..1068b3c5 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/src_dml.sql @@ -0,0 +1,61 @@ +-- in redis 7.0, RDB_TYPE_STREAM_LIST_PACKS_2 (19): +-- LP_ENCODING_6BIT_STR: Used for strings less than 64 bytes +-- LP_ENCODING_12BIT_STR: Used for strings less than 4k bytes +-- LP_ENCODING_32BIT_STR: Used for strings less than 4G + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +XADD 1-1 1526919030474-57 f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +XADD 2-1 * f_1 value_1 f_2 value_2 f_3 value_3 + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +-- this will add 2 entries in the same stream +XADD 3-1 * name John age 30 name Lucy age 20 +XADD 3-1 * name Mike age 40 name Jack age 30 + +-- the longest string will be compressed in listpack by: LP_ENCODING_32BIT_STR +XADD 4-1 * f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" f_1 abcdefg f_2 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 + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808 +XADD 5-1 * f_1 0 f_1 -1 f_2 -128 f_3 127 f_4 -4096 f_5 4095 f_6 -32768 f_7 32767 f_8 -8388607 f_9 8388607 f_10 -2147483648 f_11 2147483647 f_12 -9223372036854775808 f_13 9223372036854775807 f_14 9223372036854775808 + + + +-- multi field-value pairs for the same stream id +XADD 6-1 1538561700643-0 a b c d +-- multi stream ids for the same master key +XADD 6-1 * e f g h +XADD 6-1 * i j k l + +-- empty stream +XADD 7-1 MAXLEN 0 1538561700643-0 x y + +-- XDEL +XADD 8-1 1538561700640-0 a 1 +XADD 8-1 * b 2 +XADD 8-1 * c 3 +XDEL 8-1 1538561700640-0 + + +XADD 9-1 1538561700643-0 a b c d +XADD 9-1 1538561700643-1 e f g h +XADD 9-1 1538561700643-2 i j k l +XADD 9-1 1538561700643-3 m n o p + +-- XGROUP +XGROUP CREATE 9-1 my-group-1 0 +XGROUP CREATE 9-1 my-group-3 0 + +-- XAUTOCLAIM +XAUTOCLAIM 9-1 my-group-1 my-group-2 1 0-0 COUNT 2 + +-- XCLAIM +XCLAIM 9-1 my-group-3 my-group-4 1 1538561700643-1 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/task_config.ini new file mode 100644 index 00000000..48e72dba --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_stream_test/task_config.ini @@ -0,0 +1,35 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +method=rewrite +url= +batch_size=1 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_dml.sql new file mode 100644 index 00000000..f59b9712 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/src_dml.sql @@ -0,0 +1,519 @@ +-- APPEND +SET 1-1 val_0 +APPEND 1-1 append_0 + +-- BITFIELD +-- SET +BITFIELD 2-1 SET i8 #0 100 SET i8 #1 200 +-- INCRBY +BITFIELD 2-2 incrby i5 100 1 +BITFIELD 2-3 incrby i5 100 1 GET u4 0 +-- OVERFLOW +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 incrby u2 100 1 OVERFLOW SAT incrby u2 102 1 +BITFIELD 2-4 OVERFLOW FAIL incrby u2 102 1 + +-- BITOP +-- AND +SET 3-1 "foobar" +SET 3-2 "abcdef" +BITOP AND 3-3 3-1 3-2 +-- OR +BITOP OR 3-4 3-1 3-2 +-- XOR +BITOP XOR 3-5 3-1 3-2 +-- NOT +BITOP NOT 3-6 3-1 + +-- BLMOVE -- version: 6.2.0 +RPUSH 4-1 a b c +RPUSH 4-2 x y z +BLMOVE 4-1 4-2 LEFT LEFT 0 + +-- BLMPOP -- version: 7.0.0 +-- BLMPOP timeout numkeys key [key ...] [COUNT count] +LPUSH 5-1 a b c d +LPUSH 5-2 1 2 3 4 +BLMPOP 0 2 5-1 5-2 LEFT COUNT 3 + +-- BLPOP +RPUSH 6-1 a b c +BLPOP 6-1 0 +-- LRANGE 6-1 0 -1 + +-- BRPOP +RPUSH 7-1 a b c +BRPOP 7-1 0 +-- LRANGE 7-1 0 -1 + +-- BRPOPLPUSH +RPUSH 8-1 a b c +BRPOPLPUSH 8-1 19 0 + +-- BZMPOP +ZADD 9-1 1 a 2 b 3 c +ZADD 9-2 1 d 2 e 3 f +BZMPOP 1 2 9-1 9-2 MIN +-- ZRANGE 9-2 0 -1 WITHSCORES + +-- BZPOPMAX +ZADD 10-1 0 a 1 b 2 c +BZPOPMAX 10-1 23 0 + +-- BZPOPMIN +ZADD 11-1 0 a 1 b 2 c +BZPOPMIN 11-1 25 0 +-- ZRANGE 11-1 0 -1 WITHSCORES + +-- COPY +SET 12-1 "sheep" +COPY 12-1 12-2 +GET 12-2 + +-- DECR +SET 13-1 "10" +DECR 13-1 + +-- DECRBY +SET 14-1 "10" +DECRBY 14-1 3 + +-- EXPIRE +SET 15-1 "Hello" +EXPIRE 15-1 1 +EXPIRE 15-1 1 XX +EXPIRE 15-1 1 NX +SET 15-2 "Hello" +-- NOT expire during test +EXPIRE 15-2 1000000000 + +-- EXPIREAT +SET 16-1 "Hello" +EXPIREAT 16-1 1 +SET 16-2 "Hello" +-- NOT expire during test +EXPIREAT 16-2 4102416000 + +-- GEOADD +GEOADD 17-1 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" +-- GEODIST 17-1 Palermo Catania + +-- GETDEL +SET 18-1 "Hello" +GETDEL 18-1 + +-- GETEX +SET 19-1 "Hello" +GETEX 19-1 EX 1 + +-- GETSET +SET 20-1 "Hello" +GETSET 20-1 "World" + +-- HSET +HSET 21-1 field1 "hello" field2 "world" + +-- HINCRBY +HSET 22-1 field 5 +HINCRBY 22-1 field 1 +HINCRBY 22-1 field -2 + +-- HINCRBYFLOAT +HSET 23-1 field_1 10.50 +HINCRBYFLOAT 23-1 field_1 0.1 +HINCRBYFLOAT 23-1 field_2 -5 + +-- HMSET +HMSET 24-1 field1 "Hello" field2 "World" + +-- HSET +HSET 24-1 field2 "Hi" field3 "World" + +-- HSETNX +HSETNX 25-1 field "Hello" +HSETNX 25-1 field "World" + +-- INCR +SET 26-1 "10" +INCR 26-1 + +-- INCRBY +SET 27-1 "10" +INCRBY 27-1 5 + +-- INCRBYFLOAT +SET 28-1 10.50 +INCRBYFLOAT 28-1 0.1 +INCRBYFLOAT 28-1 -5 + +-- LINSERT +RPUSH 29-1 "Hello" +RPUSH 29-1 "World" +LINSERT 29-1 BEFORE "World" "There" +-- LRANGE 29-1 0 -1 + +-- LMOVE +RPUSH 30-1 "one" +RPUSH 30-1 "two" +RPUSH 30-1 "three" +LMOVE 30-1 30-2 RIGHT LEFT +LMOVE 30-1 30-2 LEFT RIGHT +-- LRANGE 30-1 0 -1 +-- LRANGE 30-2 0 -1 + +-- LMPOP +LPUSH 31-1 "one" "two" "three" "four" "five" +LMPOP 1 31-1 LEFT +-- LRANGE 31-1 0 -1 +-- LMPOP 1 31-1 RIGHT COUNT 10 + +-- LPOP +RPUSH 32-1 "one" "two" "three" "four" "five" +LPOP 32-1 +LPOP 32-1 2 +-- LRANGE 32-1 0 -1 + +-- LPUSH +LPUSH 33-1 "world" +LPUSH 33-1 "hello" +-- LRANGE 33-1 0 -1 + +-- LPUSHX +LPUSH 34-1 "World" +LPUSHX 34-1 "Hello" +LPUSHX 34-2 "Hello" +-- LRANGE 34-1 0 -1 +-- LRANGE 34-2 0 -1 + +-- LREM +RPUSH 35-1 "hello" +RPUSH 35-1 "hello" +RPUSH 35-1 "foo" +RPUSH 35-1 "hello" +LREM 35-1 -2 "hello" +-- LRANGE 35-1 0 -1 + +-- LSET +RPUSH 36-1 "one" +RPUSH 36-1 "two" +RPUSH 36-1 "three" +LSET 36-1 0 "four" +LSET 36-1 -2 "five" +-- LRANGE 36-1 0 -1 + +-- LTRIM +RPUSH 37-1 "one" +RPUSH 37-1 "two" +RPUSH 37-1 "three" +LTRIM 37-1 1 -1 +-- LRANGE 37-1 0 -1 + +-- MOVE +SET 38-1 1 +MOVE 38-1 1 + +-- MSET +MSET 39-1 "Hello" 39-2 "World" + +-- MSETNX +MSETNX 40-1 "Hello" 40-2 "there" +MSETNX 40-2 "new" 40-3 "world" +MGET 40-1 40-2 40-3 + +-- PERSIST +SET 41-1 "Hello" +EXPIRE 41-1 10 +PERSIST 41-1 + +-- PEXPIRE +SET 42-1 "Hello" +-- NOT expire during test +PEXPIRE 42-1 1500000000 +SET 42-2 "Hello" +PEXPIRE 42-2 1000 XX +SET 42-3 "Hello" +PEXPIRE 42-3 1000 NX + +-- PEXPIREAT +SET 43-1 "Hello" +PEXPIREAT 43-1 1555555555005 +SET 43-2 "Hello" +-- NOT expire during test +PEXPIREAT 43-2 15555555550050000 + +-- PEXPIRETIME 43-1 + +-- PFADD +PFADD 44-1 a b c d e f g +-- PFCOUNT 44-1 +-- GET 44-1 + +-- PFMERGE +PFADD 45-1 foo bar zap a +PFADD 45-2 a b c foo +PFMERGE 45-3 45-1 45-2 +-- PFCOUNT 45-3 +-- GET 45-3 + +-- PSETEX (deprecated) +PSETEX 46-1 1000 "Hello" +-- PTTL 46-1 +-- NOT expire during test +PSETEX 46-2 100000000 "Hello" +-- GET 46-2 + +-- RENAME +SET 47-1 "Hello" +RENAME 47-1 47-2 +GET 47-2 + +-- RENAMENX +SET 48-1 "Hello" +SET 48-2 "World" +RENAMENX 48-1 48-2 +-- GET 48-2 + +-- RPOP +RPUSH 49-1 "one" "two" "three" "four" "five" +RPOP 49-1 +RPOP 49-1 2 +-- LRANGE 49-1 0 -1 + +-- RPOPLPUSH (deprecated) +RPUSH 50-1 "one" +RPUSH 50-1 "two" +RPUSH 50-1 "three" +RPOPLPUSH 50-1 50-2 +-- LRANGE 50-1 0 -1 +-- LRANGE 50-2 0 -1 + +-- RPUSH +RPUSH 51-1 "hello" +RPUSH 51-1 "world" +-- LRANGE 51-1 0 -1 + +-- RPUSHX +RPUSH 52-1 "Hello" +RPUSHX 52-1 "World" +RPUSHX 52-2 "World" +-- LRANGE 52-1 0 -1 +-- LRANGE 52-2 0 -1 + +-- SADD +SADD 53-1 "Hello" +SADD 53-1 "World" +SADD 53-1 "World" +SADD 53-2 1000 +SADD 53-2 2000 +SADD 53-2 3000 +-- SMEMBERS 53-1 +-- SORT 53-1 ALPHA + +-- SDIFFSTORE +SADD 54-1 "a" +SADD 54-1 "b" +SADD 54-1 "c" +SADD 54-2 "c" +SADD 54-2 "d" +SADD 54-2 "e" +SDIFFSTORE 54-3 54-1 54-2 +-- SMEMBERS 54-3 +-- SORT 54-3 ALPHA + +-- SETBIT +SETBIT 55-1 7 1 +SETBIT 55-1 7 0 +-- GET 55-1 + +-- SETEX +SETEX 56-1 1 "Hello" +-- GET 56-1 +-- NOT expire during test +SETEX 56-2 100000000 "Hello" + +-- SETNX +SETNX 57-1 "Hello" +SETNX 57-1 "World" +-- GET 57-1 + +-- SETRANGE +SET 58-1 "Hello World" +SETRANGE 58-1 6 "Redis" +-- GET 58-1 +SETRANGE 58-2 6 "Redis" +-- GET 58-2 + +-- SINTERSTORE +SADD 59-1 "a" +SADD 59-1 "b" +SADD 59-1 "c" +SADD 59-2 "c" +SADD 59-2 "d" +SADD 59-2 "e" +SINTERSTORE 59-3 59-1 59-2 +-- SMEMBERS 59-3 + +-- SMOVE +SADD 60-1 "one" +SADD 60-1 "two" +SADD 60-2 "three" +SMOVE 60-1 60-2 "two" +-- SMEMBERS 60-1 +-- SMEMBERS 60-2 + +-- SPOP +SADD 61-1 "one" +SADD 61-1 "two" +SADD 61-1 "three" +SPOP 61-1 +-- SMEMBERS 61-1 +SADD 61-1 "four" +SADD 61-1 "five" +SPOP 61-1 3 +-- SMEMBERS 61-1 + +-- SREM +SADD 62-1 "one" +SADD 62-1 "two" +SADD 62-1 "three" +SREM 62-1 "one" +SREM 62-1 "four" +-- SMEMBERS 62-1 + +-- SUNIONSTORE +SADD 63-1 "a" +SADD 63-2 "b" +SUNIONSTORE key 63-1 63-2 +-- SMEMBERS key + +-- SWAPDB +SWAPDB 0 1 + +-- UNLINK +SET 64-1 "Hello" +SET 64-2 "World" +UNLINK 64-1 64-2 64-3 + +-- -- XACK +-- XADD mystream1 1526569495631-0 message "Hello," +-- XACK mystream1 mygroup 1526569495631-0 +-- -- XRANGE mystream1 - + + +-- XADD +XADD 65-1 1526919030474-55 message "Hello," +XADD 65-1 1526919030474-* message " World!" +XADD 65-1 * name Sara surname OConnor +XADD 65-1 * field1 value1 field2 value2 field3 value3 +-- XLEN 65-1 +-- XRANGE 65-1 - + + +-- -- XAUTOCLAIM +-- XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + +-- -- XCLAIM +-- XCLAIM mystream mygroup Alice 3600000 1526569498055-0 + +-- XDEL +XADD 66-1 1538561700640-0 a 1 +XADD 66-1 * b 2 +XADD 66-1 * c 3 +XDEL 66-1 1538561700640-0 +-- XRANGE 66-1 - + + +-- XGROUP CREATE mystream mygroup 0 + +-- XTRIM +XTRIM 67-1 MAXLEN 1000 +XADD 67-1 * field1 A field2 B field3 C field4 D +XTRIM 67-1 MAXLEN 2 +-- XRANGE 67-1 - + + +-- ZADD +ZADD 68-1 1 "one" +ZADD 68-1 1 "uno" +ZADD 68-1 2 "two" 3 "three" +-- ZRANGE 68-1 0 -1 WITHSCORES + +-- ZDIFFSTORE +ZADD 69-1 1 "one" +ZADD 69-1 2 "two" +ZADD 69-1 3 "three" +ZADD 69-2 1 "one" +ZADD 69-2 2 "two" +ZDIFFSTORE 69-3 2 69-1 69-2 +-- ZRANGE 69-3 0 -1 WITHSCORES + +-- ZINCRBY +ZADD 70-1 1 "one" +ZADD 70-1 2 "two" +ZINCRBY 70-1 2 "one" +-- ZRANGE 70-1 0 -1 WITHSCORES + +-- ZINTERSTORE +ZADD 71-1 1 "one" +ZADD 71-1 2 "two" +ZADD 71-2 1 "one" +ZADD 71-2 2 "two" +ZADD 71-2 3 "three" +ZINTERSTORE 71-3 2 71-1 71-2 WEIGHTS 2 3 +-- ZRANGE 71-3 0 -1 WITHSCORES + +-- ZMPOP +ZADD 72-1 1 "one" 2 "two" 3 "three" +ZMPOP 1 72-1 MIN +-- ZRANGE 72-1 0 -1 WITHSCORES + +-- ZPOPMAX +ZADD 73-1 1 "one" +ZADD 73-1 2 "two" +ZADD 73-1 3 "three" +ZPOPMAX 73-1 + +-- ZPOPMIN +ZADD 74-1 1 "one" +ZADD 74-1 2 "two" +ZADD 74-1 3 "three" +ZPOPMIN 74-1 + +-- ZRANGESTORE +ZADD 75-1 1 "one" 2 "two" 3 "three" 4 "four" +ZRANGESTORE 75-2 75-1 2 -1 +-- ZRANGE 75-2 0 -1 + +-- ZREM +ZADD 76-1 1 "one" +ZADD 76-1 2 "two" +ZADD 76-1 3 "three" +ZREM 76-1 "two" +-- ZRANGE 76-1 0 -1 WITHSCORES + +-- ZREMRANGEBYLEX +ZADD 77-1 0 aaaa 0 b 0 c 0 d 0 e +ZADD 77-1 0 foo 0 zap 0 zip 0 ALPHA 0 alpha +ZREMRANGEBYLEX 77-1 [alpha [omega +ZRANGE 77-1 0 -1 + +-- ZREMRANGEBYRANK +ZADD 78-1 1 "one" +ZADD 78-1 2 "two" +ZADD 78-1 3 "three" +ZREMRANGEBYRANK 78-1 0 1 +-- ZRANGE 78-1 0 -1 WITHSCORES + +-- ZREMRANGEBYSCORE +ZADD 79-1 1 "one" +ZADD 79-1 2 "two" +ZADD 79-1 3 "three" +ZREMRANGEBYSCORE 79-1 -inf (2 +-- ZRANGE 79-1 0 -1 WITHSCORES + +-- ZUNIONSTORE +ZADD 80-1 1 "one" +ZADD 80-1 2 "two" +ZADD 80-2 1 "one" +ZADD 80-2 2 "two" +ZADD 80-2 3 "three" +ZUNIONSTORE 80-3 2 80-1 zset2 WEIGHTS 2 3 +-- ZRANGE 80-3 0 -1 WITHSCORES \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/task_config.ini new file mode 100644 index 00000000..b3bf0091 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/rewrite_test/task_config.ini @@ -0,0 +1,35 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +method=rewrite +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_dml.sql new file mode 100644 index 00000000..f868b4f0 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/src_dml.sql @@ -0,0 +1,33 @@ +-- RDB_TYPE_SET(2) +SADD 1-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SADD 1-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" + +-- in redis 2.8/4.0/5.0/6.0/6.2/7.0, RDB_TYPE_SET_INT_SET (11) +-- encoded as i16 +SADD 1-2 -1 +SADD 1-2 0 +SADD 1-2 1 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-4 -1 +SADD 1-4 -2147483648 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i32 +SADD 1-5 1 +SADD 1-5 2147483647 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-6 -1 +SADD 1-6 -9223372036854775808 + +-- RDB_TYPE_SET_INT_SET (11) +-- encoded as i64 +SADD 1-7 1 +SADD 1-7 9223372036854775807 + +-- RDB_TYPE_SET(2) +SADD 1-8 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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/set_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_dml.sql new file mode 100644 index 00000000..1068b3c5 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/src_dml.sql @@ -0,0 +1,61 @@ +-- in redis 7.0, RDB_TYPE_STREAM_LIST_PACKS_2 (19): +-- LP_ENCODING_6BIT_STR: Used for strings less than 64 bytes +-- LP_ENCODING_12BIT_STR: Used for strings less than 4k bytes +-- LP_ENCODING_32BIT_STR: Used for strings less than 4G + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +XADD 1-1 1526919030474-57 f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +XADD 2-1 * f_1 value_1 f_2 value_2 f_3 value_3 + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +-- this will add 2 entries in the same stream +XADD 3-1 * name John age 30 name Lucy age 20 +XADD 3-1 * name Mike age 40 name Jack age 30 + +-- the longest string will be compressed in listpack by: LP_ENCODING_32BIT_STR +XADD 4-1 * f_1 abc f_2 "abc\r\nδΈ­ζ–‡πŸ˜€" f_1 abcdefg f_2 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 + +-- RDB_TYPE_STREAM_LIST_PACKS_2 (19) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808 +XADD 5-1 * f_1 0 f_1 -1 f_2 -128 f_3 127 f_4 -4096 f_5 4095 f_6 -32768 f_7 32767 f_8 -8388607 f_9 8388607 f_10 -2147483648 f_11 2147483647 f_12 -9223372036854775808 f_13 9223372036854775807 f_14 9223372036854775808 + + + +-- multi field-value pairs for the same stream id +XADD 6-1 1538561700643-0 a b c d +-- multi stream ids for the same master key +XADD 6-1 * e f g h +XADD 6-1 * i j k l + +-- empty stream +XADD 7-1 MAXLEN 0 1538561700643-0 x y + +-- XDEL +XADD 8-1 1538561700640-0 a 1 +XADD 8-1 * b 2 +XADD 8-1 * c 3 +XDEL 8-1 1538561700640-0 + + +XADD 9-1 1538561700643-0 a b c d +XADD 9-1 1538561700643-1 e f g h +XADD 9-1 1538561700643-2 i j k l +XADD 9-1 1538561700643-3 m n o p + +-- XGROUP +XGROUP CREATE 9-1 my-group-1 0 +XGROUP CREATE 9-1 my-group-3 0 + +-- XAUTOCLAIM +XAUTOCLAIM 9-1 my-group-1 my-group-2 1 0-0 COUNT 2 + +-- XCLAIM +XCLAIM 9-1 my-group-3 my-group-4 1 1538561700643-1 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/stream_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_dml.sql new file mode 100644 index 00000000..7e8b1c15 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/src_dml.sql @@ -0,0 +1,9 @@ +-- RDB_TYPE_STRING (0) +SET 1-1 val_0 +SET 2-1 "abc\r\n δΈ­ζ–‡πŸ˜€ \r\n" +SET 3-1 "\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 4-1 "abc\r\nabc δΈ­ζ–‡πŸ˜€ \r\n" +SET 5-1 0.1111110 +SET 6-1 0 +SET 7-1 10000 +SET 8-1 24YLsf3sJ0X7n3docweHrAiyn8OXtzs9BKRgabc\r\nabcδΈ­ζ–‡πŸ˜€\r\n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 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/string_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_dml.sql new file mode 100644 index 00000000..06b7effa --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/src_dml.sql @@ -0,0 +1,15 @@ +-- score will be stored as double +-- 1-1, RDB_TYPE_ZSET_2 (5) +ZADD 1-1 1 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\r\nδΈ­ζ–‡πŸ˜€aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 2.1 "abc\r\nδΈ­ζ–‡πŸ˜€" 3.333333 c +-- 1-2, RDB_TYPE_ZSET_2 (5) +ZADD 1-2 1 aaaaaaaaaaaaaaaa 2.1 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 + +-- in redis 7.0, RDB_TYPE_ZSET_LIST_PACK (17) +-- LP_ENCODING_7BIT_UINT: 0 to 127 +-- LP_ENCODING_13BIT_INT: -4096 to 4095 +-- LP_ENCODING_16BIT_INT: (-32768 to -4096) and (4095 to 32767) +-- LP_ENCODING_24BIT_INT: (βˆ’8388608 to -32768) and (32767 to 8388607) +-- LP_ENCODING_32BIT_INT: (-2147483648 to βˆ’8388608) and (8388607 to 2147483647) +-- LP_ENCODING_64BIT_INT: (-9223372036854775808 to -2147483648) and (2147483647 to 9223372036854775807) +-- LP_ENCODING_6BIT_STR: 9223372036854775808, and other float numbers +ZADD 2-1 1.1 0 2.1 12 3.1 13 4.1 -1 5.1 -16 6.1 15 7.1 -128 8.1 127 9.1 -32768 10.1 32767 11.1 -8388607 12.1 8388607 13.1 -2147483648 14.1 2147483647 15.1 -9223372036854775808 16.1 9223372036854775807 17.1 9223372036854775808 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/7_0/zset_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rebloom/.env b/dt-tests/tests/redis_to_redis/snapshot/rebloom/.env new file mode 100644 index 00000000..d1f445cd --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rebloom/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a8835528242004c93a70d11ccbccfbe1-2001293504.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a3aae6c850439452bbcb3982b3a58eeb-137133407.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_dml.sql new file mode 100644 index 00000000..43cb429a --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/src_dml.sql @@ -0,0 +1,55 @@ + +-- BF.ADD +BF.ADD 1-1 item1 +-- BF.EXISTS 1-1 item1 +-- BF.DEBUG 1-1 + +-- BF.INSERT +-- Add three items to a filter, then create the filter with default parameters if it does not already exist. +BF.INSERT 2-1 ITEMS item1 item2 item3 +-- Add one item to a filter, then create the filter with a capacity of 10000 if it does not already exist. +BF.INSERT 2-2 CAPACITY 10000 ITEMS item1 +-- Add two items to a filter, then return error if the filter does not already exist. +BF.ADD 2-3 item1 +BF.INSERT 2-3 NOCREATE ITEMS item2 item3 + +-- BF.SCANDUMP + +-- BF.LOADCHUNK + +-- BF.MADD +BF.MADD 3-1 item1 item2 item3 + +-- BF.RESERVE +BF.RESERVE 4-1 0.01 1000 +BF.RESERVE 4-2 0.01 1000 EXPANSION 2 +BF.RESERVE 4-3 0.01 1000 NONSCALING + +-- CF.ADD +CF.ADD 5-1 item1 +-- CF.DEBUG 5-1 + +-- CF.ADDNX +CF.ADDNX 6-1 item1 + +-- CF.INSERT +CF.INSERT 7-1 ITEMS item1 item2 item2 +CF.INSERT 7-2 CAPACITY 1000 ITEMS item1 item2 +CF.ADD 7-3 item3 +CF.INSERT 7-3 CAPACITY 1000 NOCREATE ITEMS item1 item2 +CF.RESERVE 7-4 2 BUCKETSIZE 1 EXPANSION 0 +CF.INSERT 7-4 ITEMS 1 1 1 1 + +-- CF.INSERTNX +CF.INSERTNX 8-1 CAPACITY 1000 ITEMS item1 item2 +CF.INSERTNX 8-2 CAPACITY 1000 ITEMS item1 item2 item3 +CF.ADD 8-3 item3 +CF.INSERTNX 8-3 CAPACITY 1000 NOCREATE ITEMS item1 item2 + +-- CF.RESERVE +CF.RESERVE 9-1 1000 +CF.RESERVE 9-2 1000 BUCKETSIZE 8 MAXITERATIONS 20 EXPANSION 2 + +-- CF.SCANDUMP + +-- CF.LOADCHUNK \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rebloom/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/redisearch/.env b/dt-tests/tests/redis_to_redis/snapshot/redisearch/.env new file mode 100644 index 00000000..9ffa6c3b --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/redisearch/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://ad821353ffc3743ab863116d537009d6-1349279413.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://adc59f71fd1824ad28b2aa6af5e30d40-808149200.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_dml.sql new file mode 100644 index 00000000..2e28eb54 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/src_dml.sql @@ -0,0 +1,56 @@ +-- FT.ALIASADD +FT.CREATE 1-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE +FT.ALIASADD alias-1-1 1-1 +-- FT._LIST +-- FT.INFO 1-1 + +-- FT.ALIASDEL +FT.CREATE 2-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALIASADD alias-2-1 2-1 +FT.ALIASDEL alias-2-1 + +-- FT.ALIASUPDATE +FT.CREATE 3-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALIASADD alias-3-1 3-1 +FT.ALIASUPDATE alias-3-2 3-1 + +-- FT.ALTER +FT.CREATE 4-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.ALTER 4-1 SCHEMA ADD id2 NUMERIC SORTABLE + +-- FT.CREATE +-- Create an index that stores the title, publication date, and categories of blog post hashes whose keys start with blog:post: (for example, blog:post:1). +FT.CREATE 5-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE +-- Index the sku attribute from a hash as both a TAG and as TEXT: +FT.CREATE 5-2 ON HASH PREFIX 1 blog:post: SCHEMA sku AS sku_text TEXT sku AS sku_tag TAG SORTABLE +-- Index two different hashes, one containing author data and one containing books, in the same index: +FT.CREATE 5-3 ON HASH PREFIX 2 author:details: book:details: SCHEMA author_id TAG SORTABLE author_ids TAG title TEXT name TEXT +-- Index authors whose names start with G. +FT.CREATE 5-4 ON HASH PREFIX 1 author:details FILTER 'startswith(@name, "G")' SCHEMA name TEXT +-- Index only books that have a subtitle. +FT.CREATE 5-5 ON HASH PREFIX 1 book:details FILTER '@subtitle != ""' SCHEMA title TEXT +-- Index books that have a "categories" attribute where each category is separated by a ; character. +FT.CREATE 5-6 ON HASH PREFIX 1 book:details FILTER '@subtitle != ""' SCHEMA title TEXT categories TAG SEPARATOR ; +-- Index a JSON document using a JSON Path expression. +FT.CREATE 5-7 ON JSON SCHEMA $.title AS title TEXT $.categories AS categories TAG + +-- FT.DROPINDEX +FT.CREATE 6-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.DROPINDEX 6-1 DD + +-- FT.SYNUPDATE +FT.CREATE 7-1 ON HASH PREFIX 1 blog:post: SCHEMA title TEXT +FT.SYNUPDATE 7-1 synonym hello hi shalom + +-- -- FT.DICTADD +-- FT.DICTADD 7-1 foo bar "hello world" + +-- FT.PROFILE idx SEARCH QUERY "hello world" + +-- FT.SUGADD sug "hello world" 1 +-- FT.SUGGET + +-- FT.SUGDEL +-- FT.SUGDEL sug "hello" + + diff --git a/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/redisearch/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rejson/.env b/dt-tests/tests/redis_to_redis/snapshot/rejson/.env new file mode 100644 index 00000000..b3336059 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rejson/.env @@ -0,0 +1,2 @@ +redis_extractor_url=redis://a2682297c8b264a4f8b1fb572aae8a72-1436023536.cn-northwest-1.elb.amazonaws.com.cn:6379 +redis_sinker_url=redis://a0b40abe73f5b45d78840e5f375974a9-87513695.cn-northwest-1.elb.amazonaws.com.cn:6379 \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/dst_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/dst_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/dst_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_ddl.sql b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_ddl.sql new file mode 100644 index 00000000..c6cb0c67 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_ddl.sql @@ -0,0 +1 @@ +flushall \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_dml.sql b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_dml.sql new file mode 100644 index 00000000..fb62c566 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/src_dml.sql @@ -0,0 +1,74 @@ +-- JSON.SET +JSON.SET 1-1 $ '{"a":2}' +JSON.SET 1-1 $.b '8' +-- JSON.GET 1-1 $ + +JSON.SET 2-1 $ '{"f1": {"a":1}, "f2":{"a":2}}' +JSON.SET 2-1 $..a 3 + +-- JSON.ARRAPPEND +JSON.SET 3-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRAPPEND 3-1 $.colors '"blue"' + +-- JSON.ARRINDEX +JSON.SET 4-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRINDEX 4-1 $..colors '"silver"' + +-- JSON.ARRINSERT +JSON.SET 5-1 $ '{"price":99.98,"stock":25,"colors":["black","silver"]}' +JSON.ARRINSERT 5-1 $.colors 2 '"yellow"' '"gold"' + +-- JSON.ARRPOP +JSON.SET 6-1 $ '[{"name":"Healthy headphones","description":"Wireless Bluetooth headphones with noise-cancelling technology","connection":{"wireless":true,"type":"Bluetooth"},"price":99.98,"stock":25,"colors":["black","silver"],"max_level":[60,70,80]},{"name":"Noisy headphones","description":"Wireless Bluetooth headphones with noise-cancelling technology","connection":{"wireless":true,"type":"Bluetooth"},"price":99.98,"stock":25,"colors":["black","silver"],"max_level":[80,90,100,120]}]' +JSON.ARRPOP 6-1 $.[1].max_level 0 + +-- -- JSON.ARRTRIM +JSON.SET 7-1 $ "[[{\"name\":\"Healthy-headphones\",\"description\":\"Wireless-Bluetooth-headphones-with-noise-cancelling-technology\",\"connection\":{\"wireless\":true,\"type\":\"Bluetooth\"},\"price\":99.98,\"stock\":25,\"colors\":[\"black\",\"silver\"],\"max_level\":[60,70,80]},{\"name\":\"Noisy-headphones\",\"description\":\"Wireless-Bluetooth-headphones-with-noise-cancelling-technology\",\"connection\":{\"wireless\":true,\"type\":\"Bluetooth\"},\"price\":99.98,\"stock\":25,\"colors\":[\"black\",\"silver\"],\"max_level\":[85,90,100,120]}]]" +JSON.ARRAPPEND 7-1 $.[1].max_level 140 160 180 200 220 240 260 280 +JSON.ARRTRIM 7-1 $.[1].max_level 4 8 + +-- JSON.CLEAR +JSON.SET 8-1 $ '{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14}' +JSON.CLEAR 8-1 $.* + +-- JSON.DEL +JSON.SET 9-1 $ '{"a": 1, "nested": {"a": 2, "b": 3}}' +JSON.DEL 9-1 $..a + +-- JSON.FORGET +JSON.SET 10-1 $ '{"a": 1, "nested": {"a": 2, "b": 3}}' +JSON.FORGET 10-1 $..a + +-- JSON.MERGE +-- Create a unexistent path-value +JSON.SET 11-1 $ '{"a":2}' +JSON.MERGE 11-1 $.b '8' +-- Delete on existing value +JSON.SET 11-2 $ '{"a":2}' +JSON.MERGE 11-2 $.a 'null' +-- Replace an Array +JSON.SET 11-3 $ '{"a":[2,4,6,8]}' +JSON.MERGE 11-3 $.a '[10,12]' + +-- JSON.MSET +JSON.MSET 12-2 $ '{"a":2}' +JSON.MSET 12-3 $ '{"a":2}' +JSON.MSET 12-1 $ '{"a":2}' 12-2 $.f.a '3' 12-3 $ '{"f1": {"a":1}, "f2":{"a":2}}' + +-- JSON.NUMINCRBY +JSON.SET 13-1 . '{"a":"b","b":[{"a":2}, {"a":5}, {"a":"c"}]}' +JSON.NUMINCRBY 13-1 $.a 2 +JSON.NUMINCRBY 13-1 $..a 2 + +-- JSON.NUMMULTBY +JSON.SET 14-1 . '{"a":"b","b":[{"a":2}, {"a":5}, {"a":"c"}]}' +JSON.NUMMULTBY 14-1 $.a 2 +JSON.NUMMULTBY 14-1 $..a 2 + +-- JSON.STRAPPEND +JSON.SET 15-1 $ '{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}' +JSON.STRAPPEND 15-1 $..a '"baz"' + +-- JSON.TOGGLE +JSON.SET 16-1 $ '{"bool": true}' +JSON.TOGGLE 16-1 $.bool diff --git a/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/task_config.ini b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/task_config.ini new file mode 100644 index 00000000..7eaca284 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot/rejson/cmds_test/task_config.ini @@ -0,0 +1,34 @@ +[extractor] +db_type=redis +extract_type=snapshot +repl_port=10008 +url= + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url= +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=info +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/redis_to_redis/snapshot_2_8_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_2_8_tests.rs new file mode 100644 index 00000000..e2626809 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_2_8_tests.rs @@ -0,0 +1,47 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/cmds_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_hash_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/hash_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_list_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/list_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_set_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/set_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_string_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/string_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_zset_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/zset_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_length_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/2_8/length_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_4_0_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_4_0_tests.rs new file mode 100644 index 00000000..c2d1d793 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_4_0_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/4_0/cmds_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_5_0_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_5_0_tests.rs new file mode 100644 index 00000000..21ce4542 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_5_0_tests.rs @@ -0,0 +1,53 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/cmds_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_hash_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/hash_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_list_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/list_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_set_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/set_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_stream_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/stream_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_string_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/string_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_zset_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/zset_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_length_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/5_0/length_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_6_0_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_6_0_tests.rs new file mode 100644 index 00000000..cb1dcf93 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_6_0_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/6_0/cmds_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_6_2_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_6_2_tests.rs new file mode 100644 index 00000000..8f450d3d --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_6_2_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/6_2/cmds_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_7_0_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_7_0_tests.rs new file mode 100644 index 00000000..78e25fdb --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_7_0_tests.rs @@ -0,0 +1,79 @@ +#[cfg(test)] +mod test { + + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_basic_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/basic_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_multi_dbs_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/multi_dbs_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/cmds_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_hash_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/hash_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_list_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/list_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_set_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/set_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_stream_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/stream_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_string_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/string_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_zset_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/zset_test").await; + } + + #[tokio::test] + #[serial] + async fn snapshot_length_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/length_test").await; + } + + // test sinking rdb data by rewrite instead of restore + #[tokio::test] + #[serial] + async fn cdc_rewrite_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/rewrite_test").await; + } + + #[tokio::test] + #[serial] + async fn cdc_rewrite_stream_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/7_0/rewrite_stream_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_rebloom_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_rebloom_tests.rs new file mode 100644 index 00000000..bd461681 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_rebloom_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_snapshot_test("redis_to_redis/snapshot/rebloom/cmds_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_redisearch_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_redisearch_tests.rs new file mode 100644 index 00000000..587881b8 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_redisearch_tests.rs @@ -0,0 +1,15 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + // TODO, fix psync for redisearch + // #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_redisearch_snapshot_test( + "redis_to_redis/snapshot/redisearch/cmds_test", + ) + .await; + } +} diff --git a/dt-tests/tests/redis_to_redis/snapshot_rejson_tests.rs b/dt-tests/tests/redis_to_redis/snapshot_rejson_tests.rs new file mode 100644 index 00000000..100464fa --- /dev/null +++ b/dt-tests/tests/redis_to_redis/snapshot_rejson_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod test { + use crate::test_runner::test_base::TestBase; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn snapshot_cmds_test() { + TestBase::run_redis_rejson_snapshot_test("redis_to_redis/snapshot/rejson/cmds_test").await; + } +} diff --git a/dt-tests/tests/redis_to_redis/task_config.ini b/dt-tests/tests/redis_to_redis/task_config.ini new file mode 100644 index 00000000..ffe49ca1 --- /dev/null +++ b/dt-tests/tests/redis_to_redis/task_config.ini @@ -0,0 +1,38 @@ +[extractor] +db_type=redis +extract_type=cdc +run_id= +repl_port=10008 +repl_offset=0 +now_db_id=3 +heartbeat_interval_secs=10 +url=redis://:123456@127.0.0.1:6380 + +[filter] +do_dbs= +do_events= +ignore_dbs= +ignore_tbs= +do_tbs= + +[sinker] +db_type=redis +sink_type=write +url=redis://:123456@127.0.0.1:6381 +batch_size=2 + +[router] +db_map= +field_map= +tb_map= + +[pipeline] +buffer_size=4 +parallel_size=2 +checkpoint_interval_secs=1 +parallel_type=redis + +[runtime] +log_level=debug +log4rs_file=./log4rs.yaml +log_dir=./logs \ No newline at end of file diff --git a/dt-tests/tests/test_config_util.rs b/dt-tests/tests/test_config_util.rs index a39add1d..a92b35b9 100644 --- a/dt-tests/tests/test_config_util.rs +++ b/dt-tests/tests/test_config_util.rs @@ -7,14 +7,11 @@ use std::{ }; use configparser::ini::Ini; -use dt_common::{ - config::{ - config_enums::{DbType, ParallelType}, - extractor_config::ExtractorConfig, - sinker_config::SinkerConfig, - task_config::TaskConfig, - }, - utils::config_url_util::ConfigUrlUtil, +use dt_common::config::{ + config_enums::{DbType, ParallelType}, + extractor_config::ExtractorConfig, + sinker_config::SinkerConfig, + task_config::TaskConfig, }; pub struct TestConfigUtil {} @@ -123,29 +120,66 @@ impl TestConfigUtil { ]) } + pub fn load_env(test_dir: &str, env_file_name: &str) -> bool { + let mut env_file = String::new(); + // recursively search .env from test dir up to parent dirs + let mut dir = Path::new(test_dir); + let final_file_name = if env_file_name == "" { + ".env" + } else { + env_file_name + }; + loop { + let path = dir.join(final_file_name); + if fs::metadata(&path).is_ok() { + env_file = path.to_str().unwrap().to_string(); + break; + } + + if let Some(parent_dir) = dir.parent() { + dir = parent_dir; + } else { + break; + } + } + + println!("use env file: {}", env_file); + if env_file.is_empty() { + return false; + } + + dotenv::from_path(env_file).unwrap(); + + true + } + pub fn update_task_config_url( src_task_config_file: &str, dst_task_config_file: &str, - project_root: &str, + test_dir: &str, policy: &str, ) { - let env_path = format!("{}/{}/tests/.env", project_root, TEST_PROJECT); - dotenv::from_path(env_path).unwrap(); + // environment variable settings in .env.local have higher priority + Self::load_env(test_dir, ".env.local"); + + if !Self::load_env(test_dir, ".env") { + return; + } - let mut update_configs = Vec::new(); let config = TaskConfig::new(&src_task_config_file); + let mut update_configs = Vec::new(); match policy { Self::REPLACE_PARAM => { update_configs.push(( EXTRACTOR.to_string(), URL.to_string(), - ConfigUrlUtil::convert_with_envs(config.extractor.get_url()).unwrap(), + Self::convert_with_envs(config.extractor.get_url()).unwrap(), )); update_configs.push(( SINKER.to_string(), URL.to_string(), - ConfigUrlUtil::convert_with_envs(config.sinker.get_url()).unwrap(), + Self::convert_with_envs(config.sinker.get_url()).unwrap(), )); } _ => { @@ -155,64 +189,28 @@ impl TestConfigUtil { return; } } - Err(_) => return, - } - - match &config.extractor.get_db_type() { - DbType::Mysql => { - update_configs.push(( - EXTRACTOR.to_string(), - URL.to_string(), - env::var("mysql_extractor_url").unwrap(), - )); - } - - DbType::Pg => { - update_configs.push(( - EXTRACTOR.to_string(), - URL.to_string(), - env::var("pg_extractor_url").unwrap(), - )); - } - - DbType::Mongo => { - update_configs.push(( - EXTRACTOR.to_string(), - URL.to_string(), - env::var("mongo_extractor_url").unwrap(), - )); - } - - _ => {} + Err(_) => {} } - match &config.sinker.get_db_type() { - DbType::Mysql => { - update_configs.push(( - SINKER.to_string(), - URL.to_string(), - env::var("mysql_sinker_url").unwrap(), - )); - } - - DbType::Pg => { - update_configs.push(( - SINKER.to_string(), - URL.to_string(), - env::var("pg_sinker_url").unwrap(), - )); - } - - DbType::Mongo => { - update_configs.push(( - SINKER.to_string(), - URL.to_string(), - env::var("mongo_sinker_url").unwrap(), - )); - } - - _ => {} - } + let extractor_url = match &config.extractor.get_db_type() { + DbType::Mysql => env::var("mysql_extractor_url").unwrap(), + DbType::Pg => env::var("pg_extractor_url").unwrap(), + DbType::Mongo => env::var("mongo_extractor_url").unwrap(), + DbType::Redis => env::var("redis_extractor_url").unwrap(), + DbType::Kafka => env::var("kafka_extractor_url").unwrap(), + _ => String::new(), + }; + update_configs.push((EXTRACTOR.into(), URL.into(), extractor_url)); + + let sinker_url = match &config.sinker.get_db_type() { + DbType::Mysql => env::var("mysql_sinker_url").unwrap(), + DbType::Pg => env::var("pg_sinker_url").unwrap(), + DbType::Mongo => env::var("mongo_sinker_url").unwrap(), + DbType::Redis => env::var("redis_sinker_url").unwrap(), + DbType::Kafka => env::var("kafka_sinker_url").unwrap(), + _ => String::new(), + }; + update_configs.push((SINKER.into(), URL.into(), sinker_url)); } } @@ -320,4 +318,36 @@ impl TestConfigUtil { let opt = env::var("do_clean_after_test"); opt.is_ok_and(|x| x == "true") } + + // convert_with_envs: format the database_url with envs, such as: + // change: mysql://{test_user}:{test_password}@{test_url} + // to: mysql://test:123456@127.0.0.1:3306 + // when have the envs: test_user=test, test_password=123456, test_url=127.0.0.1:3306 + pub fn convert_with_envs(database_url: String) -> Option { + if database_url.is_empty() { + return None; + } + let (mut new_url_bytes, mut pos, mut left_pos): (Vec, i64, i64) = (vec![], 0, -1); + + for ch in database_url.chars() { + if ch == '{' { + left_pos = pos; + } else if ch == '}' && pos > left_pos && left_pos >= 0 { + let new_env = String::from_utf8( + database_url.as_bytes()[(left_pos + 1) as usize..pos as usize].to_vec(), + ) + .unwrap(); + if env::var(&new_env).is_ok() { + let env_val_tmp = env::var(new_env).unwrap(); + new_url_bytes.extend_from_slice(env_val_tmp.as_bytes()); + } + left_pos = -1; + } else if left_pos == -1 { + new_url_bytes.push(ch as u8); + } + pos += 1; + } + + Some(String::from_utf8(new_url_bytes).unwrap()) + } } diff --git a/dt-tests/tests/test_runner/base_test_runner.rs b/dt-tests/tests/test_runner/base_test_runner.rs index 46d5bba5..ae378273 100644 --- a/dt-tests/tests/test_runner/base_test_runner.rs +++ b/dt-tests/tests/test_runner/base_test_runner.rs @@ -36,7 +36,7 @@ impl BaseTestRunner { ) -> Result { let project_root = TestConfigUtil::get_project_root(); let tmp_dir = if config_tmp_relative_dir.is_empty() { - format!("{}/tmp", project_root) + format!("{}/tmp/{}", project_root, relative_test_dir) } else { format!("{}/tmp/{}", project_root, config_tmp_relative_dir) }; @@ -55,7 +55,7 @@ impl BaseTestRunner { TestConfigUtil::update_task_config_url( &dst_task_config_file, &dst_task_config_file, - &project_root, + &test_dir, config_replace_police, ); @@ -98,7 +98,7 @@ impl BaseTestRunner { Ok(task) } - pub async fn wait_task_finish(task: &JoinHandle<()>) -> Result<(), Error> { + pub async fn wait_task_finish(&self, task: &JoinHandle<()>) -> Result<(), Error> { task.abort(); while !task.is_finished() { TimeUtil::sleep_millis(1).await; diff --git a/dt-tests/tests/test_runner/mod.rs b/dt-tests/tests/test_runner/mod.rs index f88bb715..bb81871a 100644 --- a/dt-tests/tests/test_runner/mod.rs +++ b/dt-tests/tests/test_runner/mod.rs @@ -1,7 +1,9 @@ pub mod base_test_runner; pub mod mongo_test_runner; +pub mod precheck_test_runner; pub mod rdb_check_test_runner; -pub mod rdb_precheck_test_runner; +pub mod rdb_kafka_rdb_test_runner; pub mod rdb_struct_test_runner; pub mod rdb_test_runner; +pub mod redis_test_runner; pub mod test_base; diff --git a/dt-tests/tests/test_runner/mongo_test_runner.rs b/dt-tests/tests/test_runner/mongo_test_runner.rs index 4fd33e08..1b121d00 100644 --- a/dt-tests/tests/test_runner/mongo_test_runner.rs +++ b/dt-tests/tests/test_runner/mongo_test_runner.rs @@ -1,13 +1,13 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use dt_common::{ config::{ extractor_config::ExtractorConfig, sinker_config::SinkerConfig, task_config::TaskConfig, }, - constants::MongoConstants, error::Error, utils::time_util::TimeUtil, }; +use dt_meta::mongo::{mongo_constant::MongoConstants, mongo_key::MongoKey}; use dt_task::task_util::TaskUtil; use mongodb::{ bson::{doc, Document}, @@ -15,6 +15,9 @@ use mongodb::{ Client, }; use regex::Regex; +use sqlx::types::chrono::Utc; + +use crate::test_config_util::TestConfigUtil; use super::base_test_runner::BaseTestRunner; @@ -57,81 +60,140 @@ impl MongoTestRunner { }) } - pub async fn run_cdc_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { - self.execute_test_ddl_sqls().await?; + pub async fn run_cdc_resume_test( + &self, + start_millis: u64, + parse_millis: u64, + ) -> Result<(), Error> { + self.execute_test_prepare_sqls().await?; + + // update start_timestamp to make sure the subsequent cdc task can get old events + let start_timestamp = Utc::now().timestamp().to_string(); + let config = vec![( + "extractor".into(), + "start_timestamp".into(), + start_timestamp, + )]; + TestConfigUtil::update_task_config( + &self.base.task_config_file, + &self.base.task_config_file, + &config, + ); + + // execute sqls in src before cdc task starts + let src_mongo_client = self.src_mongo_client.as_ref().unwrap(); + let src_sqls = Self::slice_sqls_by_db(&self.base.src_dml_sqls); + for (db, sqls) in src_sqls.iter() { + let (src_insert_sqls, src_update_sqls, src_delete_sqls) = + Self::slice_sqls_by_type(sqls); + // insert + self.execute_dmls(src_mongo_client, db, &src_insert_sqls) + .await + .unwrap(); + // update + self.execute_dmls(src_mongo_client, db, &src_update_sqls) + .await + .unwrap(); + // delete + self.execute_dmls(src_mongo_client, db, &src_delete_sqls) + .await + .unwrap(); + } + TimeUtil::sleep_millis(start_millis).await; let task = self.base.spawn_task().await?; TimeUtil::sleep_millis(start_millis).await; + for (db, _) in src_sqls.iter() { + self.compare_db_data(db).await; + } - let mut src_insert_sqls = Vec::new(); - let mut src_update_sqls = Vec::new(); - let mut src_delete_sqls = Vec::new(); - for sql in self.base.src_dml_sqls.iter() { - if sql.contains("insertOne") { - src_insert_sqls.push(sql.clone()); - } - if sql.contains("updateOne") { - src_update_sqls.push(sql.clone()); - } - if sql.contains("deleteOne") { - src_delete_sqls.push(sql.clone()); - } + for (db, sqls) in src_sqls.iter() { + let (_, _, src_delete_sqls) = Self::slice_sqls_by_type(sqls); + // delete + self.execute_dmls(src_mongo_client, db, &src_delete_sqls) + .await + .unwrap(); + } + TimeUtil::sleep_millis(parse_millis).await; + for (db, _) in src_sqls.iter() { + self.compare_db_data(db).await; } - let src_mongo_client = self.src_mongo_client.as_ref().unwrap(); - let db = Self::get_db(&self.base.src_dml_sqls[0]); + self.base.wait_task_finish(&task).await + } - // insert - self.execute_dmls(src_mongo_client, &db, &src_insert_sqls) - .await - .unwrap(); - TimeUtil::sleep_millis(parse_millis).await; - self.compare_data_for_tbs().await; + pub async fn run_cdc_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { + self.execute_test_prepare_sqls().await?; - // update - self.execute_dmls(src_mongo_client, &db, &src_update_sqls) - .await - .unwrap(); - TimeUtil::sleep_millis(parse_millis).await; - self.compare_data_for_tbs().await; + let task = self.base.spawn_task().await?; + TimeUtil::sleep_millis(start_millis).await; - // delete - self.execute_dmls(src_mongo_client, &db, &src_delete_sqls) - .await - .unwrap(); - TimeUtil::sleep_millis(parse_millis).await; - self.compare_data_for_tbs().await; + let src_mongo_client = self.src_mongo_client.as_ref().unwrap(); - BaseTestRunner::wait_task_finish(&task).await + let src_sqls = Self::slice_sqls_by_db(&self.base.src_dml_sqls); + for (db, sqls) in src_sqls.iter() { + let (src_insert_sqls, src_update_sqls, src_delete_sqls) = + Self::slice_sqls_by_type(sqls); + // insert + self.execute_dmls(src_mongo_client, db, &src_insert_sqls) + .await + .unwrap(); + TimeUtil::sleep_millis(parse_millis).await; + self.compare_db_data(db).await; + + // update + self.execute_dmls(src_mongo_client, db, &src_update_sqls) + .await + .unwrap(); + TimeUtil::sleep_millis(parse_millis).await; + self.compare_db_data(db).await; + + // delete + self.execute_dmls(src_mongo_client, db, &src_delete_sqls) + .await + .unwrap(); + TimeUtil::sleep_millis(parse_millis).await; + self.compare_db_data(db).await; + } + self.base.wait_task_finish(&task).await } pub async fn run_snapshot_test(&self, compare_data: bool) -> Result<(), Error> { - self.execute_test_ddl_sqls().await?; + self.execute_test_prepare_sqls().await?; let src_mongo_client = self.src_mongo_client.as_ref().unwrap(); - let db = Self::get_db(&self.base.src_dml_sqls[0]); - self.execute_dmls(src_mongo_client, &db, &self.base.src_dml_sqls) - .await?; + + let src_sqls = Self::slice_sqls_by_db(&self.base.src_dml_sqls); + for (db, sqls) in src_sqls.iter() { + self.execute_dmls(src_mongo_client, db, sqls).await?; + } self.base.start_task().await?; if compare_data { - self.compare_data_for_tbs().await; + for (db, _) in src_sqls.iter() { + self.compare_db_data(db).await; + } } - Ok(()) } - async fn execute_test_ddl_sqls(&self) -> Result<(), Error> { + pub async fn execute_test_prepare_sqls(&self) -> Result<(), Error> { let src_mongo_client = self.src_mongo_client.as_ref().unwrap(); let dst_mongo_client = self.dst_mongo_client.as_ref().unwrap(); - let src_db = Self::get_db(&self.base.src_ddl_sqls[0]); - let dst_db = Self::get_db(&self.base.dst_ddl_sqls[0]); - self.execute_ddls(src_mongo_client, &src_db, &self.base.src_ddl_sqls) - .await?; - self.execute_ddls(dst_mongo_client, &dst_db, &self.base.dst_ddl_sqls) - .await + let src_sqls = Self::slice_sqls_by_db(&self.base.src_ddl_sqls); + let dst_sqls = Self::slice_sqls_by_db(&self.base.dst_ddl_sqls); + + for (db, sqls) in src_sqls.iter() { + self.execute_ddls(src_mongo_client, db, sqls).await?; + self.execute_dmls(src_mongo_client, db, sqls).await?; + } + for (db, sqls) in dst_sqls.iter() { + self.execute_ddls(dst_mongo_client, db, sqls).await?; + self.execute_dmls(dst_mongo_client, db, sqls).await?; + } + Ok(()) } async fn execute_ddls( @@ -140,10 +202,6 @@ impl MongoTestRunner { db: &str, sqls: &Vec, ) -> Result<(), Error> { - if sqls.is_empty() { - return Ok(()); - } - for sql in sqls.iter() { if sql.contains("drop") { self.execute_drop(client, &db, sql).await.unwrap(); @@ -161,18 +219,14 @@ impl MongoTestRunner { db: &str, sqls: &Vec, ) -> Result<(), Error> { - if sqls.is_empty() { - return Ok(()); - } - for sql in sqls.iter() { - if sql.contains("insertOne") { + if sql.contains("insert") { self.execute_insert(client, &db, sql).await?; } - if sql.contains("updateOne") { + if sql.contains("update") { self.execute_update(client, &db, sql).await?; } - if sql.contains("deleteOne") { + if sql.contains("delete") { self.execute_delete(client, &db, sql).await?; } } @@ -185,18 +239,6 @@ impl MongoTestRunner { cap.get(1).unwrap().as_str().to_string() } - fn get_tbs(sqls: &Vec) -> HashSet { - let mut tbs = HashSet::new(); - let re = Regex::new(r"db.(\w+).insertOne").unwrap(); - for sql in sqls.iter() { - if let Some(cap) = re.captures(sql) { - let tb = cap.get(1).unwrap().as_str().to_string(); - tbs.insert(tb); - } - } - tbs - } - async fn execute_drop(&self, client: &Client, db: &str, sql: &str) -> Result<(), Error> { let re = Regex::new(r"db.(\w+).drop()").unwrap(); let cap = re.captures(sql).unwrap(); @@ -226,58 +268,58 @@ impl MongoTestRunner { async fn execute_insert(&self, client: &Client, db: &str, sql: &str) -> Result<(), Error> { // example: db.tb_2.insertOne({ "name": "a", "age": "1" }) - let re = Regex::new(r"db.(\w+).insertOne\(([\w\W]+)\)").unwrap(); + let re = Regex::new(r"db.(\w+).insert(One|Many)\(([\w\W]+)\)").unwrap(); let cap = re.captures(sql).unwrap(); let tb = cap.get(1).unwrap().as_str(); - let doc_content = cap.get(2).unwrap().as_str(); + let doc_content = cap.get(3).unwrap().as_str(); - let doc = Document::from(serde_json::from_str(doc_content).unwrap()); - client - .database(db) - .collection::(tb) - .insert_one(doc, None) - .await - .unwrap(); + let coll = client.database(db).collection::(tb); + if sql.contains("insertOne") { + let doc: Document = serde_json::from_str(doc_content).unwrap(); + coll.insert_one(doc, None).await.unwrap(); + } else { + let docs: Vec = serde_json::from_str(doc_content).unwrap(); + coll.insert_many(docs, None).await.unwrap(); + } Ok(()) } async fn execute_delete(&self, client: &Client, db: &str, sql: &str) -> Result<(), Error> { - let re = Regex::new(r"db.(\w+).deleteOne\(([\w\W]+)\)").unwrap(); + let re = Regex::new(r"db.(\w+).delete(One|Many)\(([\w\W]+)\)").unwrap(); let cap = re.captures(sql).unwrap(); let tb = cap.get(1).unwrap().as_str(); - let doc = cap.get(2).unwrap().as_str(); + let doc = cap.get(3).unwrap().as_str(); - let doc = Document::from(serde_json::from_str(doc).unwrap()); - client - .database(db) - .collection::(tb) - .delete_one(doc, None) - .await - .unwrap(); + let doc: Document = serde_json::from_str(doc).unwrap(); + let coll = client.database(db).collection::(tb); + if sql.contains("deleteOne") { + coll.delete_one(doc, None).await.unwrap(); + } else { + coll.delete_many(doc, None).await.unwrap(); + } Ok(()) } async fn execute_update(&self, client: &Client, db: &str, sql: &str) -> Result<(), Error> { - let re = Regex::new(r"db.(\w+).updateOne\(([\w\W]+),([\w\W]+)\)").unwrap(); + let re = Regex::new(r"db.(\w+).update(One|Many)\(([\w\W]+),([\w\W]+)\)").unwrap(); let cap = re.captures(sql).unwrap(); let tb = cap.get(1).unwrap().as_str(); - let query_doc = cap.get(2).unwrap().as_str(); - let update_doc = cap.get(3).unwrap().as_str(); - - let query_doc = Document::from(serde_json::from_str(query_doc).unwrap()); - let update_doc = Document::from(serde_json::from_str(update_doc).unwrap()); - client - .database(db) - .collection::(tb) - .update_one(query_doc, update_doc, None) - .await - .unwrap(); + let query_doc = cap.get(3).unwrap().as_str(); + let update_doc = cap.get(4).unwrap().as_str(); + + let query_doc: Document = serde_json::from_str(query_doc).unwrap(); + let update_doc: Document = serde_json::from_str(update_doc).unwrap(); + let coll = client.database(db).collection::(tb); + if sql.contains("updateOne") { + coll.update_one(query_doc, update_doc, None).await.unwrap(); + } else { + coll.update_many(query_doc, update_doc, None).await.unwrap(); + } Ok(()) } - async fn compare_data_for_tbs(&self) { - let db = Self::get_db(&self.base.src_ddl_sqls[0]); - let tbs = Self::get_tbs(&self.base.src_dml_sqls); + async fn compare_db_data(&self, db: &str) { + let tbs = self.list_tb(&db, SRC).await; for tb in tbs.iter() { self.compare_tb_data(&db, tb).await; } @@ -289,11 +331,31 @@ impl MongoTestRunner { assert_eq!(src_data.len(), dst_data.len()); for id in src_data.keys() { - assert_eq!(src_data.get(id), dst_data.get(id)); + let src_value = src_data.get(id); + let dst_value = dst_data.get(id); + println!( + "compare tb data, db: {}, tb: {}, src_value: {:?}, dst_value: {:?}", + db, tb, src_value, dst_value + ); + assert_eq!(src_value, dst_value); } } - pub async fn fetch_data(&self, db: &str, tb: &str, from: &str) -> HashMap { + pub async fn list_tb(&self, db: &str, from: &str) -> Vec { + let client = if from == SRC { + self.src_mongo_client.as_ref().unwrap() + } else { + self.dst_mongo_client.as_ref().unwrap() + }; + let tbs = client + .database(db) + .list_collection_names(None) + .await + .unwrap(); + tbs + } + + pub async fn fetch_data(&self, db: &str, tb: &str, from: &str) -> HashMap { let client = if from == SRC { self.src_mongo_client.as_ref().unwrap() } else { @@ -309,9 +371,45 @@ impl MongoTestRunner { let mut results = HashMap::new(); while cursor.advance().await.unwrap() { let doc = cursor.deserialize_current().unwrap(); - let id = doc.get_object_id(MongoConstants::ID).unwrap().to_string(); + let id = MongoKey::from_doc(&doc).unwrap(); results.insert(id, doc); } results } + + fn slice_sqls_by_db(sqls: &Vec) -> HashMap> { + let mut db = String::new(); + let mut sliced_sqls: HashMap> = HashMap::new(); + for sql in sqls.iter() { + if sql.starts_with("use") { + db = Self::get_db(sql); + continue; + } + + if let Some(sqls) = sliced_sqls.get_mut(&db) { + sqls.push(sql.into()); + } else { + sliced_sqls.insert(db.clone(), vec![sql.into()]); + } + } + sliced_sqls + } + + fn slice_sqls_by_type(sqls: &Vec) -> (Vec, Vec, Vec) { + let mut insert_sqls = Vec::new(); + let mut update_sqls = Vec::new(); + let mut delete_sqls = Vec::new(); + for sql in sqls.iter() { + if sql.contains("insert") { + insert_sqls.push(sql.clone()); + } + if sql.contains("update") { + update_sqls.push(sql.clone()); + } + if sql.contains("delete") { + delete_sqls.push(sql.clone()); + } + } + (insert_sqls, update_sqls, delete_sqls) + } } diff --git a/dt-tests/tests/test_runner/precheck_test_runner.rs b/dt-tests/tests/test_runner/precheck_test_runner.rs new file mode 100644 index 00000000..aa973481 --- /dev/null +++ b/dt-tests/tests/test_runner/precheck_test_runner.rs @@ -0,0 +1,101 @@ +use std::collections::{HashMap, HashSet}; + +use dt_common::{ + config::{config_enums::DbType, task_config::TaskConfig}, + error::Error, +}; + +use dt_precheck::{ + builder::prechecker_builder::PrecheckerBuilder, config::task_config::PrecheckTaskConfig, + meta::check_result::CheckResult, +}; + +use super::{ + base_test_runner::BaseTestRunner, mongo_test_runner::MongoTestRunner, + rdb_test_runner::RdbTestRunner, redis_test_runner::RedisTestRunner, +}; + +pub struct PrecheckTestRunner { + pub db_type: DbType, + checker_connector: PrecheckerBuilder, + test_dir: String, +} + +impl PrecheckTestRunner { + pub async fn new(test_dir: &str) -> Result { + let base = BaseTestRunner::new(test_dir).await.unwrap(); + let task_config = TaskConfig::new(&base.task_config_file); + let precheck_config = PrecheckTaskConfig::new(&base.task_config_file).unwrap(); + let checker_connector = + PrecheckerBuilder::build(precheck_config.precheck.clone(), task_config.clone()); + + Ok(Self { + checker_connector, + db_type: task_config.extractor.get_db_type().clone(), + test_dir: test_dir.to_owned(), + }) + } + + pub async fn run_check( + &self, + ignore_check_items: &HashSet, + src_expected_results: &HashMap, + dst_expected_results: &HashMap, + ) -> Result<(), Error> { + self.before_check().await?; + + let results: Vec> = + self.checker_connector.check().await?; + + let compare = |result: &CheckResult, expected_results: &HashMap| { + if let Some(expected) = expected_results.get(&result.check_type_name) { + assert_eq!(&result.is_validate, expected); + } else { + // by default, is_validate == true + assert!(&result.is_validate); + } + }; + + for i in results.iter() { + let result = i.as_ref().unwrap(); + if ignore_check_items.contains(&result.check_type_name) { + continue; + } + + println!( + "comparing precheck result, item: {}, is_source: {}", + result.check_type_name, result.is_source + ); + + if result.is_source { + compare(result, src_expected_results); + } else { + compare(result, dst_expected_results); + } + } + + Ok(()) + } + + async fn before_check(&self) -> Result<(), Error> { + match self.db_type { + DbType::Mysql | DbType::Pg => { + let base = RdbTestRunner::new(&self.test_dir).await?; + base.execute_test_ddl_sqls().await?; + } + + DbType::Mongo => { + let base = MongoTestRunner::new(&self.test_dir).await?; + base.execute_test_prepare_sqls().await?; + } + + DbType::Redis => { + let mut base = RedisTestRunner::new_default(&self.test_dir).await?; + base.execute_test_ddl_sqls()?; + } + + _ => {} + } + Ok(()) + } +} diff --git a/dt-tests/tests/test_runner/rdb_kafka_rdb_test_runner.rs b/dt-tests/tests/test_runner/rdb_kafka_rdb_test_runner.rs new file mode 100644 index 00000000..4eef289c --- /dev/null +++ b/dt-tests/tests/test_runner/rdb_kafka_rdb_test_runner.rs @@ -0,0 +1,144 @@ +use super::base_test_runner::BaseTestRunner; +use super::rdb_test_runner::RdbTestRunner; +use dt_common::config::sinker_config::SinkerConfig; +use dt_common::config::task_config::TaskConfig; +use dt_common::error::Error; +use dt_common::utils::time_util::TimeUtil; +use rdkafka::admin::{AdminClient, AdminOptions, NewTopic, TopicReplication}; +use rdkafka::client::DefaultClientContext; +use rdkafka::ClientConfig; +use regex::Regex; + +/// This is used for test cases: rdb(src) -> kafka -> rdb(dst). +/// There are 2 tasks running: +/// rdb(src) -> kafka +/// kafka -> rdb(dst) +/// And we need another dummy task runner to compare rdb(src) and rdb(dst) +/// rdb(src) -> rdb(dst) +pub struct RdbKafkaRdbTestRunner { + src_to_dst_runner: RdbTestRunner, + src_to_kafka_runner: BaseTestRunner, + kafka_to_dst_runner: BaseTestRunner, +} + +#[allow(dead_code)] +impl RdbKafkaRdbTestRunner { + pub async fn new(relative_test_dir: &str) -> Result { + let src_to_dst_runner = + RdbTestRunner::new(&format!("{}/src_to_dst", relative_test_dir)).await?; + let src_to_kafka_runner = + BaseTestRunner::new(&format!("{}/src_to_kafka", relative_test_dir)).await?; + let kafka_to_dst_runner = + BaseTestRunner::new(&format!("{}/kafka_to_dst", relative_test_dir)).await?; + Ok(Self { + src_to_dst_runner, + src_to_kafka_runner, + kafka_to_dst_runner, + }) + } + + pub async fn run_snapshot_test( + &self, + start_millis: u64, + parse_millis: u64, + ) -> Result<(), Error> { + self.src_to_dst_runner.execute_test_ddl_sqls().await?; + self.prepare_kafka().await?; + + // prepare src data + self.src_to_dst_runner.execute_test_dml_sqls().await?; + + // kafka -> dst + let kafka_to_dst_task = self.kafka_to_dst_runner.spawn_task().await?; + TimeUtil::sleep_millis(start_millis).await; + + // src -> kafka + self.src_to_kafka_runner.start_task().await?; + TimeUtil::sleep_millis(parse_millis).await; + + // compare data + let db_tbs = self.src_to_dst_runner.get_compare_db_tbs().await?; + assert!( + self.src_to_dst_runner + .compare_data_for_tbs(&db_tbs.clone(), &db_tbs.clone()) + .await? + ); + + // stop + self.kafka_to_dst_runner + .wait_task_finish(&kafka_to_dst_task) + .await?; + + Ok(()) + } + + pub async fn run_cdc_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { + self.src_to_dst_runner.execute_test_ddl_sqls().await?; + self.prepare_kafka().await?; + TimeUtil::sleep_millis(start_millis).await; + + // kafka -> dst & src -> kafka + let kafka_to_dst_task = self.kafka_to_dst_runner.spawn_task().await?; + let src_to_kafka_task = self.src_to_kafka_runner.spawn_task().await?; + TimeUtil::sleep_millis(start_millis).await; + + // execute test sqls and compare + self.src_to_dst_runner + .execute_test_sqls_and_compare(parse_millis) + .await?; + + // stop + self.kafka_to_dst_runner + .wait_task_finish(&kafka_to_dst_task) + .await?; + self.src_to_kafka_runner + .wait_task_finish(&src_to_kafka_task) + .await?; + Ok(()) + } + + async fn prepare_kafka(&self) -> Result<(), Error> { + let mut topics: Vec = vec![]; + for sql in self.src_to_kafka_runner.dst_ddl_sqls.iter() { + let re = Regex::new(r"create topic ([\w\W]+)").unwrap(); + let cap = re.captures(sql).unwrap(); + topics.push(cap.get(1).unwrap().as_str().into()); + } + + let config = TaskConfig::new(&self.src_to_kafka_runner.task_config_file); + match config.sinker { + SinkerConfig::Kafka { url, .. } => { + let client = Self::create_kafka_admin_client(&url); + for topic in topics.iter() { + Self::delete_topic(&client, topic).await; + Self::create_topic(&client, topic).await; + } + } + _ => {} + } + Ok(()) + } + + fn create_kafka_admin_client(url: &str) -> AdminClient { + let mut config = ClientConfig::new(); + config.set("bootstrap.servers", url); + config.set("session.timeout.ms", "10000"); + let client: AdminClient = config.create().unwrap(); + client + } + + async fn create_topic(client: &AdminClient, topic: &str) { + let topic = NewTopic::new(topic, 1, TopicReplication::Fixed(1)); + client + .create_topics(&[topic], &AdminOptions::new()) + .await + .unwrap(); + } + + async fn delete_topic(client: &AdminClient, topic: &str) { + client + .delete_topics(&[topic], &AdminOptions::new()) + .await + .unwrap(); + } +} diff --git a/dt-tests/tests/test_runner/rdb_precheck_test_runner.rs b/dt-tests/tests/test_runner/rdb_precheck_test_runner.rs deleted file mode 100644 index 2ba7da2a..00000000 --- a/dt-tests/tests/test_runner/rdb_precheck_test_runner.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use dt_common::{config::task_config::TaskConfig, error::Error}; - -use dt_precheck::{ - builder::prechecker_builder::PrecheckerBuilder, config::task_config::PrecheckTaskConfig, - meta::check_result::CheckResult, -}; - -use super::rdb_test_runner::RdbTestRunner; - -pub struct RdbPrecheckTestRunner { - pub base: RdbTestRunner, - checker_connector: PrecheckerBuilder, -} - -impl RdbPrecheckTestRunner { - pub async fn new(relative_test_dir: &str) -> Result { - let base = RdbTestRunner::new(relative_test_dir).await.unwrap(); - - let task_config = TaskConfig::new(&base.base.task_config_file); - let precheck_config = PrecheckTaskConfig::new(&base.base.task_config_file).unwrap(); - let checker_connector = - PrecheckerBuilder::build(precheck_config.precheck.clone(), task_config.clone()); - - Ok(Self { - base, - checker_connector, - }) - } - - pub async fn run_check(&self) -> Vec> { - self.checker_connector.check().await.unwrap() - } - - pub async fn validate( - &self, - results: &Vec>, - ignore_check_items: &HashSet, - src_expected_results: &HashMap, - dst_expected_results: &HashMap, - ) { - let compare = |result: &CheckResult, expected_results: &HashMap| { - if let Some(expected) = expected_results.get(&result.check_type_name) { - assert_eq!(&result.is_validate, expected); - } else { - // by default, is_validate == true - assert!(&result.is_validate); - } - }; - - for i in results.iter() { - let result = i.as_ref().unwrap(); - - if ignore_check_items.contains(&result.check_type_name) { - continue; - } - - println!( - "comparing precheck result, item: {}, is_source: {}", - result.check_type_name, result.is_source - ); - - if result.is_source { - compare(result, src_expected_results); - } else { - compare(result, dst_expected_results); - } - } - } -} diff --git a/dt-tests/tests/test_runner/rdb_struct_test_runner.rs b/dt-tests/tests/test_runner/rdb_struct_test_runner.rs index d1b35aca..44fc0261 100644 --- a/dt-tests/tests/test_runner/rdb_struct_test_runner.rs +++ b/dt-tests/tests/test_runner/rdb_struct_test_runner.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use concurrent_queue::ConcurrentQueue; use dt_common::{ @@ -31,6 +34,8 @@ impl RdbStructTestRunner { let (src_url, dst_url, log_level, dbs, mut filter, buffer, shut_down) = self.build_extractor_parameters().await; + let buffer = Arc::new(buffer); + let shut_down = Arc::new(shut_down); for db in dbs.iter() { if filter.filter_db(db) { continue; @@ -39,10 +44,10 @@ impl RdbStructTestRunner { let mut src_fetcher = ExtractorUtil::create_mysql_struct_extractor( &src_url, db, - &buffer, + buffer.clone(), filter.clone(), &log_level, - &shut_down, + shut_down.clone(), ) .await? .build_fetcher(); @@ -50,10 +55,10 @@ impl RdbStructTestRunner { let mut dst_fetcher = ExtractorUtil::create_mysql_struct_extractor( &dst_url, db, - &buffer, + buffer.clone(), filter.clone(), &log_level, - &shut_down, + shut_down.clone(), ) .await? .build_fetcher(); @@ -77,6 +82,8 @@ impl RdbStructTestRunner { let (src_url, dst_url, log_level, dbs, mut filter, buffer, shut_down) = self.build_extractor_parameters().await; + let buffer = Arc::new(buffer); + let shut_down = Arc::new(shut_down); for db in dbs.iter() { if filter.filter_db(db) { continue; @@ -85,10 +92,10 @@ impl RdbStructTestRunner { let mut src_fetcher = ExtractorUtil::create_pg_struct_extractor( &src_url, db, - &buffer, + buffer.clone(), filter.clone(), &log_level, - &shut_down, + shut_down.clone(), ) .await? .build_fetcher(); @@ -96,10 +103,10 @@ impl RdbStructTestRunner { let mut dst_fetcher = ExtractorUtil::create_pg_struct_extractor( &dst_url, db, - &buffer, + buffer.clone(), filter.clone(), &log_level, - &shut_down, + shut_down.clone(), ) .await? .build_fetcher(); diff --git a/dt-tests/tests/test_runner/rdb_test_runner.rs b/dt-tests/tests/test_runner/rdb_test_runner.rs index 89a911d4..d2be8c4c 100644 --- a/dt-tests/tests/test_runner/rdb_test_runner.rs +++ b/dt-tests/tests/test_runner/rdb_test_runner.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use dt_common::{ config::{ config_enums::DbType, config_token_parser::ConfigTokenParser, @@ -7,8 +9,11 @@ use dt_common::{ utils::{sql_util::SqlUtil, time_util::TimeUtil}, }; use dt_connector::rdb_query_builder::RdbQueryBuilder; -use dt_meta::{mysql::mysql_meta_manager::MysqlMetaManager, pg::pg_meta_manager::PgMetaManager}; -use dt_task::task_util::TaskUtil; +use dt_meta::{ + ddl_type::DdlType, mysql::mysql_meta_manager::MysqlMetaManager, + pg::pg_meta_manager::PgMetaManager, sql_parser::ddl_parser::DdlParser, +}; +use dt_task::{extractor_util::ExtractorUtil, task_util::TaskUtil}; use futures::TryStreamExt; use sqlx::{MySql, Pool, Postgres, Row}; @@ -26,6 +31,7 @@ pub struct RdbTestRunner { pub const SRC: &str = "src"; pub const DST: &str = "dst"; +pub const PUBLIC: &str = "public"; #[allow(dead_code)] impl RdbTestRunner { @@ -52,33 +58,51 @@ impl RdbTestRunner { let mut src_conn_pool_pg = None; let mut dst_conn_pool_pg = None; + let (src_db_type, src_url, dst_db_type, dst_url) = Self::parse_conn_info(&base); + if src_db_type == DbType::Mysql { + src_conn_pool_mysql = Some(TaskUtil::create_mysql_conn_pool(&src_url, 1, false).await?); + } else { + src_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&src_url, 1, false).await?); + } + + if dst_db_type == DbType::Mysql { + dst_conn_pool_mysql = Some(TaskUtil::create_mysql_conn_pool(&dst_url, 1, false).await?); + } else { + dst_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&dst_url, 1, false).await?); + } + + Ok(Self { + src_conn_pool_mysql, + dst_conn_pool_mysql, + src_conn_pool_pg, + dst_conn_pool_pg, + base, + }) + } + + fn parse_conn_info(base: &BaseTestRunner) -> (DbType, String, DbType, String) { + let mut src_db_type = DbType::Mysql; + let mut src_url = String::new(); + let mut dst_db_type = DbType::Mysql; + let mut dst_url = String::new(); + let config = TaskConfig::new(&base.task_config_file); match config.extractor { ExtractorConfig::MysqlCdc { url, .. } | ExtractorConfig::MysqlSnapshot { url, .. } | ExtractorConfig::MysqlCheck { url, .. } | ExtractorConfig::MysqlStruct { url, .. } => { - src_conn_pool_mysql = Some(TaskUtil::create_mysql_conn_pool(&url, 1, false).await?); + src_url = url.clone(); } ExtractorConfig::PgCdc { url, .. } | ExtractorConfig::PgSnapshot { url, .. } | ExtractorConfig::PgCheck { url, .. } | ExtractorConfig::PgStruct { url, .. } => { - src_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&url, 1, false).await?); + src_db_type = DbType::Pg; + src_url = url.clone(); } - ExtractorConfig::Basic { url, db_type, .. } => match db_type { - DbType::Mysql => { - src_conn_pool_mysql = - Some(TaskUtil::create_mysql_conn_pool(&url, 1, false).await?); - } - DbType::Pg => { - src_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&url, 1, false).await?); - } - _ => {} - }, - _ => {} } @@ -86,41 +110,23 @@ impl RdbTestRunner { SinkerConfig::Mysql { url, .. } | SinkerConfig::MysqlCheck { url, .. } | SinkerConfig::MysqlStruct { url, .. } => { - dst_conn_pool_mysql = Some(TaskUtil::create_mysql_conn_pool(&url, 1, false).await?); + dst_url = url.clone(); } SinkerConfig::Pg { url, .. } | SinkerConfig::PgCheck { url, .. } | SinkerConfig::PgStruct { url, .. } => { - dst_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&url, 1, false).await?); + dst_db_type = DbType::Pg; + dst_url = url.clone(); } - SinkerConfig::Basic { url, db_type, .. } => match db_type { - DbType::Mysql => { - dst_conn_pool_mysql = - Some(TaskUtil::create_mysql_conn_pool(&url, 1, false).await?); - } - DbType::Pg => { - dst_conn_pool_pg = Some(TaskUtil::create_pg_conn_pool(&url, 1, false).await?); - } - _ => {} - }, - _ => {} } - Ok(Self { - src_conn_pool_mysql, - dst_conn_pool_mysql, - src_conn_pool_pg, - dst_conn_pool_pg, - base, - }) + (src_db_type, src_url, dst_db_type, dst_url) } pub async fn run_snapshot_test(&self, compare_data: bool) -> Result<(), Error> { - let (src_tbs, dst_tbs, cols_list) = Self::parse_ddl(&self.base.src_ddl_sqls).unwrap(); - // prepare src and dst tables self.execute_test_ddl_sqls().await?; self.execute_test_dml_sqls().await?; @@ -129,19 +135,33 @@ impl RdbTestRunner { self.base.start_task().await?; // compare data + let db_tbs = self.get_compare_db_tbs().await?; if compare_data { assert!( - self.compare_data_for_tbs(&src_tbs, &dst_tbs, &cols_list) + self.compare_data_for_tbs(&db_tbs.clone(), &db_tbs.clone()) .await? - ); + ) } - Ok(()) } - pub async fn run_cdc_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { - let (src_tbs, dst_tbs, cols_list) = Self::parse_ddl(&self.base.src_ddl_sqls).unwrap(); + pub async fn run_ddl_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { + self.execute_test_ddl_sqls().await?; + let task = self.base.spawn_task().await?; + TimeUtil::sleep_millis(start_millis).await; + + self.execute_src_sqls(&self.base.src_dml_sqls).await?; + TimeUtil::sleep_millis(parse_millis).await; + + let db_tbs = self.get_compare_db_tbs().await?; + assert!( + self.compare_data_for_tbs(&db_tbs.clone(), &db_tbs.clone()) + .await? + ); + self.base.wait_task_finish(&task).await + } + pub async fn run_cdc_test(&self, start_millis: u64, parse_millis: u64) -> Result<(), Error> { // prepare src and dst tables self.execute_test_ddl_sqls().await?; @@ -149,6 +169,12 @@ impl RdbTestRunner { let task = self.base.spawn_task().await?; TimeUtil::sleep_millis(start_millis).await; + self.execute_test_sqls_and_compare(parse_millis).await?; + + self.base.wait_task_finish(&task).await + } + + pub async fn execute_test_sqls_and_compare(&self, parse_millis: u64) -> Result<(), Error> { // load dml sqls let mut src_insert_sqls = Vec::new(); let mut src_update_sqls = Vec::new(); @@ -166,29 +192,27 @@ impl RdbTestRunner { // insert src data self.execute_src_sqls(&src_insert_sqls).await?; - TimeUtil::sleep_millis(start_millis).await; - assert!( - self.compare_data_for_tbs(&src_tbs, &dst_tbs, &cols_list) - .await? - ); + TimeUtil::sleep_millis(parse_millis).await; + + let db_tbs = self.get_compare_db_tbs().await?; + let src_db_tbs = db_tbs.clone(); + let dst_db_tbs = db_tbs.clone(); + assert!(self.compare_data_for_tbs(&src_db_tbs, &dst_db_tbs).await?); // update src data self.execute_src_sqls(&src_update_sqls).await?; - TimeUtil::sleep_millis(parse_millis).await; - assert!( - self.compare_data_for_tbs(&src_tbs, &dst_tbs, &cols_list) - .await? - ); + if !src_update_sqls.is_empty() { + TimeUtil::sleep_millis(parse_millis).await; + } + assert!(self.compare_data_for_tbs(&src_db_tbs, &dst_db_tbs).await?); // delete src data self.execute_src_sqls(&src_delete_sqls).await?; - TimeUtil::sleep_millis(parse_millis).await; - assert!( - self.compare_data_for_tbs(&src_tbs, &dst_tbs, &cols_list) - .await? - ); - - BaseTestRunner::wait_task_finish(&task).await + if !src_delete_sqls.is_empty() { + TimeUtil::sleep_millis(parse_millis).await; + } + assert!(self.compare_data_for_tbs(&src_db_tbs, &dst_db_tbs).await?); + Ok(()) } pub async fn initialization_ddl(&self) -> Result<(), Error> { @@ -227,22 +251,23 @@ impl RdbTestRunner { pub async fn run_cycle_cdc_data_check( &self, + transaction_database: String, transaction_table_full_name: String, expect_num: Option, ) -> Result<(), Error> { - let (src_tbs, dst_tbs, cols_list) = Self::parse_ddl_with_cycle( - &self.base.src_ddl_sqls, - transaction_table_full_name.to_owned(), - ) - .unwrap(); - let dml_count = match expect_num { Some(num) => num, None => self.base.src_dml_sqls.len() as u8, }; + let db_tbs = self.get_compare_db_tbs_from_sqls()?; + let db_tbs_without_transaction: Vec<(String, String)> = db_tbs + .iter() + .filter(|s| !transaction_database.eq(s.0.as_str())) + .map(|s| (s.0.clone(), s.1.clone())) + .collect(); assert!( - self.compare_data_for_tbs(&src_tbs, &dst_tbs, &cols_list) + self.compare_data_for_tbs(&db_tbs_without_transaction, &db_tbs_without_transaction) .await? ); @@ -291,7 +316,7 @@ impl RdbTestRunner { self.execute_src_sqls(&self.base.src_dml_sqls).await?; TimeUtil::sleep_millis(parse_millis).await; - BaseTestRunner::wait_task_finish(&task).await + self.base.wait_task_finish(&task).await } pub async fn execute_test_dml_sqls(&self) -> Result<(), Error> { @@ -313,7 +338,7 @@ impl RdbTestRunner { } } - async fn execute_src_sqls(&self, sqls: &Vec) -> Result<(), Error> { + pub async fn execute_src_sqls(&self, sqls: &Vec) -> Result<(), Error> { if let Some(pool) = &self.src_conn_pool_mysql { Self::execute_sqls_mysql(sqls, pool).await?; } @@ -333,11 +358,10 @@ impl RdbTestRunner { Ok(()) } - async fn compare_data_for_tbs( + pub async fn compare_data_for_tbs( &self, - src_tbs: &Vec, - dst_tbs: &Vec, - cols_list: &Vec>, + src_db_tbs: &Vec<(String, String)>, + dst_db_tbs: &Vec<(String, String)>, ) -> Result { let filtered_tbs_file = format!("{}/filtered_tbs.txt", &self.base.test_dir); let filtered_tbs = if BaseTestRunner::check_file_exists(&filtered_tbs_file) { @@ -346,16 +370,14 @@ impl RdbTestRunner { Vec::new() }; - for i in 0..src_tbs.len() { - let src_tb = &src_tbs[i]; - let dst_tb = &dst_tbs[i]; - if filtered_tbs.contains(src_tb) { - let dst_data = self.fetch_data(&dst_tb, DST).await?; + for i in 0..src_db_tbs.len() { + if filtered_tbs.contains(&format!("{}.{}", &src_db_tbs[i].0, &src_db_tbs[i].1)) { + let dst_data = self.fetch_data(&dst_db_tbs[i], DST).await?; if dst_data.len() > 0 { return Ok(false); } } else { - if !self.compare_tb_data(src_tb, dst_tb, &cols_list[i]).await? { + if !self.compare_tb_data(&src_db_tbs[i], &dst_db_tbs[i]).await? { return Ok(false); } } @@ -365,22 +387,22 @@ impl RdbTestRunner { async fn compare_tb_data( &self, - src_tb: &str, - dst_tb: &str, - cols: &Vec, + src_db_tb: &(String, String), + dst_db_tb: &(String, String), ) -> Result { - let src_data = self.fetch_data(src_tb, SRC).await?; - let dst_data = self.fetch_data(dst_tb, DST).await?; + let src_data = self.fetch_data(src_db_tb, SRC).await?; + let dst_data = self.fetch_data(dst_db_tb, DST).await?; println!( - "comparing row data for src_tb: {}, src_data count: {}", - src_tb, + "comparing row data for src_tb: {:?}, src_data count: {}", + src_db_tb, src_data.len() ); - if !Self::compare_row_data(cols, &src_data, &dst_data) { + let cols = self.get_tb_cols(src_db_tb).await?; + if !Self::compare_row_data(&cols, &src_data, &dst_data) { println!( - "compare_tb_data failed, src_tb: {}, dst_tb: {}", - src_tb, dst_tb + "compare tb data failed, src_tb: {:?}, dst_tb: {:?}", + src_db_tb, dst_db_tb, ); return Ok(false); } @@ -402,8 +424,8 @@ impl RdbTestRunner { let dst_col_value = &dst_row_data[j]; if src_col_value != dst_col_value { println!( - "col: {}, src_col_value: {:?}, dst_col_value: {:?}", - cols[j], src_col_value, dst_col_value + "row index: {}, col: {}, src_col_value: {:?}, dst_col_value: {:?}", + i, cols[j], src_col_value, dst_col_value ); return false; } @@ -419,7 +441,12 @@ impl RdbTestRunner { col_order: u8, expect_num: u8, ) -> Result { - let result = self.fetch_data(full_tb_name, from).await?; + let db_tb: Vec<&str> = full_tb_name.split(".").collect(); + assert_eq!(db_tb.len(), 2); + + let result = self + .fetch_data(&(db_tb[0].to_string(), db_tb[1].to_string()), from) + .await?; assert!(result.len() == 1); let row_data = result.get(0).unwrap(); @@ -437,7 +464,7 @@ impl RdbTestRunner { pub async fn fetch_data( &self, - full_tb_name: &str, + db_tb: &(String, String), from: &str, ) -> Result>>>, Error> { let conn_pool_mysql = if from == SRC { @@ -453,16 +480,16 @@ impl RdbTestRunner { }; let data = if let Some(pool) = conn_pool_mysql { - self.fetch_data_mysql(full_tb_name, pool).await? + self.fetch_data_mysql(db_tb, pool, from).await? } else if let Some(pool) = conn_pool_pg { - self.fetch_data_pg(full_tb_name, pool).await? + self.fetch_data_pg(db_tb, pool, from).await? } else { Vec::new() }; Ok(data) } - fn parse_full_tb_name(full_tb_name: &str, db_type: DbType) -> (String, String) { + pub fn parse_full_tb_name(full_tb_name: &str, db_type: &DbType) -> (String, String) { let escape_pairs = SqlUtil::get_escape_pairs(&db_type); let tokens = ConfigTokenParser::parse(full_tb_name, &vec!['.'], &escape_pairs); let (db, tb) = if tokens.len() > 1 { @@ -479,22 +506,22 @@ impl RdbTestRunner { async fn fetch_data_mysql( &self, - full_tb_name: &str, + db_tb: &(String, String), conn_pool: &Pool, + from: &str, ) -> Result>>>, Error> { - let (db, tb) = Self::parse_full_tb_name(full_tb_name, DbType::Mysql); - let mut meta_manager = MysqlMetaManager::new(conn_pool.clone()).init().await?; - let tb_meta = meta_manager.get_tb_meta(&db, &tb).await?; - let cols = &tb_meta.basic.cols; - - let sql = format!("SELECT * FROM `{}`.`{}` ORDER BY `{}`", &db, &tb, &cols[0],); + let cols = self.get_tb_cols_with_direction(db_tb, Some(from)).await?; + let sql = format!( + "SELECT * FROM `{}`.`{}` ORDER BY `{}`", + &db_tb.0, &db_tb.1, &cols[0], + ); let query = sqlx::query(&sql); let mut rows = query.fetch(conn_pool); let mut result = Vec::new(); while let Some(row) = rows.try_next().await.unwrap() { let mut row_values = Vec::new(); - for col in cols { + for col in cols.iter() { let value: Option> = row.get_unchecked(col.as_str()); row_values.push(value); } @@ -506,22 +533,19 @@ impl RdbTestRunner { async fn fetch_data_pg( &self, - full_tb_name: &str, + db_tb: &(String, String), conn_pool: &Pool, + from: &str, ) -> Result>>>, Error> { - let (mut db, tb) = Self::parse_full_tb_name(full_tb_name, DbType::Pg); - if db.is_empty() { - db = "public".to_string(); - } let mut meta_manager = PgMetaManager::new(conn_pool.clone()).init().await?; - let tb_meta = meta_manager.get_tb_meta(&db, &tb).await?; - let cols = &tb_meta.basic.cols; + let tb_meta = meta_manager.get_tb_meta(&db_tb.0, &db_tb.1).await?; let query_builder = RdbQueryBuilder::new_for_pg(&tb_meta); - let cols_str = query_builder.build_extract_cols_str().unwrap(); + let cols = self.get_tb_cols_with_direction(&db_tb, Some(from)).await?; + let cols_str = query_builder.build_extract_cols_str().unwrap(); let sql = format!( r#"SELECT {} FROM "{}"."{}" ORDER BY "{}" ASC"#, - cols_str, &db, &tb, &cols[0], + cols_str, &db_tb.0, &db_tb.1, &cols[0], ); let query = sqlx::query(&sql); let mut rows = query.fetch(conn_pool); @@ -529,7 +553,7 @@ impl RdbTestRunner { let mut result = Vec::new(); while let Some(row) = rows.try_next().await.unwrap() { let mut row_values = Vec::new(); - for col in cols { + for col in cols.iter() { let value: Option> = row.get_unchecked(col.as_str()); row_values.push(value); } @@ -557,104 +581,106 @@ impl RdbTestRunner { Ok(()) } - fn parse_ddl( - src_ddl_sqls: &Vec, - ) -> Result<(Vec, Vec, Vec>), Error> { - let mut src_tbs = Vec::new(); - let mut cols_list = Vec::new(); - for sql in src_ddl_sqls { - if sql.to_lowercase().contains("create table") { - let (tb, cols) = Self::parse_create_table(&sql).unwrap(); - src_tbs.push(tb); - cols_list.push(cols); + /// get compare dbs from src_ddl_sqls + async fn get_compare_dbs(&self, url: &str, db_type: &DbType) -> Result, Error> { + let all_dbs = ExtractorUtil::list_dbs(url, db_type).await?; + let mut dbs = HashSet::new(); + for sql in self.base.src_ddl_sqls.iter() { + if !sql.to_lowercase().contains("database") { + continue; } - } - let dst_tbs = src_tbs.clone(); - - Ok((src_tbs, dst_tbs, cols_list)) - } - - fn parse_ddl_with_cycle( - src_ddl_sqls: &Vec, - transaction_table_full_name: String, - ) -> Result<(Vec, Vec, Vec>), Error> { - let mut src_tbs = Vec::new(); - let mut cols_list = Vec::new(); - let transaction_db = transaction_table_full_name - .split('.') - .collect::>() - .get(0) - .unwrap() - .clone(); - - for sql in src_ddl_sqls { - let new_sql = sql - .to_lowercase() - .replace("create table if not exists", "create table"); - if new_sql.to_lowercase().contains("create table") { - let (tb, cols) = Self::parse_create_table(&new_sql).unwrap(); - if tb.starts_with(format!("{}.", transaction_db).as_str()) { - continue; + let ddl = DdlParser::parse(sql).unwrap(); + if ddl.0 == DdlType::DropDatabase || ddl.0 == DdlType::CreateDatabase { + let db = ddl.1.unwrap(); + // db may be dropped in test sql (ddl test) + if all_dbs.contains(&db) { + dbs.insert(db); } - src_tbs.push(tb); - cols_list.push(cols); } } - let dst_tbs = src_tbs.clone(); - - Ok((src_tbs, dst_tbs, cols_list)) + Ok(dbs) } - fn parse_create_table(sql: &str) -> Result<(String, Vec), Error> { - // since both (sqlparser = "0.30.0", sql-parse = "0.11.0") have bugs in parsing create table sql, - // we take a simple workaround to get table name and column names. + /// get compare tbs + pub async fn get_compare_db_tbs(&self) -> Result, Error> { + let mut db_tbs = vec![]; - // CREATE TABLE bitbin_table (pk SERIAL, bvunlimited1 BIT VARYING, p GEOMETRY(POINT,3187), PRIMARY KEY(pk)); - let mut tokens = Vec::new(); - let mut token = String::new(); - let mut brakets = 0; + let (src_db_type, src_url, _, _) = Self::parse_conn_info(&self.base); - for b in sql.as_bytes() { - let c = char::from(*b); - if c == ' ' || c == '(' || c == ')' || c == ',' { - if !token.is_empty() { - tokens.push(token.clone()); - token.clear(); + if src_db_type == DbType::Mysql { + let dbs = self.get_compare_dbs(&src_url, &src_db_type).await?; + for db in dbs.iter() { + let tbs = ExtractorUtil::list_tbs(&src_url, db, &src_db_type).await?; + for tb in tbs.iter() { + db_tbs.push((db.to_string(), tb.to_string())); } + } + } else { + db_tbs = self.get_compare_db_tbs_from_sqls()? + } - if c == '(' { - brakets += 1; - } else if c == ')' { - brakets -= 1; - if brakets == 0 { - break; - } - } + Ok(db_tbs) + } - if c == ',' && brakets == 1 { - tokens.push(",".to_string()); - } + pub fn get_compare_db_tbs_from_sqls(&self) -> Result, Error> { + let mut db_tbs = vec![]; + + for sql in self.base.src_ddl_sqls.iter() { + if !sql.to_lowercase().contains("table") { continue; } - token.push(c); + let ddl = DdlParser::parse(sql).unwrap(); + if ddl.0 == DdlType::CreateTable { + let db = if let Some(db) = ddl.1 { + db + } else { + PUBLIC.to_string() + }; + let tb = ddl.2.unwrap(); + db_tbs.push((db, tb)); + } } - let tb = tokens[2].clone(); - let mut cols = vec![tokens[3].clone()]; - for i in 3..tokens.len() { - let token_lowercase = tokens[i].to_lowercase(); - if token_lowercase == "primary" - || token_lowercase == "unique" - || token_lowercase == "key" - { - break; - } + Ok(db_tbs) + } - if tokens[i - 1] == "," { - cols.push(tokens[i].clone()); - } - } + async fn get_tb_cols(&self, db_tb: &(String, String)) -> Result, Error> { + self.get_tb_cols_with_direction(db_tb, None).await + } - Ok((tb, cols)) + async fn get_tb_cols_with_direction( + &self, + db_tb: &(String, String), + from: Option<&str>, + ) -> Result, Error> { + let f = match from { + Some(s) => s, + None => self::SRC, + }; + + let conn_pool_mysql = if f == SRC { + &self.src_conn_pool_mysql + } else { + &self.dst_conn_pool_mysql + }; + + let conn_pool_pg = if f == SRC { + &self.src_conn_pool_pg + } else { + &self.dst_conn_pool_pg + }; + + let cols = if let Some(conn_pool) = conn_pool_mysql { + let mut meta_manager = MysqlMetaManager::new(conn_pool.clone()).init().await?; + let tb_meta = meta_manager.get_tb_meta(&db_tb.0, &db_tb.1).await?; + tb_meta.basic.cols.clone() + } else if let Some(conn_pool) = conn_pool_pg { + let mut meta_manager = PgMetaManager::new(conn_pool.clone()).init().await?; + let tb_meta = meta_manager.get_tb_meta(&db_tb.0, &db_tb.1).await?; + tb_meta.basic.cols.clone() + } else { + vec![] + }; + Ok(cols) } } diff --git a/dt-tests/tests/test_runner/redis_test_runner.rs b/dt-tests/tests/test_runner/redis_test_runner.rs new file mode 100644 index 00000000..b2d098f5 --- /dev/null +++ b/dt-tests/tests/test_runner/redis_test_runner.rs @@ -0,0 +1,386 @@ +use std::collections::HashMap; + +use super::base_test_runner::BaseTestRunner; +use dt_common::{ + config::{ + config_token_parser::ConfigTokenParser, extractor_config::ExtractorConfig, + sinker_config::SinkerConfig, task_config::TaskConfig, + }, + error::Error, + utils::time_util::TimeUtil, +}; +use dt_connector::sinker::redis::cmd_encoder::CmdEncoder; +use dt_meta::redis::redis_object::RedisCmd; +use dt_task::task_util::TaskUtil; +use redis::{Connection, ConnectionLike, Value}; + +const SRC: &str = "src"; +const DST: &str = "dst"; + +const SYSTEM_KEYS: [&str; 4] = ["backup1", "backup2", "backup3", "backup4"]; + +pub struct RedisTestRunner { + pub base: BaseTestRunner, + src_conn: Connection, + dst_conn: Connection, + delimiters: Vec, + escape_pairs: Vec<(char, char)>, +} + +impl RedisTestRunner { + pub async fn new_default(relative_test_dir: &str) -> Result { + Self::new(relative_test_dir, vec![' '], vec![('"', '"')]).await + } + + pub async fn new( + relative_test_dir: &str, + delimiters: Vec, + escape_pairs: Vec<(char, char)>, + ) -> Result { + let base = BaseTestRunner::new(relative_test_dir).await.unwrap(); + + let config = TaskConfig::new(&base.task_config_file); + let src_conn = match config.extractor { + ExtractorConfig::RedisSnapshot { url, .. } | ExtractorConfig::RedisCdc { url, .. } => { + TaskUtil::create_redis_conn(&url).await.unwrap() + } + _ => { + return Err(Error::ConfigError("unsupported extractor config".into())); + } + }; + + let dst_conn = match config.sinker { + SinkerConfig::Redis { url, .. } => TaskUtil::create_redis_conn(&url).await.unwrap(), + _ => { + return Err(Error::ConfigError("unsupported sinker config".into())); + } + }; + + Ok(Self { + base, + src_conn, + dst_conn, + delimiters, + escape_pairs, + }) + } + + pub async fn run_snapshot_test(&mut self) -> Result<(), Error> { + self.execute_test_ddl_sqls()?; + + println!("src: {}", TaskUtil::get_redis_version(&mut self.src_conn)?); + println!("dst: {}", TaskUtil::get_redis_version(&mut self.dst_conn)?); + + self.execute_cmds(SRC, &self.base.src_dml_sqls.clone()); + self.base.start_task().await?; + self.compare_all_data() + } + + pub async fn run_cdc_test( + &mut self, + start_millis: u64, + parse_millis: u64, + ) -> Result<(), Error> { + self.execute_test_ddl_sqls()?; + + let task = self.base.spawn_task().await?; + TimeUtil::sleep_millis(start_millis).await; + + println!("src: {}", TaskUtil::get_redis_version(&mut self.src_conn)?); + println!("dst: {}", TaskUtil::get_redis_version(&mut self.dst_conn)?); + + self.execute_cmds(SRC, &self.base.src_dml_sqls.clone()); + TimeUtil::sleep_millis(parse_millis).await; + self.compare_all_data()?; + + self.base.wait_task_finish(&task).await + } + + pub fn execute_test_ddl_sqls(&mut self) -> Result<(), Error> { + self.execute_cmds(SRC, &self.base.src_ddl_sqls.clone()); + self.execute_cmds(DST, &self.base.dst_ddl_sqls.clone()); + Ok(()) + } + + fn compare_all_data(&mut self) -> Result<(), Error> { + let dbs = self.list_dbs(SRC); + for db in dbs { + println!("compare data for db: {}", db); + self.compare_data(db)?; + } + Ok(()) + } + + fn compare_data(&mut self, db: String) -> Result<(), Error> { + self.execute_cmd(SRC, &format!("SELECT {}", db)); + self.execute_cmd(DST, &format!("SELECT {}", db)); + + let mut string_keys = Vec::new(); + let mut hash_keys = Vec::new(); + let mut list_keys = Vec::new(); + let mut stream_keys = Vec::new(); + let mut set_keys = Vec::new(); + let mut zset_keys = Vec::new(); + + let mut json_keys = Vec::new(); + let mut bf_bloom_keys = Vec::new(); + let mut cf_bloom_keys = Vec::new(); + + let keys = self.list_keys(SRC, "*"); + for i in keys.iter() { + let key = i.clone(); + let key_type = self.get_key_type(SRC, &key); + match key_type.to_lowercase().as_str() { + "string" => string_keys.push(key), + "hash" => hash_keys.push(key), + "zset" => zset_keys.push(key), + "stream" => stream_keys.push(key), + "set" => set_keys.push(key), + "list" => list_keys.push(key), + "rejson-rl" => json_keys.push(key), + "mbbloom--" => bf_bloom_keys.push(key), + "mbbloomcf" => cf_bloom_keys.push(key), + _ => { + println!("unkown type: {} for key: {}", key_type, key); + string_keys.push(key) + } + } + } + + self.compare_string_entries(&string_keys); + self.compare_hash_entries(&hash_keys); + self.compare_list_entries(&list_keys); + self.compare_set_entries(&set_keys); + self.compare_zset_entries(&zset_keys); + self.compare_stream_entries(&stream_keys); + self.compare_rejson_entries(&json_keys); + self.compare_bf_bloom_entries(&bf_bloom_keys); + self.compare_cf_bloom_entries(&cf_bloom_keys); + self.check_expire(&keys); + Ok(()) + } + + fn check_expire(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("PTTL {}", self.escape_key(key)); + let src_result = self.execute_cmd(SRC, &cmd); + let dst_result = self.execute_cmd(DST, &cmd); + + let get_expire = |result: Value| -> i64 { + match result { + Value::Int(expire) => return expire, + _ => { + // should never happen + return -1000; + } + } + }; + + let src_expire = get_expire(src_result); + let dst_expire = get_expire(dst_result); + assert_ne!(src_expire, -1000); + assert_ne!(dst_expire, -1000); + if src_expire > 0 { + println!("src key: {} expire in {}", key, src_expire); + println!("dst key: {} expire in {}", key, dst_expire); + assert!(dst_expire > 0) + } else { + assert!(dst_expire < 0) + } + } + } + + fn compare_string_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("GET {}", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_hash_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("HGETALL {}", self.escape_key(key)); + let src_result = self.execute_cmd(SRC, &cmd); + let dst_result = self.execute_cmd(DST, &cmd); + + let build_kvs = |result: redis::Value| { + let mut kvs = HashMap::new(); + if let redis::Value::Bulk(mut values) = result { + for _i in (0..values.len()).step_by(2) { + let k = values.remove(0); + let v = values.remove(0); + if let redis::Value::Data(k) = k { + kvs.insert(k, v); + } else { + assert!(false); + } + } + } else { + assert!(false); + } + kvs + }; + + let src_kvs = build_kvs(src_result); + let dst_kvs = build_kvs(dst_result); + println!( + "compare results for cmd: {}, \r\n src_kvs: {:?} \r\n dst_kvs: {:?}", + cmd, src_kvs, dst_kvs + ); + assert_eq!(src_kvs, dst_kvs); + } + } + + fn compare_list_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("LRANGE {} 0 -1", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_set_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("SORT {} ALPHA", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_zset_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("ZRANGE {} 0 -1 WITHSCORES", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_stream_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("XRANGE {} - +", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_rejson_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("JSON.GET {}", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_bf_bloom_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("BF.DEBUG {}", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_cf_bloom_entries(&mut self, keys: &Vec) { + for key in keys { + let cmd = format!("CF.DEBUG {}", self.escape_key(key)); + self.compare_cmd_results(&cmd); + } + } + + fn compare_cmd_results(&mut self, cmd: &str) { + let src_result = self.execute_cmd(SRC, cmd); + let dst_result = self.execute_cmd(DST, cmd); + println!( + "compare results for cmd: {}, \r\n src_kvs: {:?} \r\n dst_kvs: {:?}", + cmd, src_result, dst_result + ); + assert_eq!(src_result, dst_result); + } + + fn list_dbs(&mut self, from: &str) -> Vec { + let mut dbs = Vec::new(); + let cmd = "INFO keyspace"; + match self.execute_cmd(from, &cmd) { + redis::Value::Data(data) => { + let spaces = String::from_utf8(data).unwrap(); + for space in spaces.split("\r\n").collect::>() { + if space.contains("db") { + let tokens: Vec<&str> = space.split(":").collect::>(); + dbs.push(tokens[0].trim_start_matches("db").to_string()); + } + } + } + _ => {} + } + dbs + } + + fn list_keys(&mut self, from: &str, match_pattern: &str) -> Vec { + let mut keys = Vec::new(); + let cmd = format!("KEYS {}", match_pattern); + match self.execute_cmd(from, &cmd) { + redis::Value::Bulk(values) => { + for v in values { + match v { + redis::Value::Data(data) => { + let key = String::from_utf8(data).unwrap(); + if SYSTEM_KEYS.contains(&key.as_str()) { + continue; + } + keys.push(key) + } + _ => assert!(false), + } + } + } + _ => assert!(false), + } + keys.sort(); + keys + } + + fn get_key_type(&mut self, from: &str, key: &str) -> String { + let cmd = format!("type {}", self.escape_key(key)); + let value = self.execute_cmd(from, &cmd); + match value { + redis::Value::Status(key_type) => { + return key_type; + } + _ => assert!(false), + } + String::new() + } + + fn escape_key(&self, key: &str) -> String { + format!( + "{}{}{}", + self.escape_pairs[0].0, key, self.escape_pairs[0].1 + ) + } + + fn execute_cmds(&mut self, from: &str, cmds: &Vec) { + for cmd in cmds.iter() { + self.execute_cmd(from, cmd); + } + } + + fn execute_cmd(&mut self, from: &str, cmd: &str) -> Value { + println!("execute cmd: {:?}", cmd); + let packed_cmd = self.pack_cmd(cmd); + let conn = if from == SRC { + &mut self.src_conn + } else { + &mut self.dst_conn + }; + conn.req_packed_command(&packed_cmd).unwrap() + } + + fn pack_cmd(&self, cmd: &str) -> Vec { + // parse cmd args + let mut redis_cmd = RedisCmd::new(); + for arg in ConfigTokenParser::parse(cmd, &self.delimiters, &self.escape_pairs) { + let mut arg = arg.clone(); + for (left, right) in &self.escape_pairs { + arg = arg + .trim_start_matches(*left) + .trim_end_matches(*right) + .to_string(); + } + redis_cmd.add_str_arg(&arg); + } + CmdEncoder::encode(&redis_cmd) + } +} diff --git a/dt-tests/tests/test_runner/test_base.rs b/dt-tests/tests/test_runner/test_base.rs index 89720a17..e1b5c7ff 100644 --- a/dt-tests/tests/test_runner/test_base.rs +++ b/dt-tests/tests/test_runner/test_base.rs @@ -1,17 +1,16 @@ use std::collections::{HashMap, HashSet}; +use dt_common::config::config_enums::DbType; use dt_common::utils::time_util::TimeUtil; use futures::executor::block_on; -use crate::{ - test_config_util::TestConfigUtil, - test_runner::{base_test_runner::BaseTestRunner, rdb_test_runner::DST}, -}; +use crate::{test_config_util::TestConfigUtil, test_runner::rdb_test_runner::DST}; use super::{ - mongo_test_runner::MongoTestRunner, rdb_check_test_runner::RdbCheckTestRunner, - rdb_precheck_test_runner::RdbPrecheckTestRunner, rdb_struct_test_runner::RdbStructTestRunner, - rdb_test_runner::RdbTestRunner, + mongo_test_runner::MongoTestRunner, precheck_test_runner::PrecheckTestRunner, + rdb_check_test_runner::RdbCheckTestRunner, rdb_kafka_rdb_test_runner::RdbKafkaRdbTestRunner, + rdb_struct_test_runner::RdbStructTestRunner, rdb_test_runner::RdbTestRunner, + redis_test_runner::RedisTestRunner, }; pub struct TestBase {} @@ -25,18 +24,24 @@ impl TestBase { pub async fn run_snapshot_test_and_check_dst_count( test_dir: &str, + db_type: &DbType, dst_expected_counts: HashMap<&str, usize>, ) { let runner = RdbTestRunner::new(test_dir).await.unwrap(); runner.run_snapshot_test(false).await.unwrap(); - let assert_dst_count = |tb: &str, count: usize| { - let dst_data = block_on(runner.fetch_data(tb, DST)).unwrap(); + let assert_dst_count = |db_tb: &(String, String), count: usize| { + let dst_data = block_on(runner.fetch_data(db_tb, DST)).unwrap(); + println!( + "check dst table {:?} record count, expect: {}", + db_tb, count + ); assert_eq!(dst_data.len(), count); }; - for (tb, count) in dst_expected_counts.iter() { - assert_dst_count(tb, *count); + for (db_tb, count) in dst_expected_counts { + let db_tb = RdbTestRunner::parse_full_tb_name(db_tb, db_type); + assert_dst_count(&db_tb, count); } } @@ -50,6 +55,14 @@ impl TestBase { // .unwrap(); } + pub async fn run_ddl_test(test_dir: &str, start_millis: u64, parse_millis: u64) { + let runner = RdbTestRunner::new(test_dir).await.unwrap(); + runner + .run_ddl_test(start_millis, parse_millis) + .await + .unwrap(); + } + pub async fn run_cycle_cdc_test( test_dir: &str, start_millis: u64, @@ -102,13 +115,20 @@ impl TestBase { }; runner - .run_cycle_cdc_data_check(transaction_full_name, expect_num) + .run_cycle_cdc_data_check( + String::from(transaction_database), + transaction_full_name, + expect_num, + ) .await .unwrap(); } for handler in handlers { - BaseTestRunner::wait_task_finish(&handler).await.unwrap(); + handler.abort(); + while !handler.is_finished() { + TimeUtil::sleep_millis(1).await; + } } } @@ -157,6 +177,51 @@ impl TestBase { .unwrap(); } + pub async fn run_mongo_cdc_resume_test(test_dir: &str, start_millis: u64, parse_millis: u64) { + let runner = MongoTestRunner::new(test_dir).await.unwrap(); + runner + .run_cdc_resume_test(start_millis, parse_millis) + .await + .unwrap(); + } + + pub async fn run_redis_snapshot_test(test_dir: &str) { + let mut runner = RedisTestRunner::new_default(test_dir).await.unwrap(); + runner.run_snapshot_test().await.unwrap(); + } + + pub async fn run_redis_rejson_snapshot_test(test_dir: &str) { + let mut runner = RedisTestRunner::new(test_dir, vec![' '], vec![('\'', '\'')]) + .await + .unwrap(); + runner.run_snapshot_test().await.unwrap(); + } + + pub async fn run_redis_redisearch_snapshot_test(test_dir: &str) { + let mut runner = RedisTestRunner::new(test_dir, vec![' '], vec![('\'', '\'')]) + .await + .unwrap(); + runner.run_snapshot_test().await.unwrap(); + } + + pub async fn run_redis_cdc_test(test_dir: &str, start_millis: u64, parse_millis: u64) { + let mut runner = RedisTestRunner::new_default(test_dir).await.unwrap(); + runner + .run_cdc_test(start_millis, parse_millis) + .await + .unwrap(); + } + + pub async fn run_redis_rejson_cdc_test(test_dir: &str, start_millis: u64, parse_millis: u64) { + let mut runner = RedisTestRunner::new(test_dir, vec![' '], vec![('\'', '\'')]) + .await + .unwrap(); + runner + .run_cdc_test(start_millis, parse_millis) + .await + .unwrap(); + } + pub async fn run_mysql_struct_test(test_dir: &str) { let mut runner = RdbStructTestRunner::new(test_dir).await.unwrap(); runner.run_mysql_struct_test().await.unwrap(); @@ -177,21 +242,34 @@ impl TestBase { src_expected_results: &HashMap, dst_expected_results: &HashMap, ) { - let runner = RdbPrecheckTestRunner::new(test_dir).await.unwrap(); - runner.base.execute_test_ddl_sqls().await.unwrap(); - let results: Vec< - Result, - > = runner.run_check().await; - + let runner = PrecheckTestRunner::new(test_dir).await.unwrap(); runner - .validate( - &results, + .run_check( ignore_check_items, - &src_expected_results, - &dst_expected_results, + src_expected_results, + dst_expected_results, ) - .await; + .await + .unwrap(); + } - runner.base.execute_clean_sqls().await.unwrap(); + pub async fn run_rdb_kafka_rdb_cdc_test(test_dir: &str, start_millis: u64, parse_millis: u64) { + let runner = RdbKafkaRdbTestRunner::new(test_dir).await.unwrap(); + runner + .run_cdc_test(start_millis, parse_millis) + .await + .unwrap(); + } + + pub async fn run_rdb_kafka_rdb_snapshot_test( + test_dir: &str, + start_millis: u64, + parse_millis: u64, + ) { + let runner = RdbKafkaRdbTestRunner::new(test_dir).await.unwrap(); + runner + .run_snapshot_test(start_millis, parse_millis) + .await + .unwrap(); } } diff --git a/plugins/mysql-binlog-connector-rust b/plugins/mysql-binlog-connector-rust index c29ea996..836bc343 160000 --- a/plugins/mysql-binlog-connector-rust +++ b/plugins/mysql-binlog-connector-rust @@ -1 +1 @@ -Subproject commit c29ea9967ba667a7d985ea5ad1ad26949b6fdf35 +Subproject commit 836bc343234ba6f7023702d97301772f4b41720b