diff --git a/BUILD b/BUILD index c5dd23f..525f546 100644 --- a/BUILD +++ b/BUILD @@ -29,6 +29,7 @@ rust_binary( "@crates//:futures", "@crates//:glob", "@crates//:home", + "@crates//:itertools", "@crates//:rpassword", "@crates//:rustyline", "@crates//:sentry", diff --git a/Cargo.lock b/Cargo.lock index f132a35..7a999ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,14 +276,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] name = "chrono-tz" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", "chrono-tz-build", @@ -293,21 +293,21 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +checksum = "c7d8d1efd5109b9c1cd3b7966bd071cdfb53bb6eb0b22a473a68c2f70a11a1eb" dependencies = [ "parse-zoneinfo", - "phf", "phf_codegen", + "phf_shared", "uncased", ] [[package]] name = "clap" -version = "4.5.47" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -1072,17 +1072,6 @@ dependencies = [ "hashbrown 0.15.2", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1123,6 +1112,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.175" @@ -1153,13 +1148,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1236,6 +1240,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1318,12 +1331,9 @@ dependencies = [ [[package]] name = "parse-zoneinfo" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] +checksum = "e3c406c9e2aa74554e662d2c2ee11cd3e73756988800be7e6f5eddb16fed4699" [[package]] name = "percent-encoding" @@ -1388,18 +1398,19 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ + "phf_macros", "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +checksum = "efbdcb6f01d193b17f0b9c3360fa7e0e620991b193ff08702f78b3ce365d7e61" dependencies = [ "phf_generator", "phf_shared", @@ -1407,19 +1418,33 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" dependencies = [ + "fastrand", "phf_shared", - "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", + "uncased", ] [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ "siphasher", "uncased", @@ -1996,9 +2021,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2006,18 +2031,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2049,6 +2074,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2231,6 +2265,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.37" @@ -2274,29 +2317,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2447,6 +2487,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2473,13 +2514,33 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -2511,7 +2572,7 @@ dependencies = [ [[package]] name = "typedb-driver" version = "0.0.0" -source = "git+https://github.com/typedb/typedb-driver?tag=3.5.0#d89b208e76834f8d263b974cc2041544107d74a9" +source = "git+https://github.com/typedb/typedb-driver?rev=7c7a715303a8be91e87bac6bbe42db475257e339#7c7a715303a8be91e87bac6bbe42db475257e339" dependencies = [ "chrono", "chrono-tz", @@ -2527,6 +2588,8 @@ dependencies = [ "tokio-stream", "tonic", "tonic-types", + "tracing", + "tracing-subscriber", "typedb-protocol", "uuid", ] @@ -2534,7 +2597,7 @@ dependencies = [ [[package]] name = "typedb-protocol" version = "0.0.0" -source = "git+https://github.com/typedb/typedb-protocol?tag=3.4.0#3fc4c0fe0ac37c517af0e31415347299cff0307c" +source = "git+https://github.com/typedb/typedb-protocol?rev=b007ebc43e307ca4b6354fedbbfc3223361044dc#b007ebc43e307ca4b6354fedbbfc3223361044dc" dependencies = [ "prost", "tonic", @@ -2558,7 +2621,7 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typeql" version = "0.0.0" -source = "git+https://github.com/typedb/typeql?tag=3.5.0#6dd83bd45775a061ddad76436caa18c385d11b2b" +source = "git+https://github.com/typedb/typeql?tag=3.7.0-rc0#6d96bdfed9724faf21ab14025ecc17d0483947ce" dependencies = [ "chrono", "itertools 0.10.5", @@ -2839,6 +2902,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -2866,6 +2935,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index fc59140..b09edac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ features = {} [dependencies] [dependencies.tokio] - features = ["bytes", "default", "fs", "full", "io-std", "io-util", "libc", "macros", "mio", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] - version = "1.47.1" + features = ["bytes", "default", "fs", "full", "io-std", "io-util", "libc", "macros", "mio", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "test-util", "time", "tokio-macros"] + version = "1.48.0" default-features = false [dependencies.glob] @@ -55,7 +55,7 @@ features = {} [dependencies.clap] features = ["color", "default", "derive", "error-context", "help", "std", "suggestions", "usage", "wrap_help"] - version = "4.5.45" + version = "4.5.52" default-features = false [dependencies.typeql] @@ -66,13 +66,13 @@ features = {} [dependencies.typedb-driver] features = [] + rev = "7c7a715303a8be91e87bac6bbe42db475257e339" git = "https://github.com/typedb/typedb-driver" - tag = "3.7.0-rc3" default-features = false [dependencies.serde_json] features = ["alloc", "default", "indexmap", "preserve_order", "raw_value", "std"] - version = "1.0.143" + version = "1.0.145" default-features = false [dependencies.sentry] @@ -85,14 +85,14 @@ features = {} version = "2.12.1" default-features = false -[[test]] - path = "tests/assembly/test_assembly.rs" - name = "test-assembly" - [[test]] path = "tests/assembly/test_script.rs" name = "test-script" +[[test]] + path = "tests/assembly/test_assembly.rs" + name = "test-assembly" + [[bin]] path = "src/main.rs" name = "typedb-console" diff --git a/WORKSPACE b/WORKSPACE index d29ebac..5434508 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -75,8 +75,8 @@ rust_register_toolchains( "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", ], - rust_analyzer_version = "1.81.0", - versions = ["1.81.0"], + rust_analyzer_version = "1.84.0", + versions = ["1.84.0"], ) rust_analyzer_toolchain_tools_repository( diff --git a/dependencies/typedb/repositories.bzl b/dependencies/typedb/repositories.bzl index cb2deb8..be3590b 100644 --- a/dependencies/typedb/repositories.bzl +++ b/dependencies/typedb/repositories.bzl @@ -5,17 +5,23 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") def typedb_dependencies(): - git_repository( - name = "typedb_dependencies", - remote = "https://github.com/typedb/typedb-dependencies", - commit = "f6e710f9857b1c30ad1764c1c41afce4e4e02981", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies - ) + # TODO: Return ref after merge to master + git_repository( + name = "typedb_dependencies", + remote = "https://github.com/typedb/typedb-dependencies", + commit = "19a70bcad19b9a28814016f183ac3e3a23c1ff0d", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies + ) def typedb_driver(): + # TODO: Return ref after merge to master +# native.local_repository( +# name = "typedb_driver", +# path = "../typedb-driver", +# ) git_repository( name = "typedb_driver", remote = "https://github.com/typedb/typedb-driver", - tag = "3.7.0-rc3", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_driver + commit = "7c7a715303a8be91e87bac6bbe42db475257e339", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_driver ) def typeql(): diff --git a/src/cli.rs b/src/cli.rs index f0276b2..535d479 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,10 +24,29 @@ pub struct Args { #[arg(long, value_name = "path to script file")] pub script: Vec, - /// TypeDB address to connect to. If using TLS encryption, this must start with "https://" - #[arg(long, value_name = ADDRESS_VALUE_NAME)] + /// TypeDB address to connect to (host:port). If using TLS encryption, this must start with "https://". + #[arg(long, value_name = ADDRESS_VALUE_NAME, conflicts_with_all = ["addresses", "address_translation"])] pub address: Option, + /// A comma-separated list of TypeDB replica addresses of a single cluster to connect to. + #[arg(long, value_name = "host1:port1,host2:port2", conflicts_with_all = ["address", "address_translation"])] + pub addresses: Option, + + /// A comma-separated list of public=private address pairs. Public addresses are the user-facing + /// addresses of the replicas, and private addresses are the addresses used for the server-side + /// connection between replicas. + #[arg(long, value_name = "public=private,...", conflicts_with_all = ["address", "addresses"])] + pub address_translation: Option, + + /// If used in a cluster environment, disables attempts to redirect requests to server replicas, + /// limiting Console to communicate only with the single address specified in the `address` + /// argument. + /// Use for administrative / debug purposes to test a specific replica only: this option will + /// lower the success rate of Console's operations in production. + #[arg(long = "replication-disabled", default_value = "false")] + pub replication_disabled: bool, + + // TODO: Add cluster-related retries/attempts flags from Driver Options? /// Username for authentication #[arg(long, value_name = USERNAME_VALUE_NAME)] pub username: Option, @@ -48,8 +67,8 @@ pub struct Args { /// Disable error reporting. Error reporting helps TypeDB improve by reporting /// errors and crashes to the development team. - #[arg(long = "diagnostics-disable", default_value = "false")] - pub diagnostics_disable: bool, + #[arg(long = "diagnostics-disabled", default_value = "false")] + pub diagnostics_disabled: bool, /// Print the Console binary version #[arg(long = "version")] diff --git a/src/main.rs b/src/main.rs index 59c0438..7c60ce1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ */ use std::{ + collections::HashMap, env, env::temp_dir, error::Error, @@ -22,15 +23,16 @@ use clap::Parser; use home::home_dir; use rustyline::error::ReadlineError; use sentry::ClientOptions; -use typedb_driver::{Credentials, DriverOptions, Transaction, TransactionType, TypeDBDriver}; +use typedb_driver::{Addresses, Credentials, DriverOptions, Transaction, TransactionType, TypeDBDriver}; use crate::{ cli::{Args, ADDRESS_VALUE_NAME, USERNAME_VALUE_NAME}, completions::{database_name_completer_fn, file_completer}, operations::{ database_create, database_create_init, database_delete, database_export, database_import, database_list, - database_schema, transaction_close, transaction_commit, transaction_query, transaction_read, - transaction_rollback, transaction_schema, transaction_source, transaction_write, user_create, user_delete, + database_schema, replica_deregister, replica_list, replica_primary, replica_register, server_version, + transaction_close, transaction_commit, transaction_query, transaction_read, transaction_rollback, + transaction_schema, transaction_source, transaction_write, user_create, user_delete, user_list, user_update_password, }, repl::{ @@ -126,13 +128,19 @@ fn main() { println!("{}", VERSION); exit(ExitCode::Success as i32); } - let address = match args.address { - Some(address) => address, - None => { - println_error!("missing server address ('{}').", format_argument!("--address <{ADDRESS_VALUE_NAME}>")); - exit(ExitCode::UserInputError as i32); - } - }; + let address_info = parse_addresses(&args); + if !args.tls_disabled && !address_info.only_https { + println_error!( + "\ + TLS connections can only be enabled when connecting to HTTPS endpoints. \ + For example, using 'https://:port'.\n\ + Please modify the address or disable TLS ('{}'). {}\ + ", + format_argument!("--tls-disabled"), + format_warning!("WARNING: this will send passwords over plaintext!"), + ); + exit(ExitCode::UserInputError as i32); + } let username = match args.username { Some(username) => username, None => { @@ -146,28 +154,20 @@ fn main() { if args.password.is_none() { args.password = Some(LineReaderHidden::new().readline(&format!("password for '{username}': "))); } - if !args.diagnostics_disable { + if !args.diagnostics_disabled { init_diagnostics() } - if !args.tls_disabled && !address.starts_with("https:") { - println_error!( - "\ - TLS connections can only be enabled when connecting to HTTPS endpoints. \ - For example, using 'https://:port'.\n\ - Please modify the address or disable TLS ('{}'). {}\ - ", - format_argument!("--tls-disabled"), - format_warning!("WARNING: this will send passwords over plaintext!"), - ); - exit(ExitCode::UserInputError as i32); - } let tls_root_ca_path = args.tls_root_ca.as_ref().map(|value| Path::new(value)); - let runtime = BackgroundRuntime::new(); + let driver_options = DriverOptions::new() + .use_replication(!args.replication_disabled) + .is_tls_enabled(!args.tls_disabled) + .tls_root_ca(tls_root_ca_path) + .unwrap(); let driver = match runtime.run(TypeDBDriver::new( - address, + address_info.addresses, Credentials::new(&username, args.password.as_ref().unwrap()), - DriverOptions::new(!args.tls_disabled, tls_root_ca_path).unwrap(), + driver_options, )) { Ok(driver) => Arc::new(driver), Err(err) => { @@ -332,6 +332,28 @@ fn execute_commands(context: &mut ConsoleContext, mut input: &str, must_log_comm } fn entry_repl(driver: Arc, runtime: BackgroundRuntime) -> Repl { + let server_commands = + Subcommand::new("server").add(CommandLeaf::new("version", "Retrieve server version.", server_version)); + + let replica_commands = Subcommand::new("replica") + .add(CommandLeaf::new("list", "List replicas.", replica_list)) + .add(CommandLeaf::new("primary", "Get current primary replica.", replica_primary)) + .add(CommandLeaf::new_with_inputs( + "register", + "Register new replica. Requires a clustering address, not a connection address.", + vec![ + CommandInput::new_required("replica id", get_word, None), + CommandInput::new_required("clustering address", get_word, None), + ], + replica_register, + )) + .add(CommandLeaf::new_with_input( + "deregister", + "Deregister existing replica.", + CommandInput::new_required("replica id", get_word, None), + replica_deregister, + )); + let database_commands = Subcommand::new("database") .add(CommandLeaf::new("list", "List databases on the server.", database_list)) .add(CommandLeaf::new_with_input( @@ -451,8 +473,10 @@ fn entry_repl(driver: Arc, runtime: BackgroundRuntime) -> Repl &'static str { } } +struct AddressInfo { + only_https: bool, + addresses: Addresses, +} + +fn parse_addresses(args: &Args) -> AddressInfo { + if let Some(address) = &args.address { + AddressInfo { + only_https: is_https_address(address), + addresses: Addresses::try_from_address_str(address).unwrap(), + } + } else if let Some(addresses) = &args.addresses { + let split = addresses.split(',').map(str::to_string).collect::>(); + let only_https = split.iter().all(|address| is_https_address(address)); + AddressInfo { only_https, addresses: Addresses::try_from_addresses_str(split).unwrap() } + } else if let Some(translation) = &args.address_translation { + let mut map = HashMap::new(); + let mut only_https = true; + for pair in translation.split(',') { + let (public_address, private_address) = pair + .split_once('=') + .unwrap_or_else(|| panic!("Invalid address pair: {pair}. Must be of form public=private")); + only_https = only_https && is_https_address(public_address); + map.insert(public_address.to_string(), private_address.to_string()); + } + println!("Translation map:: {map:?}"); // TODO: Remove + AddressInfo { only_https, addresses: Addresses::try_from_translation_str(map).unwrap() } + } else { + panic!("At least one of --address, --addresses, or --address-translation must be provided."); + } +} + +fn is_https_address(address: &str) -> bool { + address.starts_with("https:") +} + fn init_diagnostics() { let _ = sentry::init(( DIAGNOSTICS_REPORTING_URI, diff --git a/src/operations.rs b/src/operations.rs index dc2e691..8875c68 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -16,11 +16,21 @@ use ureq; use crate::{ constants::DEFAULT_TRANSACTION_TIMEOUT, - printer::{print_document, print_row}, + printer::{print_document, print_replicas_table, print_row}, repl::command::{parse_one_query, CommandResult, ReplError}, transaction_repl, ConsoleContext, }; +pub(crate) fn server_version(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let server_version = context + .background_runtime + .run(async move { driver.server_version().await }) + .map_err(|err| Box::new(err) as Box)?; + println!("{}", server_version); + Ok(()) +} + pub(crate) fn database_list(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { let driver = context.driver.clone(); let databases = context @@ -132,7 +142,7 @@ pub(crate) fn user_list(context: &mut ConsoleContext, _input: &[String]) -> Comm println!("No users are present."); } else { for user in users { - println!("{}", user.name); + println!("{}", user.name()); } } Ok(()) @@ -184,13 +194,13 @@ pub(crate) fn user_update_password(context: &mut ConsoleContext, input: &[String }; let current_user = driver .users() - .get_current_user() + .get_current() .await .map_err(|err| Box::new(err) as Box)? .expect("Could not fetch currently logged in user."); user.update_password(new_password).await.map_err(|err| Box::new(err) as Box)?; - Ok(current_user.name == username) + Ok(current_user.name() == username) })?; if updated_current_user { println!("Successfully updated current user's password, exiting console. Please log in with the updated credentials."); @@ -201,6 +211,59 @@ pub(crate) fn user_update_password(context: &mut ConsoleContext, input: &[String Ok(()) } +pub(crate) fn replica_list(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + context.background_runtime.run(async move { + let replicas = driver.replicas().await.map_err(|err| Box::new(err) as Box)?; + if replicas.is_empty() { + println!("No replicas are present."); + } else { + print_replicas_table(replicas); + } + Ok(()) + }) +} + +pub(crate) fn replica_primary(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let primary_replica = context + .background_runtime + .run(async move { driver.primary_replica().await.map_err(|err| Box::new(err) as Box) })?; + if let Some(primary_replica) = primary_replica { + println!("{}", primary_replica.address()); + } else { + println!("No primary replica is present."); + } + Ok(()) +} + +pub(crate) fn replica_register(context: &mut ConsoleContext, input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let replica_id: u64 = input[0].parse().map_err(|_| { + Box::new(ReplError { message: format!("Replica id '{}' must be a number.", input[0]) }) as Box + })?; + let address = input[1].clone(); + context + .background_runtime + .run(async move { driver.register_replica(replica_id, address).await }) + .map_err(|err| Box::new(err) as Box)?; + println!("Successfully registered replica."); + Ok(()) +} + +pub(crate) fn replica_deregister(context: &mut ConsoleContext, input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let replica_id: u64 = input[0].parse().map_err(|_| { + Box::new(ReplError { message: format!("Replica id '{}' must be a number.", input[0]) }) as Box + })?; + context + .background_runtime + .run(async move { driver.deregister_replica(replica_id).await }) + .map_err(|err| Box::new(err) as Box)?; + println!("Successfully deregistered replica."); + Ok(()) +} + pub(crate) fn transaction_read(context: &mut ConsoleContext, input: &[String]) -> CommandResult { let driver = context.driver.clone(); let db_name = &input[0]; diff --git a/src/printer.rs b/src/printer.rs index b26bb57..f26c307 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -4,39 +4,44 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::collections::HashSet; + use clap::builder::styling::{AnsiColor, Color, Style}; +use itertools::Itertools; use typedb_driver::{ answer::{ConceptDocument, ConceptRow}, concept::{Concept, Value}, - IID, + Replica, ReplicaRole, ServerReplica, IID, }; const TABLE_INDENT: &'static str = " "; const CONTENT_INDENT: &'static str = " "; const TABLE_DASHES: usize = 7; -pub const ERROR_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))).bold(); -pub const WARNING_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))).bold(); -pub const ARGUMENT_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))); +pub const STYLE_RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))); +pub const STYLE_GREEN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))); +pub const STYLE_ERROR: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))).bold(); +pub const STYLE_WARNING: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))).bold(); +pub const STYLE_ARGUMENT: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))); #[macro_export] macro_rules! format_error { ($($arg:tt)*) => { - $crate::format_colored!($crate::printer::ERROR_STYLE, $($arg)*) + $crate::format_colored!($crate::printer::STYLE_ERROR, $($arg)*) }; } #[macro_export] macro_rules! format_warning { ($($arg:tt)*) => { - $crate::format_colored!($crate::printer::WARNING_STYLE, $($arg)*) + $crate::format_colored!($crate::printer::STYLE_WARNING, $($arg)*) }; } #[macro_export] macro_rules! format_argument { ($($arg:tt)*) => { - $crate::format_colored!($crate::printer::ARGUMENT_STYLE, $($arg)*) + $crate::format_colored!($crate::printer::STYLE_ARGUMENT, $($arg)*) }; } @@ -67,6 +72,103 @@ fn println(string: &str) { println!("{}", string) } +pub(crate) fn print_replicas_table(replicas: HashSet) { + const COLUMN_NUM: usize = 5; + #[derive(Debug)] + struct Row { + id: String, + address: String, + role: String, + term: String, + status: (String, Style), + } + + let mut rows = Vec::new(); + rows.push(Row { + id: "id".to_string(), + address: "address".to_string(), + role: "role".to_string(), + term: "term".to_string(), + status: ("status".to_string(), Style::new()), + }); + + for replica in replicas.into_iter().sorted_by_key(|replica| replica.id()) { + let role = match replica.role() { + Some(ReplicaRole::Primary) => "primary", + Some(ReplicaRole::Candidate) => "candidate", + Some(ReplicaRole::Secondary) => "secondary", + None => "", + } + .to_string(); + + let term = replica.term().map(|t| t.to_string()).unwrap_or_default(); + + let status = match &replica { + ServerReplica::Available(_) => ("available".to_string(), STYLE_GREEN), + ServerReplica::Unavailable { .. } => ("unavailable".to_string(), STYLE_RED), + }; + + rows.push(Row { + id: replica.id().to_string(), + address: replica.address().map(|address| address.to_string()).unwrap_or_default(), + role, + term, + status, + }); + } + + // Compute max content length per column (without padding) + let mut width_id = 0usize; + let mut width_address = 0usize; + let mut width_role = 0usize; + let mut width_term = 0usize; + let mut width_status = 0usize; + + for r in &rows { + width_id = width_id.max(r.id.len()); + width_address = width_address.max(r.address.len()); + width_role = width_role.max(r.role.len()); + width_term = width_term.max(r.term.len()); + width_status = width_status.max(r.status.0.len()); + } + + // Add 2 spaces per column (one at the beginning and one at the end) + width_id += 2; + width_address += 2; + width_role += 2; + width_term += 2; + width_status += 2; + + fn print_cell(content: &str, width: usize, style: Option