Skip to main content

cmd/
cli.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use clap::Parser;
16use cli::Tool;
17use common_telemetry::logging::{LoggingOptions, TracingOptions};
18use plugins::SubCommand;
19use snafu::ResultExt;
20use tracing_appender::non_blocking::WorkerGuard;
21
22use crate::options::GlobalOptions;
23use crate::{App, Result, error};
24pub const APP_NAME: &str = "greptime-cli";
25use async_trait::async_trait;
26
27pub struct Instance {
28    tool: Box<dyn Tool>,
29
30    // Keep the logging guard to prevent the worker from being dropped.
31    _guard: Vec<WorkerGuard>,
32}
33
34impl Instance {
35    pub fn new(tool: Box<dyn Tool>, guard: Vec<WorkerGuard>) -> Self {
36        Self {
37            tool,
38            _guard: guard,
39        }
40    }
41
42    pub async fn start(&mut self) -> Result<()> {
43        self.tool.do_work().await.context(error::StartCliSnafu)
44    }
45}
46
47#[async_trait]
48impl App for Instance {
49    fn name(&self) -> &str {
50        APP_NAME
51    }
52
53    async fn start(&mut self) -> Result<()> {
54        self.start().await
55    }
56
57    fn wait_signal(&self) -> bool {
58        false
59    }
60
61    async fn stop(&mut self) -> Result<()> {
62        Ok(())
63    }
64}
65
66#[derive(Parser)]
67pub struct Command {
68    #[clap(subcommand)]
69    cmd: SubCommand,
70}
71
72impl Command {
73    pub async fn build(&self, opts: LoggingOptions) -> Result<Instance> {
74        let guard = common_telemetry::init_global_logging(
75            APP_NAME,
76            &opts,
77            &TracingOptions::default(),
78            None,
79            None,
80        );
81
82        let tool = self.cmd.build().await.context(error::BuildCliSnafu)?;
83        let instance = Instance {
84            tool,
85            _guard: guard,
86        };
87        Ok(instance)
88    }
89
90    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<LoggingOptions> {
91        let mut logging_opts = LoggingOptions::default();
92
93        if let Some(dir) = &global_options.log_dir {
94            logging_opts.dir.clone_from(dir);
95        }
96
97        logging_opts.level.clone_from(&global_options.log_level);
98
99        Ok(logging_opts)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use std::net::TcpListener;
106    use std::ops::RangeInclusive;
107
108    use clap::Parser;
109    use client::{Client, Database};
110    use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
111    use common_grpc::channel_manager::ChannelManager;
112    use common_telemetry::logging::LoggingOptions;
113    use rand::Rng;
114
115    use crate::error::Result as CmdResult;
116    use crate::options::GlobalOptions;
117    use crate::{App, cli, standalone};
118
119    fn random_standalone_addrs() -> (String, String, String, String) {
120        let offset = choose_random_unused_port_offset(14000..=24000, 10);
121
122        (
123            format!("127.0.0.1:{}", 4000 + offset),
124            format!("127.0.0.1:{}", 4001 + offset),
125            format!("127.0.0.1:{}", 4002 + offset),
126            format!("127.0.0.1:{}", 4003 + offset),
127        )
128    }
129
130    fn choose_random_unused_port_offset(
131        port_range: RangeInclusive<u16>,
132        max_attempts: usize,
133    ) -> u16 {
134        let mut rng = rand::rng();
135
136        for _ in 0..max_attempts {
137            let http_port = rng.random_range(port_range.clone());
138            let offset = http_port - 4000;
139            let ports = [4000 + offset, 4001 + offset, 4002 + offset, 4003 + offset];
140
141            let listeners = ports
142                .into_iter()
143                .map(|port| TcpListener::bind(("127.0.0.1", port)))
144                .collect::<Result<Vec<_>, _>>();
145
146            if listeners.is_ok() {
147                return offset;
148            }
149        }
150
151        panic!("failed to find unused standalone test ports");
152    }
153
154    #[tokio::test(flavor = "multi_thread")]
155    async fn test_export_create_table_with_quoted_names() -> CmdResult<()> {
156        let output_dir = tempfile::tempdir().unwrap();
157        let (http_addr, rpc_addr, mysql_addr, postgres_addr) = random_standalone_addrs();
158
159        let standalone = standalone::Command::parse_from([
160            "standalone",
161            "start",
162            "--data-home",
163            &*output_dir.path().to_string_lossy(),
164            "--http-addr",
165            &http_addr,
166            "--grpc-bind-addr",
167            &rpc_addr,
168            "--mysql-addr",
169            &mysql_addr,
170            "--postgres-addr",
171            &postgres_addr,
172        ]);
173
174        let standalone_opts = standalone.load_options(&GlobalOptions::default()).unwrap();
175        let mut instance = standalone.build(standalone_opts).await?;
176        instance.start().await?;
177
178        let client = Client::with_query_and_control_managers(
179            ChannelManager::new(),
180            ChannelManager::new(),
181            [rpc_addr.as_str()],
182        );
183        let database = Database::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, client);
184        database
185            .sql(r#"CREATE DATABASE "cli.export.create_table";"#)
186            .await
187            .unwrap();
188        database
189            .sql(
190                r#"CREATE TABLE "cli.export.create_table"."a.b.c"(
191                        ts TIMESTAMP,
192                        TIME INDEX (ts)
193                    ) engine=mito;
194                "#,
195            )
196            .await
197            .unwrap();
198
199        let output_dir = tempfile::tempdir().unwrap();
200        let cli = cli::Command::parse_from([
201            "cli",
202            "data",
203            "export",
204            "--addr",
205            &http_addr,
206            "--output-dir",
207            &*output_dir.path().to_string_lossy(),
208            "--target",
209            "schema",
210        ]);
211        let mut cli_app = cli.build(LoggingOptions::default()).await?;
212        cli_app.start().await?;
213
214        instance.stop().await?;
215
216        let output_file = output_dir
217            .path()
218            .join("greptime")
219            .join("cli.export.create_table")
220            .join("create_tables.sql");
221        let res = std::fs::read_to_string(output_file).unwrap();
222        let expect = r#"CREATE TABLE IF NOT EXISTS "a.b.c" (
223  "ts" TIMESTAMP(3) NOT NULL,
224  TIME INDEX ("ts")
225)
226
227ENGINE=mito
228;
229"#;
230        assert_eq!(res.trim(), expect.trim());
231
232        Ok(())
233    }
234}