use sqlparser::ast::ObjectName;
use sqlparser::dialect::PostgreSqlDialect;
use sqlparser::keywords::Keyword;
use sqlparser::parser::Parser;
use sqlparser::tokenizer::{Token, TokenWithSpan};
pub(super) fn split_statements(tokens: &[TokenWithSpan]) -> Vec<&[TokenWithSpan]> {
let mut statements = Vec::new();
let mut start = 0;
let mut depth = 0usize;
for (index, token) in tokens.iter().enumerate() {
match &token.token {
Token::SemiColon if depth == 0 => {
statements.push(&tokens[start..index]);
start = index + 1;
}
token
if is_word(token, "BEGIN")
&& depth == 0
&& next_significant(&tokens[index + 1..])
.is_some_and(|next| is_word(&next.token, "ATOMIC")) =>
{
depth = 1;
}
token if depth > 0 && is_word(token, "CASE") => depth += 1,
token if depth > 0 && is_word(token, "END") => depth -= 1,
_ => {}
}
}
statements.push(&tokens[start..]);
statements
}
pub(super) enum Recovered {
Skip,
RenameRelation { name: ObjectName, new_name: String },
}
pub(super) fn recover_statement(
dialect: &PostgreSqlDialect,
chunk: &[TokenWithSpan],
) -> Option<Recovered> {
let mut parser = Parser::new(dialect).with_tokens_with_locations(chunk.to_vec());
if parser.parse_keyword(Keyword::DO) {
return Some(Recovered::Skip);
}
if parser.parse_keyword(Keyword::CREATE) {
let _ = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
return parser
.parse_one_of_keywords(&[Keyword::FUNCTION, Keyword::PROCEDURE])
.map(|_| Recovered::Skip);
}
if !parser.parse_keyword(Keyword::ALTER) {
return None;
}
let relation = parser.parse_keyword(Keyword::INDEX)
|| parser.parse_keyword(Keyword::SEQUENCE)
|| parser.parse_keyword(Keyword::VIEW)
|| parser.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW]);
if !relation {
return None;
}
let _ = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
let rename = parser.parse_object_name(false).ok().and_then(|name| {
parser
.parse_keywords(&[Keyword::RENAME, Keyword::TO])
.then(|| parser.parse_identifier().ok())
.flatten()
.filter(|_| parser.peek_token().token == Token::EOF)
.map(|new_name| Recovered::RenameRelation {
name,
new_name: super::names::ident_name(&new_name),
})
});
Some(rename.unwrap_or(Recovered::Skip))
}
pub(super) enum AlterRewrite {
Skip,
Tokens(Vec<TokenWithSpan>),
}
pub(super) fn rewrite_alter_table(
dialect: &PostgreSqlDialect,
chunk: &[TokenWithSpan],
) -> Option<AlterRewrite> {
let mut parser = Parser::new(dialect).with_tokens_with_locations(chunk.to_vec());
if !parser.parse_keywords(&[Keyword::ALTER, Keyword::TABLE]) {
return None;
}
let _ = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
let _ = parser.parse_keyword(Keyword::ONLY);
parser.parse_object_name(false).ok()?;
let header = &chunk[..parser.index()];
let mut kept: Vec<Vec<TokenWithSpan>> = Vec::new();
for clause in split_clauses(&chunk[header.len()..]) {
let mut statement = header.to_vec();
statement.extend_from_slice(clause);
if parses(dialect, statement) {
kept.push(clause.to_vec());
continue;
}
match rewrite_clause(clause)? {
ClauseRewrite::Drop => {}
ClauseRewrite::Replace(tokens) => kept.push(tokens),
}
}
if kept.is_empty() {
return Some(AlterRewrite::Skip);
}
let mut statement = header.to_vec();
for (index, clause) in kept.into_iter().enumerate() {
if index > 0 {
statement.push(TokenWithSpan::wrap(Token::Comma));
}
statement.extend(clause);
}
parses(dialect, statement.clone()).then_some(AlterRewrite::Tokens(statement))
}
enum ClauseRewrite {
Drop,
Replace(Vec<TokenWithSpan>),
}
fn rewrite_clause(clause: &[TokenWithSpan]) -> Option<ClauseRewrite> {
let words: Vec<&TokenWithSpan> = clause
.iter()
.filter(|token| !matches!(token.token, Token::Whitespace(_) | Token::EOF))
.collect();
let at = |index: usize, word: &str| words.get(index).is_some_and(|t| is_word(&t.token, word));
let open_paren_at = |index: usize| words.get(index).is_some_and(|t| t.token == Token::LParen);
let table_level = (at(0, "RESET") && open_paren_at(1))
|| (at(0, "CLUSTER") && at(1, "ON"))
|| (at(0, "SET") && at(1, "WITHOUT") && at(2, "CLUSTER"))
|| (at(0, "SET") && (at(1, "TABLESPACE") || at(1, "LOGGED") || at(1, "UNLOGGED")))
|| (at(0, "FORCE") && at(1, "ROW") && at(2, "LEVEL") && at(3, "SECURITY"))
|| (at(0, "NO") && at(1, "FORCE") && at(2, "ROW") && at(3, "LEVEL"))
|| (at(0, "ALTER") && at(1, "CONSTRAINT"));
if table_level {
return Some(ClauseRewrite::Drop);
}
if !at(0, "ALTER") {
return None;
}
let name = if at(1, "COLUMN") { 2 } else { 1 };
words
.get(name)
.filter(|t| matches!(t.token, Token::Word(_)))?;
let op = name + 1;
let sequence_option = |index: usize| {
[
"INCREMENT",
"MINVALUE",
"MAXVALUE",
"START",
"CACHE",
"CYCLE",
"NO",
"OWNED",
"SEQUENCE",
]
.iter()
.any(|word| at(index, word))
};
let unchanged = (at(op, "SET")
&& (at(op + 1, "STATISTICS")
|| at(op + 1, "STORAGE")
|| at(op + 1, "COMPRESSION")
|| at(op + 1, "GENERATED")
|| open_paren_at(op + 1)
|| sequence_option(op + 1)))
|| (at(op, "RESET") && open_paren_at(op + 1))
|| at(op, "RESTART");
if unchanged {
return Some(ClauseRewrite::Drop);
}
if at(op, "DROP") && (at(op + 1, "IDENTITY") || at(op + 1, "EXPRESSION")) {
let rest = words.len() - (op + 2);
let if_exists = rest == 2 && at(op + 2, "IF") && at(op + 3, "EXISTS");
if rest != 0 && !if_exists {
return None;
}
let mut tokens: Vec<TokenWithSpan> =
words[..op].iter().map(|token| (*token).clone()).collect();
let span = words[op].span;
for keyword in ["DROP", "DEFAULT"] {
tokens.push(TokenWithSpan::new(Token::make_keyword(keyword), span));
}
return Some(ClauseRewrite::Replace(spaced(tokens)));
}
let is_type = at(op, "TYPE") || (at(op, "SET") && at(op + 1, "DATA") && at(op + 2, "TYPE"));
let collate = clause
.iter()
.position(|token| is_word(&token.token, "COLLATE"));
if let (true, Some(collate)) = (is_type, collate) {
let after = collate_name_end(clause, collate + 1)?;
let mut tokens = clause[..collate].to_vec();
tokens.extend_from_slice(&clause[after..]);
return Some(ClauseRewrite::Replace(tokens));
}
None
}
fn collate_name_end(clause: &[TokenWithSpan], start: usize) -> Option<usize> {
let mut index = start;
let mut expect_name = true;
while let Some(token) = clause.get(index) {
match &token.token {
Token::Whitespace(_) => {}
Token::Word(_) if expect_name => expect_name = false,
Token::Period if !expect_name => expect_name = true,
_ => break,
}
index += 1;
}
(!expect_name).then_some(index)
}
fn spaced(tokens: Vec<TokenWithSpan>) -> Vec<TokenWithSpan> {
let mut spaced = Vec::with_capacity(tokens.len() * 2);
for token in tokens {
let needs_space = !matches!(token.token, Token::Whitespace(_))
&& spaced
.last()
.is_some_and(|last: &TokenWithSpan| !matches!(last.token, Token::Whitespace(_)));
if needs_space {
spaced.push(TokenWithSpan::new(
Token::Whitespace(sqlparser::tokenizer::Whitespace::Space),
token.span,
));
}
spaced.push(token);
}
spaced
}
fn split_clauses(tokens: &[TokenWithSpan]) -> Vec<&[TokenWithSpan]> {
let mut clauses = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (index, token) in tokens.iter().enumerate() {
match token.token {
Token::LParen => depth += 1,
Token::RParen => depth = depth.saturating_sub(1),
Token::Comma if depth == 0 => {
clauses.push(&tokens[start..index]);
start = index + 1;
}
_ => {}
}
}
clauses.push(&tokens[start..]);
clauses
}
fn parses(dialect: &PostgreSqlDialect, tokens: Vec<TokenWithSpan>) -> bool {
Parser::new(dialect)
.with_tokens_with_locations(tokens)
.parse_statements()
.is_ok()
}
fn next_significant(tokens: &[TokenWithSpan]) -> Option<&TokenWithSpan> {
tokens
.iter()
.find(|token| !matches!(token.token, Token::Whitespace(_)))
}
fn is_word(token: &Token, word: &str) -> bool {
matches!(token, Token::Word(w) if w.quote_style.is_none() && w.value.eq_ignore_ascii_case(word))
}