use super::*;
impl DmlParser {
pub fn parse_copy_statement(&mut self, ctx: &mut ParseContext) -> Result<Stmt, ParseError> {
use crate::parser::ast::stmt::{CopyDirection, CopyStmt, CopyTarget};
let start_span = ctx.current_span();
ctx.expect_token(TokenKind::Copy)?;
let target = if ctx.match_token(TokenKind::Vertex) {
let tag = ctx.expect_identifier()?;
CopyTarget::Vertex(tag)
} else if ctx.match_token(TokenKind::Edge) {
let edge = ctx.expect_identifier()?;
CopyTarget::Edge(edge)
} else if ctx.match_token(TokenKind::Tag) {
let tag = ctx.expect_identifier()?;
CopyTarget::Vertex(tag)
} else {
let name = ctx.expect_identifier()?;
CopyTarget::Vertex(name)
};
let direction = if ctx.match_token(TokenKind::To) {
CopyDirection::To
} else {
ctx.expect_token(TokenKind::From)?;
CopyDirection::From
};
let mut file_paths = Vec::new();
if ctx.check_token(TokenKind::LParen) {
ctx.expect_token(TokenKind::LParen)?;
loop {
if ctx.check_token(TokenKind::RParen) {
ctx.next_token();
break;
}
file_paths.push(ctx.expect_string_literal()?);
if ctx.match_token(TokenKind::Comma) {
continue;
}
ctx.expect_token(TokenKind::RParen)?;
break;
}
if file_paths.is_empty() {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"COPY FROM file list must contain at least one file".to_string(),
ctx.current_position(),
));
}
} else {
file_paths.push(ctx.expect_string_literal()?);
}
let mut by_column = false;
if ctx.check_token(TokenKind::By) || ctx.check_keyword("BY") {
if !ctx.match_token(TokenKind::By) {
let _ = ctx.consume_keyword("BY");
}
if ctx.check_keyword("COLUMN") {
let _ = ctx.consume_keyword("COLUMN");
by_column = true;
} else {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"Expected COLUMN after BY in COPY statement".to_string(),
ctx.current_position(),
));
}
}
if direction == CopyDirection::To && file_paths.len() > 1 {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"COPY TO supports a single file path".to_string(),
ctx.current_position(),
));
}
if direction == CopyDirection::To && by_column {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"BY COLUMN is only valid for COPY FROM".to_string(),
ctx.current_position(),
));
}
let mut header = true;
let mut delimiter = ',';
let mut batch_size: Option<usize> = None;
fn consume_option_name(ctx: &mut ParseContext, kind: TokenKind, name: &str) {
if !ctx.match_token(kind) {
let _ = ctx.consume_keyword(name);
}
}
fn consume_optional_assign(ctx: &mut ParseContext) {
if !ctx.match_token(TokenKind::Assign) {
let _ = ctx.match_token(TokenKind::Eq);
}
}
let parse_delimiter_value = |ctx: &mut ParseContext| -> Result<char, ParseError> {
consume_optional_assign(ctx);
match ctx.current_token().kind.clone() {
TokenKind::StringLiteral(s) | TokenKind::Identifier(s) => {
ctx.next_token();
s.chars().next().ok_or_else(|| {
ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"DELIMITER must be a single character".to_string(),
ctx.current_position(),
)
})
}
other => Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::UnexpectedToken,
format!("Expected delimiter string, found {other:?}"),
ctx.current_position(),
)),
}
};
let parse_bool_value = |ctx: &mut ParseContext| -> Option<bool> {
match ctx.current_token().kind.clone() {
TokenKind::BooleanLiteral(b) => {
ctx.next_token();
Some(b)
}
TokenKind::Identifier(s) if s.eq_ignore_ascii_case("true") => {
ctx.next_token();
Some(true)
}
TokenKind::Identifier(s) if s.eq_ignore_ascii_case("false") => {
ctx.next_token();
Some(false)
}
_ => None,
}
};
let mut in_parens = false;
if (ctx.match_token(TokenKind::With) || !ctx.check_token(TokenKind::With))
&& ctx.match_token(TokenKind::LParen)
{
in_parens = true;
}
loop {
if ctx.match_token(TokenKind::Comma) {
continue;
}
if in_parens && ctx.match_token(TokenKind::RParen) {
break;
}
if ctx.check_token(TokenKind::Header) || ctx.check_keyword("HEADER") {
consume_option_name(ctx, TokenKind::Header, "HEADER");
header = parse_bool_value(ctx).unwrap_or(true);
continue;
}
if ctx.check_token(TokenKind::Delimiter) || ctx.check_keyword("DELIMITER") {
consume_option_name(ctx, TokenKind::Delimiter, "DELIMITER");
delimiter = parse_delimiter_value(ctx)?;
continue;
}
if ctx.check_keyword("BATCH_SIZE") || ctx.check_keyword("BATCH") {
if ctx.check_keyword("BATCH_SIZE") {
let _ = ctx.consume_keyword("BATCH_SIZE");
} else {
let _ = ctx.consume_keyword("BATCH");
let _ = ctx.consume_keyword("SIZE");
}
consume_optional_assign(ctx);
let batch = ctx.expect_integer_literal()?;
if batch < 0 {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"BATCH_SIZE must be positive".to_string(),
ctx.current_position(),
));
}
batch_size = Some(batch as usize);
continue;
}
if ctx.check_token(TokenKind::Csv) || ctx.check_keyword("CSV") {
consume_option_name(ctx, TokenKind::Csv, "CSV");
continue;
}
if ctx.check_token(TokenKind::No) || ctx.check_keyword("NO") {
ctx.next_token();
if ctx.check_token(TokenKind::Header) || ctx.check_keyword("HEADER") {
consume_option_name(ctx, TokenKind::Header, "HEADER");
header = false;
continue;
}
break;
}
if in_parens {
if matches!(
ctx.current_token().kind,
TokenKind::Eof | TokenKind::Semicolon
) {
break;
}
break;
}
if ctx.match_token(TokenKind::With) {
if ctx.match_token(TokenKind::LParen) {
in_parens = true;
}
continue;
}
break;
}
if !by_column && (ctx.check_token(TokenKind::By) || ctx.check_keyword("BY")) {
if !ctx.match_token(TokenKind::By) {
let _ = ctx.consume_keyword("BY");
}
if ctx.check_keyword("COLUMN") {
let _ = ctx.consume_keyword("COLUMN");
by_column = true;
} else {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"Expected COLUMN after BY in COPY statement".to_string(),
ctx.current_position(),
));
}
}
if direction == CopyDirection::To && by_column {
return Err(ParseError::new(
crate::parser::core::error::ParseErrorKind::SyntaxError,
"BY COLUMN is only valid for COPY FROM".to_string(),
ctx.current_position(),
));
}
let span = ctx.span_since(start_span.start);
Ok(Stmt::Copy(CopyStmt {
span,
target,
direction,
file_paths,
by_column,
header,
delimiter,
batch_size,
}))
}
}