use crate::ast::{ConflictAction, ReferentialAction};
use inillucent_value::Affinity;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IndexOrigin {
Created,
Unique,
PrimaryKey,
Module,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IndexMetric {
Cosine,
L2,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ColumnInfo {
pub name: Vec<u8>,
pub folded: Vec<u8>,
pub declared_type: Vec<u8>,
pub affinity: Affinity,
pub collation: Vec<u8>,
pub not_null: bool,
pub not_null_conflict: Option<ConflictAction>,
pub primary_key_conflict: Option<ConflictAction>,
pub default_sql: Option<Vec<u8>>,
pub primary_key_position: Option<u16>,
pub hidden: bool,
pub generated: bool,
pub stored: bool,
pub generated_sql: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexColumnInfo {
pub column: Option<u16>,
pub expr_sql: Option<Vec<u8>>,
pub collation: Vec<u8>,
pub descending: bool,
pub declared_descending: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexInfo {
pub name: Vec<u8>,
pub folded: Vec<u8>,
pub root: u32,
pub unique: bool,
pub columns: Vec<IndexColumnInfo>,
pub partial_sql: Option<Vec<u8>>,
pub origin: IndexOrigin,
pub conflict: Option<ConflictAction>,
pub prefix_rows: Vec<i64>,
pub analysed_rows: Option<i64>,
pub metric: Option<IndexMetric>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableKind {
Table,
View,
Virtual,
Subquery,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewBody {
pub ast: crate::ast::Ast,
pub select: crate::ast::SelectId,
pub columns: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TriggerEventInfo {
Insert,
Delete,
Update(Vec<Vec<u8>>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TriggerInfo {
pub name: Vec<u8>,
pub folded: Vec<u8>,
pub time: crate::ast::TriggerTime,
pub event: TriggerEventInfo,
pub ast: crate::ast::Ast,
pub when: Option<crate::ast::ExprId>,
pub body: Vec<crate::ast::Statement>,
}
impl ColumnInfo {
pub fn is_vector(&self) -> bool {
let declared = self.declared_type.to_ascii_lowercase();
let Some(rest) = declared.strip_prefix(b"vector".as_slice()) else {
return false;
};
rest.is_empty()
|| rest
.first()
.is_some_and(|byte| !byte.is_ascii_alphanumeric())
}
pub fn vector_dimensions(&self) -> Option<usize> {
let declared = self.declared_type.to_ascii_lowercase();
let rest = declared.strip_prefix(b"vector".as_slice())?;
let inside: Vec<u8> = rest
.iter()
.copied()
.skip_while(|byte| byte.is_ascii_whitespace())
.collect();
let inside = inside.strip_prefix(b"(".as_slice())?;
let inside = inside.strip_suffix(b")".as_slice())?;
let text = std::str::from_utf8(inside).ok()?.trim();
let width: usize = text.parse().ok()?;
(width > 0).then_some(width)
}
}
impl TriggerInfo {
pub fn fires_for(&self, event: &TriggerEventInfo, changed: &[Vec<u8>]) -> bool {
match (&self.event, event) {
(TriggerEventInfo::Insert, TriggerEventInfo::Insert) => true,
(TriggerEventInfo::Delete, TriggerEventInfo::Delete) => true,
(TriggerEventInfo::Update(of), TriggerEventInfo::Update(_)) => {
of.is_empty() || of.iter().any(|name| changed.contains(name))
}
_ => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TableInfo {
pub name: Vec<u8>,
pub folded: Vec<u8>,
pub database: usize,
pub root: u32,
pub columns: Vec<ColumnInfo>,
pub rowid_alias: Option<u16>,
pub without_rowid: bool,
pub strict: bool,
pub autoincrement: bool,
pub kind: TableKind,
pub create_sql: Vec<u8>,
pub indexes: Vec<IndexInfo>,
pub view: Option<Box<ViewBody>>,
pub triggers: Vec<TriggerInfo>,
pub analysed_rows: Option<i64>,
pub foreign_key_triggers: Vec<ForeignKeyTrigger>,
pub foreign_keys: Vec<ForeignKeyInfo>,
pub checks: Vec<CheckInfo>,
pub module: Option<crate::vtab::ModuleRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForeignKeyTrigger {
pub is_check: bool,
pub deferred: bool,
pub trigger: Option<TriggerInfo>,
pub fault: Vec<u8>,
pub self_referencing: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForeignKeyInfo {
pub id: u32,
pub columns: Vec<u16>,
pub parent: Vec<u8>,
pub parent_folded: Vec<u8>,
pub parent_columns: Vec<Vec<u8>>,
pub on_delete: ReferentialAction,
pub on_update: ReferentialAction,
pub match_clause: Vec<u8>,
pub deferrable: bool,
pub initially_deferred: bool,
pub cyclic: bool,
}
impl ForeignKeyInfo {
pub fn is_deferred(&self) -> bool {
self.deferrable && self.initially_deferred
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckInfo {
pub name: Option<Vec<u8>>,
pub expr_sql: Vec<u8>,
pub conflict: Option<ConflictAction>,
}
impl TableInfo {
pub fn column_position(&self, folded: &[u8]) -> Option<u16> {
self.columns
.iter()
.position(|column| column.folded == folded)
.map(|index| index as u16)
}
pub fn column(&self, position: u16) -> Option<&ColumnInfo> {
self.columns.get(position as usize)
}
pub fn has_rowid(&self) -> bool {
matches!(self.kind, TableKind::Table | TableKind::Virtual) && !self.without_rowid
}
pub fn eponymous(
name: Vec<u8>,
columns: Vec<ColumnInfo>,
module: crate::vtab::ModuleRef,
without_rowid: bool,
) -> TableInfo {
let folded = name.to_ascii_lowercase();
TableInfo {
name,
folded,
database: 0,
root: 0,
columns,
rowid_alias: None,
without_rowid,
strict: false,
autoincrement: false,
kind: TableKind::Virtual,
create_sql: Vec::new(),
foreign_keys: Vec::new(),
foreign_key_triggers: Vec::new(),
module: Some(module),
view: None,
triggers: Vec::new(),
analysed_rows: None,
indexes: Vec::new(),
checks: Vec::new(),
}
}
pub fn subquery(name: Vec<u8>, database: usize, columns: Vec<ColumnInfo>) -> TableInfo {
let folded = name.to_ascii_lowercase();
TableInfo {
name,
folded,
database,
root: 0,
columns,
rowid_alias: None,
without_rowid: true,
strict: false,
autoincrement: false,
kind: TableKind::Subquery,
create_sql: Vec::new(),
foreign_keys: Vec::new(),
foreign_key_triggers: Vec::new(),
module: None,
view: None,
triggers: Vec::new(),
analysed_rows: None,
indexes: Vec::new(),
checks: Vec::new(),
}
}
pub fn record_slot(&self, column: u16) -> Option<usize> {
if self.without_rowid {
return self
.record_order()
.iter()
.position(|stored| *stored == column);
}
let mut slot = 0usize;
for (position, info) in self.columns.iter().enumerate() {
if info.generated && !info.stored {
if position == usize::from(column) {
return None;
}
continue;
}
if position == usize::from(column) {
return Some(slot);
}
slot = slot.saturating_add(1);
}
None
}
pub fn primary_key(&self) -> Vec<u16> {
let mut keys: Vec<(u16, u16)> = self
.columns
.iter()
.enumerate()
.filter_map(|(position, column)| {
column
.primary_key_position
.map(|key| (key, position as u16))
})
.collect();
keys.sort_by_key(|(key, _)| *key);
keys.into_iter().map(|(_, position)| position).collect()
}
pub fn record_order(&self) -> Vec<u16> {
let stored = |position: usize| {
self.columns
.get(position)
.is_some_and(|column| !column.generated || column.stored)
};
if !self.without_rowid {
return (0..self.columns.len())
.filter(|position| stored(*position))
.map(|position| position as u16)
.collect();
}
let keys = self.primary_key();
let mut order = keys.clone();
for position in 0..self.columns.len() {
if keys.contains(&(position as u16)) || !stored(position) {
continue;
}
order.push(position as u16);
}
order
}
pub fn is_rowid_name(&self, folded: &[u8]) -> bool {
if !self.has_rowid() {
return false;
}
let spelled = folded == b"rowid" || folded == b"_rowid_" || folded == b"oid";
spelled && self.column_position(folded).is_none()
}
}
pub trait CatalogView {
fn database_count(&self) -> usize;
fn database_name(&self, index: usize) -> &[u8];
fn database_index(&self, folded: &[u8]) -> Option<usize>;
fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo>;
fn shared_table(
&self,
database: Option<&[u8]>,
folded: &[u8],
) -> Option<std::rc::Rc<TableInfo>> {
self.find_table(database, folded)
.map(|table| std::rc::Rc::new(table.clone()))
}
fn find_index(
&self,
database: Option<&[u8]>,
folded: &[u8],
) -> Option<(&TableInfo, &IndexInfo)>;
fn find_trigger(
&self,
database: Option<&[u8]>,
folded: &[u8],
) -> Option<(&TableInfo, &TriggerInfo)> {
let wanted = database.and_then(|name| self.database_index(name));
for table in self.every_table() {
if wanted.is_some_and(|index| index != table.database) {
continue;
}
if let Some(trigger) = table.triggers.iter().find(|one| one.folded == folded) {
return Some((table, trigger));
}
}
None
}
fn every_table(&self) -> Vec<&TableInfo>;
fn tables_of(&self, database: usize) -> Vec<&TableInfo>;
fn schema_cookie(&self, database: usize) -> u32;
fn generation(&self) -> u64;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StaticCatalog {
pub databases: Vec<(Vec<u8>, u32)>,
pub tables: Vec<std::rc::Rc<TableInfo>>,
pub eponymous: Vec<std::rc::Rc<TableInfo>>,
pub generation: u64,
}
impl StaticCatalog {
pub fn empty() -> StaticCatalog {
StaticCatalog {
databases: vec![(b"main".to_vec(), 0)],
tables: Vec::new(),
eponymous: Vec::new(),
generation: 0,
}
}
pub fn with_eponymous(mut self, table: TableInfo) -> StaticCatalog {
self.eponymous.push(std::rc::Rc::new(table));
self
}
pub fn table_named(&self, folded: &[u8]) -> Option<&TableInfo> {
self.tables
.iter()
.map(std::rc::Rc::as_ref)
.find(|table| table.folded == folded)
}
pub fn with_table(mut self, table: TableInfo) -> StaticCatalog {
self.tables.push(std::rc::Rc::new(table));
self
}
}
impl CatalogView for StaticCatalog {
fn shared_table(
&self,
database: Option<&[u8]>,
folded: &[u8],
) -> Option<std::rc::Rc<TableInfo>> {
if let Some(database) = database {
let index = self.database_index(database)?;
return self
.tables
.iter()
.find(|table| table.database == index && table.folded == folded)
.map(std::rc::Rc::clone);
}
for index in self.search_order() {
if let Some(found) = self
.tables
.iter()
.find(|table| table.database == index && table.folded == folded)
{
return Some(std::rc::Rc::clone(found));
}
}
self.eponymous
.iter()
.find(|table| table.folded == folded)
.map(std::rc::Rc::clone)
}
fn database_count(&self) -> usize {
self.databases.len()
}
fn database_name(&self, index: usize) -> &[u8] {
self.databases.get(index).map_or(&[], |(name, _)| name)
}
fn database_index(&self, folded: &[u8]) -> Option<usize> {
self.databases
.iter()
.position(|(name, _)| name.eq_ignore_ascii_case(folded))
}
fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo> {
if let Some(database) = database {
let index = self.database_index(database)?;
return self
.tables
.iter()
.find(|table| table.database == index && table.folded == folded)
.map(std::rc::Rc::as_ref);
}
for index in self.search_order() {
if let Some(found) = self
.tables
.iter()
.find(|table| table.database == index && table.folded == folded)
{
return Some(found.as_ref());
}
}
self.eponymous
.iter()
.find(|table| table.folded == folded)
.map(std::rc::Rc::as_ref)
}
fn every_table(&self) -> Vec<&TableInfo> {
self.tables.iter().map(std::rc::Rc::as_ref).collect()
}
fn find_index(
&self,
database: Option<&[u8]>,
folded: &[u8],
) -> Option<(&TableInfo, &IndexInfo)> {
let wanted = database.and_then(|name| self.database_index(name));
for table in &self.tables {
if wanted.is_some_and(|index| index != table.database) {
continue;
}
if let Some(index) = table.indexes.iter().find(|index| index.folded == folded) {
return Some((table, index));
}
}
None
}
fn tables_of(&self, database: usize) -> Vec<&TableInfo> {
self.tables
.iter()
.filter(|table| table.database == database)
.map(std::rc::Rc::as_ref)
.collect()
}
fn schema_cookie(&self, database: usize) -> u32 {
self.databases
.get(database)
.map_or(0, |(_, cookie)| *cookie)
}
fn generation(&self) -> u64 {
self.generation
}
}
impl StaticCatalog {
fn search_order(&self) -> Vec<usize> {
let mut order: Vec<usize> = Vec::with_capacity(self.databases.len());
if let Some(temp) = self
.databases
.iter()
.position(|(name, _)| name.eq_ignore_ascii_case(b"temp"))
{
order.push(temp);
}
for (index, _) in self.databases.iter().enumerate() {
if !order.contains(&index) {
order.push(index);
}
}
order
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table(name: &[u8], database: usize) -> TableInfo {
TableInfo {
name: name.to_vec(),
folded: name.to_ascii_lowercase(),
database,
root: 2,
columns: vec![ColumnInfo {
name: b"a".to_vec(),
folded: b"a".to_vec(),
declared_type: Vec::new(),
affinity: Affinity::Blob,
collation: b"binary".to_vec(),
not_null: false,
not_null_conflict: None,
primary_key_conflict: None,
default_sql: None,
primary_key_position: None,
hidden: false,
generated: false,
stored: false,
generated_sql: None,
}],
rowid_alias: None,
without_rowid: false,
strict: false,
autoincrement: false,
kind: TableKind::Table,
create_sql: Vec::new(),
indexes: Vec::new(),
view: None,
triggers: Vec::new(),
analysed_rows: None,
checks: Vec::new(),
foreign_keys: Vec::new(),
foreign_key_triggers: Vec::new(),
module: None,
}
}
#[test]
fn temp_is_searched_before_main() {
let catalog = StaticCatalog {
databases: vec![(b"main".to_vec(), 1), (b"temp".to_vec(), 2)],
tables: vec![
std::rc::Rc::new(table(b"t", 0)),
std::rc::Rc::new(table(b"t", 1)),
],
eponymous: Vec::new(),
generation: 7,
};
let found = catalog.find_table(None, b"t").expect("it resolves");
assert_eq!(found.database, 1);
let qualified = catalog
.find_table(Some(b"main"), b"t")
.expect("it resolves");
assert_eq!(qualified.database, 0);
}
#[test]
fn the_rowid_spellings_resolve_unless_shadowed() {
let mut plain = table(b"t", 0);
assert!(plain.is_rowid_name(b"rowid"));
assert!(plain.is_rowid_name(b"_rowid_"));
assert!(plain.is_rowid_name(b"oid"));
assert!(!plain.is_rowid_name(b"id"));
if let Some(column) = plain.columns.first_mut() {
column.name = b"oid".to_vec();
column.folded = b"oid".to_vec();
}
assert!(!plain.is_rowid_name(b"oid"));
assert!(plain.is_rowid_name(b"rowid"));
let mut without = table(b"t", 0);
without.without_rowid = true;
assert!(!without.is_rowid_name(b"rowid"));
}
#[test]
fn a_missing_name_is_none() {
let catalog = StaticCatalog::empty();
assert!(catalog.find_table(None, b"nope").is_none());
assert!(catalog.find_table(Some(b"nodb"), b"t").is_none());
assert_eq!(catalog.database_name(99), b"");
assert_eq!(catalog.schema_cookie(99), 0);
}
}