use crate::ast::{self, ObjectKind, TransactionBehaviour};
use crate::bind::{no_such_table, refused, schema_refused, unsupported, Binder, BoundExpr};
use crate::catalog_view::CatalogView;
use crate::catalog_view::TableKind;
use crate::diagnostic::ParseError;
use crate::lexer::Span;
use inillucent_value::Collation;
fn expression_children(ast: &crate::ast::Ast, expr: ast::ExprId) -> Vec<ast::ExprId> {
let mut out = Vec::new();
let Some(node) = ast.expr(expr) else {
return out;
};
match node {
ast::Expr::Unary { operand, .. } => out.push(*operand),
ast::Expr::Binary { left, right, .. } => {
out.push(*left);
out.push(*right);
}
ast::Expr::Collate { operand, .. } | ast::Expr::Cast { operand, .. } => out.push(*operand),
ast::Expr::IsNull { operand, .. } => out.push(*operand),
ast::Expr::Is { left, right, .. } => {
out.push(*left);
out.push(*right);
}
ast::Expr::Between {
operand, low, high, ..
} => {
out.push(*operand);
out.push(*low);
out.push(*high);
}
ast::Expr::In { operand, rhs, .. } => {
out.push(*operand);
if let ast::InRhs::List(items) = rhs {
out.extend(items.iter().copied());
}
}
ast::Expr::Case {
operand,
branches,
otherwise,
} => {
if let Some(operand) = operand {
out.push(*operand);
}
for (when, then) in branches {
out.push(*when);
out.push(*then);
}
if let Some(otherwise) = otherwise {
out.push(*otherwise);
}
}
ast::Expr::Pattern {
operand,
pattern,
escape,
..
} => {
out.push(*operand);
out.push(*pattern);
if let Some(escape) = escape {
out.push(*escape);
}
}
ast::Expr::Function {
arguments: Some(arguments),
..
} => out.extend(arguments.iter().copied()),
_ => {}
}
out
}
fn mentions_name(sql: &[u8], folded: &[u8]) -> bool {
let mut lexer = crate::lexer::Lexer::at(sql, 0);
loop {
let Ok(token) = lexer.next_token() else {
return false;
};
match token.kind {
crate::lexer::TokenKind::EndOfInput => return false,
crate::lexer::TokenKind::Identifier { keyword: None, .. }
if token.span.slice(sql).to_ascii_lowercase() == folded =>
{
return true;
}
_ => {}
}
}
}
fn no_such_collation_sequence(name: &[u8], span: Span) -> ParseError {
ParseError::new(
crate::diagnostic::ParseErrorKind::Unexpected {
found: format!(
"unable to identify the object to be reindexed: {}",
String::from_utf8_lossy(name)
),
expected: Vec::new(),
},
span,
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BeginKind {
Deferred,
Immediate,
Exclusive,
}
impl BeginKind {
pub fn of(behaviour: Option<TransactionBehaviour>) -> BeginKind {
match behaviour {
None | Some(TransactionBehaviour::Deferred) => BeginKind::Deferred,
Some(TransactionBehaviour::Immediate) => BeginKind::Immediate,
Some(TransactionBehaviour::Exclusive) => BeginKind::Exclusive,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AddedColumnRisk {
pub null_without_default: bool,
pub non_constant_default: bool,
pub generated_stored: bool,
}
impl AddedColumnRisk {
pub fn refusal(&self) -> Option<&'static str> {
if self.null_without_default {
return Some("Cannot add a NOT NULL column with default value NULL");
}
if self.non_constant_default {
return Some("Cannot add a column with non-constant default");
}
if self.generated_stored {
return Some("cannot add a STORED column");
}
None
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AlterKind {
RenameTable {
to: Vec<u8>,
},
RenameColumn {
from: Vec<u8>,
to: Vec<u8>,
},
AddColumn {
start: u32,
end: u32,
risk: AddedColumnRisk,
},
DropColumn {
name: Vec<u8>,
position: u16,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexKeyColumn {
pub column: Option<u16>,
pub expr_sql: Option<Vec<u8>>,
pub collation: Vec<u8>,
pub descending: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Directive {
Begin(BeginKind),
Commit,
Rollback {
savepoint: Option<Vec<u8>>,
},
Savepoint(Vec<u8>),
Release(Vec<u8>),
CreateTable {
if_not_exists: bool,
database: usize,
name: Vec<u8>,
name_offset: u32,
exists: bool,
},
CreateTableAsSelect {
if_not_exists: bool,
database: usize,
name: Vec<u8>,
exists: bool,
create_sql: Vec<u8>,
select_sql: Vec<u8>,
},
CreateVirtualTable {
if_not_exists: bool,
database: usize,
name: Vec<u8>,
module: Vec<u8>,
arguments: Vec<Vec<u8>>,
name_offset: u32,
exists: bool,
},
Alter {
database: usize,
table: Vec<u8>,
action: AlterKind,
},
Reindex {
database: usize,
indexes: Vec<Vec<u8>>,
},
Vacuum {
database: usize,
into: Option<Vec<u8>>,
},
Attach {
file: Vec<u8>,
schema: Vec<u8>,
},
Detach {
schema: Vec<u8>,
},
Analyze {
database: usize,
table: Option<Vec<u8>>,
},
CreateView {
if_not_exists: bool,
database: usize,
name: Vec<u8>,
name_offset: u32,
exists: bool,
},
CreateTrigger {
database: usize,
name: Vec<u8>,
name_offset: u32,
table: Vec<u8>,
exists: bool,
},
CreateIndex {
unique: bool,
if_not_exists: bool,
database: usize,
name: Vec<u8>,
name_offset: u32,
table: Vec<u8>,
table_root: u32,
using: Option<Vec<u8>>,
columns: Vec<IndexKeyColumn>,
settings: Vec<(Vec<u8>, Vec<u8>)>,
exists: bool,
},
Drop {
kind: ObjectKind,
if_exists: bool,
database: usize,
name: Vec<u8>,
root: u32,
index_roots: Vec<u32>,
exists: bool,
},
Pragma {
database: Option<usize>,
name: Vec<u8>,
argument: Option<PragmaArgument>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum PragmaArgument {
Name(Vec<u8>),
Value(BoundExpr),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Uniqueness {
Unique,
Duplicates,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IfNotExists {
Skip,
Refuse,
}
pub struct CreateIndexSpec<'a> {
pub unique: Uniqueness,
pub if_not_exists: IfNotExists,
pub database: Option<ast::NameId>,
pub name: ast::NameId,
pub table: ast::NameId,
pub using: Option<ast::NameId>,
pub columns: &'a [ast::IndexedColumn],
pub settings: &'a [Vec<u8>],
pub filter: Option<ast::ExprId>,
}
pub(crate) struct CreateTriggerParts<'p> {
pub temporary: bool,
pub if_not_exists: bool,
pub database: Option<ast::NameId>,
pub name: ast::NameId,
pub time: Option<ast::TriggerTime>,
pub table: ast::NameId,
pub for_each_row: bool,
pub when: Option<ast::ExprId>,
pub body: &'p [ast::Statement],
}
impl<'a> Binder<'a> {
pub fn bind_directive(&mut self, statement: &ast::Statement) -> Result<Directive, ParseError> {
match statement {
ast::Statement::Begin { behaviour } => Ok(Directive::Begin(BeginKind::of(*behaviour))),
ast::Statement::Commit => Ok(Directive::Commit),
ast::Statement::Rollback { savepoint } => Ok(Directive::Rollback {
savepoint: savepoint.map(|id| self.ast.text(id).to_vec()),
}),
ast::Statement::Savepoint(name) => {
Ok(Directive::Savepoint(self.ast.text(*name).to_vec()))
}
ast::Statement::Release(name) => Ok(Directive::Release(self.ast.text(*name).to_vec())),
ast::Statement::CreateTable {
temporary,
if_not_exists,
database,
name,
body,
} => self.bind_create_table(*temporary, *if_not_exists, *database, *name, body),
ast::Statement::CreateVirtualTable {
if_not_exists,
database,
name,
module,
arguments,
} => {
self.bind_create_virtual_table(*if_not_exists, *database, *name, *module, arguments)
}
ast::Statement::CreateIndex {
unique,
if_not_exists,
database,
name,
table,
using,
columns,
settings,
filter,
} => self.bind_create_index(&CreateIndexSpec {
unique: if *unique {
Uniqueness::Unique
} else {
Uniqueness::Duplicates
},
if_not_exists: if *if_not_exists {
IfNotExists::Skip
} else {
IfNotExists::Refuse
},
database: *database,
name: *name,
table: *table,
using: *using,
columns,
settings,
filter: *filter,
}),
ast::Statement::Analyze { database, name } => self.bind_analyze(*database, *name),
ast::Statement::AlterTable {
database,
table,
action,
} => self.bind_alter(*database, *table, action),
ast::Statement::Reindex { database, name } => self.bind_reindex(*database, *name),
ast::Statement::Vacuum { database, into } => self.bind_vacuum(*database, *into),
ast::Statement::Attach { file, schema, key } => self.bind_attach(*file, *schema, *key),
ast::Statement::Detach { schema } => self.bind_detach(*schema),
ast::Statement::CreateView {
temporary,
if_not_exists,
database,
name,
columns,
select,
} => self.bind_create_view(
*temporary,
*if_not_exists,
*database,
*name,
columns,
*select,
),
ast::Statement::CreateTrigger {
temporary,
if_not_exists,
database,
name,
time,
event: _,
table,
for_each_row,
when,
body,
} => self.bind_create_trigger(CreateTriggerParts {
temporary: *temporary,
if_not_exists: *if_not_exists,
database: *database,
name: *name,
time: *time,
table: *table,
for_each_row: *for_each_row,
when: *when,
body,
}),
ast::Statement::Drop {
kind,
if_exists,
database,
name,
} => self.bind_drop(*kind, *if_exists, *database, *name),
ast::Statement::Pragma {
database,
name,
value,
} => self.bind_pragma(*database, *name, value),
_ => Err(unsupported(
"this statement is not implemented yet",
Span::default(),
)),
}
}
fn bind_create_virtual_table(
&mut self,
if_not_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
module: ast::NameId,
arguments: &[Vec<u8>],
) -> Result<Directive, ParseError> {
let index = self.resolve_database(database)?;
let written = self.ast.text(name).to_vec();
if written.to_ascii_lowercase().starts_with(b"sqlite_") {
return Err(refused(
format!(
"object name reserved for internal use: {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let folded = self.ast.folded(name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
let exists = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !if_not_exists {
return Err(refused(
format!("table {} already exists", String::from_utf8_lossy(&written)),
Span::default(),
));
}
Ok(Directive::CreateVirtualTable {
if_not_exists,
database: index,
name: written,
module: self.ast.text(module).to_vec(),
arguments: arguments.to_vec(),
name_offset: self
.ast
.name(name)
.map(|entry| entry.span.start)
.unwrap_or_default(),
exists,
})
}
fn bind_create_table(
&mut self,
temporary: bool,
if_not_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
body: &ast::CreateTableBody,
) -> Result<Directive, ParseError> {
let temp = self.temporary_database(temporary, database)?;
let (columns, constraints, without_rowid, strict) = match body {
ast::CreateTableBody::AsSelect(select) => {
return self.bind_create_table_as_select(
temp,
if_not_exists,
database,
name,
*select,
)
}
ast::CreateTableBody::Columns {
columns,
constraints,
without_rowid,
strict,
} => (columns, constraints, without_rowid, strict),
};
if *without_rowid && !self.declares_primary_key(columns, constraints) {
return Err(schema_refused(
format!(
"PRIMARY KEY missing on table {}",
String::from_utf8_lossy(self.ast.text(name))
),
Span::default(),
));
}
self.check_autoincrement(columns, *without_rowid)?;
if *strict {
self.check_strict(columns)?;
}
self.check_generated(columns)?;
if columns.is_empty() {
return Err(refused(
"a table must have at least one column",
Span::default(),
));
}
let index = match temp {
Some(index) => index,
None => self.resolve_database(database)?,
};
let written = self.ast.text(name).to_vec();
if written.to_ascii_lowercase().starts_with(b"sqlite_") {
return Err(refused(
format!(
"object name reserved for internal use: {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let folded = self.ast.folded(name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
let exists = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !if_not_exists {
return Err(refused(
format!("table {} already exists", String::from_utf8_lossy(&written)),
Span::default(),
));
}
self.record_write_dependency(index);
Ok(Directive::CreateTable {
if_not_exists,
database: index,
name: written,
name_offset: self.name_offset(name),
exists,
})
}
fn bind_create_table_as_select(
&mut self,
temp: Option<usize>,
if_not_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
select: ast::SelectId,
) -> Result<Directive, ParseError> {
let index = match temp {
Some(index) => index,
None => self.resolve_database(database)?,
};
let written = self.ast.text(name).to_vec();
if written.to_ascii_lowercase().starts_with(b"sqlite_") {
return Err(refused(
format!(
"object name reserved for internal use: {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let folded = self.ast.folded(name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
let exists = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !if_not_exists {
return Err(refused(
format!("table {} already exists", String::from_utf8_lossy(&written)),
Span::default(),
));
}
let span = self
.ast
.select(select)
.map(|held| held.span)
.ok_or_else(|| refused("the query could not be read", Span::default()))?;
let select_sql = self
.source
.get(span.start as usize..span.end as usize)
.ok_or_else(|| refused("the query could not be read", span))?
.to_vec();
let bound = self.bind_select(select)?;
if bound.columns.is_empty() {
return Err(refused(
"a table must have at least one column",
Span::default(),
));
}
let mut width = identifier_width(&written);
for column in &bound.columns {
width = width
.saturating_add(identifier_width(&column.name))
.saturating_add(5);
}
let (open, between, close): (&[u8], &[u8], &[u8]) = if width < 50 {
(b"", b",", b")")
} else {
(b"\n ", b",\n ", b"\n)")
};
let mut create_sql = Vec::new();
create_sql.extend_from_slice(b"CREATE TABLE ");
create_sql.extend_from_slice(&written);
create_sql.push(b'(');
let mut seen: Vec<Vec<u8>> = Vec::with_capacity(bound.columns.len());
for (position, column) in bound.columns.iter().enumerate() {
create_sql.extend_from_slice(if position > 0 { between } else { open });
let folded = column.name.to_ascii_lowercase();
if seen.contains(&folded) {
return Err(refused(
format!(
"duplicate column name: {}",
String::from_utf8_lossy(&column.name)
),
Span::default(),
));
}
seen.push(folded);
create_sql.extend_from_slice("ed_name(&column.name));
create_sql.extend_from_slice(affinity_type(&column.declared_type));
}
create_sql.extend_from_slice(close);
self.record_write_dependency(index);
Ok(Directive::CreateTableAsSelect {
if_not_exists,
database: index,
name: written,
exists,
create_sql,
select_sql,
})
}
fn declares_primary_key(
&self,
columns: &[ast::ColumnDef],
constraints: &[(Option<ast::NameId>, ast::TableConstraint)],
) -> bool {
let on_column = columns.iter().any(|column| {
column.constraints.iter().any(|(_, constraint)| {
matches!(constraint, ast::ColumnConstraint::PrimaryKey { .. })
})
});
on_column
|| constraints.iter().any(|(_, constraint)| {
matches!(constraint, ast::TableConstraint::PrimaryKey { .. })
})
}
fn check_generated(&self, columns: &[ast::ColumnDef]) -> Result<(), ParseError> {
let names: Vec<Vec<u8>> = columns
.iter()
.map(|column| self.ast.folded(column.name).to_vec())
.collect();
let mut generated: Vec<(usize, Vec<usize>)> = Vec::new();
for (position, column) in columns.iter().enumerate() {
let mut expr = None;
let mut has_default = false;
let mut in_primary_key = false;
for (_, constraint) in &column.constraints {
match constraint {
ast::ColumnConstraint::Generated { expr: body, .. } => expr = Some(*body),
ast::ColumnConstraint::Default(_) => has_default = true,
ast::ColumnConstraint::PrimaryKey { .. } => in_primary_key = true,
_ => {}
}
}
let Some(expr) = expr else {
continue;
};
let written = String::from_utf8_lossy(self.ast.text(column.name)).into_owned();
if has_default {
return Err(refused(
format!("cannot use DEFAULT on a generated column: {written}"),
Span::default(),
));
}
if in_primary_key {
return Err(refused(
format!("generated columns cannot be part of the PRIMARY KEY: {written}"),
Span::default(),
));
}
let mut reads = Vec::new();
self.expression_names(expr, &mut reads);
let mut resolved = Vec::new();
for name in &reads {
let Some(found) = names.iter().position(|candidate| candidate == name) else {
return Err(crate::bind::no_such_column(name, Span::default()));
};
resolved.push(found);
}
generated.push((position, resolved));
}
let mut settled: Vec<usize> = (0..columns.len())
.filter(|position| !generated.iter().any(|(owner, _)| owner == position))
.collect();
let mut pending = generated;
loop {
let before = pending.len();
let mut still = Vec::new();
for (position, reads) in pending {
if reads.iter().all(|read| settled.contains(read)) {
settled.push(position);
} else {
still.push((position, reads));
}
}
pending = still;
if pending.is_empty() || pending.len() == before {
break;
}
}
if let Some((position, _)) = pending.first() {
let written = columns
.get(*position)
.map(|column| String::from_utf8_lossy(self.ast.text(column.name)).into_owned())
.unwrap_or_default();
return Err(refused(
format!("generated column loop on {written}"),
Span::default(),
));
}
Ok(())
}
fn expression_names(&self, expr: ast::ExprId, into: &mut Vec<Vec<u8>>) {
let Some(node) = self.ast.expr(expr) else {
return;
};
if let ast::Expr::Column { column, .. } = node {
let name = self.ast.folded(*column).to_vec();
if !into.contains(&name) {
into.push(name);
}
}
for child in expression_children(self.ast, expr) {
self.expression_names(child, into);
}
}
fn check_strict(&self, columns: &[ast::ColumnDef]) -> Result<(), ParseError> {
for column in columns {
let Some(declared) = column.declared_type.as_ref() else {
return Err(refused(
format!(
"missing datatype for {}",
String::from_utf8_lossy(self.ast.text(column.name))
),
Span::default(),
));
};
let folded = declared.to_ascii_uppercase();
let allowed = matches!(
folded.as_slice(),
b"INT" | b"INTEGER" | b"REAL" | b"TEXT" | b"BLOB" | b"ANY"
);
if !allowed {
return Err(refused(
format!(
"unknown datatype for {}: \"{}\"",
String::from_utf8_lossy(self.ast.text(column.name)),
String::from_utf8_lossy(declared)
),
Span::default(),
));
}
}
Ok(())
}
fn bind_analyze(
&mut self,
database: Option<ast::NameId>,
name: Option<ast::NameId>,
) -> Result<Directive, ParseError> {
let index = self.resolve_database(database)?;
self.record_write_dependency(index);
let Some(name) = name else {
return Ok(Directive::Analyze {
database: index,
table: None,
});
};
let folded = self.ast.folded(name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
if self
.catalog
.database_index(&folded)
.is_some_and(|found| found == index)
{
return Ok(Directive::Analyze {
database: index,
table: None,
});
}
if let Some(table) = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
{
return Ok(Directive::Analyze {
database: index,
table: Some(table.name.clone()),
});
}
if let Some((table, _)) = self
.catalog
.find_index(Some(database_name.as_slice()), &folded)
{
return Ok(Directive::Analyze {
database: index,
table: Some(table.name.clone()),
});
}
Err(no_such_table(self.ast.text(name), Span::default()))
}
fn bind_alter(
&mut self,
database: Option<ast::NameId>,
table: ast::NameId,
action: &ast::AlterAction,
) -> Result<Directive, ParseError> {
let written = match database {
Some(_) => Some(
self.catalog
.database_name(self.resolve_database(database)?)
.to_vec(),
),
None => None,
};
let folded = self.ast.folded(table).to_vec();
let Some(target) = self
.catalog
.find_table(written.as_deref(), &folded)
.cloned()
else {
return Err(no_such_table(self.ast.text(table), Span::default()));
};
let index = target.database;
let database_name = self.catalog.database_name(index).to_vec();
if target.kind != crate::catalog_view::TableKind::Table {
return Err(refused(
format!(
"cannot alter {}: not a table",
String::from_utf8_lossy(&target.name)
),
Span::default(),
));
}
if target.folded.starts_with(b"sqlite_") {
return Err(refused(
format!(
"table {} may not be altered",
String::from_utf8_lossy(&target.name)
),
Span::default(),
));
}
self.record_write_dependency(index);
let kind = match action {
ast::AlterAction::RenameTo(name) => {
let to = self.ast.text(*name).to_vec();
let to_folded = self.ast.folded(*name).to_vec();
if self
.catalog
.find_table(Some(database_name.as_slice()), &to_folded)
.is_some()
{
return Err(refused(
format!(
"there is already another table or index with this name: {}",
String::from_utf8_lossy(&to)
),
Span::default(),
));
}
AlterKind::RenameTable { to }
}
ast::AlterAction::RenameColumn { from, to } => {
let from_folded = self.ast.folded(*from).to_vec();
let Some(position) = target.column_position(&from_folded) else {
return Err(crate::bind::no_such_column(
self.ast.text(*from),
Span::default(),
));
};
let to_folded = self.ast.folded(*to).to_vec();
if target.column_position(&to_folded).is_some() {
return Err(refused(
format!(
"duplicate column name: {}",
String::from_utf8_lossy(self.ast.text(*to))
),
Span::default(),
));
}
let stored = target
.column(position)
.map(|column| column.name.clone())
.unwrap_or_default();
AlterKind::RenameColumn {
from: stored,
to: self.ast.text(*to).to_vec(),
}
}
ast::AlterAction::AddColumn(definition) => {
let risk = self.check_added_column(&target, definition)?;
AlterKind::AddColumn {
start: definition.span.start,
end: definition.span.end,
risk,
}
}
ast::AlterAction::DropColumn(name) => {
let folded = self.ast.folded(*name).to_vec();
let Some(position) = target.column_position(&folded) else {
return Err(crate::bind::no_such_column(
self.ast.text(*name),
Span::default(),
));
};
self.check_dropped_column(&target, position)?;
let stored = target
.column(position)
.map(|column| column.name.clone())
.unwrap_or_default();
AlterKind::DropColumn {
name: stored,
position,
}
}
};
Ok(Directive::Alter {
database: index,
table: target.name.clone(),
action: kind,
})
}
fn check_added_column(
&self,
table: &crate::catalog_view::TableInfo,
definition: &ast::ColumnDef,
) -> Result<AddedColumnRisk, ParseError> {
let folded = self.ast.folded(definition.name).to_vec();
if table.column_position(&folded).is_some() {
return Err(refused(
format!(
"duplicate column name: {}",
String::from_utf8_lossy(self.ast.text(definition.name))
),
Span::default(),
));
}
let mut not_null = false;
let mut has_default = false;
let mut constant = true;
let mut generated_stored = false;
for (_, constraint) in &definition.constraints {
match constraint {
ast::ColumnConstraint::PrimaryKey { .. } => {
return Err(schema_refused(
"Cannot add a PRIMARY KEY column",
Span::default(),
))
}
ast::ColumnConstraint::Unique(_) => {
return Err(schema_refused(
"Cannot add a UNIQUE column",
Span::default(),
))
}
ast::ColumnConstraint::NotNull(_) => not_null = true,
ast::ColumnConstraint::Default(expr) => {
has_default = true;
if !self.constant_default(*expr) {
constant = false;
}
}
ast::ColumnConstraint::Generated { stored, .. } if *stored => {
generated_stored = true;
}
_ => {}
}
}
Ok(AddedColumnRisk {
null_without_default: not_null && !has_default,
non_constant_default: !constant,
generated_stored,
})
}
fn constant_default(&self, expr: ast::ExprId) -> bool {
match self.ast.expr(expr) {
Some(ast::Expr::Literal(_)) => true,
Some(ast::Expr::Unary { operand, .. }) => self.constant_default(*operand),
_ => false,
}
}
fn check_dropped_column(
&self,
table: &crate::catalog_view::TableInfo,
position: u16,
) -> Result<(), ParseError> {
let named = table
.column(position)
.map(|column| String::from_utf8_lossy(&column.name).into_owned())
.unwrap_or_default();
if table.columns.len() <= 1 {
return Err(refused(
format!("cannot drop column \"{named}\": no other columns exist"),
Span::default(),
));
}
if table.rowid_alias == Some(position)
|| table
.column(position)
.is_some_and(|column| column.primary_key_position.is_some())
{
return Err(refused(
format!("cannot drop column \"{named}\": PRIMARY KEY"),
Span::default(),
));
}
let indexed = table
.indexes
.iter()
.any(|index| index.columns.iter().any(|key| key.column == Some(position)));
if indexed {
return Err(refused(
format!("cannot drop column \"{named}\": indexed"),
Span::default(),
));
}
let folded = table
.column(position)
.map(|column| column.folded.clone())
.unwrap_or_default();
let referenced = table
.checks
.iter()
.any(|check| mentions_name(&check.expr_sql, &folded))
|| table.columns.iter().enumerate().any(|(other, column)| {
other != usize::from(position)
&& column
.generated_sql
.as_ref()
.is_some_and(|sql| mentions_name(sql, &folded))
});
if referenced {
return Err(refused(
format!(
"error in table {}: cannot drop column \"{named}\"",
String::from_utf8_lossy(&table.name)
),
Span::default(),
));
}
Ok(())
}
fn bind_reindex(
&mut self,
database: Option<ast::NameId>,
name: Option<ast::NameId>,
) -> Result<Directive, ParseError> {
let index = self.resolve_database(database)?;
self.record_write_dependency(index);
let database_name = self.catalog.database_name(index).to_vec();
let everything = |catalog: &dyn CatalogView| -> Vec<Vec<u8>> {
catalog
.tables_of(index)
.into_iter()
.flat_map(|table| table.indexes.iter().map(|entry| entry.name.clone()))
.filter(|name| !name.is_empty())
.collect()
};
let Some(name) = name else {
return Ok(Directive::Reindex {
database: index,
indexes: everything(self.catalog),
});
};
let folded = self.ast.folded(name).to_vec();
if let Some(table) = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
{
return Ok(Directive::Reindex {
database: index,
indexes: table
.indexes
.iter()
.map(|entry| entry.name.clone())
.collect(),
});
}
if let Some((_, entry)) = self
.catalog
.find_index(Some(database_name.as_slice()), &folded)
{
return Ok(Directive::Reindex {
database: index,
indexes: vec![entry.name.clone()],
});
}
if Collation::from_name(core::str::from_utf8(&folded).unwrap_or("")).is_some() {
let wanted = folded.clone();
let indexes = self
.catalog
.tables_of(index)
.into_iter()
.flat_map(|table| table.indexes.iter())
.filter(|entry| {
entry
.columns
.iter()
.any(|key| key.collation.eq_ignore_ascii_case(&wanted))
})
.map(|entry| entry.name.clone())
.collect();
return Ok(Directive::Reindex {
database: index,
indexes,
});
}
Err(no_such_collation_sequence(
self.ast.text(name),
Span::default(),
))
}
fn bind_vacuum(
&mut self,
database: Option<ast::NameId>,
into: Option<ast::ExprId>,
) -> Result<Directive, ParseError> {
let target = match into {
Some(expr) => Some(self.literal_path(expr)?),
None => None,
};
let index = self.resolve_database(database)?;
self.record_write_dependency(index);
Ok(Directive::Vacuum {
database: index,
into: target,
})
}
pub(crate) fn bind_attach(
&mut self,
file: ast::ExprId,
schema: ast::ExprId,
key: Option<ast::ExprId>,
) -> Result<Directive, ParseError> {
if key.is_some() {
return Err(unsupported("ATTACH ... KEY", Span::default()));
}
Ok(Directive::Attach {
file: self.literal_path(file)?,
schema: self.literal_or_name(schema)?,
})
}
pub(crate) fn bind_detach(&mut self, schema: ast::ExprId) -> Result<Directive, ParseError> {
Ok(Directive::Detach {
schema: self.literal_or_name(schema)?,
})
}
fn literal_or_name(&mut self, expr: ast::ExprId) -> Result<Vec<u8>, ParseError> {
match self.ast.expr(expr) {
Some(ast::Expr::Literal(ast::Literal::String(text))) => Ok(text.clone()),
Some(ast::Expr::Column {
table: None,
column,
..
}) => Ok(self.ast.text(*column).to_vec()),
_ => Err(unsupported(
"a schema name that is not a word or a string",
Span::default(),
)),
}
}
fn literal_path(&mut self, expr: ast::ExprId) -> Result<Vec<u8>, ParseError> {
match self.ast.expr(expr) {
Some(ast::Expr::Literal(ast::Literal::String(text))) => Ok(text.clone()),
_ => Err(unsupported(
"VACUUM INTO with a name that is not a literal",
Span::default(),
)),
}
}
fn bind_create_view(
&mut self,
temporary: bool,
if_not_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
columns: &[ast::NameId],
select: ast::SelectId,
) -> Result<Directive, ParseError> {
let temp = self.temporary_database(temporary, database)?;
let index = match temp {
Some(index) => index,
None => self.resolve_database(database)?,
};
let written = self.ast.text(name).to_vec();
if written.to_ascii_lowercase().starts_with(b"sqlite_") {
return Err(refused(
format!(
"object name reserved for internal use: {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let folded = self.ast.folded(name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
let exists = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !if_not_exists {
return Err(refused(
format!("table {} already exists", String::from_utf8_lossy(&written)),
Span::default(),
));
}
if !exists {
let saved = core::mem::take(&mut self.scopes);
let bound = self.bind_select(select);
self.scopes = saved;
let bound = bound?;
if !columns.is_empty() && columns.len() != bound.columns.len() {
return Err(refused(
format!(
"expected {} columns for {} but got {}",
columns.len(),
String::from_utf8_lossy(&written),
bound.columns.len()
),
Span::default(),
));
}
}
self.record_write_dependency(index);
Ok(Directive::CreateView {
if_not_exists,
database: index,
name: written,
name_offset: self.name_offset(name),
exists,
})
}
fn check_autoincrement(
&mut self,
columns: &[ast::ColumnDef],
without_rowid: bool,
) -> Result<(), ParseError> {
for column in columns {
let declared = column.declared_type.clone().unwrap_or_default();
for (_, constraint) in &column.constraints {
let ast::ColumnConstraint::PrimaryKey {
autoincrement: true,
..
} = constraint
else {
continue;
};
if without_rowid {
return Err(refused(
"AUTOINCREMENT not allowed on WITHOUT ROWID tables",
Span::default(),
));
}
if !declared.eq_ignore_ascii_case(b"integer") {
return Err(refused(
"AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY",
Span::default(),
));
}
}
}
Ok(())
}
fn bind_create_trigger(
&mut self,
parts: CreateTriggerParts<'_>,
) -> Result<Directive, ParseError> {
let temp = self.temporary_database(parts.temporary, parts.database)?;
let _ = parts.for_each_row;
let index = match temp {
Some(index) => index,
None => self.resolve_database(parts.database)?,
};
let written = self.ast.text(parts.name).to_vec();
if written.to_ascii_lowercase().starts_with(b"sqlite_") {
return Err(refused(
format!(
"object name reserved for internal use: {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let folded = self.ast.folded(parts.name).to_vec();
let database_name = self.catalog.database_name(index).to_vec();
let table_folded = self.ast.folded(parts.table).to_vec();
let scope = temp.map_or(Some(database_name.as_slice()), |_| None);
let Some(target) = self.catalog.find_table(scope, &table_folded).cloned() else {
return Err(crate::bind::no_such_table(
self.ast.text(parts.table),
Span::default(),
));
};
let exists = self
.catalog
.find_trigger(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !parts.if_not_exists {
return Err(refused(
format!(
"trigger {} already exists",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let instead_of = parts.time == Some(ast::TriggerTime::InsteadOf);
match target.kind {
TableKind::View if !instead_of => {
return Err(refused(
format!(
"cannot create {} trigger on view: {}",
if parts.time == Some(ast::TriggerTime::After) {
"AFTER"
} else {
"BEFORE"
},
String::from_utf8_lossy(&target.name)
),
Span::default(),
));
}
TableKind::Table if instead_of => {
return Err(refused(
format!(
"cannot create INSTEAD OF trigger on table: {}",
String::from_utf8_lossy(&target.name)
),
Span::default(),
));
}
TableKind::Virtual | TableKind::Subquery => {
return Err(unsupported("a trigger on that object", Span::default()));
}
_ => {}
}
let _ = (parts.time, parts.when, parts.body);
self.record_write_dependency(index);
Ok(Directive::CreateTrigger {
database: index,
name: written,
name_offset: self.name_offset(parts.name),
table: target.name.clone(),
exists,
})
}
fn bind_create_index(&mut self, spec: &CreateIndexSpec<'_>) -> Result<Directive, ParseError> {
let CreateIndexSpec {
database,
name,
table,
using,
columns,
settings,
..
} = *spec;
let unique = spec.unique == Uniqueness::Unique;
let if_not_exists = spec.if_not_exists == IfNotExists::Skip;
let using = match using {
None => None,
Some(named) => {
let folded = self.ast.folded(named).to_vec();
if folded != b"inillucent_hnsw" && folded != b"ivfflat" {
return Err(unsupported(
"an index USING a module other than inillucent_hnsw or ivfflat",
Span::default(),
));
}
Some(folded)
}
};
let parsed_settings = index_settings(&using, settings)?;
let index = self.resolve_database(database)?;
let database_name = self.catalog.database_name(index).to_vec();
let table_folded = self.ast.folded(table).to_vec();
let Some(target) = self
.catalog
.find_table(Some(database_name.as_slice()), &table_folded)
.cloned()
else {
return Err(no_such_table(self.ast.text(table), Span::default()));
};
let written = self.ast.text(name).to_vec();
let folded = self.ast.folded(name).to_vec();
let exists = self
.catalog
.find_index(Some(database_name.as_slice()), &folded)
.is_some();
if exists && !if_not_exists {
return Err(refused(
format!("index {} already exists", String::from_utf8_lossy(&written)),
Span::default(),
));
}
let mut keys = Vec::with_capacity(columns.len());
for column in columns {
let (expr, written_collation) = match self.ast.expr(column.expr) {
Some(ast::Expr::Collate { operand, collation }) => {
(self.ast.expr(*operand), Some(*collation))
}
other => (other, column.collation),
};
let named = match expr {
Some(ast::Expr::Column {
table: None,
column: name,
..
}) => Some(*name),
_ => None,
};
let Some(name) = named else {
let collation = match written_collation {
Some(collation) => self.ast.folded(collation).to_vec(),
None => b"binary".to_vec(),
};
keys.push(IndexKeyColumn {
column: None,
expr_sql: Some(self.ast.expr_span(column.expr).slice(self.source).to_vec()),
collation,
descending: column.order == ast::SortOrder::Descending,
});
continue;
};
let folded = self.ast.folded(name).to_vec();
let Some(position) = target.column_position(&folded) else {
return Err(crate::bind::no_such_column(
self.ast.text(name),
Span::default(),
));
};
let collation = match written_collation {
Some(collation) => self.ast.folded(collation).to_vec(),
None => target
.column(position)
.map(|column| column.collation.clone())
.unwrap_or_else(|| b"binary".to_vec()),
};
keys.push(IndexKeyColumn {
column: Some(position),
expr_sql: None,
collation,
descending: column.order == ast::SortOrder::Descending,
});
}
self.record_write_dependency(index);
Ok(Directive::CreateIndex {
unique,
if_not_exists,
database: index,
name: written,
name_offset: self.name_offset(name),
table: target.name.clone(),
table_root: target.root,
using,
columns: keys,
settings: parsed_settings,
exists,
})
}
fn bind_drop(
&mut self,
kind: ObjectKind,
if_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
) -> Result<Directive, ParseError> {
let index = self.resolve_database(database)?;
let database_name = self.catalog.database_name(index).to_vec();
let written = self.ast.text(name).to_vec();
let folded = self.ast.folded(name).to_vec();
self.record_write_dependency(index);
if kind == ObjectKind::Trigger {
let exists = self
.catalog
.find_trigger(Some(database_name.as_slice()), &folded)
.is_some();
if !exists && !if_exists {
return Err(refused(
format!("no such trigger: {}", String::from_utf8_lossy(&written)),
Span::default(),
));
}
return Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: 0,
index_roots: Vec::new(),
exists,
});
}
if kind == ObjectKind::View {
let found = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.cloned();
let exists = found
.as_ref()
.is_some_and(|table| table.kind == crate::catalog_view::TableKind::View);
if !exists && !if_exists {
return Err(refused(
format!("no such view: {}", String::from_utf8_lossy(&written)),
Span::default(),
));
}
return Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: 0,
index_roots: Vec::new(),
exists,
});
}
if kind == ObjectKind::Table {
let found = self
.catalog
.find_table(Some(database_name.as_slice()), &folded)
.cloned();
let Some(table) = found else {
if if_exists {
return Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: 0,
index_roots: Vec::new(),
exists: false,
});
}
return Err(no_such_table(&written, Span::default()));
};
if table.kind == crate::catalog_view::TableKind::View {
return Err(refused(
format!(
"use DROP VIEW to delete view {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
let index_roots = table
.indexes
.iter()
.map(|index| index.root)
.filter(|root| *root != 0 && *root != table.root)
.collect();
return Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: table.root,
index_roots,
exists: true,
});
}
let found = self.find_index_root(index, &folded);
let Some(root) = found else {
if if_exists {
return Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: 0,
index_roots: Vec::new(),
exists: false,
});
}
return Err(refused(
format!("no such index: {}", String::from_utf8_lossy(&written)),
Span::default(),
));
};
Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root,
index_roots: Vec::new(),
exists: true,
})
}
fn bind_pragma(
&mut self,
database: Option<ast::NameId>,
name: ast::NameId,
value: &ast::PragmaValue,
) -> Result<Directive, ParseError> {
let argument = match value {
ast::PragmaValue::None => None,
ast::PragmaValue::Name(name) => {
Some(PragmaArgument::Name(self.ast.text(*name).to_vec()))
}
ast::PragmaValue::Value(expr) => Some(PragmaArgument::Value(self.bind_expr(*expr)?)),
};
let database = match database {
Some(id) => Some(self.resolve_database(Some(id))?),
None => None,
};
Ok(Directive::Pragma {
database,
name: self.ast.folded(name).to_vec(),
argument,
})
}
fn temporary_database(
&self,
temporary: bool,
database: Option<ast::NameId>,
) -> Result<Option<usize>, ParseError> {
if !temporary {
return Ok(None);
}
if database.is_some() {
return Err(refused(
"temporary table name must be unqualified",
Span::default(),
));
}
self.catalog
.database_index(b"temp")
.map(Some)
.ok_or_else(|| refused("no temporary database", Span::default()))
}
fn resolve_database(&self, database: Option<ast::NameId>) -> Result<usize, ParseError> {
let Some(id) = database else {
return Ok(0);
};
let folded = self.ast.folded(id);
self.catalog.database_index(folded).ok_or_else(|| {
refused(
format!(
"unknown database {}",
String::from_utf8_lossy(self.ast.text(id))
),
Span::default(),
)
})
}
fn name_offset(&self, name: ast::NameId) -> u32 {
self.ast.name(name).map_or(0, |name| name.span.start)
}
fn find_index_root(&self, database: usize, folded: &[u8]) -> Option<u32> {
let name = self.catalog.database_name(database).to_vec();
self.catalog
.find_index(Some(name.as_slice()), folded)
.map(|(_, index)| index.root)
}
}
fn quoted_name(name: &[u8]) -> Vec<u8> {
let plain = !name.is_empty()
&& !name.first().is_some_and(u8::is_ascii_digit)
&& name
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_');
if plain {
return name.to_vec();
}
let mut out = Vec::with_capacity(name.len().saturating_add(2));
out.push(b'"');
for byte in name {
if *byte == b'"' {
out.push(b'"');
}
out.push(*byte);
}
out.push(b'"');
out
}
fn affinity_type(declared: &[u8]) -> &'static [u8] {
match inillucent_value::affinity::for_column(declared) {
inillucent_value::affinity::Affinity::Blob => b"",
inillucent_value::affinity::Affinity::Text => b" TEXT",
inillucent_value::affinity::Affinity::Integer => b" INT",
inillucent_value::affinity::Affinity::Real => b" REAL",
inillucent_value::affinity::Affinity::Numeric
| inillucent_value::affinity::Affinity::FlexNum => b" NUM",
}
}
fn identifier_width(name: &[u8]) -> usize {
name.len()
.saturating_add(2)
.saturating_add(name.iter().filter(|byte| **byte == b'"').count())
}
const INDEX_SETTINGS: [(&str, &str); 10] = [
("m", "m"),
("ef_construction", "ef_construction"),
("ef_search", "ef_search"),
("mode", "mode"),
("metric", "metric"),
("distance", "metric"),
("threads", "threads"),
("compact", "compact"),
("lists", "lists"),
("probes", "probes"),
];
fn index_settings(
using: &Option<Vec<u8>>,
settings: &[Vec<u8>],
) -> Result<Vec<(Vec<u8>, Vec<u8>)>, ParseError> {
if settings.is_empty() {
return Ok(Vec::new());
}
if using.is_none() {
return Err(unsupported(
"WITH ( ... ) on an index that is not USING a module",
Span::default(),
));
}
let mut held = Vec::with_capacity(settings.len());
for setting in settings {
let text = String::from_utf8_lossy(setting).to_string();
let Some((name, value)) = text.split_once('=') else {
return Err(refused(
format!("index setting {} is not name = value", text.trim()),
Span::default(),
));
};
let folded = name.trim().to_ascii_lowercase();
let Some((_, option)) = INDEX_SETTINGS
.iter()
.find(|(known, _)| *known == folded.as_str())
else {
return Err(refused(
format!("no such index setting: {folded}"),
Span::default(),
));
};
let value = value
.trim()
.trim_matches(|held| held == '\'' || held == '"');
held.push((option.as_bytes().to_vec(), value.as_bytes().to_vec()));
}
Ok(held)
}