1use 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 _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}