use crate::error::{Result, SqliteError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Generated {
No,
Virtual,
Stored,
}
impl Generated {
pub fn is_generated(self) -> bool {
!matches!(self, Generated::No)
}
pub fn is_virtual(self) -> bool {
matches!(self, Generated::Virtual)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnDef {
pub name: String,
pub declared_type: Option<String>,
pub not_null: bool,
pub default: Option<String>,
pub generated: Generated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyColumn {
pub name: String,
pub descending: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constraint {
Unique { columns: Vec<String> },
Check { expression: String },
ForeignKey {
columns: Vec<String>,
references: String,
},
Collation { column: String, name: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableDef {
pub name: String,
pub columns: Vec<ColumnDef>,
pub primary_key: Vec<KeyColumn>,
pub without_rowid: bool,
pub strict: bool,
pub unsupported_constraints: Vec<Constraint>,
inline_pk_desc: bool,
}
impl TableDef {
pub fn column(&self, name: &str) -> Option<&ColumnDef> {
self.columns
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
}
pub fn column_index(&self, name: &str) -> Option<usize> {
self.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
}
pub fn rowid_alias(&self) -> Option<&ColumnDef> {
if self.without_rowid || self.primary_key.len() != 1 || self.inline_pk_desc {
return None;
}
let key = &self.primary_key[0];
let column = self.column(&key.name)?;
match &column.declared_type {
Some(declared) if declared.trim().eq_ignore_ascii_case("integer") => Some(column),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Token {
Word(String),
Quoted(String),
Literal(String),
Punct(char),
}
#[derive(Debug, Clone)]
struct Tok {
token: Token,
start: usize,
end: usize,
}
impl Token {
fn word(&self) -> Option<&str> {
match self {
Token::Word(w) => Some(w),
_ => None,
}
}
fn is_word(&self, keyword: &str) -> bool {
matches!(self, Token::Word(w) if w.eq_ignore_ascii_case(keyword))
}
fn is_punct(&self, c: char) -> bool {
matches!(self, Token::Punct(p) if *p == c)
}
fn identifier(&self) -> Option<String> {
match self {
Token::Word(w) => Some(w.clone()),
Token::Quoted(q) => Some(q.clone()),
_ => None,
}
}
fn text(&self) -> String {
match self {
Token::Word(w) => w.clone(),
Token::Quoted(q) => format!("\"{}\"", q.replace('"', "\"\"")),
Token::Literal(l) => l.clone(),
Token::Punct(p) => p.to_string(),
}
}
}
fn tokenize(sql: &str) -> Result<Vec<Tok>> {
let chars: Vec<char> = sql.chars().collect();
let mut offsets: Vec<usize> = sql.char_indices().map(|(i, _)| i).collect();
offsets.push(sql.len());
let mut tokens: Vec<Tok> = Vec::new();
let mut at = 0usize;
while at < chars.len() {
let c = chars[at];
if c.is_whitespace() {
at += 1;
continue;
}
let began = at;
if c == '-' && chars.get(at + 1) == Some(&'-') {
while at < chars.len() && chars[at] != '\n' {
at += 1;
}
continue;
}
if c == '/' && chars.get(at + 1) == Some(&'*') {
at += 2;
while at < chars.len() && !(chars[at] == '*' && chars.get(at + 1) == Some(&'/')) {
at += 1;
}
at = (at + 2).min(chars.len());
continue;
}
if let Some((open, close)) = match c {
'"' => Some(('"', '"')),
'`' => Some(('`', '`')),
'[' => Some(('[', ']')),
_ => None,
} {
at += 1;
let mut value = String::new();
loop {
let Some(&ch) = chars.get(at) else {
return Err(SqliteError::unsupported(format!(
"a create statement has an unterminated {open} quoted name"
)));
};
at += 1;
if ch == close {
if close != ']' && chars.get(at) == Some(&close) {
value.push(close);
at += 1;
continue;
}
break;
}
value.push(ch);
}
tokens.push(Tok {
token: Token::Quoted(value),
start: offsets[began],
end: offsets[at],
});
continue;
}
if c == '\'' {
let mut text = String::from("'");
at += 1;
loop {
let Some(&ch) = chars.get(at) else {
return Err(SqliteError::unsupported(
"a create statement has an unterminated string",
));
};
at += 1;
text.push(ch);
if ch == '\'' {
if chars.get(at) == Some(&'\'') {
text.push('\'');
at += 1;
continue;
}
break;
}
}
tokens.push(Tok {
token: Token::Literal(text),
start: offsets[began],
end: offsets[at],
});
continue;
}
if c.is_ascii_digit() || (c == '.' && chars.get(at + 1).is_some_and(|d| d.is_ascii_digit()))
{
let start = at;
while at < chars.len()
&& (chars[at].is_ascii_alphanumeric() || chars[at] == '.' || {
(chars[at] == '+' || chars[at] == '-')
&& matches!(chars[at - 1], 'e' | 'E')
&& chars[start..at].iter().all(|c| *c != 'x' && *c != 'X')
})
{
at += 1;
}
tokens.push(Tok {
token: Token::Literal(chars[start..at].iter().collect()),
start: offsets[began],
end: offsets[at],
});
continue;
}
if c.is_alphabetic() || c == '_' || !c.is_ascii() {
let start = at;
while at < chars.len()
&& (chars[at].is_alphanumeric()
|| chars[at] == '_'
|| chars[at] == '$'
|| !chars[at].is_ascii())
{
at += 1;
}
let word: String = chars[start..at].iter().collect();
if (word.eq_ignore_ascii_case("x")) && chars.get(at) == Some(&'\'') {
let mut text = word;
text.push('\'');
at += 1;
while let Some(&ch) = chars.get(at) {
at += 1;
text.push(ch);
if ch == '\'' {
break;
}
}
tokens.push(Tok {
token: Token::Literal(text),
start: offsets[began],
end: offsets[at],
});
} else {
tokens.push(Tok {
token: Token::Word(word),
start: offsets[began],
end: offsets[at],
});
}
continue;
}
tokens.push(Tok {
token: Token::Punct(c),
start: offsets[began],
end: offsets[at + 1],
});
at += 1;
}
Ok(tokens)
}
const CONSTRAINT_WORDS: &[&str] = &[
"constraint",
"primary",
"not",
"null",
"unique",
"check",
"default",
"collate",
"references",
"generated",
"as",
];
struct Parser<'a> {
sql: &'a str,
tokens: Vec<Tok>,
at: usize,
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.at).map(|t| &t.token)
}
fn at_word(&self, offset: usize, keyword: &str) -> bool {
self.tokens
.get(self.at + offset)
.is_some_and(|t| t.token.is_word(keyword))
}
fn start_of(&self, index: usize) -> usize {
match self.tokens.get(index) {
Some(t) => t.start,
None => self.sql.len(),
}
}
fn end_before(&self, index: usize) -> usize {
match index.checked_sub(1).and_then(|i| self.tokens.get(i)) {
Some(t) => t.end,
None => 0,
}
}
fn text_since(&self, from: usize) -> String {
let start = self.start_of(from);
let end = self.end_before(self.at);
if end <= start {
String::new()
} else {
self.sql[start..end].trim().to_string()
}
}
fn next(&mut self) -> Option<Token> {
let token = self.tokens.get(self.at).map(|t| t.token.clone());
if token.is_some() {
self.at += 1;
}
token
}
fn eat_word(&mut self, keyword: &str) -> bool {
if self.peek().is_some_and(|t| t.is_word(keyword)) {
self.at += 1;
true
} else {
false
}
}
fn eat_punct(&mut self, c: char) -> bool {
if self.peek().is_some_and(|t| t.is_punct(c)) {
self.at += 1;
true
} else {
false
}
}
fn expect_word(&mut self, keyword: &str) -> Result<()> {
if self.eat_word(keyword) {
Ok(())
} else {
Err(self.gap(&format!("expected {keyword}")))
}
}
fn expect_punct(&mut self, c: char) -> Result<()> {
if self.eat_punct(c) {
Ok(())
} else {
Err(self.gap(&format!("expected {c}")))
}
}
fn identifier(&mut self) -> Result<String> {
match self.next().and_then(|t| t.identifier()) {
Some(name) => Ok(name),
None => Err(self.gap("expected a name")),
}
}
fn gap(&self, what: &str) -> SqliteError {
let found = match self.tokens.get(self.at) {
Some(t) => format!("{:?}", t.token.text()),
None => "the end of the statement".to_string(),
};
SqliteError::unsupported(format!(
"cannot read this create statement: {what}, found {found} at token {}",
self.at
))
}
fn at_constraint_word(&self) -> bool {
self.peek()
.and_then(|t| t.word())
.is_some_and(|w| CONSTRAINT_WORDS.iter().any(|k| w.eq_ignore_ascii_case(k)))
}
fn balanced(&mut self) -> Result<String> {
let from = self.at;
self.expect_punct('(')?;
let mut depth = 1usize;
while depth > 0 {
let Some(token) = self.next() else {
return Err(self.gap("expected a closing paren"));
};
if token.is_punct('(') {
depth += 1;
} else if token.is_punct(')') {
depth -= 1;
}
}
Ok(self.text_since(from))
}
fn column_list(&mut self) -> Result<Vec<String>> {
self.expect_punct('(')?;
let mut names = Vec::new();
loop {
names.push(self.identifier()?);
if self.eat_word("collate") {
self.identifier()?;
}
let _ = self.eat_word("asc") || self.eat_word("desc");
if self.eat_punct(',') {
continue;
}
self.expect_punct(')')?;
return Ok(names);
}
}
fn skip_to_end_of_item(&mut self) -> Result<()> {
loop {
match self.peek() {
None => return Ok(()),
Some(t) if t.is_punct(',') || t.is_punct(')') => return Ok(()),
Some(t) if t.is_punct('(') => {
self.balanced()?;
}
_ => {
self.at += 1;
}
}
}
}
fn eat_generated_kind(&mut self) -> Generated {
if self.eat_word("stored") {
Generated::Stored
} else {
self.eat_word("virtual");
Generated::Virtual
}
}
fn eat_conflict_clause(&mut self) {
if self.at_word(0, "on") && self.at_word(1, "conflict") {
self.at += 2;
if self.peek().and_then(|t| t.word()).is_some() {
self.at += 1;
}
}
}
}
pub fn parse_create_table(sql: &str) -> Result<TableDef> {
let tokens = tokenize(sql)?;
let mut parser = Parser { sql, tokens, at: 0 };
parser.expect_word("create")?;
let _ = parser.eat_word("temp") || parser.eat_word("temporary");
parser.expect_word("table")?;
if parser.eat_word("if") {
parser.expect_word("not")?;
parser.expect_word("exists")?;
}
let mut name = parser.identifier()?;
if parser.eat_punct('.') {
name = parser.identifier()?;
}
if parser.peek().is_some_and(|t| t.is_word("as")) {
return Err(SqliteError::unsupported(format!(
"{name} was created by `create table ... as select`, so the file does not record \
its column types; this reader cannot import it"
)));
}
parser.expect_punct('(')?;
let mut columns: Vec<ColumnDef> = Vec::new();
let mut primary_key: Vec<KeyColumn> = Vec::new();
let mut constraints: Vec<Constraint> = Vec::new();
let mut inline_pk_desc = false;
loop {
if parser.peek().is_none() {
return Err(parser.gap("the column list is not closed"));
}
if parser.eat_punct(')') {
break;
}
let is_table_constraint = {
let offset = if parser.at_word(0, "constraint") {
2
} else {
0
};
parser.at_word(offset, "primary")
|| parser.at_word(offset, "unique")
|| parser.at_word(offset, "check")
|| parser.at_word(offset, "foreign")
};
if is_table_constraint {
if parser.eat_word("constraint") {
parser.identifier()?;
}
if parser.eat_word("primary") {
parser.expect_word("key")?;
if !primary_key.is_empty() {
return Err(SqliteError::unsupported(format!(
"{name} declares more than one primary key"
)));
}
parser.expect_punct('(')?;
loop {
let column = parser.identifier()?;
if parser.eat_word("collate") {
parser.identifier()?;
}
let descending = if parser.eat_word("desc") {
true
} else {
parser.eat_word("asc");
false
};
primary_key.push(KeyColumn {
name: column,
descending,
});
if parser.eat_punct(',') {
continue;
}
parser.expect_punct(')')?;
break;
}
parser.eat_conflict_clause();
} else if parser.eat_word("unique") {
let columns = parser.column_list()?;
constraints.push(Constraint::Unique { columns });
parser.eat_conflict_clause();
parser.skip_to_end_of_item()?;
} else if parser.eat_word("check") {
let expression = parser.balanced()?;
constraints.push(Constraint::Check { expression });
parser.skip_to_end_of_item()?;
} else if parser.eat_word("foreign") {
parser.expect_word("key")?;
let columns = parser.column_list()?;
let references = if parser.eat_word("references") {
parser.identifier()?
} else {
String::new()
};
constraints.push(Constraint::ForeignKey {
columns,
references,
});
parser.skip_to_end_of_item()?;
} else {
parser.skip_to_end_of_item()?;
}
} else {
let column_name = parser.identifier()?;
let type_from = parser.at;
while !parser.at_constraint_word() {
match parser.peek() {
None => break,
Some(t) if t.is_punct(',') || t.is_punct(')') => break,
Some(t) if t.is_punct('(') => {
parser.balanced()?;
}
Some(_) => {
parser.at += 1;
}
}
}
let declared_type = parser.text_since(type_from);
let mut column = ColumnDef {
name: column_name,
declared_type: if declared_type.is_empty() {
None
} else {
Some(declared_type)
},
not_null: false,
default: None,
generated: Generated::No,
};
loop {
if parser.eat_word("constraint") {
parser.identifier()?;
continue;
}
if parser.eat_word("primary") {
parser.expect_word("key")?;
if !primary_key.is_empty() {
return Err(SqliteError::unsupported(format!(
"{name} declares more than one primary key"
)));
}
let descending = if parser.eat_word("desc") {
true
} else {
parser.eat_word("asc");
false
};
inline_pk_desc = descending;
primary_key.push(KeyColumn {
name: column.name.clone(),
descending,
});
parser.eat_conflict_clause();
parser.eat_word("autoincrement");
continue;
}
if parser.eat_word("not") {
parser.expect_word("null")?;
column.not_null = true;
parser.eat_conflict_clause();
continue;
}
if parser.eat_word("null") {
parser.eat_conflict_clause();
continue;
}
if parser.eat_word("unique") {
constraints.push(Constraint::Unique {
columns: vec![column.name.clone()],
});
parser.eat_conflict_clause();
continue;
}
if parser.eat_word("check") {
let expression = parser.balanced()?;
constraints.push(Constraint::Check { expression });
continue;
}
if parser.eat_word("default") {
let from = parser.at;
if parser.peek().is_some_and(|t| t.is_punct('(')) {
parser.balanced()?;
} else {
let _ = parser.eat_punct('-') || parser.eat_punct('+');
if parser.next().is_none() {
return Err(parser.gap("expected a default value"));
}
}
column.default = Some(parser.text_since(from));
continue;
}
if parser.eat_word("collate") {
let name = parser.identifier()?;
if !name.eq_ignore_ascii_case("binary") {
constraints.push(Constraint::Collation {
column: column.name.clone(),
name,
});
}
continue;
}
if parser.eat_word("references") {
let references = parser.identifier().unwrap_or_default();
constraints.push(Constraint::ForeignKey {
columns: vec![column.name.clone()],
references,
});
parser.skip_to_end_of_item()?;
continue;
}
if parser.eat_word("generated") {
parser.expect_word("always")?;
parser.expect_word("as")?;
parser.balanced()?;
column.generated = parser.eat_generated_kind();
continue;
}
if parser.peek().is_some_and(|t| t.is_word("as")) {
parser.at += 1;
parser.balanced()?;
column.generated = parser.eat_generated_kind();
continue;
}
break;
}
columns.push(column);
}
if parser.eat_punct(',') {
continue;
}
parser.expect_punct(')')?;
break;
}
let mut without_rowid = false;
let mut strict = false;
loop {
if parser.eat_word("without") {
parser.expect_word("rowid")?;
without_rowid = true;
} else if parser.eat_word("strict") {
strict = true;
} else {
break;
}
if !parser.eat_punct(',') {
break;
}
}
let _ = parser.eat_punct(';');
if columns.is_empty() {
return Err(SqliteError::unsupported(format!(
"{name} was parsed as having no columns"
)));
}
for key in &primary_key {
if !columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(&key.name))
{
return Err(SqliteError::unsupported(format!(
"{name} has a primary key over {}, which is not one of its columns",
key.name
)));
}
}
Ok(TableDef {
name,
columns,
primary_key,
without_rowid,
strict,
unsupported_constraints: constraints,
inline_pk_desc,
})
}