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