1use std::collections::BTreeMap;
16use std::collections::HashSet;
17use std::fmt;
18use std::fmt::Display;
19use std::fmt::Formatter;
20use std::str::FromStr;
21
22use derive_visitor::Drive;
23use derive_visitor::DriveMut;
24use itertools::Itertools;
25use percent_encoding::percent_decode_str;
26use url::Url;
27
28use crate::ParseError;
29use crate::Result;
30use crate::ast::Expr;
31use crate::ast::Hint;
32use crate::ast::Identifier;
33use crate::ast::Query;
34use crate::ast::SelectTarget;
35use crate::ast::With;
36use crate::ast::WithOptions;
37use crate::ast::quote::QuotedString;
38use crate::ast::write_comma_separated_list;
39use crate::ast::write_comma_separated_map;
40use crate::ast::write_comma_separated_string_list;
41use crate::ast::write_comma_separated_string_map;
42use crate::ast::write_dot_separated_list;
43
44#[derive(Debug, Clone, PartialEq, Drive, DriveMut)]
52pub struct CopyIntoTableStmt {
53 pub with: Option<With>,
54 pub src: CopyIntoTableSource,
55 pub catalog: Option<Identifier>,
56 pub database: Option<Identifier>,
57 pub table: Identifier,
58 pub dst_columns: Option<Vec<Identifier>>,
59
60 pub hints: Option<Hint>,
61
62 pub file_format: FileFormatOptions,
63
64 pub files: Option<Vec<String>>,
66 pub pattern: Option<LiteralStringOrVariable>,
67
68 pub options: CopyIntoTableOptions,
69}
70
71impl CopyIntoTableStmt {
72 pub fn apply_option(
73 &mut self,
74 opt: CopyIntoTableOption,
75 ) -> std::result::Result<(), &'static str> {
76 match opt {
77 CopyIntoTableOption::Files(v) => self.files = Some(v),
78 CopyIntoTableOption::Pattern(v) => self.pattern = Some(v),
79 CopyIntoTableOption::FileFormat(v) => self.file_format = v,
80 CopyIntoTableOption::SizeLimit(v) => self.options.size_limit = v,
81 CopyIntoTableOption::MaxFiles(v) => self.options.max_files = v,
82 CopyIntoTableOption::SplitSize(v) => self.options.split_size = v,
83 CopyIntoTableOption::Purge(v) => self.options.purge = v,
84 CopyIntoTableOption::Force(v) => self.options.force = v,
85 CopyIntoTableOption::DisableVariantCheck(v) => self.options.disable_variant_check = v,
86 CopyIntoTableOption::ReturnFailedOnly(v) => self.options.return_failed_only = v,
87 CopyIntoTableOption::OnError(v) => self.options.on_error = OnErrorMode::from_str(&v)?,
88 CopyIntoTableOption::ColumnMatchMode(v) => {
89 self.options.column_match_mode = Some(ColumnMatchMode::from_str(&v)?)
90 }
91 }
92 Ok(())
93 }
94}
95
96impl Display for CopyIntoTableStmt {
97 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
98 if let Some(cte) = &self.with {
99 write!(f, "WITH {} ", cte)?;
100 }
101 write!(f, "COPY")?;
102 if let Some(hints) = &self.hints {
103 write!(f, "{} ", hints)?;
104 }
105
106 write!(f, " INTO ")?;
107 write_dot_separated_list(
108 f,
109 self.catalog
110 .iter()
111 .chain(self.database.iter())
112 .chain(Some(&self.table)),
113 )?;
114
115 if let Some(columns) = &self.dst_columns {
116 write!(f, "({})", columns.iter().map(|c| c.to_string()).join(","))?;
117 }
118 write!(f, " FROM {}", self.src)?;
119
120 if let Some(files) = &self.files {
121 write!(f, " FILES = (")?;
122 write_comma_separated_string_list(f, files)?;
123 write!(f, " )")?;
124 }
125
126 if let Some(pattern) = &self.pattern {
127 write!(f, " PATTERN = {}", pattern)?;
128 }
129
130 if !self.file_format.is_empty() {
131 write!(f, " FILE_FORMAT = ({})", self.file_format)?;
132 }
133 write!(f, " {}", self.options)?;
134 Ok(())
135 }
136}
137
138#[derive(
139 serde::Serialize, serde::Deserialize, Debug, Clone, Default, PartialEq, Drive, DriveMut, Eq,
140)]
141pub struct CopyIntoTableOptions {
142 pub on_error: OnErrorMode,
143 pub max_files: usize,
144 pub force: bool,
145 pub purge: bool,
146 pub disable_variant_check: bool,
147 pub return_failed_only: bool,
148 pub column_match_mode: Option<ColumnMatchMode>,
149
150 pub size_limit: usize,
152 pub split_size: usize,
153 pub validation_mode: String,
154}
155
156impl CopyIntoTableOptions {
157 fn parse_uint(k: &str, v: &String) -> std::result::Result<usize, String> {
158 usize::from_str(v).map_err(|e| format!("can not parse {}={} as uint: {}", k, v, e))
159 }
160 fn parse_bool(k: &str, v: &String) -> std::result::Result<bool, String> {
161 bool::from_str(v).map_err(|e| format!("can not parse {}={} as bool: {}", k, v, e))
162 }
163
164 pub fn set_column_match_mode(&mut self, mode: ColumnMatchMode) {
165 self.column_match_mode = Some(mode);
166 }
167
168 pub fn apply(
169 &mut self,
170 opts: &BTreeMap<String, String>,
171 ignore_unknown: bool,
172 ) -> std::result::Result<(), String> {
173 if opts.is_empty() {
174 return Ok(());
175 }
176 for (k, v) in opts.iter() {
177 match k.as_str() {
178 "on_error" => {
179 let on_error = OnErrorMode::from_str(v)?;
180 self.on_error = on_error;
181 }
182 "column_match_mode" => {
183 let column_match_mode = ColumnMatchMode::from_str(v)?;
184 self.column_match_mode = Some(column_match_mode);
185 }
186 "size_limit" => {
187 self.size_limit = Self::parse_uint(k, v)?;
188 }
189 "max_files" => {
190 self.max_files = Self::parse_uint(k, v)?;
191 }
192 "split_size" => {
193 self.split_size = Self::parse_uint(k, v)?;
194 }
195 "purge" => {
196 self.purge = Self::parse_bool(k, v)?;
197 }
198 "disable_variant_check" => {
199 self.disable_variant_check = Self::parse_bool(k, v)?;
200 }
201 "return_failed_only" => {
202 self.return_failed_only = Self::parse_bool(k, v)?;
203 }
204 _ => {
205 if !ignore_unknown {
206 return Err(format!("Unknown stage copy option {}", k));
207 }
208 }
209 }
210 }
211 Ok(())
212 }
213}
214
215impl Display for CopyIntoTableOptions {
216 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
217 if !self.validation_mode.is_empty() {
218 write!(f, "VALIDATION_MODE = {}", self.validation_mode)?;
219 }
220
221 if self.size_limit != 0 {
222 write!(f, " SIZE_LIMIT = {}", self.size_limit)?;
223 }
224
225 if self.max_files != 0 {
226 write!(f, " MAX_FILES = {}", self.max_files)?;
227 }
228
229 if self.split_size != 0 {
230 write!(f, " SPLIT_SIZE = {}", self.split_size)?;
231 }
232
233 write!(f, " PURGE = {}", self.purge)?;
234 write!(f, " FORCE = {}", self.force)?;
235 write!(f, " DISABLE_VARIANT_CHECK = {}", self.disable_variant_check)?;
236 write!(f, " ON_ERROR = {}", self.on_error)?;
237 write!(f, " RETURN_FAILED_ONLY = {}", self.return_failed_only)?;
238 if let Some(mode) = &self.column_match_mode {
239 write!(f, " COLUMN_MATCH_MODE = {}", mode)?;
240 }
241 Ok(())
242 }
243}
244
245#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Drive, DriveMut, Eq)]
246pub struct CopyIntoLocationOptions {
247 pub single: bool,
248 pub max_file_size: usize,
249 pub detailed_output: bool,
250 pub use_raw_path: bool,
251 pub include_query_id: bool,
252 pub overwrite: bool,
253}
254
255#[derive(
256 serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Drive, DriveMut, Eq, Default,
257)]
258pub struct CopyIntoLocationOptionsRaw {
259 pub single: Option<bool>,
260 pub max_file_size: Option<usize>,
261 pub detailed_output: Option<bool>,
262 pub use_raw_path: Option<bool>,
263 pub include_query_id: Option<bool>,
264 pub overwrite: Option<bool>,
265}
266
267impl CopyIntoLocationOptionsRaw {
268 pub fn with_defaults(&self) -> CopyIntoLocationOptions {
269 let defaults = CopyIntoLocationOptions::default();
270 CopyIntoLocationOptions {
271 single: self.single.unwrap_or(defaults.single),
272 max_file_size: self.max_file_size.unwrap_or(defaults.max_file_size),
273 detailed_output: self.detailed_output.unwrap_or(defaults.detailed_output),
274 use_raw_path: self.use_raw_path.unwrap_or(defaults.use_raw_path),
275 include_query_id: self.include_query_id.unwrap_or(defaults.include_query_id),
276 overwrite: self.overwrite.unwrap_or(defaults.overwrite),
277 }
278 }
279}
280
281impl Default for CopyIntoLocationOptions {
282 fn default() -> Self {
283 Self {
284 single: Default::default(),
285 max_file_size: Default::default(),
286 detailed_output: false,
287 use_raw_path: false,
288 include_query_id: true,
289 overwrite: false,
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Drive, DriveMut)]
296pub struct CopyIntoLocationStmt {
297 pub with: Option<With>,
298 pub hints: Option<Hint>,
299 pub src: CopyIntoLocationSource,
300 pub dst: FileLocation,
301 pub partition_by: Option<Expr>,
302 pub file_format: FileFormatOptions,
303 pub options: CopyIntoLocationOptionsRaw,
304}
305
306impl Display for CopyIntoLocationStmt {
307 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
308 if let Some(cte) = &self.with {
309 write!(f, "WITH {} ", cte)?;
310 }
311 write!(f, "COPY")?;
312 if let Some(hints) = &self.hints {
313 write!(f, "{} ", hints)?;
314 }
315 write!(f, " INTO {}", self.dst)?;
316 write!(f, " FROM {}", self.src)?;
317 if let Some(partition_by) = &self.partition_by {
318 write!(f, " PARTITION BY ({partition_by})")?;
319 }
320
321 if !self.file_format.is_empty() {
322 write!(f, " FILE_FORMAT = ({})", self.file_format)?;
323 }
324 if let Some(single) = self.options.single {
325 write!(f, " SINGLE = {single}")?;
326 }
327 if let Some(max_file_size) = self.options.max_file_size {
328 write!(f, " MAX_FILE_SIZE = {max_file_size}")?;
329 }
330 if let Some(detailed_output) = self.options.detailed_output {
331 write!(f, " DETAILED_OUTPUT = {detailed_output}")?;
332 }
333 if let Some(include_query_id) = self.options.include_query_id {
334 write!(f, " INCLUDE_QUERY_ID = {include_query_id}")?;
335 }
336 if let Some(use_raw_path) = self.options.use_raw_path {
337 write!(f, " USE_RAW_PATH = {use_raw_path}")?;
338 }
339 if let Some(overwrite) = self.options.overwrite {
340 write!(f, " OVERWRITE = {overwrite}")?;
341 }
342
343 Ok(())
344 }
345}
346
347impl CopyIntoLocationStmt {
348 pub fn apply_option(&mut self, opt: CopyIntoLocationOption) {
349 match opt {
350 CopyIntoLocationOption::FileFormat(v) => self.file_format = v,
351 CopyIntoLocationOption::Single(v) => self.options.single = Some(v),
352 CopyIntoLocationOption::MaxFileSize(v) => self.options.max_file_size = Some(v),
353 CopyIntoLocationOption::DetailedOutput(v) => self.options.detailed_output = Some(v),
354 CopyIntoLocationOption::IncludeQueryID(v) => self.options.include_query_id = Some(v),
355 CopyIntoLocationOption::UseRawPath(v) => self.options.use_raw_path = Some(v),
356 CopyIntoLocationOption::OverWrite(v) => self.options.overwrite = Some(v),
357 }
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Drive, DriveMut)]
362pub enum CopyIntoTableSource {
363 Location(FileLocation),
364 Query {
367 select_list: Vec<SelectTarget>,
368 from: FileLocation,
369 alias_name: Option<Identifier>,
371 },
372}
373
374impl Display for CopyIntoTableSource {
375 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
376 match self {
377 CopyIntoTableSource::Location(location) => write!(f, "{location}"),
378 CopyIntoTableSource::Query {
379 select_list,
380 from,
381 alias_name,
382 } => {
383 write!(f, "(SELECT ")?;
384 write_comma_separated_list(f, select_list)?;
385 write!(f, " FROM {from}")?;
386 if let Some(a) = alias_name {
387 write!(f, " AS {}", a.name)?;
388 }
389 write!(f, ")")?;
390 Ok(())
391 }
392 }
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Drive, DriveMut)]
397pub enum CopyIntoLocationSource {
398 Query(Box<Query>),
399 Table {
401 catalog: Option<Identifier>,
402 database: Option<Identifier>,
403 table: Identifier,
404 with_options: Option<WithOptions>,
405 },
406}
407
408impl Display for CopyIntoLocationSource {
409 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
410 match self {
411 CopyIntoLocationSource::Query(query) => {
412 write!(f, "({query})")
413 }
414 CopyIntoLocationSource::Table {
415 catalog,
416 database,
417 table,
418 with_options,
419 } => {
420 write_dot_separated_list(
421 f,
422 catalog.iter().chain(database.iter()).chain(Some(table)),
423 )?;
424 if let Some(with_options) = with_options {
425 write!(f, " {with_options}")?;
426 }
427 Ok(())
428 }
429 }
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Drive, DriveMut)]
434pub struct Connection {
435 #[drive(skip)]
436 visited_keys: HashSet<String>,
437 pub conns: BTreeMap<String, String>,
438}
439
440impl Connection {
441 pub fn new(conns: BTreeMap<String, String>) -> Self {
442 Self {
443 visited_keys: HashSet::new(),
444 conns,
445 }
446 }
447
448 pub fn mask(&self) -> Self {
449 let mut conns = BTreeMap::new();
450 for (k, v) in &self.conns {
451 conns.insert(k.to_string(), mask_string(v, 3));
452 }
453 Self {
454 visited_keys: self.visited_keys.clone(),
455 conns,
456 }
457 }
458
459 pub fn get(&mut self, key: &str) -> Option<&String> {
460 self.visited_keys.insert(key.to_string());
461 self.conns.get(key)
462 }
463
464 pub fn check(&self) -> Result<()> {
465 let conn_keys = HashSet::from_iter(self.conns.keys().cloned());
466 let diffs: Vec<String> = conn_keys
467 .difference(&self.visited_keys)
468 .map(|x| x.to_string())
469 .collect();
470
471 if !diffs.is_empty() {
472 return Err(ParseError(
473 None,
474 format!(
475 "connection params invalid: expected [{}], got [{}]",
476 self.visited_keys
477 .iter()
478 .cloned()
479 .collect::<Vec<_>>()
480 .join(","),
481 diffs.join(",")
482 ),
483 ));
484 }
485 Ok(())
486 }
487}
488
489impl Display for Connection {
490 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
491 if !self.conns.is_empty() {
492 write!(f, " CONNECTION = ( ")?;
493 write_comma_separated_string_map(f, &self.conns)?;
494 write!(f, " )")?;
495 }
496 Ok(())
497 }
498}
499
500fn mask_string(s: &str, unmask_len: usize) -> String {
502 if s.len() <= unmask_len {
503 s.to_string()
504 } else {
505 let mut ret = "******".to_string();
506 ret.push_str(&s[(s.len() - unmask_len)..]);
507 ret
508 }
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Drive, DriveMut)]
515pub struct UriLocation {
516 pub protocol: String,
517 pub name: String,
518 pub path: String,
519 pub connection: Connection,
520}
521
522impl UriLocation {
523 pub fn new(
524 protocol: String,
525 name: String,
526 path: String,
527 conns: BTreeMap<String, String>,
528 ) -> Self {
529 Self {
530 protocol,
531 name,
532 path,
533 connection: Connection::new(conns),
534 }
535 }
536
537 pub fn from_uri(uri: String, conns: BTreeMap<String, String>) -> Result<Self> {
538 if let Some(path) = uri.strip_prefix("fs://") {
540 if !path.starts_with('/') {
541 return Err(ParseError(
542 None,
543 format!("Invalid uri: {}. fs location must start with 'fs:///'", uri),
544 ));
545 }
546 return Ok(UriLocation::new(
547 "fs".to_string(),
548 "".to_string(),
549 path.to_string(),
550 BTreeMap::default(),
551 ));
552 }
553
554 let parsed =
555 Url::parse(&uri).map_err(|e| ParseError(None, format!("invalid uri {}", e)))?;
556
557 let protocol = parsed.scheme().to_string();
558
559 let name = parsed
560 .host_str()
561 .map(|hostname| {
562 if let Some(port) = parsed.port() {
563 format!("{}:{}", hostname, port)
564 } else {
565 hostname.to_string()
566 }
567 })
568 .ok_or_else(|| ParseError(None, "invalid uri".to_string()))?;
569
570 let path = if parsed.path().is_empty() {
571 "/".to_string()
572 } else {
573 percent_decode_str(parsed.path())
574 .decode_utf8_lossy()
575 .to_string()
576 };
577
578 Ok(Self {
579 protocol,
580 name,
581 path,
582 connection: Connection::new(conns),
583 })
584 }
585
586 pub fn mask(&self) -> Self {
587 Self {
588 connection: self.connection.mask(),
589 ..self.clone()
590 }
591 }
592}
593
594impl Display for UriLocation {
595 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
596 write!(f, "'{}://{}{}'", self.protocol, self.name, self.path)?;
597 write!(f, "{}", self.connection)?;
598 Ok(())
599 }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, Drive, DriveMut)]
614pub enum FileLocation {
615 Stage(String),
616 Uri(UriLocation),
617}
618
619impl Display for FileLocation {
620 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
621 match self {
622 FileLocation::Uri(loc) => {
623 write!(f, "{}", loc)
624 }
625 FileLocation::Stage(loc) => {
626 write!(f, "'@{}'", loc)
627 }
628 }
629 }
630}
631
632#[derive(Debug, Clone, PartialEq, Drive, DriveMut)]
639pub enum LiteralStringOrVariable {
640 Literal(String),
641 Variable(String),
642}
643
644impl Display for LiteralStringOrVariable {
645 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
646 match self {
647 LiteralStringOrVariable::Literal(s) => {
648 write!(f, "'{s}'")
649 }
650 LiteralStringOrVariable::Variable(s) => {
651 write!(f, "${s}")
652 }
653 }
654 }
655}
656
657pub enum CopyIntoTableOption {
658 Files(Vec<String>),
659 Pattern(LiteralStringOrVariable),
660 FileFormat(FileFormatOptions),
661 SizeLimit(usize),
662 MaxFiles(usize),
663 SplitSize(usize),
664 Purge(bool),
665 Force(bool),
666 DisableVariantCheck(bool),
667 ReturnFailedOnly(bool),
668 OnError(String),
669 ColumnMatchMode(String),
670}
671
672pub enum CopyIntoLocationOption {
673 FileFormat(FileFormatOptions),
674 MaxFileSize(usize),
675 Single(bool),
676 IncludeQueryID(bool),
677 UseRawPath(bool),
678 DetailedOutput(bool),
679 OverWrite(bool),
680}
681
682#[derive(Clone, Debug, PartialEq, Eq, Default, Drive, DriveMut)]
683pub struct FileFormatOptions {
684 pub options: BTreeMap<String, FileFormatValue>,
685}
686
687impl FileFormatOptions {
688 pub fn is_empty(&self) -> bool {
689 self.options.is_empty()
690 }
691}
692
693impl Display for FileFormatOptions {
694 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
695 write_comma_separated_map(f, &self.options)
696 }
697}
698
699#[derive(Clone, Debug, PartialEq, Eq, Drive, DriveMut)]
700pub enum FileFormatValue {
701 Keyword(String),
702 Bool(bool),
703 U64(u64),
704 String(String),
705 StringList(Vec<String>),
706}
707
708impl FileFormatValue {
709 pub fn to_meta_value(&self) -> String {
710 match self {
711 FileFormatValue::Keyword(v) => v.clone(),
712 FileFormatValue::Bool(v) => v.to_string(),
713 FileFormatValue::U64(v) => v.to_string(),
714 FileFormatValue::String(v) => v.clone(),
715 FileFormatValue::StringList(v) => serde_json::to_string(v).unwrap(),
716 }
717 }
718}
719
720impl Display for FileFormatValue {
721 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
722 match self {
723 FileFormatValue::Keyword(v) => write!(f, "{v}"),
724 FileFormatValue::Bool(v) => write!(f, "{v}"),
725 FileFormatValue::U64(v) => write!(f, "{v}"),
726 FileFormatValue::String(v) => {
727 write!(f, "{}", QuotedString(v, '\''))
728 }
729 FileFormatValue::StringList(v) => {
730 write!(f, "(")?;
731 for (i, s) in v.iter().enumerate() {
732 if i > 0 {
733 write!(f, ", ")?;
734 }
735 write!(f, "{}", QuotedString(s, '\''))?;
736 }
737 write!(f, ")")
738 }
739 }
740 }
741}
742
743#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Drive, DriveMut, Eq)]
744pub enum OnErrorMode {
745 Continue,
746 SkipFileNum(u64),
747 AbortNum(u64),
748}
749
750impl Default for OnErrorMode {
751 fn default() -> Self {
752 Self::AbortNum(1)
753 }
754}
755
756impl Display for OnErrorMode {
757 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
758 match self {
759 OnErrorMode::Continue => {
760 write!(f, "continue")
761 }
762 OnErrorMode::SkipFileNum(n) => {
763 if *n <= 1 {
764 write!(f, "skipfile")
765 } else {
766 write!(f, "skipfile_{}", n)
767 }
768 }
769 OnErrorMode::AbortNum(n) => {
770 if *n <= 1 {
771 write!(f, "abort")
772 } else {
773 write!(f, "abort_{}", n)
774 }
775 }
776 }
777 }
778}
779
780const ERROR_MODE_MSG: &str =
781 "OnError must one of {{ CONTINUE | SKIP_FILE | SKIP_FILE_<num> | ABORT | ABORT_<num> }}";
782impl FromStr for OnErrorMode {
783 type Err = &'static str;
784
785 fn from_str(s: &str) -> std::result::Result<Self, &'static str> {
786 match s.to_uppercase().as_str() {
787 "" | "ABORT" => Ok(OnErrorMode::AbortNum(1)),
788 "CONTINUE" => Ok(OnErrorMode::Continue),
789 "SKIP_FILE" => Ok(OnErrorMode::SkipFileNum(1)),
790 v => {
791 if v.starts_with("ABORT_") {
792 let num_str = v.replace("ABORT_", "");
793 let nums = num_str.parse::<u64>();
794 match nums {
795 Ok(n) if n < 1 => Err(ERROR_MODE_MSG),
796 Ok(n) => Ok(OnErrorMode::AbortNum(n)),
797 Err(_) => Err(ERROR_MODE_MSG),
798 }
799 } else {
800 let num_str = v.replace("SKIP_FILE_", "");
801 let nums = num_str.parse::<u64>();
802 match nums {
803 Ok(n) if n < 1 => Err(ERROR_MODE_MSG),
804 Ok(n) => Ok(OnErrorMode::SkipFileNum(n)),
805 Err(_) => Err(ERROR_MODE_MSG),
806 }
807 }
808 }
809 }
810 }
811}
812
813#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Drive, DriveMut, Eq)]
814pub enum ColumnMatchMode {
815 CaseSensitive,
816 CaseInsensitive,
817 Position,
818}
819
820impl Display for ColumnMatchMode {
821 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
822 match self {
823 ColumnMatchMode::CaseSensitive => write!(f, "CASE_SENSITIVE"),
824 ColumnMatchMode::CaseInsensitive => write!(f, "CASE_INSENSITIVE"),
825 ColumnMatchMode::Position => write!(f, "POSITION"),
826 }
827 }
828}
829
830const COLUMN_MATCH_MODE_MSG: &str =
831 "ColumnMatchMode must be one of {{ CASE_SENSITIVE | CASE_INSENSITIVE | POSITION }}";
832impl FromStr for ColumnMatchMode {
833 type Err = &'static str;
834
835 fn from_str(s: &str) -> std::result::Result<Self, &'static str> {
836 match s.to_uppercase().as_str() {
837 "CASE_SENSITIVE" => Ok(Self::CaseSensitive),
838 "CASE_INSENSITIVE" => Ok(Self::CaseInsensitive),
839 "POSITION" => Ok(Self::Position),
840 _ => Err(COLUMN_MATCH_MODE_MSG),
841 }
842 }
843}