use super::classifier::{CreateDeterminantKind, classify_segment, determinant_for_word};
use super::types::{Location, SqlSegment, SqlSegmentationExt};
use crate::dialects::Dialect;
use crate::parsers::ParseError;
use crate::parsers::tokens::{SqlToken, SqlTokenKind};
pub fn segment_sql(sql: &str, dialect: Dialect) -> Result<Vec<SqlSegment>, ParseError> {
let tokens = dialect
.tokenizer()
.tokenize(sql)
.map_err(|error| ParseError::segment(dialect, error.line, error.column, error.message))?;
Scanner::new(sql, dialect, tokens).scan()
}
struct Scanner<'a> {
sql: &'a str,
dialect: Dialect,
tokens: Vec<SqlToken>,
token_index: usize,
segment_start: usize,
meaningful: bool,
ordinal: usize,
delimiter: String,
paren_depth: usize,
bracket_depth: usize,
brace_depth: usize,
body_depth: usize,
first_word: Option<String>,
create_kind: Option<CreateDeterminantKind>,
recent_words: Vec<String>,
segments: Vec<SqlSegment>,
}
impl<'a> Scanner<'a> {
fn new(sql: &'a str, dialect: Dialect, tokens: Vec<SqlToken>) -> Self {
Self {
sql,
dialect,
tokens,
token_index: 0,
segment_start: 0,
meaningful: false,
ordinal: 1,
delimiter: ";".to_string(),
paren_depth: 0,
bracket_depth: 0,
brace_depth: 0,
body_depth: 0,
first_word: None,
create_kind: None,
recent_words: Vec::new(),
segments: Vec::new(),
}
}
fn scan(mut self) -> Result<Vec<SqlSegment>, ParseError> {
while self.token_index < self.tokens.len() {
if self.consume_delimiter_directive()? || self.consume_active_delimiter()? {
continue;
}
let token = self.tokens[self.token_index].clone();
self.observe_token(&token)?;
self.token_index += 1;
}
self.finish_at_eof()?;
Ok(self.segments)
}
fn observe_token(&mut self, token: &SqlToken) -> Result<(), ParseError> {
if token.is_trivia() {
return Ok(());
}
if let Some(word) = token.canonical_word() {
if self.should_split_before_word(token, word) {
self.emit_segment(token.span.start)?;
self.reset_segment_start(token.span.start);
}
self.meaningful = true;
self.observe_word(word);
return Ok(());
}
match token.kind {
SqlTokenKind::LeftParen => self.paren_depth += 1,
SqlTokenKind::RightParen => self.paren_depth = self.paren_depth.saturating_sub(1),
SqlTokenKind::LeftBracket => self.bracket_depth += 1,
SqlTokenKind::RightBracket => self.bracket_depth = self.bracket_depth.saturating_sub(1),
SqlTokenKind::LeftBrace => self.brace_depth += 1,
SqlTokenKind::RightBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
SqlTokenKind::Semicolon if self.delimiter == ";" && self.can_split() => {
self.emit_segment(token.span.start)?;
self.reset_segment_start(token.span.end);
return Ok(());
}
_ => {}
}
self.meaningful = true;
Ok(())
}
fn should_split_before_word(&self, token: &SqlToken, word: &str) -> bool {
self.meaningful
&& self.can_split()
&& self.dialect.starts_statement(word)
&& self.at_line_statement_start(token.span.start)
&& self.allows_new_statement_boundary(word)
}
fn allows_new_statement_boundary(&self, word: &str) -> bool {
if self.first_word.as_deref() == Some("WITH") {
return false;
}
if word == "REPLACE" && self.recent_words.ends_with(&["CREATE".into(), "OR".into()]) {
return false;
}
if self.first_word.as_deref() == Some("CREATE")
&& self
.create_kind
.is_some_and(CreateDeterminantKind::protects_statement_body)
{
return false;
}
!(self.first_word.as_deref() == Some("INSERT") && matches!(word, "SELECT" | "WITH"))
}
fn observe_word(&mut self, word: &str) {
if self.first_word.is_none() {
self.first_word = Some(word.to_string());
}
self.observe_create_kind(word);
if ((self.dialect.tracks_sqlite_trigger_body()
&& self
.create_kind
.is_some_and(CreateDeterminantKind::tracks_sqlite_body))
|| (self.dialect.tracks_mysql_body_blocks()
&& self
.create_kind
.is_some_and(CreateDeterminantKind::tracks_mysql_body)))
&& self.paren_depth == 0
{
match word {
"BEGIN" => self.body_depth += 1,
"END" if self.body_depth > 0 => self.body_depth -= 1,
_ => {}
}
}
self.recent_words.push(word.to_string());
if self.recent_words.len() > 6 {
self.recent_words.remove(0);
}
}
fn observe_create_kind(&mut self, word: &str) {
if self.first_word.as_deref() != Some("CREATE") || self.create_kind.is_some() {
return;
}
if let Some(kind) = determinant_for_word(word) {
self.create_kind = Some(kind);
}
}
fn consume_delimiter_directive(&mut self) -> Result<bool, ParseError> {
if !self.dialect.supports_delimiter_directive() {
return Ok(false);
}
let token = self.tokens[self.token_index].clone();
if token.canonical_word() != Some("DELIMITER")
|| !self.at_line_statement_start(token.span.start)
{
return Ok(false);
}
if self.meaningful {
self.emit_segment(token.span.start)?;
}
let line_end = self.sql[token.span.start..]
.find('\n')
.map_or(self.sql.len(), |offset| token.span.start + offset);
let delimiter = self.sql[token.span.end..line_end].trim();
if delimiter.is_empty() {
return Err(ParseError::segment(
self.dialect,
token.line,
token.column,
"DELIMITER directive requires a delimiter",
));
}
self.delimiter = delimiter.to_string();
while self.token_index < self.tokens.len()
&& self.tokens[self.token_index].span.start < line_end
{
self.token_index += 1;
}
let next = if line_end < self.sql.len() {
line_end + '\n'.len_utf8()
} else {
line_end
};
self.reset_segment_start(next);
Ok(true)
}
fn consume_active_delimiter(&mut self) -> Result<bool, ParseError> {
if self.delimiter == ";" || !self.can_split() {
return Ok(false);
}
let start = self.tokens[self.token_index].span.start;
if !self.sql[start..].starts_with(&self.delimiter) {
return Ok(false);
}
self.emit_segment(start)?;
let end = start + self.delimiter.len();
while self.token_index < self.tokens.len() && self.tokens[self.token_index].span.start < end
{
self.token_index += 1;
}
self.reset_segment_start(end);
Ok(true)
}
fn emit_segment(&mut self, end: usize) -> Result<(), ParseError> {
if !self.meaningful {
return Ok(());
}
let Some(end) = trim_trailing_whitespace(self.sql, self.segment_start, end) else {
return Ok(());
};
let start = self.segment_start;
let start_loc = location_at(self.sql, start);
let end_loc = location_at(self.sql, end.saturating_sub(1));
let segment_sql = self.sql[start..end].to_string();
self.segments.push(SqlSegment {
ordinal: self.ordinal,
kind: classify_segment(self.dialect, &segment_sql),
sql: segment_sql,
start_byte: start,
end_byte: end,
start_line: start_loc.line,
start_column: start_loc.column,
end_line: end_loc.line,
end_column: end_loc.column,
});
self.ordinal += 1;
Ok(())
}
fn finish_at_eof(&mut self) -> Result<(), ParseError> {
if self.paren_depth > 0 || self.bracket_depth > 0 || self.brace_depth > 0 {
let location = location_at(self.sql, self.sql.len());
return Err(ParseError::segment(
self.dialect,
location.line,
location.column,
"unbalanced bracket depth at end of SQL",
));
}
if self.body_depth > 0 {
let location = location_at(self.sql, self.sql.len());
return Err(ParseError::segment(
self.dialect,
location.line,
location.column,
"unterminated statement body at end of SQL",
));
}
self.emit_segment(self.sql.len())
}
fn can_split(&self) -> bool {
self.paren_depth == 0
&& self.bracket_depth == 0
&& self.brace_depth == 0
&& self.body_depth == 0
}
fn reset_segment_start(&mut self, start: usize) {
self.segment_start = start;
self.meaningful = false;
self.first_word = None;
self.create_kind = None;
self.recent_words.clear();
self.body_depth = 0;
}
fn at_line_statement_start(&self, byte: usize) -> bool {
let line_start = self.sql[..byte].rfind('\n').map_or(0, |index| index + 1);
self.sql[line_start..byte].chars().all(char::is_whitespace)
}
}
fn location_at(sql: &str, byte: usize) -> Location {
let mut line = 1usize;
let mut column = 1usize;
for (index, ch) in sql.char_indices() {
if index >= byte {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Location::new(line, column)
}
fn trim_trailing_whitespace(sql: &str, start: usize, end: usize) -> Option<usize> {
let mut trimmed_end = end;
while trimmed_end > start {
let ch = sql[start..trimmed_end].chars().next_back()?;
if !ch.is_whitespace() {
break;
}
trimmed_end -= ch.len_utf8();
}
(start < trimmed_end).then_some(trimmed_end)
}