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;
pub(crate) 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::Raise {
message: Some(message),
..
} => out.push(*message),
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 declared_unique(create_sql: &[u8], position: usize) -> bool {
let limits = inillucent_base::limits::Limits::default();
let Ok(parsed) = crate::parser::parse_next_statement(create_sql, 0, &limits) else {
return false;
};
let ast::Statement::CreateTable {
body: ast::CreateTableBody::Columns { columns, .. },
..
} = &parsed.statement
else {
return false;
};
columns.get(position).is_some_and(|column| {
column
.constraints
.iter()
.any(|(_, constraint)| matches!(constraint, ast::ColumnConstraint::Unique(_)))
})
}
fn refuse_nulls_order(columns: &[ast::IndexedColumn]) -> Result<(), ParseError> {
for column in columns {
let word = match column.nulls {
Some(ast::NullOrder::First) => "FIRST",
Some(ast::NullOrder::Last) => "LAST",
None => continue,
};
return Err(refused(
format!("unsupported use of NULLS {word}"),
Span::default(),
));
}
Ok(())
}
fn no_such_quoted_column(name: &[u8]) -> ParseError {
refused(
format!("no such column: \"{}\"", String::from_utf8_lossy(name)),
Span::default(),
)
}
fn not_a_table_message(
action: &ast::AlterAction,
target: &crate::catalog_view::TableInfo,
) -> String {
let name = String::from_utf8_lossy(&target.name).into_owned();
if target.kind != TableKind::View {
return format!("cannot alter {name}: not a table");
}
match action {
ast::AlterAction::RenameTo(_) => format!("view {name} may not be altered"),
ast::AlterAction::RenameColumn { .. } => {
format!("cannot rename columns of view \"{name}\"")
}
ast::AlterAction::AddColumn(_) => "Cannot add a column to a view".to_string(),
ast::AlterAction::DropColumn(_) => format!("cannot drop column from view \"{name}\""),
ast::AlterAction::SetNotNull { .. }
| ast::AlterAction::DropNotNull(_)
| ast::AlterAction::AddCheck { .. }
| ast::AlterAction::DropConstraint(_) => {
format!("cannot edit constraints of view \"{name}\"")
}
}
}
fn no_such_collation_sequence(name: &[u8], span: Span) -> ParseError {
let _ = (name, span);
ParseError::new(
crate::diagnostic::ParseErrorKind::Refused(
"unable to identify the object to be reindexed".to_string(),
),
Span::default(),
)
}
#[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 references_with_default: bool,
pub null_without_default: bool,
pub non_constant_default: bool,
pub generated_stored: bool,
}
impl AddedColumnRisk {
pub fn refusal(&self, foreign_keys: bool) -> Option<&'static str> {
if foreign_keys && self.references_with_default {
return Some("Cannot add a REFERENCES column with non-NULL default value");
}
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>,
to_quoted: bool,
},
AddColumn {
start: u32,
end: u32,
risk: AddedColumnRisk,
},
AddColumnFailsAfter {
message: String,
},
DropColumn {
name: Vec<u8>,
position: u16,
},
SetNotNull {
name: Vec<u8>,
position: u16,
start: u32,
end: u32,
},
DropNotNull {
name: Vec<u8>,
position: u16,
},
AddCheck {
name: Option<Vec<u8>>,
start: u32,
end: u32,
expr_start: u32,
expr_end: u32,
},
DropConstraint {
name: Vec<u8>,
},
}
#[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,
refusal: Option<String>,
},
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>>,
into_sql: Option<String>,
},
Attach {
file: Vec<u8>,
schema: Vec<u8>,
key: Option<Vec<u8>>,
},
Detach {
schema: Vec<u8>,
},
Analyze {
database: usize,
table: Option<Vec<u8>>,
every_schema: bool,
},
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 table_database: Option<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,
table_database,
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,
table_database: *table_database,
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(self.already_exists(&database_name, &folded, name));
}
if !exists {
self.refuse_index_namesake(&database_name, &folded, &written)?;
}
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, false)?;
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),
};
self.check_table_autoincrement(columns, constraints, *without_rowid)?;
for (_, constraint) in constraints {
match constraint {
ast::TableConstraint::PrimaryKey { columns, .. }
| ast::TableConstraint::Unique { columns, .. } => refuse_nulls_order(columns)?,
_ => {}
}
}
self.check_table_shape(self.ast.text(name), columns, constraints)?;
self.check_table_declarations(self.ast.text(name), columns, constraints)?;
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)?;
self.check_column_collations(columns)?;
if *strict {
self.check_strict(columns, name)?;
}
let refusal = self.check_generated(columns, constraints)?;
self.refuse_all_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(self.already_exists(&database_name, &folded, name));
}
if !exists {
self.refuse_index_namesake(&database_name, &folded, &written)?;
}
self.record_write_dependency(index);
Ok(Directive::CreateTable {
if_not_exists,
database: index,
name: written,
name_offset: self.name_offset(name),
exists,
refusal,
})
}
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(self.already_exists(&database_name, &folded, name));
}
if !exists {
self.refuse_index_namesake(&database_name, &folded, &written)?;
}
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 written_names: Vec<Vec<u8>> = bound
.columns
.iter()
.map(|column| column.name.clone())
.collect();
let names = crate::bind::unique_column_names(&written_names);
let mut width = identifier_width(&written);
for name in &names {
width = width
.saturating_add(identifier_width(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("ed_name(&written));
create_sql.push(b'(');
for (position, (column, name)) in bound.columns.iter().zip(&names).enumerate() {
create_sql.extend_from_slice(if position > 0 { between } else { open });
create_sql.extend_from_slice("ed_name(name));
create_sql
.extend_from_slice(affinity_type(&column.declared_type, column.expr.affinity()));
}
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],
constraints: &[(Option<ast::NameId>, ast::TableConstraint)],
) -> Result<Option<String>, 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 Some(expr) = self.check_generated_clauses(column)? else {
continue;
};
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));
}
self.check_key_has_no_generated(columns, constraints)?;
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.last() {
let written = columns
.get(*position)
.map(|column| String::from_utf8_lossy(self.ast.text(column.name)).into_owned())
.unwrap_or_default();
return Ok(Some(format!("generated column loop on \"{written}\"")));
}
Ok(None)
}
pub(crate) fn check_generated_clauses(
&self,
column: &ast::ColumnDef,
) -> Result<Option<ast::ExprId>, ParseError> {
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,
bad_storage,
..
} => {
if has_default || expr.is_some() || *bad_storage {
return Err(refused(
format!(
"error in generated column \"{}\"",
String::from_utf8_lossy(self.ast.text(column.name))
),
Span::default(),
));
}
expr = Some(*body);
}
ast::ColumnConstraint::Default(_) => {
if expr.is_some() {
return Err(refused(
"cannot use DEFAULT on a generated column",
Span::default(),
));
}
has_default = true;
}
ast::ColumnConstraint::PrimaryKey { .. } => in_primary_key = true,
_ => {}
}
}
if expr.is_some() && in_primary_key {
return Err(refused(
"generated columns cannot be part of the PRIMARY KEY",
Span::default(),
));
}
Ok(expr)
}
fn check_key_has_no_generated(
&self,
columns: &[ast::ColumnDef],
constraints: &[(Option<ast::NameId>, ast::TableConstraint)],
) -> Result<(), ParseError> {
for (_, constraint) in constraints {
let ast::TableConstraint::PrimaryKey { columns: keys, .. } = constraint else {
continue;
};
for key in keys {
let Some(ast::Expr::Column { column: named, .. }) = self.ast.expr(key.expr) else {
continue;
};
let folded = self.ast.folded(*named);
let generated = columns.iter().any(|column| {
self.ast.folded(column.name) == folded
&& column.constraints.iter().any(|(_, constraint)| {
matches!(constraint, ast::ColumnConstraint::Generated { .. })
})
});
if generated {
return Err(refused(
"generated columns cannot be part of the PRIMARY KEY",
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_column_collations(&self, columns: &[ast::ColumnDef]) -> Result<(), ParseError> {
for column in columns {
for (_, constraint) in &column.constraints {
let ast::ColumnConstraint::Collate(name) = constraint else {
continue;
};
let written = self.ast.text(*name);
if self.collation_named(written).is_none() {
return Err(crate::bind::no_such_collation(written, Span::default()));
}
}
}
Ok(())
}
fn check_strict(
&self,
columns: &[ast::ColumnDef],
table: ast::NameId,
) -> Result<(), ParseError> {
let table = String::from_utf8_lossy(self.ast.text(table)).into_owned();
for column in columns {
let Some(declared) = column.declared_type.as_ref() else {
return Err(refused(
format!(
"missing datatype for {table}.{}",
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 {table}.{}: \"{}\"",
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 Some(name) = name else {
let temp = self.catalog.database_index(b"temp");
for index in 0..self.catalog.database_count() {
if Some(index) != temp {
self.record_write_dependency(index);
}
}
return Ok(Directive::Analyze {
database: 0,
table: None,
every_schema: true,
});
};
let folded = self.ast.folded(name).to_vec();
if database.is_none() {
if let Some(index) = self.catalog.database_index(&folded) {
self.record_write_dependency(index);
return Ok(Directive::Analyze {
database: index,
table: None,
every_schema: false,
});
}
}
let schema_name = match database {
Some(_) => {
let index = self.resolve_database(database)?;
Some(self.catalog.database_name(index).to_vec())
}
None => None,
};
let found = self
.catalog
.find_index(schema_name.as_deref(), &folded)
.map(|(table, index)| (table.database, index.name.clone()))
.or_else(|| {
self.catalog
.find_table(schema_name.as_deref(), &folded)
.map(|table| (table.database, table.name.clone()))
});
let Some((index, table)) = found else {
return Err(no_such_table(self.ast.text(name), Span::default()));
};
self.record_write_dependency(index);
Ok(Directive::Analyze {
database: index,
table: Some(table),
every_schema: false,
})
}
fn bind_alter(
&mut self,
database: Option<ast::NameId>,
table: ast::NameId,
action: &ast::AlterAction,
) -> Result<Directive, ParseError> {
let written = match database {
Some(qualifier) => match self.resolve_database(database) {
Ok(found) => Some(self.catalog.database_name(found).to_vec()),
Err(_) => {
let written = [self.ast.text(qualifier), b".", self.ast.text(table)].concat();
return Err(no_such_table(&written, Span::default()));
}
},
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(
not_a_table_message(action, &target),
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) => self.bind_rename_to(*name, &database_name)?,
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(no_such_quoted_column(self.ast.text(*from)));
};
let stored = target
.column(position)
.map(|column| column.name.clone())
.unwrap_or_default();
let to_quoted = self
.ast
.name(*to)
.is_some_and(|name| name.quote != crate::lexer::QuoteForm::Bare);
AlterKind::RenameColumn {
from: stored,
to: self.ast.text(*to).to_vec(),
to_quoted,
}
}
ast::AlterAction::AddColumn(definition) => {
let risk = self.check_added_column(&target, definition)?;
match definition.deferred_failure {
Some(reason) => AlterKind::AddColumnFailsAfter {
message: format!(
"error in table {} after add column: {reason}",
String::from_utf8_lossy(&target.name)
),
},
None => 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(no_such_quoted_column(self.ast.text(*name)));
};
self.check_dropped_column(&target, position, self.ast.text(*name))?;
let stored = target
.column(position)
.map(|column| column.name.clone())
.unwrap_or_default();
AlterKind::DropColumn {
name: stored,
position,
}
}
other => self.bind_constraint_alter(&target, other)?,
};
Ok(Directive::Alter {
database: index,
table: target.name.clone(),
action: kind,
})
}
fn bind_rename_to(
&self,
name: ast::NameId,
database_name: &[u8],
) -> Result<AlterKind, ParseError> {
let to = self.ast.text(name).to_vec();
let to_folded = self.ast.folded(name).to_vec();
let written = String::from_utf8_lossy(&to).into_owned();
if to_folded.starts_with(b"sqlite_") {
return Err(refused(
format!("object name reserved for internal use: {written}"),
Span::default(),
));
}
let taken = self
.catalog
.find_table(Some(database_name), &to_folded)
.is_some()
|| self
.catalog
.find_index(Some(database_name), &to_folded)
.is_some();
if taken {
return Err(refused(
format!("there is already another table or index with this name: {written}"),
Span::default(),
));
}
Ok(AlterKind::RenameTable { to })
}
fn bind_constraint_alter(
&self,
target: &crate::catalog_view::TableInfo,
action: &ast::AlterAction,
) -> Result<AlterKind, ParseError> {
Ok(match action {
ast::AlterAction::SetNotNull { column, start, end } => {
let (name, position) = self.constrained_column(target, *column)?;
AlterKind::SetNotNull {
name,
position,
start: *start,
end: *end,
}
}
ast::AlterAction::DropNotNull(column) => {
let (name, position) = self.constrained_column(target, *column)?;
AlterKind::DropNotNull { name, position }
}
ast::AlterAction::AddCheck {
name,
expr,
start,
end,
} => {
self.check_names_resolve(target, *expr)?;
let span = self.ast.expr_span(*expr);
AlterKind::AddCheck {
name: name.map(|id| self.ast.text(id).to_vec()),
start: *start,
end: *end,
expr_start: span.start,
expr_end: span.end,
}
}
ast::AlterAction::DropConstraint(name) => AlterKind::DropConstraint {
name: self.ast.text(*name).to_vec(),
},
_ => return Err(unsupported("that ALTER TABLE form", Span::default())),
})
}
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(),
));
}
self.check_column_constraints(definition)?;
self.check_generated_clauses(definition)?;
let mut not_null = false;
let mut has_default = false;
let mut constant = true;
let mut generated = false;
let mut generated_stored = false;
let mut references = 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 = !self.is_null_literal(*expr);
if !self.constant_default(*expr) {
constant = false;
}
}
ast::ColumnConstraint::References(_) => references = true,
ast::ColumnConstraint::Generated { stored, .. } => {
generated = true;
generated_stored = *stored;
}
_ => {}
}
}
Ok(AddedColumnRisk {
references_with_default: !generated && references && has_default,
null_without_default: !generated && not_null && !has_default,
non_constant_default: !generated && !constant && has_default,
generated_stored,
})
}
fn constant_default(&self, expr: ast::ExprId) -> bool {
match self.ast.expr(expr) {
Some(ast::Expr::Literal(
ast::Literal::CurrentDate
| ast::Literal::CurrentTime
| ast::Literal::CurrentTimestamp,
)) => false,
Some(ast::Expr::Literal(_)) => true,
Some(ast::Expr::Column {
database: None,
table: None,
..
}) => true,
Some(ast::Expr::Unary {
op: ast::UnaryOp::Negate | ast::UnaryOp::Identity,
operand,
})
| Some(ast::Expr::Cast { operand, .. }) => self.constant_default(*operand),
_ => false,
}
}
fn constrained_column(
&self,
table: &crate::catalog_view::TableInfo,
column: ast::NameId,
) -> Result<(Vec<u8>, u16), ParseError> {
let folded = self.ast.folded(column).to_vec();
let Some(position) = table.column_position(&folded) else {
return Err(crate::bind::no_such_column(
self.ast.text(column),
Span::default(),
));
};
let stored = table
.column(position)
.map(|found| found.name.clone())
.unwrap_or_default();
Ok((stored, position))
}
fn check_names_resolve(
&self,
table: &crate::catalog_view::TableInfo,
expr: ast::ExprId,
) -> Result<(), ParseError> {
let mut pending = vec![expr];
while let Some(id) = pending.pop() {
pending.extend(expression_children(self.ast, id));
let Some(ast::Expr::Column {
table: qualifier,
column,
..
}) = self.ast.expr(id)
else {
continue;
};
let folded = self.ast.folded(*column);
let own = qualifier.is_none_or(|name| self.ast.folded(name) == table.folded.as_slice());
let rowid = matches!(folded, b"rowid" | b"oid" | b"_rowid_");
if own && (rowid || table.column_position(folded).is_some()) {
continue;
}
let written = match qualifier {
Some(name) => [self.ast.text(*name), b".", self.ast.text(*column)].concat(),
None => self.ast.text(*column).to_vec(),
};
return Err(crate::bind::no_such_column(
&written,
self.ast.expr_span(id),
));
}
Ok(())
}
fn is_null_literal(&self, expr: ast::ExprId) -> bool {
matches!(
self.ast.expr(expr),
Some(ast::Expr::Literal(ast::Literal::Null))
)
}
fn check_dropped_column(
&self,
table: &crate::catalog_view::TableInfo,
position: u16,
written: &[u8],
) -> Result<(), ParseError> {
let named = String::from_utf8_lossy(written).into_owned();
let in_primary_key = table.rowid_alias == Some(position)
|| table
.column(position)
.is_some_and(|column| column.primary_key_position.is_some());
if in_primary_key {
return Err(refused(
format!("cannot drop PRIMARY KEY column: \"{named}\""),
Span::default(),
));
}
if declared_unique(&table.create_sql, usize::from(position)) {
return Err(refused(
format!("cannot drop UNIQUE column: \"{named}\""),
Span::default(),
));
}
if table.columns.len() <= 1 {
return Err(refused(
format!("cannot drop column \"{named}\": no other columns exist"),
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 schema_name = database.map(|_| self.catalog.database_name(index).to_vec());
let qualified = database.is_some();
let every_index = |catalog: &dyn CatalogView| -> Vec<Vec<u8>> {
let tables = if qualified {
catalog.tables_of(index)
} else {
catalog.every_table()
};
tables
.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: every_index(self.catalog),
});
};
let folded = self.ast.folded(name).to_vec();
if let Some(table) = self.catalog.find_table(schema_name.as_deref(), &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(schema_name.as_deref(), &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 tables = if qualified {
self.catalog.tables_of(index)
} else {
self.catalog.every_table()
};
let indexes = tables
.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 literal = into.and_then(|expr| match self.ast.expr(expr) {
Some(ast::Expr::Literal(ast::Literal::String(text))) => Some(text.clone()),
_ => None,
});
let into_sql = match (into, &literal) {
(Some(expr), None) => {
let span = self.ast.expr_span(expr);
let written = self
.source
.get(span.start as usize..span.end as usize)
.ok_or_else(|| refused("the file name could not be read", span))?;
Some(String::from_utf8_lossy(written).into_owned())
}
_ => None,
};
let index = self.resolve_database(database)?;
self.record_write_dependency(index);
Ok(Directive::Vacuum {
database: index,
into: literal,
into_sql,
})
}
pub(crate) fn bind_attach(
&mut self,
file: ast::ExprId,
schema: ast::ExprId,
key: Option<ast::ExprId>,
) -> Result<Directive, ParseError> {
let key = match key.map(|expr| self.ast.expr(expr)) {
None => None,
Some(Some(ast::Expr::Literal(ast::Literal::String(text)))) => Some(text.clone()),
Some(Some(ast::Expr::Column {
table: None,
column,
..
})) => Some(self.ast.text(*column).to_vec()),
Some(_) => {
return Err(unsupported(
"an ATTACH KEY that is not a string literal",
Span::default(),
))
}
};
Ok(Directive::Attach {
file: self.literal_path(file, "ATTACH with a file name that is not a literal")?,
schema: self.literal_or_name(schema)?,
key,
})
}
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,
refused: &'static str,
) -> Result<Vec<u8>, ParseError> {
match self.ast.expr(expr) {
Some(ast::Expr::Literal(ast::Literal::String(text))) => Ok(text.clone()),
_ => Err(unsupported(refused, 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, false)?;
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(self.already_exists(&database_name, &folded, name));
}
if !exists {
self.refuse_index_namesake(&database_name, &folded, &written)?;
self.refuse_view_parameters()?;
if temp.is_none() && !database_name.eq_ignore_ascii_case(b"temp") {
self.refuse_view_in_another_database(&written, &database_name)?;
}
}
let _ = select;
self.record_write_dependency(index);
Ok(Directive::CreateView {
if_not_exists,
database: index,
name: written,
name_offset: self.name_offset(name),
exists,
})
}
fn refuse_view_in_another_database(&self, view: &[u8], home: &[u8]) -> Result<(), ParseError> {
for index in 0..self.ast.from_term_count() {
let Some(term) = self.ast.from_term(ast::FromTermId(index as u32)) else {
continue;
};
let ast::FromSource::Table {
database: Some(qualifier),
..
} = &term.source
else {
continue;
};
if self.ast.text(*qualifier).eq_ignore_ascii_case(home) {
continue;
}
return Err(refused(
format!(
"view {} cannot reference objects in database {}",
String::from_utf8_lossy(view),
String::from_utf8_lossy(self.ast.text(*qualifier))
),
Span::default(),
));
}
Ok(())
}
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,
order,
..
} = 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")
|| *order == ast::SortOrder::Descending
{
return Err(refused(
"AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY",
Span::default(),
));
}
}
}
Ok(())
}
fn check_table_autoincrement(
&self,
columns: &[ast::ColumnDef],
constraints: &[(Option<ast::NameId>, ast::TableConstraint)],
without_rowid: bool,
) -> Result<(), ParseError> {
for (_, constraint) in constraints {
let ast::TableConstraint::PrimaryKey {
columns: keys,
autoincrement: true,
..
} = constraint
else {
continue;
};
if without_rowid {
return Err(refused(
"AUTOINCREMENT not allowed on WITHOUT ROWID tables",
Span::default(),
));
}
let single = match keys.as_slice() {
[key] => {
let named = match self.ast.expr(key.expr) {
Some(ast::Expr::Collate { operand, .. }) => self.ast.expr(*operand),
other => other,
};
match named {
Some(ast::Expr::Column {
table: None,
column,
..
}) => Some(self.ast.folded(*column)),
_ => None,
}
}
_ => None,
};
let integer = single.is_some_and(|folded| {
columns.iter().any(|column| {
self.ast.folded(column.name) == folded
&& column
.declared_type
.as_deref()
.is_some_and(|declared| declared.eq_ignore_ascii_case(b"integer"))
})
});
if !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, true)?;
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();
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 named = match parts.table_database {
Some(id) => {
let at = self.resolve_database(Some(id))?;
if temp.is_none() && at != index {
return Err(refused(
format!(
"trigger {} cannot reference objects in database {}",
String::from_utf8_lossy(&written),
String::from_utf8_lossy(self.ast.text(id))
),
Span::default(),
));
}
Some(self.catalog.database_name(at).to_vec())
}
None => None,
};
let scope = match &named {
Some(name) => Some(name.as_slice()),
None => temp.map_or(Some(database_name.as_slice()), |_| None),
};
let Some(target) = self.catalog.find_table(scope, &table_folded).cloned() else {
let missing = match scope {
Some(schema) => [schema, b".", self.ast.text(parts.table)].concat(),
None => self.ast.text(parts.table).to_vec(),
};
return Err(crate::bind::no_such_table(&missing, Span::default()));
};
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 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()));
}
_ => {}
}
if temp.is_none() {
self.refuse_qualified_trigger_targets(parts.body)?;
}
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 index_target(
&self,
database: Option<ast::NameId>,
table: ast::NameId,
) -> Result<(usize, Vec<u8>, crate::catalog_view::TableInfo), ParseError> {
let table_folded = self.ast.folded(table).to_vec();
let index = match database {
Some(_) => self.resolve_database(database)?,
None => match self.catalog.find_table(None, &table_folded) {
Some(found) => found.database,
None => return Err(self.index_without_table(0, &table_folded, table)),
},
};
let database_name = self.catalog.database_name(index).to_vec();
let Some(target) = self
.catalog
.find_table(Some(database_name.as_slice()), &table_folded)
.cloned()
else {
return Err(self.index_without_table(index, &table_folded, table));
};
Ok((index, database_name, target))
}
fn bind_create_index(&mut self, spec: &CreateIndexSpec<'_>) -> Result<Directive, ParseError> {
let CreateIndexSpec {
database,
name,
table,
using,
columns,
settings,
..
} = *spec;
refuse_nulls_order(columns)?;
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, database_name, target) = self.index_target(database, table)?;
self.refuse_unindexable(&target)?;
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 exists = self.check_new_index_name(&database_name, &folded, &written, if_not_exists)?;
self.check_index_declarations(&target, columns, spec.filter)?;
let keys = self.index_key_columns(&target, columns)?;
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 index_key_columns(
&self,
target: &crate::catalog_view::TableInfo,
columns: &[ast::IndexedColumn],
) -> Result<Vec<IndexKeyColumn>, ParseError> {
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,
});
}
Ok(keys)
}
fn refuse_dropping_the_schema_table(
&self,
folded: &[u8],
database: &[u8],
) -> Result<(), ParseError> {
let main = matches!(folded, b"sqlite_master" | b"sqlite_schema");
let temp = matches!(folded, b"sqlite_temp_master" | b"sqlite_temp_schema");
if !main && !temp {
return Ok(());
}
let in_temp = temp || database.eq_ignore_ascii_case(b"temp");
let said = match in_temp {
true => "table sqlite_temp_master may not be dropped",
false => "table sqlite_master may not be dropped",
};
Err(refused(said, Span::default()))
}
fn resolve_drop_database(
&self,
kind: ObjectKind,
database: Option<ast::NameId>,
written: &[u8],
folded: &[u8],
) -> Result<(Vec<u8>, usize), ParseError> {
let qualified = match database {
Some(schema) => [self.ast.text(schema), b".".as_slice(), written].concat(),
None => written.to_vec(),
};
let index = match database {
Some(_) => match self.resolve_database(database) {
Err(_) if kind == ObjectKind::Table => {
return Err(no_such_table(&qualified, Span::default()))
}
resolved => resolved?,
},
None => self.unqualified_home(kind, folded).unwrap_or(0),
};
Ok((qualified, index))
}
fn bind_drop_table(
&self,
if_exists: bool,
index: usize,
database_name: &[u8],
folded: &[u8],
written: Vec<u8>,
qualified: &[u8],
) -> Result<Directive, ParseError> {
let kind = ObjectKind::Table;
let found = self
.catalog
.find_table(Some(database_name), 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(qualified, Span::default()));
};
self.refuse_dropping_own_table(&table, &written)?;
let index_roots = table
.indexes
.iter()
.map(|held| held.root)
.filter(|root| *root != 0 && *root != table.root)
.collect();
Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root: table.root,
index_roots,
exists: true,
})
}
fn bind_drop(
&mut self,
kind: ObjectKind,
if_exists: bool,
database: Option<ast::NameId>,
name: ast::NameId,
) -> Result<Directive, ParseError> {
let written = self.ast.text(name).to_vec();
let folded = self.ast.folded(name).to_vec();
let (qualified, index) = self.resolve_drop_database(kind, database, &written, &folded)?;
let database_name = self.catalog.database_name(index).to_vec();
self.record_write_dependency(index);
if kind != ObjectKind::Trigger && kind != ObjectKind::Index {
self.refuse_dropping_the_schema_table(&folded, database_name.as_slice())?;
}
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 found.is_some() && !exists {
return Err(refused(
format!(
"use DROP TABLE to delete table {}",
String::from_utf8_lossy(&written)
),
Span::default(),
));
}
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 {
return self.bind_drop_table(
if_exists,
index,
&database_name,
&folded,
written,
&qualified,
);
}
self.refuse_dropping_constraint_index(&database_name, &folded)?;
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(),
));
};
if folded.starts_with(b"sqlite_autoindex_") {
return Err(refused(
"index associated with UNIQUE or PRIMARY KEY constraint cannot be dropped",
Span::default(),
));
}
Ok(Directive::Drop {
kind,
if_exists,
database: index,
name: written,
root,
index_roots: Vec::new(),
exists: true,
})
}
fn unqualified_home(&self, kind: ObjectKind, folded: &[u8]) -> Option<usize> {
match kind {
ObjectKind::Trigger => self
.catalog
.find_trigger(None, folded)
.map(|(table, _)| table.database),
ObjectKind::Index => self
.catalog
.find_index(None, folded)
.map(|(table, _)| table.database),
_ => self
.catalog
.find_table(None, folded)
.map(|table| table.database),
}
}
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>,
trigger: bool,
) -> Result<Option<usize>, ParseError> {
if !temporary {
return Ok(None);
}
if let Some(id) = database {
if trigger {
return Err(refused(
"temporary trigger may not have qualified name",
Span::default(),
));
}
if self.ast.folded(id) != b"temp" {
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))
),
self.ast.name(id).map_or(Span::default(), |name| name.span),
)
})
}
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'_')
&& crate::keyword::lookup(name).is_none();
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],
expression: Option<inillucent_value::affinity::Affinity>,
) -> &'static [u8] {
let affinity = match (declared.is_empty(), expression) {
(true, Some(held)) => held,
_ => inillucent_value::affinity::for_column(declared),
};
match affinity {
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)
}