Repository: edgedb/edgedb-cli
Pull request #1487 (2025-02-20 23:16:20)

Pre-refactoring to fix some async issues with link
Moving connections between runtimes is sketchy and might cause random failures (in fact, it does when we switch to gel-stream).

Rewrite the linking function to use just one runtime.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 580540c..5c377c4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1136,7 +1136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
 
 [[package]]
-name = "edgedb-cli"
+name = "gel-cli"
 version = "6.2.0-dev"
 dependencies = [
  "anes",
diff --git a/Cargo.toml b/Cargo.toml
index 258eed8..c698509 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,10 +6,10 @@ members = [
 ]
 
 [package]
-name = "edgedb-cli"
+name = "gel-cli"
 license = "MIT/Apache-2.0"
 version = "6.2.0-dev"
-authors = ["EdgeDB Inc. <hello@edgedb.com>"]
+authors = ["Gel Data Inc. <yo@geldata.com>"]
 edition = "2018"
 
 [[bin]]
diff --git a/src/portable/instance/link.rs b/src/portable/instance/link.rs
index 80c617a..2865584 100644
--- a/src/portable/instance/link.rs
+++ b/src/portable/instance/link.rs
@@ -29,6 +29,11 @@ use crate::question;
 use crate::tty_password;
 
 pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
+    run_async(cmd, opts)
+}
+
+#[tokio::main(flavor = "current_thread")]
+pub async fn run_async(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
     if matches!(cmd.name, Some(InstanceName::Cloud { .. })) {
         anyhow::bail!(
             "{BRANDING_CLOUD} instances cannot be linked\
@@ -39,7 +44,7 @@ pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
     }
 
     let mut has_branch: bool = false;
-    let config: Config = conn_params(cmd, opts, &mut has_branch)?;
+    let config: Config = conn_params(cmd, opts, &mut has_branch).await?;
     let mut creds = config.as_credentials()?;
     let root_cert_store = config.root_cert_store()?;
     let inner = WebPkiServerVerifier::builder(Arc::new(root_cert_store)).build()?;
@@ -53,18 +58,19 @@ pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
         trust_tls_cert: cmd.trust_tls_cert,
     });
     let mut config = config.with_cert_verifier(verifier.clone());
-    let mut connect_result = connect(&config);
+    let mut connect_result = connect(&config).await;
     if let Err(e) = connect_result {
         if e.is::<PasswordRequired>() {
             let password;
 
             if opts.conn_options.password_from_stdin {
-                password = tty_password::read_stdin()?
+                password = tty_password::read_stdin_async().await?
             } else if !cmd.non_interactive {
-                password = tty_password::read(format!(
+                password = tty_password::read_async(format!(
                     "Password for '{}': ",
                     config.user().escape_default()
-                ))?;
+                ))
+                .await?;
             } else {
                 return Err(e.into());
             }
@@ -75,17 +81,17 @@ pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
                 let pem = pem::encode(&pem::Pem::new("CERTIFICATE", cert.to_vec()));
                 config = config.with_pem_certificates(&pem)?;
             }
-            connect_result = Ok(connect(&config)?);
+            connect_result = Ok(connect(&config).await?);
         } else {
             return Err(e.into());
         }
     }
 
     let mut connection: Client = connect_result.unwrap();
