use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::time::Instant;
use rudb_bind::{Bound, Parameters};
use rudb_catalog::{Catalog, Entry, QualifiedName, View};
use rudb_common::stat::Provenance;
use rudb_common::{
Cancel, Clustering, Error, Field, LogicalType, Memory, Result, Rule, Session, Value,
};
use rudb_io::{Filesystem, RealFilesystem};
use rudb_metrics::{Document, LoadProfile, Report, Span, Stage};
use rudb_native::graph::Edge;
use rudb_parse::ast::{self, Ast};
use rudb_pipeline::{Lease, Morsel, Pool, Progress, Sink, keep_pages};
use rudb_plan::{Expr, Node, Plan};
use rudb_vector::{Chunk, Form, Vector};
use crate::config::Config;
use crate::connection::{Connection, single};
use crate::prepared::Prepared;
use crate::result::QueryResult;
use crate::settings::Settings;
const MEMORY: &str = ":memory:";
#[derive(Debug, Clone)]
pub struct Database {
shared: Shared,
}
#[derive(Debug, Clone)]
pub(crate) struct Shared {
inner: Arc<Inner>,
}
#[derive(Debug)]
struct Inner {
catalog: RwLock<Catalog>,
writer: Mutex<()>,
#[cfg(test)]
loading: Mutex<Option<std::sync::mpsc::Sender<()>>>,
path: Option<PathBuf>,
writable: bool,
settings: Settings,
memory: Memory,
pool: Pool,
facts: Mutex<Arc<rudb_opt::estimate::Facts>>,
relationships: Mutex<(u64, String, Arc<Vec<rudb_opt::link::Linked>>)>,
declined: Mutex<BTreeSet<(String, bool)>>,
settings_revision: AtomicU64,
native_aggregate_plan: Mutex<Option<CachedNativeAggregate>>,
}
#[derive(Debug)]
struct CachedNativeAggregate {
sql: String,
catalog_generation: u64,
settings_revision: u64,
plan: Arc<Plan>,
}
impl Drop for Inner {
fn drop(&mut self) {
let Some(path) = self.path.as_ref().filter(|_| self.writable) else {
return;
};
let catalog = self.catalog.get_mut().unwrap_or_else(PoisonError::into_inner);
let _ = persist(path, catalog);
}
}
impl Default for Database {
fn default() -> Self {
Self::new()
}
}
fn runtime(config: &Config) -> Pool {
keep_pages();
Pool::new(config.threads())
}
impl Database {
#[must_use]
pub fn new() -> Self {
Self::with_config(Config::default())
}
#[must_use]
pub fn with_config(config: Config) -> Self {
let memory = Memory::new(config.memory_limit());
let pool = runtime(&config);
let writable = !config.read_only();
let settings = Settings::new(config);
let inner = Inner {
catalog: RwLock::new(Catalog::new()),
writer: Mutex::default(),
#[cfg(test)]
loading: Mutex::default(),
path: None,
writable,
settings,
memory,
pool,
facts: Mutex::default(),
relationships: Mutex::default(),
declined: Mutex::default(),
settings_revision: AtomicU64::new(0),
native_aggregate_plan: Mutex::default(),
};
Self { shared: Shared { inner: Arc::new(inner) } }
}
#[must_use]
pub fn config(&self) -> Config {
self.shared.inner.settings.config()
}
#[must_use]
pub fn opened_with(&self) -> Config {
self.shared.inner.settings.defaults()
}
pub fn setting(&self, name: &str) -> Result<String> {
if crate::settings::is_clustering(name) {
return Ok(self.shared.read().clustering());
}
self.shared.inner.settings.value(name)
}
#[must_use]
pub fn seams(&self) -> rudb_seam::Settings {
self.shared.inner.settings.seams()
}
pub fn seams_for(&self, sql: &str) -> Result<rudb_seam::Settings> {
self.shared.seams(sql)
}
#[must_use]
pub fn memory(&self) -> &Memory {
&self.shared.inner.memory
}
pub fn open(path: &str) -> Result<Self> {
Self::open_with(path, Config::default())
}
pub fn open_with(path: &str, config: Config) -> Result<Self> {
if path.is_empty() || path == MEMORY {
return Ok(Self::with_config(config));
}
let path = PathBuf::from(path);
let mut catalog = Catalog::new();
if path.exists() {
let native = rudb_native::Catalog::open(&path)?;
let names = native.names().map(str::to_string).collect::<Vec<_>>();
for name in names {
catalog.create_native_table(native.table(&name)?)?;
}
let views = native.views().cloned().collect::<Vec<_>>();
for view in &views {
catalog.create_native_view(view)?;
}
}
let memory = Memory::new(config.memory_limit());
let pool = runtime(&config);
let writable = !config.read_only();
let settings = Settings::new(config);
let inner = Inner {
catalog: RwLock::new(catalog),
writer: Mutex::default(),
#[cfg(test)]
loading: Mutex::default(),
path: Some(path),
writable,
settings,
memory,
pool,
facts: Mutex::default(),
relationships: Mutex::default(),
declined: Mutex::default(),
settings_revision: AtomicU64::new(0),
native_aggregate_plan: Mutex::default(),
};
Ok(Self { shared: Shared { inner: Arc::new(inner) } })
}
#[must_use]
pub fn connect(&self) -> Connection {
Connection::new(self.shared.clone())
}
pub fn close(self) -> Result<()> {
let Some(path) = self.shared.inner.path.as_ref().filter(|_| self.shared.inner.writable)
else {
return Ok(());
};
let path = path.clone();
let _writing = self.shared.writing();
persist(&path, &mut self.shared.write())
}
pub fn prepare(&self, sql: &str) -> Result<Prepared> {
Prepared::new(self.shared.clone(), sql).map_err(|error| self.shared.process_error(error))
}
pub fn with_catalog<T>(&self, read: impl FnOnce(&Catalog) -> T) -> T {
read(&self.shared.read())
}
pub fn with_catalog_mut<T>(&self, write: impl FnOnce(&mut Catalog) -> T) -> T {
let _writing = self.shared.writing();
write(&mut self.shared.write())
}
pub fn create_table(&self, name: &str, columns: Vec<Field>) -> Result<()> {
let parts: Vec<&str> = name.split('.').collect();
let _writing = self.shared.writing();
let mut catalog = self.shared.write();
let resolved = catalog.resolve_for_create(&parts)?;
catalog.create_table(resolved, columns)
}
pub fn drop_table(&self, name: &str) -> Result<()> {
let parts: Vec<&str> = name.split('.').collect();
let _writing = self.shared.writing();
let mut catalog = self.shared.write();
let resolved = catalog.resolve(&parts)?;
catalog.drop_table(&resolved)
}
pub fn append(&self, name: &str, rows: &[Vec<Value>]) -> Result<()> {
let parts: Vec<&str> = name.split('.').collect();
let _writing = self.shared.writing();
let mut catalog = self.shared.write();
let resolved = catalog.resolve(&parts)?;
catalog.table_mut(&resolved)?.append_rows(rows)
}
pub fn table_len(&self, name: &str) -> Result<usize> {
let parts: Vec<&str> = name.split('.').collect();
let catalog = self.shared.read();
let resolved = catalog.resolve(&parts)?;
Ok(catalog.table(&resolved)?.rows().len())
}
#[must_use]
pub fn table_names(&self) -> Vec<String> {
self.shared.read().tables().map(|table| table.name().table.clone()).collect()
}
pub fn table_sql(&self, name: &str) -> Result<String> {
let parts: Vec<&str> = name.split('.').collect();
let catalog = self.shared.read();
let resolved = catalog.resolve(&parts)?;
let table = catalog.table(&resolved)?;
let columns: Vec<String> = table
.columns()
.iter()
.map(|field| {
let null = if field.not_null { " NOT NULL" } else { "" };
format!("{} {}{null}", field.name, field.ty)
})
.collect();
Ok(format!("CREATE TABLE {}({});", resolved.table, columns.join(", ")))
}
pub fn query(&self, sql: &str) -> Result<QueryResult> {
self.shared
.query(sql, &self.shared.token())
.map_err(|error| self.shared.process_error(error))
}
pub fn execute(&self, sql: &str) -> Result<QueryResult> {
self.shared
.execute(sql, &self.shared.token())
.map_err(|error| self.shared.process_error(error))
}
pub fn plan(&self, sql: &str) -> Result<String> {
self.shared.plan(sql).map_err(|error| self.shared.process_error(error))
}
pub fn value(&self, sql: &str) -> Result<Value> {
single(&self.query(sql)?)
}
}
fn persist(path: &Path, catalog: &mut Catalog) -> Result<()> {
let names = catalog.stored_tables().map(|table| table.name().clone()).collect::<Vec<_>>();
let views = views(catalog);
let clean = catalog
.stored_tables()
.all(|table| table.rows().is_native() && table.clustering_is_stored());
let held = committed(path)?;
if clean
&& held
.as_ref()
.is_some_and(|held| held.tables == wanted(&names) && same_views(&held.views, &views))
{
return Ok(());
}
if names.is_empty() {
let temporary = scratch(path)?;
rudb_native::Writer::empty(&temporary, &views)?;
return rename(&temporary, path);
}
if clean && held.is_some_and(|held| held.tables == wanted(&names)) {
rudb_native::Writer::restate(path, &views)?;
return rebind(path, catalog, &names);
}
if appended(path, catalog, &names, &views)? {
return rebind(path, catalog, &names);
}
let temporary = scratch(path)?;
let mut writer: Option<rudb_native::Writer> = None;
for name in &names {
let table = catalog.table(name)?;
let fields = table.columns().to_vec();
let columns = (0..fields.len()).collect::<Vec<_>>();
let mut open = match writer.take() {
None => rudb_native::Writer::create(&temporary, name.table.clone(), fields)?,
Some(writer) => writer.next(name.table.clone(), fields)?,
};
if let Some(clustering) = table.clustering() {
open = open.declare(clustering.clone())?;
}
for at in 0..table.rows().chunk_count() {
open.append(&table.rows().read(at, &columns)?)?;
}
writer = Some(open);
}
let writer = writer.ok_or_else(|| Error::internal("a catalog with tables wrote none"))?;
writer.with_views(views).finish()?;
rename(&temporary, path)?;
rebind(path, catalog, &names)
}
fn scratch(path: &Path) -> Result<PathBuf> {
let temporary = path.with_extension(format!("{}.tmp", std::process::id()));
if temporary.exists() {
std::fs::remove_file(&temporary).map_err(|error| Error::io(error.to_string()))?;
}
Ok(temporary)
}
fn rename(temporary: &Path, path: &Path) -> Result<()> {
publish(&RealFilesystem::new(), temporary, path)
}
pub(crate) fn publish(fs: &dyn Filesystem, temporary: &Path, path: &Path) -> Result<()> {
fs.rename(temporary, path)?;
fs.sync_dir(directory_of(path))
}
fn directory_of(path: &Path) -> &Path {
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => Path::new("."),
}
}
fn committed(path: &Path) -> Result<Option<Held>> {
if !path.exists() {
return Ok(None);
}
let held = rudb_native::Catalog::open(path)?;
Ok(Some(Held {
tables: held.names().map(str::to_string).collect(),
views: held.views().cloned().collect(),
}))
}
struct Held {
tables: BTreeSet<String>,
views: Vec<rudb_native::ViewEntry>,
}
fn held_rows(path: &Path) -> Result<Option<BTreeMap<String, usize>>> {
if !path.exists() {
return Ok(None);
}
let held = rudb_native::Catalog::open(path)?;
Ok(Some(held.rows().map(|(name, rows)| (name.to_string(), rows)).collect()))
}
fn wanted(names: &[QualifiedName]) -> BTreeSet<String> {
names.iter().map(|name| name.table.clone()).collect()
}
fn views(catalog: &Catalog) -> Vec<rudb_native::ViewEntry> {
catalog
.stored_views()
.map(|view| rudb_native::ViewEntry {
name: view.name().table.clone(),
sql: view.sql().to_string(),
statement: view.statement().to_string(),
aliases: view.aliases().to_vec(),
columns: view.columns(),
})
.collect()
}
fn same_views(held: &[rudb_native::ViewEntry], wanted: &[rudb_native::ViewEntry]) -> bool {
held.len() == wanted.len()
&& held.iter().zip(wanted).all(|(held, wanted)| {
held.name == wanted.name
&& held.sql == wanted.sql
&& held.statement == wanted.statement
&& held.aliases == wanted.aliases
})
}
fn index(path: &Path, catalog: &mut Catalog, links: &str) -> Result<()> {
let declared = rudb_graph::parse_links(links).unwrap_or_default();
if declared.is_empty() {
return Ok(());
}
let mut wanted: Vec<(QualifiedName, Vec<usize>)> = Vec::new();
for link in &declared {
let [column] = &link.parent.columns[..] else { continue };
let Some(table) = catalog
.tables()
.find(|table| table.name().table.eq_ignore_ascii_case(&link.parent.table))
else {
continue;
};
let Some(at) = table.column_index(column) else { continue };
let name = table.name().clone();
match wanted.iter_mut().find(|(held, _)| held == &name) {
Some((_, columns)) if columns.contains(&at) => {}
Some((_, columns)) => columns.push(at),
None => wanted.push((name, vec![at])),
}
}
if wanted.is_empty() {
return Ok(());
}
for (name, columns) in &wanted {
rudb_native::graph::build_key_maps(path, &name.table, columns)?;
}
let edges = edges_of(catalog, &declared);
let mut names = wanted.into_iter().map(|(name, _)| name).collect::<Vec<_>>();
if !edges.is_empty() {
rudb_native::graph::build_links(path, &edges)?;
for edge in &edges {
let Some(name) = catalog
.tables()
.find(|table| table.name().table == edge.child)
.map(|table| table.name().clone())
else {
continue;
};
if !names.contains(&name) {
names.push(name);
}
}
}
rebind(path, catalog, &names)
}
fn edges_of(catalog: &Catalog, declared: &[rudb_graph::Relationship]) -> Vec<Edge> {
let mut edges = Vec::new();
for link in declared {
let ([child], [parent]) = (&link.child.columns[..], &link.parent.columns[..]) else {
continue;
};
let find = |name: &str| {
catalog.tables().find(|table| table.name().table.eq_ignore_ascii_case(name))
};
let (Some(child_table), Some(parent_table)) =
(find(&link.child.table), find(&link.parent.table))
else {
continue;
};
let (Some(child_column), Some(parent_column)) =
(child_table.column_index(child), parent_table.column_index(parent))
else {
continue;
};
edges.push(Edge {
child: child_table.name().table.clone(),
child_column,
parent: parent_table.name().table.clone(),
parent_column,
});
}
edges
}
fn rebind(path: &Path, catalog: &mut Catalog, names: &[QualifiedName]) -> Result<()> {
let native = rudb_native::Catalog::open(path)?;
for name in names {
let reader = native.table(&name.table)?;
catalog.table_mut(name)?.rebind_native(reader)?;
}
Ok(())
}
fn appended(
path: &Path,
catalog: &mut Catalog,
names: &[QualifiedName],
views: &[rudb_native::ViewEntry],
) -> Result<bool> {
let Some(held) = committed(path)? else { return Ok(false) };
if held.tables.is_empty() {
return Ok(false);
}
let native = names
.iter()
.filter(|name| catalog.table(name).is_ok_and(|table| table.rows().is_native()))
.map(|name| name.table.clone())
.collect::<BTreeSet<_>>();
if held.tables != native {
return Ok(false);
}
let dirty =
names.iter().filter(|name| !native.contains(&name.table)).cloned().collect::<Vec<_>>();
let mut writer: Option<rudb_native::Writer> = None;
for name in &dirty {
let table = catalog.table(name)?;
let fields = table.columns().to_vec();
let columns = (0..fields.len()).collect::<Vec<_>>();
let mut open = match writer.take() {
None => rudb_native::Writer::open(path, name.table.clone(), fields)?,
Some(writer) => writer.next(name.table.clone(), fields)?,
};
if let Some(clustering) = table.clustering() {
open = open.declare(clustering.clone())?;
}
for at in 0..table.rows().chunk_count() {
open.append(&table.rows().read(at, &columns)?)?;
}
writer = Some(open);
}
let Some(writer) = writer else { return Ok(false) };
writer.with_views(views.to_vec()).finish()?;
Ok(true)
}
fn appendable(path: &Path, catalog: &Catalog, target: &QualifiedName) -> Result<bool> {
let Some(held) = held_rows(path)? else { return Ok(false) };
if held.get(&target.table).is_some_and(|rows| *rows > 0) {
return Ok(false);
}
let carried =
held.keys().filter(|name| *name != &target.table).cloned().collect::<BTreeSet<_>>();
let others = catalog.stored_tables().filter(|table| table.name() != target).count();
let native = catalog
.stored_tables()
.filter(|table| table.name() != target && table.rows().is_native())
.map(|table| table.name().table.clone())
.collect::<BTreeSet<_>>();
Ok(carried == native && others == native.len())
}
#[derive(Debug, Default)]
struct NativePlace {
morsel: u64,
chunk: u64,
held: Vec<((u64, u64), Chunk)>,
started: Option<Span>,
inside_wall: u64,
inside_cpu: u64,
rows: u64,
bytes: u64,
}
impl NativePlace {
fn start(&mut self) {
if self.started.is_none() {
self.started = Some(Span::start());
}
}
}
fn declared(
writer: rudb_native::Writer,
clustering: Option<Clustering>,
) -> Result<rudb_native::Writer> {
match clustering {
None => Ok(writer),
Some(clustering) => writer.declare(clustering),
}
}
const GATHER_ROWS: usize = 131_072;
#[derive(Debug)]
struct NativeSink {
writer: Mutex<Option<rudb_native::Writer>>,
preparer: rudb_native::Preparer,
temporary: Option<PathBuf>,
target: PathBuf,
table: String,
fields: Vec<Field>,
profile: Arc<LoadProfile>,
}
impl NativeSink {
fn create(
target: &Path,
name: String,
fields: Vec<Field>,
clustering: Option<Clustering>,
) -> Result<Self> {
let temporary = target.with_extension(format!("{}.tmp", std::process::id()));
if temporary.exists() {
std::fs::remove_file(&temporary).map_err(|error| Error::io(error.to_string()))?;
}
let profile = LoadProfile::begin(name.clone());
let writer = rudb_native::Writer::create(&temporary, name.clone(), fields.clone())?
.with_profile(Arc::clone(&profile));
let writer = declared(writer, clustering)?;
Ok(Self {
preparer: writer.preparer(),
writer: Mutex::new(Some(writer)),
temporary: Some(temporary),
target: target.to_path_buf(),
table: name,
fields,
profile,
})
}
fn open(
target: &Path,
name: String,
fields: Vec<Field>,
clustering: Option<Clustering>,
) -> Result<Self> {
let profile = LoadProfile::begin(name.clone());
let writer = rudb_native::Writer::open(target, name.clone(), fields.clone())?
.with_profile(Arc::clone(&profile));
let writer = declared(writer, clustering)?;
Ok(Self {
preparer: writer.preparer(),
writer: Mutex::new(Some(writer)),
temporary: None,
target: target.to_path_buf(),
table: name,
fields,
profile,
})
}
fn locked<T>(&self, work: impl FnOnce(&mut rudb_native::Writer) -> Result<T>) -> Result<T> {
let waiting = Instant::now();
let mut writer =
self.writer.lock().map_err(|_| Error::internal("native writer panicked"))?;
self.profile.waited(Stage::Write, elapsed_ns(waiting));
work(
writer
.as_mut()
.ok_or_else(|| Error::internal("native writer was already committed"))?,
)
}
fn hand_over(&self, place: &mut NativePlace) -> Result<()> {
if place.held.is_empty() {
return Ok(());
}
let parts = std::mem::take(&mut place.held);
place.bytes = parts
.iter()
.fold(place.bytes, |bytes, (_, chunk)| bytes.saturating_add(chunk.footprint() as u64));
let inside = Span::start();
let appended = self.preparer.prepare(parts).and_then(|prepared| {
let merged = self.locked(|writer| writer.merge(prepared))?;
let paged = merged.pages()?;
self.locked(|writer| writer.write(paged))
});
let (wall, cpu) = inside.stop();
place.inside_wall = place.inside_wall.saturating_add(wall);
place.inside_cpu = place.inside_cpu.saturating_add(cpu);
appended
}
}
impl Sink for NativeSink {
type Local = NativePlace;
fn parallel(&self) -> bool {
true
}
fn local(&self) -> Self::Local {
NativePlace::default()
}
fn gather(&self) -> usize {
GATHER_ROWS
}
fn at(&self, morsel: &Morsel, place: &mut Self::Local) -> Result<()> {
place.start();
self.hand_over(place)?;
place.morsel = morsel.index();
place.chunk = 0;
Ok(())
}
fn sink(&self, chunk: &Chunk, place: &mut Self::Local) -> Result<Progress> {
for (at, field) in self.fields.iter().enumerate().filter(|(_, field)| field.not_null) {
let vector = chunk.column(at)?;
let null = match vector.form() {
Form::Dictionary | Form::Rle => (0..vector.len()).any(|row| vector.is_null_at(row)),
_ => vector.validity().has_nulls(vector.len()),
};
if null {
return Err(Error::constraint(format!(
"NOT NULL constraint failed: {}.{}",
self.table, field.name
)));
}
}
place.start();
place.rows = place.rows.saturating_add(chunk.len() as u64);
place.held.push(((place.morsel, place.chunk), chunk.clone()));
place.chunk = place.chunk.saturating_add(1);
if place.held.len() == rudb_native::STRIPE_PARTS {
self.hand_over(place)?;
}
Ok(Progress::More)
}
fn combine(&self, mut local: Self::Local) -> Result<()> {
let handed = self.hand_over(&mut local);
if let Some(started) = local.started.take() {
let (wall, cpu) = started.stop();
self.profile.charge(
Stage::Convert,
wall.saturating_sub(local.inside_wall),
cpu.saturating_sub(local.inside_cpu),
);
self.profile.moved(Stage::Convert, 0, local.bytes, local.rows);
}
handed
}
fn finalize(&self, _threads: &Lease<'_>) -> Result<()> {
let writer = self
.writer
.lock()
.map_err(|_| Error::internal("native writer panicked"))?
.take()
.ok_or_else(|| Error::internal("native writer was already committed"))?;
writer.finish()?;
let Some(temporary) = &self.temporary else {
self.profile.finish();
return Ok(());
};
let renamed = {
let _timing = self.profile.span(Stage::Publish);
publish(&RealFilesystem::new(), temporary, &self.target)
};
self.profile.finish();
renamed
}
}
impl Drop for NativeSink {
fn drop(&mut self) {
self.profile.finish();
}
}
fn elapsed_ns(since: Instant) -> u64 {
u64::try_from(since.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
impl Shared {
pub(crate) fn process_error(&self, error: Error) -> Error {
if self.session().semantics().errors_as_json() { error.into_json() } else { error }
}
fn read(&self) -> RwLockReadGuard<'_, Catalog> {
self.inner.catalog.read().unwrap_or_else(PoisonError::into_inner)
}
fn write(&self) -> RwLockWriteGuard<'_, Catalog> {
self.inner.catalog.write().unwrap_or_else(PoisonError::into_inner)
}
fn writing(&self) -> MutexGuard<'_, ()> {
self.inner.writer.lock().unwrap_or_else(PoisonError::into_inner)
}
fn budget(&self) -> Budget<'_> {
Budget { memory: &self.inner.memory, pool: &self.inner.pool }
}
pub(crate) fn session(&self) -> Session {
self.inner.settings.session()
}
pub(crate) fn query(&self, sql: &str, cancel: &Cancel) -> Result<QueryResult> {
if let Some(answer) = self.cached_native_aggregate(sql, cancel)? {
return Ok(answer);
}
self.query_mirrored(sql, cancel, true)
}
fn cached_native_aggregate(&self, sql: &str, cancel: &Cancel) -> Result<Option<QueryResult>> {
let catalog = self.read();
let revision = self.inner.settings_revision.load(Ordering::Relaxed);
let cached = self
.inner
.native_aggregate_plan
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.filter(|cached| {
cached.sql == sql
&& cached.catalog_generation == catalog.generation()
&& cached.settings_revision == revision
})
.map(|cached| Arc::clone(&cached.plan));
let Some(plan) = cached else { return Ok(None) };
let seams = self.seams(sql)?;
let context = self.optimizer(&catalog)?;
let session = self.session();
let under = Under::new(self.budget(), context.facts(), &seams, &session, Rows::ForACaller);
run(sql, &plan, &catalog, cancel, under).map(Some)
}
fn remember_native_aggregate(&self, sql: &str, ast: &Ast, plan: &Plan, catalog: &Catalog) {
if !is_native_summary_aggregate(ast, plan, catalog) {
return;
}
*self.inner.native_aggregate_plan.lock().unwrap_or_else(PoisonError::into_inner) =
Some(CachedNativeAggregate {
sql: sql.to_string(),
catalog_generation: catalog.generation(),
settings_revision: self.inner.settings_revision.load(Ordering::Relaxed),
plan: Arc::new(plan.clone()),
});
}
fn query_mirrored(&self, sql: &str, cancel: &Cancel, mirror: bool) -> Result<QueryResult> {
let catalog = self.read();
let seams = self.seams(sql)?;
let context = self.optimizer(&catalog)?;
let session = self.session();
let (ast, parse_ns) =
timed(|| rudb_parse::parse_ast_with_case(sql, session.semantics().identifier_case()))?;
let outlined = mirror && self.inner.settings.config().parquet_mirror();
let (bound, bind_ns) = timed(|| {
if outlined {
rudb_bind::bind_statement_outlined(&ast, &catalog, &Parameters::new(), &session)
} else {
rudb_bind::bind_statement_with(&ast, &catalog, &Parameters::new(), &session)
}
})?;
if mirror {
let wanted = self.wanted_mirrors(&bound);
if !wanted.is_empty() || (outlined && asked_for_mirrors(&bound)) {
drop(bound);
drop(catalog);
self.mirror(&wanted);
return self.query_mirrored(sql, cancel, false);
}
}
match bound {
Bound::Query(mut plan) => {
let ((), optimize_ns) = timed(|| rudb_opt::optimize_with(&mut plan, &context))?;
self.remember_native_aggregate(sql, &ast, &plan, &catalog);
let budget = self.budget();
let under = Under::new(budget, context.facts(), &seams, &session, Rows::ForACaller)
.after(Planning { parse_ns, bind_ns, optimize_ns });
run(sql, &plan, &catalog, cancel, under)
}
Bound::Explain { mut plan, analyze, statistics } => {
let ((), optimize_ns) = timed(|| rudb_opt::optimize_with(&mut plan, &context))?;
let seams = rudb_opt::explain::Seams::new(&seams, rudb_exec::registries());
explaining(
&plan,
&catalog,
cancel,
self.budget(),
&context,
seams,
&session,
Asked { analyze, statistics },
sql,
Planning { parse_ns, bind_ns, optimize_ns },
)
}
_ => Err(Error::not_implemented("a statement that is not a query, on the query path")),
}
}
fn wanted_mirrors(&self, bound: &Bound) -> Vec<(String, bool)> {
let Bound::Query(plan) = bound else { return Vec::new() };
if plan.wanted_mirrors().is_empty() {
return Vec::new();
}
let config = self.inner.settings.config();
if !config.parquet_mirror() {
return Vec::new();
}
let declined = self.inner.declined.lock().unwrap_or_else(PoisonError::into_inner);
plan.wanted_mirrors()
.iter()
.filter(|(_, _, rows)| *rows >= config.mirror_rows())
.map(|(path, binary_as_string, _)| (path.clone(), *binary_as_string))
.filter(|wanted| !declined.contains(wanted))
.collect()
}
fn mirror(&self, wanted: &[(String, bool)]) {
let config = self.inner.settings.config();
for (path, binary_as_string) in wanted {
let added = crate::mirror::ensure(path, *binary_as_string, config).and_then(|found| {
let Some((stamp, reader)) = found else { return Ok(false) };
self.write().add_mirror(path, *binary_as_string, stamp, reader)?;
Ok(true)
});
if !matches!(added, Ok(true)) {
self.inner
.declined
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert((path.clone(), *binary_as_string));
}
}
}
pub(crate) fn seams(&self, sql: &str) -> Result<rudb_seam::Settings> {
let mut seams = self.inner.settings.seams();
for hint in rudb_parse::hints(sql)? {
seams.hint(hint)?;
}
Ok(seams)
}
fn optimizer(&self, catalog: &Catalog) -> Result<rudb_opt::pass::Context> {
let mut context =
rudb_opt::pass::Context::without(&self.inner.settings.disabled_optimizers())?;
context.measure(self.estimates(catalog));
context.relate(self.relationships(catalog));
context.size(self.inner.settings.sizes());
context.govern(self.inner.settings.rules());
Ok(context)
}
fn relationships(&self, catalog: &Catalog) -> Arc<Vec<rudb_opt::link::Linked>> {
if !self.inner.settings.rules().enabled(Rule::GraphSections) {
return Arc::default();
}
let declared = self.inner.settings.links();
if declared.is_empty() {
return Arc::default();
}
let generation = catalog.generation();
let mut held = match self.inner.relationships.lock() {
Ok(held) => held,
Err(poisoned) => poisoned.into_inner(),
};
if held.0 != generation || held.1 != declared {
let found = Arc::new(Self::related(catalog, &declared));
*held = (generation, declared, found);
}
Arc::clone(&held.2)
}
fn related(catalog: &Catalog, declared: &str) -> Vec<rudb_opt::link::Linked> {
let mut found = Vec::new();
for link in rudb_graph::parse_links(declared).unwrap_or_default() {
let ([child_key], [parent_key]) = (&link.child.columns[..], &link.parent.columns[..])
else {
continue;
};
let built = stored_link(
catalog,
(&link.child.table, child_key),
(&link.parent.table, parent_key),
);
let sides = (&link.child.table, child_key, &link.parent.table, parent_key);
found.push(match built {
Some(true) => rudb_opt::link::Linked::verified(sides.0, sides.1, sides.2, sides.3),
Some(false) => rudb_opt::link::Linked::built(sides.0, sides.1, sides.2, sides.3),
None => rudb_opt::link::Linked::declared(sides.0, sides.1, sides.2, sides.3),
});
}
found
}
fn facts(&self, catalog: &Catalog) -> Arc<rudb_opt::estimate::Facts> {
let generation = catalog.generation();
let mut held = match self.inner.facts.lock() {
Ok(held) => held,
Err(poisoned) => poisoned.into_inner(),
};
if held.generation() != generation {
*held = Arc::new(Self::measured(catalog, generation));
}
Arc::clone(&held)
}
fn estimates(&self, catalog: &Catalog) -> Arc<rudb_opt::estimate::Facts> {
let held = self.facts(catalog);
if self.inner.settings.rules().enabled(Rule::StatsAll) {
return held;
}
Arc::new(held.without_distincts())
}
fn measured(catalog: &Catalog, generation: u64) -> rudb_opt::estimate::Facts {
let mut facts = rudb_opt::estimate::Facts::at(generation);
for table in catalog.tables().chain(catalog.mirrored_tables()) {
let name = table.name();
let rows = u64::try_from(table.rows().len()).unwrap_or(u64::MAX);
facts.record(&name.catalog, &name.schema, &name.table, rows);
let provenance = match table.rows() {
rudb_catalog::table::Rows::Memory(_) => Provenance::Sketch,
rudb_catalog::table::Rows::Native(_) => Provenance::Dictionary,
rudb_catalog::table::Rows::Grown(_, _) => Provenance::Sketch,
};
for (at, column) in table.columns().iter().enumerate() {
let Ok(Some(distinct)) = table.rows().distinct_values(at) else {
continue;
};
facts.record_distinct(
&name.catalog,
&name.schema,
&name.table,
&column.name,
distinct,
provenance,
);
}
}
facts
}
pub(crate) fn timeout(&self) -> Option<std::time::Duration> {
self.inner.settings.config().query_timeout()
}
pub(crate) fn token(&self) -> Cancel {
match self.inner.settings.config().query_timeout() {
Some(timeout) => Cancel::after(timeout),
None => Cancel::new(),
}
}
pub(crate) fn plan(&self, sql: &str) -> Result<String> {
let catalog = self.read();
let context = self.optimizer(&catalog)?;
Ok(planned(sql, &catalog, &context, &self.session())?.to_string())
}
pub(crate) fn execute(&self, sql: &str, cancel: &Cancel) -> Result<QueryResult> {
if let Some(answer) = self.cached_native_aggregate(sql, cancel)? {
return Ok(answer);
}
let session = self.session();
let (ast, parse_ns) =
timed(|| rudb_parse::parse_ast_with_case(sql, session.semantics().identifier_case()))?;
self.execute_ast(&ast, sql, &Parameters::new(), cancel, parse_ns)
}
pub(crate) fn execute_ast(
&self,
ast: &Ast,
sql: &str,
parameters: &Parameters,
cancel: &Cancel,
parse_ns: u64,
) -> Result<QueryResult> {
let _writing = self.writing();
self.execute_mirrored(ast, sql, parameters, cancel, parse_ns, true)
}
fn execute_mirrored(
&self,
ast: &Ast,
sql: &str,
parameters: &Parameters,
cancel: &Cancel,
parse_ns: u64,
mirror: bool,
) -> Result<QueryResult> {
let seams = self.seams(sql)?;
let mut catalog = self.write();
let context = self.optimizer(&catalog)?;
let session = self.session();
let outlined = mirror && self.inner.settings.config().parquet_mirror();
let (bound, bind_ns) = timed(|| {
if outlined {
rudb_bind::bind_statement_outlined(ast, &catalog, parameters, &session)
} else {
rudb_bind::bind_statement_with(ast, &catalog, parameters, &session)
}
})?;
if mirror {
let wanted = self.wanted_mirrors(&bound);
if !wanted.is_empty() || (outlined && asked_for_mirrors(&bound)) {
drop(bound);
drop(catalog);
self.mirror(&wanted);
return self.execute_mirrored(ast, sql, parameters, cancel, parse_ns, false);
}
}
match bound {
Bound::Query(mut plan) => {
let ((), optimize_ns) = timed(|| rudb_opt::optimize_with(&mut plan, &context))?;
if parameters.is_empty() {
self.remember_native_aggregate(sql, ast, &plan, &catalog);
}
let budget = self.budget();
let under = Under::new(budget, context.facts(), &seams, &session, Rows::ForACaller)
.after(Planning { parse_ns, bind_ns, optimize_ns });
run(sql, &plan, &catalog, cancel, under)
}
Bound::Explain { mut plan, analyze, statistics } => {
let ((), optimize_ns) = timed(|| rudb_opt::optimize_with(&mut plan, &context))?;
let seams = rudb_opt::explain::Seams::new(&seams, rudb_exec::registries());
explaining(
&plan,
&catalog,
cancel,
self.budget(),
&context,
seams,
&session,
Asked { analyze, statistics },
sql,
Planning { parse_ns, bind_ns, optimize_ns },
)
}
Bound::Setting(setting) if setting.pragma => {
self.inner.settings.toggle(&setting.name)?;
self.inner.settings_revision.fetch_add(1, Ordering::Relaxed);
Ok(QueryResult::empty())
}
Bound::Setting(setting) => {
let value = setting.value.as_ref();
self.inner.settings.apply(
&self.inner.memory,
&self.inner.pool,
&mut catalog,
&setting.name,
setting.scope,
value,
)?;
self.inner.settings_revision.fetch_add(1, Ordering::Relaxed);
Ok(QueryResult::empty())
}
Bound::Checkpoint => {
if let Some(path) = self.inner.path.as_ref().filter(|_| self.inner.writable) {
persist(path, &mut catalog)?;
index(path, &mut catalog, &self.inner.settings.links())?;
}
Ok(QueryResult::empty())
}
Bound::CreateTable(mut create) => {
let writable = self.inner.writable && !create.name.temporary();
if let Some(path) = self.inner.path.as_ref().filter(|_| writable) {
let fresh = create.source.is_some() && catalog.table(&create.name).is_err();
let alone = fresh && !path.exists() && catalog.stored_tables().count() == 0;
if fresh && (alone || appendable(path, &catalog, &create.name)?) {
let plan = create.source.as_mut().expect("a source, asked for above");
rudb_opt::optimize_with(plan, &context)?;
let table = create.name.table.clone();
let fields = create.columns.clone();
let sink = Arc::new(if alone {
NativeSink::create(path, table.clone(), fields, None)?
} else {
NativeSink::open(path, table.clone(), fields, None)?
});
drop(catalog);
let reading = self.read();
#[cfg(test)]
if let Some(told) = self.inner.loading.lock().unwrap().take() {
let _ = told.send(());
}
let query = rudb_exec::build_measured_into(
plan,
&reading,
cancel,
&self.inner.memory,
&seams,
&session,
sink,
)?;
query.run(cancel, &self.inner.pool)?;
drop(query);
drop(reading);
let reader = rudb_native::Catalog::open(path)?.table(&table)?;
let mut catalog = self.write();
catalog.create_table(create.name.clone(), create.columns)?;
catalog.table_mut(&create.name)?.commit_native(reader)?;
return Ok(QueryResult::empty());
}
}
create_table(
sql,
create,
&mut catalog,
cancel,
self.budget(),
&context,
&seams,
&session,
)?;
Ok(QueryResult::empty())
}
Bound::CreateView(create) => {
create_view(create, &mut catalog)?;
Ok(QueryResult::empty())
}
Bound::DropTable(drop) => {
for name in &drop.names {
match drop.kind {
Entry::Table => catalog.drop_table(name)?,
Entry::View => catalog.drop_view(name)?,
}
}
Ok(QueryResult::empty())
}
Bound::Insert(mut insert) => {
let ((), optimize_ns) =
timed(|| rudb_opt::optimize_with(&mut insert.source, &context))?;
let writable = self.inner.writable && !insert.name.temporary();
if let Some(path) = self.inner.path.as_ref().filter(|_| writable) {
let target = catalog.table(&insert.name)?;
let alone = !path.exists() && catalog.stored_tables().count() == 1;
let empty = target.rows().is_empty();
if empty && (alone || appendable(path, &catalog, &insert.name)?) {
let table = target.name().table.clone();
let fields = target.columns().to_vec();
let clustering = target.clustering().cloned();
let sink = Arc::new(if alone {
NativeSink::create(path, table.clone(), fields, clustering)?
} else {
NativeSink::open(path, table.clone(), fields, clustering)?
});
let query = rudb_exec::build_measured_into(
&insert.source,
&catalog,
cancel,
&self.inner.memory,
&seams,
&session,
sink,
)?;
query.run(cancel, &self.inner.pool)?;
drop(query);
let reader = rudb_native::Catalog::open(path)?.table(&table)?;
catalog.table_mut(&insert.name)?.commit_native(reader)?;
return Ok(QueryResult::empty());
}
}
let facts = context.facts();
let under = Under::new(self.budget(), facts, &seams, &session, Rows::ForATable)
.after(Planning { parse_ns, bind_ns, optimize_ns });
let result = run(sql, &insert.source, &catalog, cancel, under)?;
let workers = self.inner.pool.threads();
catalog.table_mut(&insert.name)?.append_all(result.into_chunks(), workers)?;
Ok(QueryResult::empty())
}
}
}
}
fn planned(
sql: &str,
catalog: &Catalog,
context: &rudb_opt::pass::Context,
session: &Session,
) -> Result<Plan> {
let mut plan = rudb_bind::bind_sql_with(sql, catalog, session)?;
rudb_opt::optimize_with(&mut plan, context)?;
Ok(plan)
}
fn stored_link(catalog: &Catalog, child: (&str, &str), parent: (&str, &str)) -> Option<bool> {
let child_table = table_named(catalog, child.0)?;
let parent_table = table_named(catalog, parent.0)?;
let (
rudb_catalog::table::Rows::Native(child_rows),
rudb_catalog::table::Rows::Native(parent_rows),
) = (child_table.rows(), parent_table.rows())
else {
return None;
};
let (Some(child_column), Some(parent_column)) =
(child_table.column_index(child.1), parent_table.column_index(parent.1))
else {
return None;
};
let edge = Edge {
child: child_table.name().table.clone(),
child_column,
parent: parent_table.name().table.clone(),
parent_column,
};
let held = rudb_native::graph::stored_link(child_rows, parent_rows, &edge)?;
Some(held.linked() == held.children())
}
fn table_named<'a>(catalog: &'a Catalog, name: &str) -> Option<&'a rudb_catalog::Table> {
catalog.tables().into_iter().find(|table| table.name().table.eq_ignore_ascii_case(name))
}
#[derive(Clone, Copy)]
struct Budget<'a> {
memory: &'a Memory,
pool: &'a Pool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Rows {
ForACaller,
ForATable,
}
#[derive(Clone, Copy, Default, Debug)]
struct Planning {
parse_ns: u64,
bind_ns: u64,
optimize_ns: u64,
}
impl Planning {
fn total_ns(self) -> u64 {
self.parse_ns.saturating_add(self.bind_ns).saturating_add(self.optimize_ns)
}
}
fn asked_for_mirrors(bound: &Bound) -> bool {
matches!(bound, Bound::Query(plan) if !plan.wanted_mirrors().is_empty())
}
fn timed<T>(what: impl FnOnce() -> Result<T>) -> Result<(T, u64)> {
let span = Span::start();
let out = what()?;
Ok((out, span.stop().0))
}
#[derive(Clone, Copy)]
struct Under<'a> {
budget: Budget<'a>,
facts: &'a rudb_opt::estimate::Facts,
seams: &'a rudb_seam::Settings,
session: &'a Session,
going: Rows,
planning: Planning,
}
impl<'a> Under<'a> {
fn new(
budget: Budget<'a>,
facts: &'a rudb_opt::estimate::Facts,
seams: &'a rudb_seam::Settings,
session: &'a Session,
going: Rows,
) -> Self {
Self { budget, facts, seams, session, going, planning: Planning::default() }
}
fn after(mut self, planning: Planning) -> Self {
self.planning = planning;
self
}
}
fn is_native_summary_aggregate(ast: &Ast, plan: &Plan, catalog: &Catalog) -> bool {
let Node::Project { input, exprs, .. } = *plan.node(plan.root()) else { return false };
let Node::Aggregate { input, index, groups, aggregates } = *plan.node(input) else {
return false;
};
let projected = plan.expr_list(exprs);
let aggregates = plan.expr_list(aggregates);
if projected.is_empty()
|| projected.len() != aggregates.len()
|| !plan.expr_list(groups).is_empty()
|| !projected.iter().enumerate().all(|(position, expr)| {
matches!(plan.expr(*expr), Expr::Column(column)
if column.table == index && column.column as usize == position)
})
{
return false;
}
let filtered = matches!(plan.node(input), Node::Filter { .. });
let input = match *plan.node(input) {
Node::Filter { input, .. } if simple_literal_filter(ast) => input,
Node::Filter { .. } => return false,
_ => input,
};
let Node::Get { catalog: source_catalog, schema, table, index: source_index, columns, .. } =
*plan.node(input)
else {
return false;
};
let direct_aggregate = |expr, expected, arguments| {
let Expr::Aggregate { name, args, distinct: false, filter: None } = plan.expr(expr) else {
return false;
};
if plan.string(*name) != expected {
return false;
}
let args = plan.expr_list(*args);
args.len() == arguments
&& args.iter().all(|arg| {
matches!(plan.expr(*arg), Expr::Column(column) if column.table == source_index)
})
};
let supported = match aggregates {
[aggregate] => {
(direct_aggregate(*aggregate, "count_star", 0)
&& (filtered || plan.field_list(columns).is_empty()))
|| (!filtered && direct_aggregate(*aggregate, "avg", 1))
}
[sum, count, avg] if !filtered => {
direct_aggregate(*sum, "sum", 1)
&& direct_aggregate(*count, "count_star", 0)
&& direct_aggregate(*avg, "avg", 1)
}
_ => false,
};
if !supported {
return false;
}
let source =
QualifiedName::new(plan.string(source_catalog), plan.string(schema), plan.string(table));
catalog.table(&source).is_ok_and(|table| table.rows().is_native())
}
fn simple_literal_filter(ast: &Ast) -> bool {
let [ast::Statement::Query(reference)] = ast.statements.as_slice() else { return false };
let query = ast.query(*reference);
if !query.ctes.is_empty()
|| !query.order_by.is_empty()
|| query.order_by_all
|| query.limit != rudb_parse::NONE
|| query.offset != rudb_parse::NONE
{
return false;
}
let ast::QueryBody::Select(reference) = query.body else { return false };
let select = ast.select(reference);
if select.distinct != ast::Distinct::No
|| !select.group_by.is_empty()
|| select.group_by_all
|| select.filter == rudb_parse::NONE
|| select.having != rudb_parse::NONE
{
return false;
}
let [source] = ast.source_list(select.from) else { return false };
if !matches!(ast.source(*source), ast::Source::Table { .. }) {
return false;
}
let ast::Expr::Binary { op: ast::BinaryOp::NotEq, left, right } = ast.expr(select.filter)
else {
return false;
};
matches!(ast.expr(left), ast::Expr::Column { .. })
&& matches!(ast.expr(right), ast::Expr::Literal { kind: ast::LiteralKind::Number, .. })
}
fn run(
sql: &str,
plan: &Plan,
catalog: &Catalog,
cancel: &Cancel,
under: Under<'_>,
) -> Result<QueryResult> {
let Under { budget: Budget { memory, pool }, facts, seams, session, going, planning } = under;
memory.forget_peak();
let report = Report::new();
let building = Span::start();
let query = rudb_exec::build_measured(plan, catalog, cancel, memory, seams, session, &report)?;
let (built_wall, built_cpu) = building.stop();
if going == Rows::ForACaller {
query.for_a_caller();
}
let names = query.schema().names();
let types = query.schema().types();
let mut held = memory.reservation();
let mut chunks = Vec::new();
let driving = Span::start();
query.run(cancel, pool)?;
while let Some(chunk) = query.next_chunk()? {
if chunk.is_empty() {
continue;
}
let chunk = match going {
Rows::ForACaller => chunk.into_flat()?,
Rows::ForATable => chunk,
};
held.grow(u64::try_from(chunk.footprint()).unwrap_or(u64::MAX))?;
chunks.push(chunk);
}
let (ran_wall, ran_cpu) = driving.stop();
let mut metrics = Document::new(sql);
metrics.settings.memory_limit = memory.limit();
metrics.settings.threads = u32::try_from(pool.threads()).unwrap_or(u32::MAX);
metrics.timing.parse_ns = planning.parse_ns;
metrics.timing.bind_ns = planning.bind_ns;
metrics.timing.optimize_ns = planning.optimize_ns;
metrics.timing.physical_ns = built_wall;
metrics.timing.execute_ns = ran_wall;
metrics.timing.total_ns =
planning.total_ns().saturating_add(built_wall).saturating_add(ran_wall);
let ran_cpu = ran_cpu.saturating_add(query.worker_cpu_ns());
metrics.resource.cpu_ns = built_cpu.saturating_add(ran_cpu);
metrics.resource.build_cpu_ns = built_cpu;
metrics.resource.peak_bytes = memory.peak();
report.fill(&mut metrics);
rudb_opt::explain::record_estimates(plan, facts, &mut metrics);
Ok(QueryResult::new(names, types, chunks, held).in_session(session.clone()).measured(metrics))
}
#[allow(clippy::too_many_arguments)]
fn explaining(
plan: &Plan,
catalog: &Catalog,
cancel: &Cancel,
budget: Budget<'_>,
context: &rudb_opt::pass::Context,
seams: rudb_opt::explain::Seams<'_>,
session: &Session,
asked: Asked,
sql: &str,
planning: Planning,
) -> Result<QueryResult> {
let facts = context.facts();
let statistics = asked.statistics();
if !asked.analyze {
let text = rudb_opt::explain::explain_with(plan, context, seams, statistics);
return explained("logical_plan", &text);
}
let mut profiled = session.clone();
profiled.set("enable_profiling", "query_tree");
let under =
Under::new(budget, facts, seams.settings(), &profiled, Rows::ForACaller).after(planning);
let result = run(sql, plan, catalog, cancel, under)?;
let measured = result.metrics().expect("a query that ran reports what it did");
let text = rudb_opt::explain::analyzed(plan, context, seams, measured, statistics);
explained("analyzed_plan", &text)
}
#[derive(Debug, Clone, Copy)]
struct Asked {
analyze: bool,
statistics: bool,
}
impl Asked {
fn statistics(self) -> rudb_opt::explain::Statistics {
if self.statistics {
rudb_opt::explain::Statistics::Asked
} else {
rudb_opt::explain::Statistics::NotAsked
}
}
}
fn explained(key: &str, text: &str) -> Result<QueryResult> {
let key = Vector::from_values(LogicalType::Varchar, &[Value::Varchar(key.to_owned())])?;
let value = Vector::from_values(LogicalType::Varchar, &[Value::Varchar(text.to_owned())])?;
Ok(QueryResult::new(
vec!["explain_key".to_owned(), "explain_value".to_owned()],
vec![LogicalType::Varchar, LogicalType::Varchar],
vec![Chunk::new(vec![key, value])?],
Memory::unlimited().reservation(),
))
}
fn create_view(create: rudb_bind::CreateView, catalog: &mut Catalog) -> Result<()> {
if create.if_not_exists && catalog.entry(&create.name).is_ok() {
return Ok(());
}
if create.or_replace && catalog.view(&create.name).is_ok() {
catalog.drop_view(&create.name)?;
}
catalog.create_view(View::new(
create.name,
create.sql,
create.statement,
create.aliases,
create.columns,
))
}
#[allow(clippy::too_many_arguments)]
fn create_table(
sql: &str,
mut create: rudb_bind::CreateTable,
catalog: &mut Catalog,
cancel: &Cancel,
budget: Budget<'_>,
context: &rudb_opt::pass::Context,
seams: &rudb_seam::Settings,
session: &Session,
) -> Result<()> {
if create.if_not_exists && catalog.table(&create.name).is_ok() {
return Ok(());
}
let rows = match &mut create.source {
Some(plan) => {
rudb_opt::optimize_with(plan, context)?;
let under = Under::new(budget, context.facts(), seams, session, Rows::ForATable);
Some(run(sql, plan, catalog, cancel, under)?)
}
None => None,
};
if create.or_replace && catalog.table(&create.name).is_ok() {
catalog.drop_table(&create.name)?;
}
catalog.create_table(create.name.clone(), create.columns)?;
if let Some(rows) = rows {
catalog.table_mut(&create.name)?.append_all(rows.into_chunks(), budget.pool.threads())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
use rudb_common::Value;
use rudb_io::{Filesystem, Op, OpenMode, SimFilesystem};
use super::{Database, publish};
#[test]
fn publishing_syncs_the_directory_after_the_rename() {
let fs = SimFilesystem::new();
fs.create_dir_all(Path::new("/data")).unwrap();
let file = fs.open(Path::new("/data/db.7.tmp"), OpenMode::CreateNew).unwrap();
file.write_at(0, b"new").unwrap();
file.sync().unwrap();
fs.clear_log();
publish(&fs, Path::new("/data/db.7.tmp"), Path::new("/data/db")).unwrap();
let ops = fs.ops();
assert_eq!(ops.len(), 2, "{ops:?}");
assert!(matches!(ops[0], Op::Rename { .. }), "{ops:?}");
assert_eq!(ops[1], Op::SyncDir { path: "/data".into() });
assert_eq!(fs.contents(Path::new("/data/db")).unwrap(), b"new".to_vec());
}
#[test]
fn a_query_runs_while_a_load_into_a_new_table_does() {
let path = std::env::temp_dir().join(format!(
"rudb-load-lock-{}-{}.rdb",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("the clock advances")
.as_nanos()
));
let database = Database::open(path.to_str().expect("a UTF-8 temporary path")).unwrap();
database.execute("CREATE TABLE small AS SELECT 7 AS a").unwrap();
let (told, loading) = mpsc::channel();
*database.shared.inner.loading.lock().unwrap() = Some(told);
let connection = database.connect();
let stopper = connection.clone();
let load = std::thread::spawn(move || {
connection.execute("CREATE TABLE big AS SELECT count(*) AS n FROM range(100000000000)")
});
loading.recv_timeout(Duration::from_secs(60)).expect("the load let go of the catalog");
let reader = database.clone();
let (answered, answer) = mpsc::channel();
std::thread::spawn(move || {
let rows = reader.query("SELECT a FROM small").map(|result| result.rows().collect());
let _ = answered.send(rows);
});
let got = answer.recv_timeout(Duration::from_secs(20));
stopper.interrupt();
let loaded = load.join().expect("the load thread ran");
let rows: Vec<Vec<Value>> =
got.expect("the query finished while the load ran").expect("the query succeeded");
assert_eq!(rows, vec![vec![Value::Integer(7)]]);
assert!(loaded.is_err(), "the load was interrupted");
assert!(
database.query("SELECT * FROM big").is_err(),
"an interrupted load leaves no table"
);
drop(database);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_bare_file_name_lives_in_the_current_directory() {
assert_eq!(super::directory_of(Path::new("db")), Path::new("."));
assert_eq!(super::directory_of(Path::new("/data/db")), Path::new("/data"));
assert_eq!(super::directory_of(Path::new("a/db")), Path::new("a"));
}
#[test]
fn two_statements_over_one_catalog_plan_from_the_same_counts() {
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER)").expect("a table");
database.execute("INSERT INTO t VALUES (1), (2), (3)").expect("three rows");
let shared = &database.shared;
let first = shared.facts(&shared.read());
let again = shared.facts(&shared.read());
assert!(std::sync::Arc::ptr_eq(&first, &again), "nothing changed, so nothing was rebuilt");
database.execute("INSERT INTO t VALUES (4)").expect("a fourth row");
let after = shared.facts(&shared.read());
assert!(!std::sync::Arc::ptr_eq(&first, &after), "the catalog changed under it");
assert!(after.generation() > first.generation(), "and it says which version it is");
let rows = |facts: &rudb_opt::estimate::Facts| {
facts.get(&rudb_opt::estimate::Key::Rows {
catalog: "memory",
schema: "main",
table: "t",
})
};
assert_eq!(rows(&first), rudb_common::Stat::exact(3, rudb_common::Provenance::RowCount));
assert_eq!(rows(&after), rudb_common::Stat::exact(4, rudb_common::Provenance::RowCount));
}
#[test]
fn every_column_of_a_result_reaches_the_caller_flat() {
use rudb_vector::Form;
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER, s VARCHAR)").expect("a table");
database
.execute("INSERT INTO t VALUES (1, 'a long string that will not fit inline'), (2, 'b')")
.expect("two rows");
let result = database.query("SELECT a, s, s || 'x' AS j FROM t WHERE a > 0").expect("runs");
assert_eq!(result.len(), 2);
for chunk in result.chunk_iter() {
for (at, column) in chunk.columns().iter().enumerate() {
assert_eq!(column.form(), Form::Flat, "column {at} came out encoded");
}
}
}
#[test]
fn a_relationship_carries_both_certificates_only_when_every_child_row_found_a_parent() {
use super::Shared;
let path = std::env::temp_dir().join(format!(
"rudb-certificates-{}-{}.rdb",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("the clock advances")
.as_nanos()
));
let database =
Database::open(path.to_str().expect("a UTF-8 temporary path")).expect("a file");
database.execute("CREATE TABLE customer (c_custkey INTEGER)").unwrap();
database.execute("CREATE TABLE orders (o_custkey INTEGER)").unwrap();
database.execute("CREATE TABLE returns (r_custkey INTEGER)").unwrap();
database.execute("CREATE TABLE zones (z_key INTEGER)").unwrap();
database.execute("CREATE TABLE visits (v_zone INTEGER)").unwrap();
database.execute("INSERT INTO customer SELECT i FROM range(1, 4001) AS r(i)").unwrap();
database
.execute("INSERT INTO orders SELECT 1 + (i - 1) / 3 FROM range(1, 10001) AS r(i)")
.unwrap();
database
.execute("INSERT INTO returns SELECT 1 + (i - 1) / 3 FROM range(1, 10001) AS r(i)")
.unwrap();
database.execute("INSERT INTO returns VALUES (9999)").unwrap();
database
.execute("INSERT INTO zones SELECT 1 + i % 500 FROM range(0, 1000) AS r(i)")
.unwrap();
database
.execute("INSERT INTO visits SELECT 1 + i % 500 FROM range(0, 2000) AS r(i)")
.unwrap();
let declared = "orders(o_custkey) -> customer(c_custkey), \
returns(r_custkey) -> customer(c_custkey), \
visits(v_zone) -> zones(z_key)";
database.execute(&format!("SET graph_links = '{declared}'")).unwrap();
database.execute("CHECKPOINT").unwrap();
let shared = &database.shared;
let found = Shared::related(&shared.read(), declared);
let certificates: Vec<(&str, bool, bool)> =
found.iter().map(|link| (link.child.as_str(), link.built, link.total)).collect();
assert_eq!(
certificates,
vec![("orders", true, true), ("returns", true, false), ("visits", false, false)],
"one unmatched child row costs the relationship its totality and not its link"
);
drop(database);
std::fs::remove_file(&path).ok();
}
}