1use std::collections::HashMap;
18
19use common_datasource::object_store::FILE_SCHEMA;
20#[cfg(windows)]
21use common_datasource::object_store::{FS_SCHEMA, parse_url};
22use common_stat::get_total_cpu_cores;
23use session::context::QueryContextRef;
24use snafu::{OptionExt, ResultExt, ensure};
25use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
26use table::TableRef;
27use table::metadata::TableType;
28use table::requests::CopyDatabaseRequest;
29use table::table_reference::TableReference;
30use tokio::sync::Semaphore;
31use url::Url;
32
33use crate::error::{self, Result};
34use crate::statement::StatementExecutor;
35
36fn is_directory_location(location: &str) -> bool {
37 if location.ends_with('/') {
38 return true;
39 }
40
41 #[cfg(windows)]
42 {
43 location.ends_with(std::path::MAIN_SEPARATOR)
44 && matches!(
45 parse_url(location),
46 Ok((schema, _, _)) if schema.eq_ignore_ascii_case(FS_SCHEMA)
47 )
48 }
49
50 #[cfg(not(windows))]
51 false
52}
53
54pub(crate) fn parse_parallelism_from_option_map(options: &HashMap<String, String>) -> usize {
56 options
57 .get("parallelism")
58 .and_then(|v| v.parse::<usize>().ok())
59 .unwrap_or_else(get_total_cpu_cores)
60 .clamp(1, Semaphore::MAX_PERMITS)
61}
62
63pub(crate) fn validate_database_export_layout(options: &HashMap<String, String>) -> Result<()> {
65 if let Some(layout) = options.get("metric_data_layout") {
66 return error::InvalidCopyParameterSnafu {
67 key: "metric_data_layout",
68 value: layout,
69 }
70 .fail();
71 }
72 Ok(())
73}
74
75pub(crate) fn validate_database_directory(location: &str) -> Result<()> {
76 ensure!(
77 is_directory_location(location),
78 error::InvalidCopyDatabasePathSnafu { value: location }
79 );
80 #[cfg(windows)]
81 if common_datasource::object_store::handle_windows_path(location).is_some() {
82 return Ok(());
83 }
84 let parsed_directory = match Url::parse(location) {
85 Ok(url) => {
86 url.query().is_none() && url.fragment().is_none() && is_directory_location(url.path())
87 }
88 Err(_) => true,
89 };
90 ensure!(
91 parsed_directory,
92 error::InvalidCopyDatabasePathSnafu { value: location }
93 );
94 Ok(())
95}
96
97pub(crate) struct DatabaseExportFile {
99 pub(crate) path: String,
100 pub(crate) location: String,
101}
102
103impl DatabaseExportFile {
104 pub(crate) fn new(directory: &str, name: &str, suffix: &str) -> Result<Self> {
105 let filename = format!("{name}{suffix}");
106 #[cfg(windows)]
107 if common_datasource::object_store::handle_windows_path(directory).is_some() {
108 return Ok(Self {
109 location: format!("{directory}{filename}"),
110 path: filename,
111 });
112 }
113 match Url::parse(directory) {
114 Ok(mut url) => {
115 url.path_segments_mut()
116 .map_err(|_| error::InvalidCopyDatabasePathSnafu { value: directory }.build())?
117 .pop_if_empty()
118 .push(&filename);
119 let path = if url.scheme().eq_ignore_ascii_case(FILE_SCHEMA) {
122 filename
123 } else {
124 url.path()
125 .rsplit('/')
126 .next()
127 .unwrap_or_default()
128 .to_string()
129 };
130 Ok(Self {
131 path,
132 location: url.into(),
133 })
134 }
135 Err(url::ParseError::RelativeUrlWithoutBase) => Ok(Self {
136 location: format!("{directory}{filename}"),
137 path: filename,
138 }),
139 Err(source) => Err(source)
140 .context(common_datasource::error::InvalidUrlSnafu { url: directory })
141 .context(error::BuildBackendSnafu),
142 }
143 }
144}
145
146pub(crate) fn database_import_source(directory: &str, path: &str) -> Result<(String, String)> {
148 let mut filename = path.rsplit('/').next().unwrap_or(path).to_string();
149 let mut location = format!("{directory}{path}");
150 #[cfg(windows)]
151 let literal_path = common_datasource::object_store::handle_windows_path(directory).is_some();
152 #[cfg(not(windows))]
153 let literal_path = false;
154 if !literal_path && let Ok(mut url) = Url::parse(directory) {
155 if url.scheme().eq_ignore_ascii_case(FILE_SCHEMA) {
156 url.path_segments_mut()
157 .map_err(|_| error::InvalidCopyDatabasePathSnafu { value: directory }.build())?
158 .pop_if_empty()
159 .extend(path.split('/'));
160 } else {
161 url.set_path(&format!("{}{path}", url.path()));
163 filename = percent_encoding::percent_decode_str(&filename)
164 .decode_utf8()
165 .ok()
166 .context(error::InvalidTableNameSnafu { table_name: path })?
167 .into_owned();
168 }
169 location = url.into();
170 }
171 let table_name = filename
172 .rsplit_once('.')
173 .map(|(stem, _)| stem)
174 .filter(|stem| !stem.is_empty())
175 .context(error::InvalidTableNameSnafu { table_name: path })?
176 .to_string();
177 Ok((table_name, location))
178}
179
180impl StatementExecutor {
181 pub async fn capture_database_export_tables(
184 &self,
185 req: &CopyDatabaseRequest,
186 names: Option<&[String]>,
187 ctx: &QueryContextRef,
188 ) -> Result<Vec<TableRef>> {
189 let mut names = match names {
190 Some(names) => names.to_vec(),
191 None => self
192 .catalog_manager
193 .table_names(&req.catalog_name, &req.schema_name, Some(ctx))
194 .await
195 .context(error::CatalogSnafu)?,
196 };
197 names.sort();
198 let mut tables = Vec::new();
199 for name in names {
200 let table = self
201 .get_table(&TableReference::full(
202 &req.catalog_name,
203 &req.schema_name,
204 &name,
205 ))
206 .await?;
207 let info = table.table_info();
208 if table.table_type() == TableType::Base
209 && (info.meta.engine != METRIC_ENGINE_NAME
210 || info
211 .meta
212 .options
213 .extra_options
214 .contains_key(LOGICAL_TABLE_METADATA_KEY))
215 {
216 tables.push(table);
217 }
218 }
219 Ok(tables)
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn directory_url_components_cannot_capture_output_names() {
229 for location in [
230 "file:///copy/fresh?attempt=/",
231 "file:///copy/fresh#attempt/",
232 "s3://bucket/fresh?attempt=/",
233 "s3://bucket/fresh#attempt/",
234 "file:///copy/fresh",
235 ] {
236 assert!(validate_database_directory(location).is_err(), "{location}");
237 }
238 for location in ["/copy/fresh/", "file:///copy/fresh/", "s3://bucket/fresh/"] {
239 validate_database_directory(location).unwrap();
240 }
241 }
242
243 #[cfg(windows)]
244 #[test]
245 fn windows_directory_names_are_literal_paths() {
246 for location in ["C:/copy/fresh#1/", r"C:\copy\fresh#1\"] {
247 validate_database_directory(location).unwrap();
248 }
249 assert!(validate_database_directory("C:/copy/fresh#1").is_err());
250 }
251
252 #[tokio::test]
253 async fn output_locations_resolve_to_writer_keys() {
254 use common_datasource::object_store::{LocalFileAccess, build_backend_for_write_with_path};
255
256 let dir = common_test_util::temp_dir::create_temp_dir("database_export_paths");
257 let access = LocalFileAccess::sandboxed(dir.path()).unwrap();
258 let file_url = Url::from_directory_path(dir.path()).unwrap().to_string();
259 let connection = HashMap::from([
260 ("region".into(), "us-east-1".into()),
261 ("access_key_id".into(), "test-key".into()),
262 ("secret_access_key".into(), "test-secret".into()),
263 ]);
264 for directory in [
265 format!("{}/", dir.path().display()),
266 file_url,
267 "s3://export-bucket/data/".into(),
268 ] {
269 for name in ["a#b", "a:b"] {
270 if cfg!(windows) && name.contains(':') && !directory.starts_with("s3:") {
271 continue;
272 }
273 let file = DatabaseExportFile::new(&directory, name, ".parquet").unwrap();
274 let backend =
275 build_backend_for_write_with_path(&file.location, &connection, &access)
276 .await
277 .unwrap();
278 assert_eq!(backend.object_path.as_deref(), Some(file.path.as_str()));
279 let (table_name, input_location) =
280 database_import_source(&directory, &file.path).unwrap();
281 assert_eq!(table_name, name);
282 assert_eq!(input_location, file.location);
283 let (table_name, nested_location) =
284 database_import_source(&directory, &format!("nested/{}", file.path)).unwrap();
285 assert_eq!(table_name, name);
286 assert_eq!(
287 nested_location,
288 file.location
289 .replace(&directory, &format!("{directory}nested/"))
290 );
291 if !directory.starts_with("s3:") {
292 backend
293 .object_store
294 .write(&file.path, "test")
295 .await
296 .unwrap();
297 assert_eq!(
298 std::fs::read(dir.path().join(format!("{name}.parquet"))).unwrap(),
299 b"test"
300 );
301 } else {
302 assert_eq!(
303 file.path,
304 if name == "a#b" {
305 "a%23b.parquet"
306 } else {
307 "a:b.parquet"
308 }
309 );
310 }
311 }
312 }
313 }
314
315 #[test]
316 fn test_parse_parallelism_from_option_map() {
317 let options = HashMap::new();
318 assert_eq!(
319 parse_parallelism_from_option_map(&options),
320 get_total_cpu_cores()
321 );
322
323 let options = HashMap::from([("parallelism".to_string(), "0".to_string())]);
324 assert_eq!(parse_parallelism_from_option_map(&options), 1);
325
326 let options = HashMap::from([("parallelism".to_string(), usize::MAX.to_string())]);
327 assert_eq!(
328 parse_parallelism_from_option_map(&options),
329 Semaphore::MAX_PERMITS
330 );
331 }
332}