-    let ver = get_server_version(&mut connection)?;
+    let ver = get_server_version(&mut connection).await?;
 
     if !has_branch && opts.conn_options.branch.is_none() && opts.conn_options.database.is_none() {
-        config = config.with_database(&get_current_branch(&mut connection)?)?;
+        config = config.with_database(&get_current_branch(&mut connection).await?)?;
 
         eprintln!(
             "using the default {} '{}'",
@@ -125,7 +131,8 @@ pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
                     let name =
                         question::String::new("Specify a new instance name for the remote server")
                             .default(&default)
-                            .ask()?;
+                            .async_ask()
+                            .await?;
                     if !is_valid_local_instance_name(&name) {
                         print::error!(
                             "Instance name must be a valid identifier, \
@@ -151,13 +158,13 @@ pub fn run(cmd: &Link, opts: &Options) -> anyhow::Result<()> {
                 cred_path.display()
             ));
             q.default(false);
-            if !q.ask()? {
+            if !q.async_ask().await? {
                 anyhow::bail!("Canceled.")
             }
         }
     }
 
-    credentials::write(&cred_path, &creds)?;
+    credentials::write_async(&cred_path, &creds).await?;
     if !cmd.quiet {
         eprintln!(
             "{} To connect run:\
@@ -333,7 +340,6 @@ fn gen_default_instance_name(input: impl fmt::Display) -> String {
     name
 }
 
-#[tokio::main(flavor = "current_thread")]
 async fn connect(cfg: &gel_tokio::Config) -> Result<Client, Error> {
     //Connection::connect(cfg).await
     let client = gel_tokio::Client::new(cfg);
@@ -342,13 +348,11 @@ async fn connect(cfg: &gel_tokio::Config) -> Result<Client, Error> {
     Ok(client)
 }
 
-#[tokio::main(flavor = "current_thread")]
 async fn conn_params(cmd: &Link, opts: &Options, has_branch: &mut bool) -> anyhow::Result<Config> {
     let mut builder = options::prepare_conn_params(opts)?;
     prompt_conn_params(&opts.conn_options, &mut builder, cmd, has_branch).await
 }
 
-#[tokio::main(flavor = "current_thread")]
 async fn get_server_version(connection: &mut Client) -> anyhow::Result<Build> {
     let ver: String = connection
         .query_required_single("SELECT sys::get_version_as_str()", &())
@@ -356,7 +360,6 @@ async fn get_server_version(connection: &mut Client) -> anyhow::Result<Build> {
     ver.parse()
 }
 
-#[tokio::main(flavor = "current_thread")]
 async fn get_current_branch(connection: &mut Client) -> anyhow::Result<String> {
     let branch = connection
         .query_required_single::<String, _>("select sys::get_current_database()", &())
diff --git a/src/portable/project/init.rs b/src/portable/project/init.rs
index aba24f5..f8b8d03 100644
--- a/src/portable/project/init.rs
+++ b/src/portable/project/init.rs
@@ -824,16 +824,15 @@ fn ask_name(
         }
         return Ok((default_name, false));
     }
-    let mut q = question::String::new(concatcp!(
+    let q = question::String::new(concatcp!(
         "Specify the name of the ",
         BRANDING,
         " instance to use with this project"
-    ));
-    let default_name_str = default_name.to_string();
-    q.default(&default_name_str);
+    ))
+    .default(&default_name.to_string());
     loop {
         let default_name_clone = default_name.clone();
-        let mut q = question::String::new(concatcp!(
+        let q = question::String::new(concatcp!(
             "Specify the name of the ",
             BRANDING,
             " instance to use with this project"
@@ -899,8 +898,7 @@ fn ask_database(project_dir: &Path, options: &Command) -> anyhow::Result<String>
         return Ok(name.clone());
     }
     let default = directory_to_name(project_dir, "edgedb");
-    let mut q = question::String::new("Specify database name:");
-    q.default(&default);
+    let mut q = question::String::new("Specify database name:").default(&default);
     loop {
         let name = q.ask()?;
         if name.trim().is_empty() {
@@ -912,8 +910,7 @@ fn ask_database(project_dir: &Path, options: &Command) -> anyhow::Result<String>
 }
 
 fn ask_branch() -> anyhow::Result<String> {
-    let mut q = question::String::new("Specify branch name:");
-    q.default("main");
+    let mut q = question::String::new("Specify branch name:").default("main");
     loop {
         let name = q.ask()?;
         if name.trim().is_empty() {
@@ -945,8 +942,8 @@ fn ask_local_version(options: &Command) -> anyhow::Result<(Query, PackageInfo)>
         "Specify the version of the ",
         BRANDING,
         " instance to use with this project"
-    ));
-    q.default(&default_ver);
+    ))
+    .default(&default_ver);
     loop {
         let value = q.ask()?;
         let value = value.trim();
@@ -1084,8 +1081,8 @@ fn ask_cloud_version(
         "Specify the version of the ",
         BRANDING,
         " instance to use with this project"
-    ));
-    q.default(&default_ver);
+    ))
+    .default(&default_ver);
     loop {
         let value = q.ask()?;
         let value = value.trim();
diff --git a/src/question.rs b/src/question.rs
index a032fc6..94d2187 100644
--- a/src/question.rs
+++ b/src/question.rs
@@ -14,8 +14,8 @@ pub struct Numeric<'a, T: Clone + 'a> {
 }
 
 pub struct String<'a> {
-    question: &'a str,
-    default: &'a str,
+    question: Cow<'a, str>,
+    default: Cow<'a, str>,
     initial: Option<std::string::String>,
 }
 
@@ -95,13 +95,14 @@ impl<T: Clone + Send> Numeric<'static, T> {
 impl<'a> String<'a> {
     pub fn new(question: &'a str) -> String {
         String {
-            question,
-            default: "",
+            question: question.into(),
+            default: Cow::default(),
             initial: None,
         }
     }
-    pub fn default(&mut self, default: &'a str) -> &mut Self {
-        self.default = default;
+    #[must_use]
+    pub fn default<'b: 'a>(mut self, default: &'b str) -> Self {
+        self.default = default.into();
         self
     }
     pub fn ask(&mut self) -> anyhow::Result<std::string::String> {
@@ -115,21 +116,23 @@ impl<'a> String<'a> {
             .initial
             .as_ref()
             .map(|s| &s[..])
-            .unwrap_or(self.default);
+            .unwrap_or(self.default.as_ref());
         let val = editor.readline_with_initial("> ", (initial, ""))?;
         let mut val = val.trim();
         if val.is_empty() {
-            val = self.default;
+            val = self.default.as_ref();
         }
         self.initial = Some(val.into());
         Ok(val.into())
     }
-}
 
-impl String<'static> {
-    #[allow(dead_code)]
-    pub async fn async_ask(mut self) -> anyhow::Result<std::string::String> {
-        spawn_blocking(move || self.ask()).await?
+    pub async fn async_ask(self) -> anyhow::Result<std::string::String> {
+        let mut question: String<'static> = String {
+            question: self.question.to_string().into(),
+            default: self.default.to_string().into(),
+            initial: self.initial.map(|s| s.to_string()),
+        };
+        spawn_blocking(move || question.ask()).await?
     }
 }
 
@@ -200,7 +203,6 @@ impl<'a> Confirm<'a> {
 }
 
 impl Confirm<'static> {
-    #[allow(dead_code)]
     pub async fn async_ask(self) -> anyhow::Result<bool> {
         spawn_blocking(move || self.ask()).await?
     }
diff --git a/src/tty_password.rs b/src/tty_password.rs
index 437d681..09e5e3e 100644
--- a/src/tty_password.rs
+++ b/src/tty_password.rs
@@ -18,3 +18,12 @@ pub fn read_stdin() -> anyhow::Result<String> {
         .context("error reading password from stdin")?;
     Ok(passwd)
 }
+
+pub async fn read_async(prompt: impl AsRef<str>) -> anyhow::Result<String> {
+    let prompt = prompt.as_ref().to_string();
+    tokio::task::spawn_blocking(|| read(prompt)).await?
+}
+
+pub async fn read_stdin_async() -> anyhow::Result<String> {
+    tokio::task::spawn_blocking(|| read_stdin()).await?
+}


Test patch:
diff --git a/tests/func/main.rs b/tests/func/main.rs
index 97186ca..f80834c 100644
--- a/tests/func/main.rs
+++ b/tests/func/main.rs
@@ -10,6 +10,7 @@ use std::fs;
 use std::path::Path;
 use std::process;
 use std::str::FromStr;
+use std::time::Duration;
 
 use assert_cmd::Command;
 use once_cell::sync::Lazy;
@@ -42,7 +43,7 @@ pub const BRANDING_CLI_CMD: &str = if cfg!(feature = "gel") {
 
 fn edgedb_cli_cmd() -> assert_cmd::Command {
     let mut cmd = Command::cargo_bin("edgedb").expect("binary found");
-
+    cmd.timeout(Duration::from_secs(60));
     cmd.env("CLICOLOR", "0").arg("--no-cli-update-check");
     cmd
 }
@@ -161,7 +162,6 @@ impl ServerGuard {
 
     pub fn ensure_instance_linked(&self) -> &'static str {
         const INSTANCE_NAME: &str = "_test_inst";
-
         edgedb_cli_cmd()
             .arg("instance")
             .arg("link")