Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//! (commonly referred to as Data Definition Language, or DDL)

#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, format, string::String, vec, vec::Vec};
use alloc::{boxed::Box, format, string::String, vec::Vec};
use core::fmt::{self, Display, Write};

#[cfg(feature = "serde")]
Expand Down
107 changes: 90 additions & 17 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2787,10 +2787,11 @@ impl fmt::Display for Declare {
}

/// Sql options of a `CREATE TABLE` statement.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum CreateTableOptions {
#[default]
None,
/// Options specified using the `WITH` keyword.
/// e.g. `WITH (description = "123")`
Expand Down Expand Up @@ -2819,12 +2820,6 @@ pub enum CreateTableOptions {
TableProperties(Vec<SqlOption>),
}

impl Default for CreateTableOptions {
fn default() -> Self {
Self::None
}
}

impl fmt::Display for CreateTableOptions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Expand Down Expand Up @@ -3227,7 +3222,7 @@ pub enum Statement {
/// WITH options (before PostgreSQL version 9.0)
legacy_options: Vec<CopyLegacyOption>,
/// VALUES a vector of values to be copied
values: Vec<Option<String>>,
values: Vec<Vec<Option<String>>>,
},
/// ```sql
/// COPY INTO <table> | <location>
Expand Down Expand Up @@ -4579,19 +4574,97 @@ impl fmt::Display for Statement {
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, ";")?;
let mut delim = "";
for v in values {
write!(f, "{delim}")?;
delim = "\t";
if let Some(v) = v {
write!(f, "{v}")?;
} else {
write!(f, "\\N")?;

// 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, "\n\\.")?;

write!(f, "\\.")?;
}
Ok(())
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ impl Dialect for BigQueryDialect {
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'
}

fn supports_hyphenated_identifiers(&self) -> bool {
true
}

/// See [doc](https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_literals)
fn supports_triple_quoted_string(&self) -> bool {
true
Expand Down
10 changes: 10 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ 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
fn supports_hyphenated_identifiers(&self) -> bool {
false
}

/// Returns whether the dialect supports path-like identifiers
fn supports_path_like_identifiers(&self) -> bool {
false
}

/// Most dialects do not have custom operators. Override this method to provide custom operators.
fn is_custom_operator_part(&self, _ch: char) -> bool {
false
Expand Down
19 changes: 15 additions & 4 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ impl Dialect for SnowflakeDialect {
|| ch == '_'
}

fn supports_path_like_identifiers(&self) -> bool {
true
}

// See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences
fn supports_string_literal_backslash_escape(&self) -> bool {
true
Expand Down Expand Up @@ -1049,9 +1053,9 @@ pub fn parse_create_stage(

pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result<Ident, ParserError> {
let mut ident = String::new();
while let Some(next_token) = parser.next_token_no_skip() {
match &next_token.token {
Token::Whitespace(_) | Token::SemiColon => break,
loop {
match &parser.next_token().token {
Token::SemiColon | Token::EOF => break,
Token::Period => {
parser.prev_token();
break;
Expand All @@ -1067,7 +1071,14 @@ pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result<Ident, ParserE
Token::Plus => ident.push('+'),
Token::Minus => ident.push('-'),
Token::Number(n, _) => ident.push_str(n),
Token::Word(w) => ident.push_str(&w.to_string()),
Token::Word(w) => {
if matches!(w.keyword, Keyword::NoKeyword) || ident.ends_with("@") {
ident.push_str(w.to_string().as_str());
} else {
parser.prev_token();
break;
}
}
_ => return parser.expected("stage name identifier", parser.peek_token()),
}
}
Expand Down
Loading