diff --git a/src/ast/dml.rs b/src/ast/dml.rs index c0bfcb19..0880fa41 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -27,9 +27,10 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ - display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, - InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, - Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, + display_comma_separated, display_separated, query::InputFormatClause, Assignment, + CopyLegacyCsvOption, CopyLegacyOption, CopyOption, CopySource, CopyTarget, Expr, FromTable, + Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, + SelectItem, Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, }; /// INSERT statement. @@ -303,3 +304,290 @@ impl Display for Update { Ok(()) } } + +/// CSV formatting options extracted from COPY options. +/// +/// This struct encapsulates the CSV formatting settings used when parsing +/// or formatting COPY statement data. It extracts relevant options from both +/// modern [`CopyOption`] and legacy [`CopyLegacyOption`] variants. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CsvFormatOptions { + /// The field delimiter character (default: tab) + pub(crate) delimiter: char, + /// The quote character used to enclose fields (default: `"`) + pub(crate) quote: char, + /// The escape character (default: `\`) + pub(crate) escape: char, + /// The string representing NULL values (default: `\\N`) + pub(crate) null_symbol: String, +} + +impl Default for CsvFormatOptions { + fn default() -> Self { + Self { + delimiter: '\t', + quote: '"', + escape: '\\', + null_symbol: "\\N".to_string(), + } + } +} + +impl CsvFormatOptions { + /// Extract CSV format options from CopyOption and CopyLegacyOption lists. + /// + /// This method processes both modern and legacy COPY options to determine + /// the CSV formatting settings. Later options in the lists override earlier ones. + /// + /// # Arguments + /// + /// * `options` - Modern COPY options (PostgreSQL 9.0+) + /// * `legacy_options` - Legacy COPY options (pre-PostgreSQL 9.0) + /// + /// # Returns + /// + /// A `CsvFormatOptions` instance with the extracted settings, using defaults + /// for any options not specified. + pub(crate) fn from_copy_options( + options: &[CopyOption], + legacy_options: &[CopyLegacyOption], + ) -> Self { + let mut csv_options = Self::default(); + + // Apply options + for option in options { + match option { + CopyOption::Delimiter(c) => { + csv_options.delimiter = *c; + } + CopyOption::Quote(c) => { + csv_options.quote = *c; + } + CopyOption::Escape(c) => { + csv_options.escape = *c; + } + CopyOption::Null(null) => { + csv_options.null_symbol = null.clone(); + } + // These options don't affect CSV formatting + CopyOption::Format(_) + | CopyOption::Freeze(_) + | CopyOption::Header(_) + | CopyOption::ForceQuote(_) + | CopyOption::ForceNotNull(_) + | CopyOption::ForceNull(_) + | CopyOption::Encoding(_) => {} + } + } + + // Apply legacy options + for option in legacy_options { + match option { + CopyLegacyOption::Delimiter(c) => { + csv_options.delimiter = *c; + } + CopyLegacyOption::Null(null) => { + csv_options.null_symbol = null.clone(); + } + CopyLegacyOption::Csv(csv_opts) => { + for csv_option in csv_opts { + match csv_option { + CopyLegacyCsvOption::Quote(c) => { + csv_options.quote = *c; + } + CopyLegacyCsvOption::Escape(c) => { + csv_options.escape = *c; + } + // These CSV options don't affect CSV formatting + CopyLegacyCsvOption::Header + | CopyLegacyCsvOption::ForceQuote(_) + | CopyLegacyCsvOption::ForceNotNull(_) => {} + } + } + } + // These legacy options don't affect CSV formatting + CopyLegacyOption::AcceptAnyDate + | CopyLegacyOption::AcceptInvChars(_) + | CopyLegacyOption::AddQuotes + | CopyLegacyOption::AllowOverwrite + | CopyLegacyOption::Binary + | CopyLegacyOption::BlankAsNull + | CopyLegacyOption::Bzip2 + | CopyLegacyOption::CleanPath + | CopyLegacyOption::CompUpdate { .. } + | CopyLegacyOption::DateFormat(_) + | CopyLegacyOption::EmptyAsNull + | CopyLegacyOption::Encrypted { .. } + | CopyLegacyOption::Escape + | CopyLegacyOption::Extension(_) + | CopyLegacyOption::FixedWidth(_) + | CopyLegacyOption::Gzip + | CopyLegacyOption::Header + | CopyLegacyOption::IamRole(_) + | CopyLegacyOption::IgnoreHeader(_) + | CopyLegacyOption::Json + | CopyLegacyOption::Manifest { .. } + | CopyLegacyOption::MaxFileSize(_) + | CopyLegacyOption::Parallel(_) + | CopyLegacyOption::Parquet + | CopyLegacyOption::PartitionBy(_) + | CopyLegacyOption::Region(_) + | CopyLegacyOption::RemoveQuotes + | CopyLegacyOption::RowGroupSize(_) + | CopyLegacyOption::StatUpdate(_) + | CopyLegacyOption::TimeFormat(_) + | CopyLegacyOption::TruncateColumns + | CopyLegacyOption::Zstd => {} + } + } + + csv_options + } + + /// Format a single CSV field, adding quotes and escaping if necessary. + /// + /// This method handles CSV field formatting according to the configured options: + /// - Writes NULL values using the configured `null_symbol` + /// - Adds quotes around fields containing delimiters, quotes, or newlines + /// - Escapes quote characters by doubling them + /// - Escapes escape characters + /// + /// # Arguments + /// + /// * `f` - The formatter to write to + /// * `field` - The field value to format, or `None` for NULL + /// + /// # Returns + /// + /// A `fmt::Result` indicating success or failure of the write operation. + fn format_csv_field(&self, f: &mut fmt::Formatter, field: Option<&str>) -> fmt::Result { + let field_value = field.unwrap_or(&self.null_symbol); + + // Check if field needs quoting + let needs_quoting = field_value.contains(self.delimiter) + || field_value.contains(self.quote) + || field_value.contains('\n') + || field_value.contains('\r'); + + if needs_quoting { + write!(f, "{}", self.quote)?; + for ch in field_value.chars() { + if ch == self.quote { + // Escape quote by doubling it + write!(f, "{}{}", self.quote, self.quote)?; + } else if ch == self.escape { + // Escape escape character + write!(f, "{}{}", self.escape, self.escape)?; + } else { + write!(f, "{}", ch)?; + } + } + write!(f, "{}", self.quote)?; + } else { + write!(f, "{}", field_value)?; + } + Ok(()) + } +} + +/// COPY statement. +/// +/// Represents a PostgreSQL COPY statement for bulk data transfer between +/// a file and a table. The statement can copy data FROM a file to a table +/// or TO a file from a table or query. +/// +/// # Syntax +/// +/// ```sql +/// COPY table_name [(column_list)] FROM { 'filename' | STDIN | PROGRAM 'command' } +/// COPY { table_name [(column_list)] | (query) } TO { 'filename' | STDOUT | PROGRAM 'command' } +/// ``` +/// +/// # Examples +/// +/// ``` +/// # use sqlparser::ast::{Copy, CopySource, CopyTarget, ObjectName}; +/// # use sqlparser::dialect::PostgreSqlDialect; +/// # use sqlparser::parser::Parser; +/// let sql = "COPY users FROM 'data.csv'"; +/// let dialect = PostgreSqlDialect {}; +/// let ast = Parser::parse_sql(&dialect, sql).unwrap(); +/// ``` +/// +/// See [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-copy.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Copy { + /// The source of 'COPY TO', or the target of 'COPY FROM'. + /// Can be a table name with optional column list, or a query (for COPY TO only). + pub source: CopySource, + /// Direction of the copy operation. + /// - `true` for COPY TO (table/query to file) + /// - `false` for COPY FROM (file to table) + pub to: bool, + /// The target of 'COPY TO', or the source of 'COPY FROM'. + /// Can be a file, STDIN, STDOUT, or a PROGRAM command. + pub target: CopyTarget, + /// Modern COPY options (PostgreSQL 9.0+), specified within parentheses. + /// Examples: FORMAT, DELIMITER, NULL, HEADER, QUOTE, ESCAPE, etc. + pub options: Vec, + /// Legacy COPY options (pre-PostgreSQL 9.0), specified without parentheses. + /// Also used by AWS Redshift extensions like IAM_ROLE, MANIFEST, etc. + pub legacy_options: Vec, + /// CSV data rows for COPY FROM STDIN statements. + /// Each row is a vector of optional strings (None represents NULL). + /// Populated only when copying from STDIN with inline data. + pub values: Vec>>, +} + +impl Display for Copy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "COPY")?; + match &self.source { + CopySource::Query(query) => write!(f, " ({query})")?, + CopySource::Table { + table_name, + columns, + } => { + write!(f, " {table_name}")?; + if !columns.is_empty() { + write!(f, " ({})", display_comma_separated(columns))?; + } + } + } + write!( + f, + " {} {}", + if self.to { "TO" } else { "FROM" }, + self.target + )?; + if !self.options.is_empty() { + write!(f, " ({})", display_comma_separated(&self.options))?; + } + if !self.legacy_options.is_empty() { + write!(f, " {}", display_separated(&self.legacy_options, " "))?; + } + + if !self.values.is_empty() { + writeln!(f, ";")?; + + let csv_options = + CsvFormatOptions::from_copy_options(&self.options, &self.legacy_options); + + // Write CSV data + for row in &self.values { + for (idx, column) in row.iter().enumerate() { + if idx > 0 { + write!(f, "{}", csv_options.delimiter)?; + } + csv_options.format_csv_field(f, column.as_deref())?; + } + writeln!(f)?; + } + + write!(f, "\\.")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0b440238..864eba42 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -73,7 +73,7 @@ pub use self::ddl::{ ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{Delete, Insert, Update}; +pub use self::dml::{Copy, CsvFormatOptions, Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -3210,20 +3210,7 @@ pub enum Statement { /// ```sql /// COPY [TO | FROM] ... /// ``` - Copy { - /// The source of 'COPY TO', or the target of 'COPY FROM' - source: CopySource, - /// If true, is a 'COPY TO' statement. If false is a 'COPY FROM' - to: bool, - /// The target of 'COPY TO', or the source of 'COPY FROM' - target: CopyTarget, - /// WITH options (from PostgreSQL version 9.0) - options: Vec, - /// WITH options (before PostgreSQL version 9.0) - legacy_options: Vec, - /// VALUES a vector of values to be copied - values: Vec>>, - }, + Copy(Copy), /// ```sql /// COPY INTO | /// ``` @@ -4278,6 +4265,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(copy: Copy) -> Self { + Statement::Copy(copy) + } +} + /// ```sql /// {COPY | REVOKE} CURRENT GRANTS /// ``` @@ -4546,128 +4539,7 @@ impl fmt::Display for Statement { Statement::Call(function) => write!(f, "CALL {function}"), - Statement::Copy { - source, - to, - target, - options, - legacy_options, - values, - } => { - write!(f, "COPY")?; - match source { - CopySource::Query(query) => write!(f, " ({query})")?, - CopySource::Table { - table_name, - columns, - } => { - write!(f, " {table_name}")?; - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } - } - } - write!(f, " {} {}", if *to { "TO" } else { "FROM" }, target)?; - if !options.is_empty() { - write!(f, " ({})", display_comma_separated(options))?; - } - if !legacy_options.is_empty() { - write!(f, " {}", display_separated(legacy_options, " "))?; - } - - let mut null_symbol = "\\N"; - let mut delimiter = '\t'; - let mut quote = '"'; - let mut escape = '\\'; - - // Apply options - for option in options { - match option { - CopyOption::Delimiter(c) => { - delimiter = *c; - } - CopyOption::Quote(c) => { - quote = *c; - } - CopyOption::Escape(c) => { - escape = *c; - } - CopyOption::Null(null) => { - null_symbol = null; - } - _ => {} - } - } - - // Apply legacy options - for option in legacy_options { - match option { - CopyLegacyOption::Delimiter(c) => { - delimiter = *c; - } - CopyLegacyOption::Null(null) => { - null_symbol = null; - } - CopyLegacyOption::Csv(csv_options) => { - for csv_option in csv_options { - match csv_option { - CopyLegacyCsvOption::Quote(c) => { - quote = *c; - } - CopyLegacyCsvOption::Escape(c) => { - escape = *c; - } - _ => {} - } - } - } - _ => {} - } - } - - if !values.is_empty() { - writeln!(f, ";")?; - - // Simple CSV writer - for row in values { - for (idx, column) in row.iter().enumerate() { - if idx > 0 { - write!(f, "{}", delimiter)?; - } - - let field_value = column.as_deref().unwrap_or(null_symbol); - - // Check if field needs quoting - let needs_quoting = field_value.contains(delimiter) - || field_value.contains(quote) - || field_value.contains('\n') - || field_value.contains('\r'); - - if needs_quoting { - write!(f, "{}", quote)?; - for ch in field_value.chars() { - if ch == quote { - // Escape quote by doubling it - write!(f, "{}{}", quote, quote)?; - } else if ch == escape { - // Escape escape character - write!(f, "{}{}", escape, escape)?; - } else { - write!(f, "{}", ch)?; - } - } - write!(f, "{}", quote)?; - } else { - write!(f, "{}", field_value)?; - } - } - writeln!(f)?; - } - - write!(f, "\\.")?; - } - Ok(()) - } + Statement::Copy(copy) => copy.fmt(f), Statement::Update(update) => update.fmt(f), Statement::Delete(delete) => delete.fmt(f), Statement::Open(open) => open.fmt(f), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7d2a0009..66a4e496 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -317,14 +317,7 @@ impl Spanned for Statement { Statement::While(stmt) => stmt.span(), Statement::Raise(stmt) => stmt.span(), Statement::Call(function) => function.span(), - Statement::Copy { - source, - to: _, - target: _, - options: _, - legacy_options: _, - values: _, - } => source.span(), + Statement::Copy(copy) => copy.source.span(), Statement::CopyIntoSnowflake { into: _, into_columns: _, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index df19a598..8f797965 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -178,7 +178,44 @@ pub trait Dialect: Debug + Any { /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; - /// Returns whether the dialect supports hyphenated identifiers + /// Returns whether the dialect supports hyphenated identifiers. + /// + /// Hyphenated identifiers are identifiers that contain hyphens (dashes) + /// within the name, such as `my-table` or `column-name`. + /// + /// # Examples + /// + /// BigQuery supports hyphenated identifiers: + /// + /// ```rust + /// # use sqlparser::{dialect::BigQueryDialect, parser::Parser}; + /// let dialect = BigQueryDialect; + /// let sql = "SELECT my-column FROM my-table"; + /// let result = Parser::parse_sql(&dialect, sql); + /// assert!(result.is_ok()); + /// ``` + /// + /// Most other dialects do not support hyphenated identifiers, + /// and in those cases such identifiers must be quoted: + /// + /// ```rust + /// # use sqlparser::{dialect::PostgreSqlDialect, parser::Parser}; + /// let dialect = PostgreSqlDialect {}; + /// let sql = "SELECT my-column FROM my-table"; + /// let result = Parser::parse_sql(&dialect, sql); + /// assert!(result.is_err()); + /// ``` + /// + /// In dialects that do not support hyphenated identifiers, the above + /// query would need to be written as: + /// + /// ```rust + /// # use sqlparser::{dialect::PostgreSqlDialect, parser::Parser}; + /// let dialect = PostgreSqlDialect {}; + /// let sql = "SELECT \"my-column\" FROM \"my-table\""; + /// let result = Parser::parse_sql(&dialect, sql); + /// assert!(result.is_ok()); + /// ``` fn supports_hyphenated_identifiers(&self) -> bool { false } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0949d1f2..54e672e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9542,55 +9542,11 @@ impl<'a> Parser<'a> { return self.expected("COPY ... FROM STDIN with CSV body", self.peek_token()); }; - let mut delimiter = '\t'; - let mut quote = '"'; - let mut escape = '\\'; - let mut null_symbol = "\\N"; - - // Apply options - for option in options { - match option { - CopyOption::Delimiter(c) => { - delimiter = *c; - } - CopyOption::Quote(c) => { - quote = *c; - } - CopyOption::Escape(c) => { - escape = *c; - } - CopyOption::Null(null) => { - null_symbol = null; - } - _ => {} - } - } - - // Apply legacy options - for option in legacy_options { - match option { - CopyLegacyOption::Delimiter(c) => { - delimiter = *c; - } - CopyLegacyOption::Null(null) => { - null_symbol = null; - } - CopyLegacyOption::Csv(csv_options) => { - for csv_option in csv_options { - match csv_option { - CopyLegacyCsvOption::Quote(c) => { - quote = *c; - } - CopyLegacyCsvOption::Escape(c) => { - escape = *c; - } - _ => {} - } - } - } - _ => {} - } - } + let csv_options = CsvFormatOptions::from_copy_options(options, legacy_options); + let delimiter = csv_options.delimiter; + let quote = csv_options.quote; + let escape = csv_options.escape; + let null_symbol = csv_options.null_symbol.as_str(); // Simple CSV parser let mut result = vec![]; @@ -9762,14 +9718,14 @@ impl<'a> Parser<'a> { } else { vec![] }; - Ok(Statement::Copy { + Ok(Statement::Copy(Copy { source, to, target, options, legacy_options, values, - }) + })) } /// Parse [Statement::Open] diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 99b7ac3f..21ea0ce1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17082,7 +17082,7 @@ fn parse_copy_options() { r#"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' IAM_ROLE 'arn:aws:iam::123456789:role/role1' CSV IGNOREHEADER 1"#, ); match copy { - Statement::Copy { legacy_options, .. } => { + Statement::Copy(Copy { legacy_options, .. }) => { assert_eq!( legacy_options, vec![ @@ -17102,7 +17102,7 @@ fn parse_copy_options() { r#"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' IAM_ROLE DEFAULT CSV IGNOREHEADER 1"#, ); match copy { - Statement::Copy { legacy_options, .. } => { + Statement::Copy(Copy { legacy_options, .. }) => { assert_eq!( legacy_options, vec![ diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bfdbabb1..0d421883 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1076,7 +1076,7 @@ fn test_copy_from() { let stmt = pg().verified_stmt("COPY users FROM 'data.csv'"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1088,13 +1088,13 @@ fn test_copy_from() { options: vec![], legacy_options: vec![], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY users FROM 'data.csv' DELIMITER ','"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1106,13 +1106,13 @@ fn test_copy_from() { options: vec![], legacy_options: vec![CopyLegacyOption::Delimiter(',')], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY users FROM 'data.csv' DELIMITER ',' CSV HEADER"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1127,7 +1127,7 @@ fn test_copy_from() { CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,]) ], values: vec![], - } + }) ); } @@ -1136,7 +1136,7 @@ fn test_copy_to() { let stmt = pg().verified_stmt("COPY users TO 'data.csv'"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1148,13 +1148,13 @@ fn test_copy_to() { options: vec![], legacy_options: vec![], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY users TO 'data.csv' DELIMITER ','"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1166,13 +1166,13 @@ fn test_copy_to() { options: vec![], legacy_options: vec![CopyLegacyOption::Delimiter(',')], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY users TO 'data.csv' DELIMITER ',' CSV HEADER"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1187,7 +1187,7 @@ fn test_copy_to() { CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,]) ], values: vec![], - } + }) ) } @@ -1213,7 +1213,7 @@ fn parse_copy_from() { )"; assert_eq!( pg_and_generic().one_statement_parses_to(sql, ""), - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["table".into()]), columns: vec!["a".into(), "b".into()], @@ -1241,7 +1241,7 @@ fn parse_copy_from() { ], legacy_options: vec![], values: vec![], - } + }) ); } @@ -1259,7 +1259,7 @@ fn parse_copy_to() { let stmt = pg().verified_stmt("COPY users TO 'data.csv'"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1271,13 +1271,13 @@ fn parse_copy_to() { options: vec![], legacy_options: vec![], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY country TO STDOUT (DELIMITER '|')"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["country".into()]), columns: vec![], @@ -1287,14 +1287,14 @@ fn parse_copy_to() { options: vec![CopyOption::Delimiter('|')], legacy_options: vec![], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY country TO PROGRAM 'gzip > /usr1/proj/bray/sql/country_data.gz'"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["country".into()]), columns: vec![], @@ -1306,13 +1306,13 @@ fn parse_copy_to() { options: vec![], legacy_options: vec![], values: vec![], - } + }) ); let stmt = pg().verified_stmt("COPY (SELECT 42 AS a, 'hello' AS b) TO 'query.csv'"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Query(Box::new(Query { with: None, body: Box::new(SetExpr::Select(Box::new(Select { @@ -1374,7 +1374,7 @@ fn parse_copy_to() { options: vec![], legacy_options: vec![], values: vec![], - } + }) ) } @@ -1383,7 +1383,7 @@ fn parse_copy_from_before_v9_0() { let stmt = pg().verified_stmt("COPY users FROM 'data.csv' BINARY DELIMITER ',' NULL 'null' CSV HEADER QUOTE '\"' ESCAPE '\\' FORCE NOT NULL column"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1405,14 +1405,14 @@ fn parse_copy_from_before_v9_0() { ]), ], values: vec![], - } + }) ); // test 'AS' keyword let sql = "COPY users FROM 'data.csv' DELIMITER AS ',' NULL AS 'null' CSV QUOTE AS '\"' ESCAPE AS '\\'"; assert_eq!( pg_and_generic().one_statement_parses_to(sql, ""), - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1431,7 +1431,7 @@ fn parse_copy_from_before_v9_0() { ]), ], values: vec![], - } + }) ); } @@ -1440,7 +1440,7 @@ fn parse_copy_to_before_v9_0() { let stmt = pg().verified_stmt("COPY users TO 'data.csv' BINARY DELIMITER ',' NULL 'null' CSV HEADER QUOTE '\"' ESCAPE '\\' FORCE QUOTE column"); assert_eq!( stmt, - Statement::Copy { + Statement::Copy(Copy { source: CopySource::Table { table_name: ObjectName::from(vec!["users".into()]), columns: vec![], @@ -1462,7 +1462,7 @@ fn parse_copy_to_before_v9_0() { ]), ], values: vec![], - } + }) ) }