use super::table_read::TableRead;
use crate::{serializable::SerializableRelationRead, storage_errors::storage_error};
use std::collections::BTreeMap;
use uqa_core::{DocId, PostingList, Predicate, Value};
use uqa_sql::{ast::ColumnDef, SQLError};
use uqa_storage::{document_store::Document, DocumentStore, StoredDocument};
#[derive(Clone, Copy)]
pub enum FieldPresence {
Required,
MissingIsNull,
}
pub trait ExactLookupOverlay {
fn is_empty(&self) -> Result<bool, SQLError>;
fn masks(&self, doc_id: DocId) -> Result<bool, SQLError>;
fn find_match(
&self,
columns: &[String],
values: &[Value],
presence: FieldPresence,
) -> Result<Option<DocId>, SQLError>;
}
pub fn matches_fields(
document: &Document,
columns: &[String],
values: &[Value],
presence: FieldPresence,
) -> Result<bool, SQLError> {
matches_fields_with_control(
document,
columns,
values,
presence,
&uqa_core::memory::ProductionControl::uncontrolled(),
)
}
pub(crate) fn matches_fields_with_control(
document: &Document,
columns: &[String],
values: &[Value],
presence: FieldPresence,
control: &uqa_core::memory::ProductionControl<'_>,
) -> Result<bool, SQLError> {
for (column, expected) in columns.iter().zip(values) {
let actual = document.get(column);
if matches!(presence, FieldPresence::Required) && actual.is_none() {
return Ok(false);
}
if !uqa_sql::expr::compare_typed_values_with_control(
actual.unwrap_or(&Value::Null),
expected,
control,
)?
.is_eq()
{
return Ok(false);
}
}
Ok(true)
}
fn matches_value(
actual: Option<&Value>,
expected: &Value,
presence: FieldPresence,
) -> Result<bool, SQLError> {
if matches!(presence, FieldPresence::Required) && actual.is_none() {
return Ok(false);
}
uqa_sql::expr::compare_typed_values_with_control(
actual.unwrap_or(&Value::Null),
expected,
&uqa_core::memory::ProductionControl::uncontrolled(),
)
.map(std::cmp::Ordering::is_eq)
}
impl ExactLookupOverlay for BTreeMap<DocId, Option<StoredDocument>> {
fn is_empty(&self) -> Result<bool, SQLError> {
Ok(self.is_empty())
}
fn masks(&self, doc_id: DocId) -> Result<bool, SQLError> {
Ok(self.contains_key(&doc_id))
}
fn find_match(
&self,
columns: &[String],
values: &[Value],
presence: FieldPresence,
) -> Result<Option<DocId>, SQLError> {
for (id, document) in self {
if let Some(document) = document {
if matches_fields(document.fields(), columns, values, presence)? {
return Ok(Some(*id));
}
}
}
Ok(None)
}
}
impl ExactLookupOverlay for super::document_changes::DocumentChanges {
fn is_empty(&self) -> Result<bool, SQLError> {
Ok(!self.has_changes())
}
fn masks(&self, id: DocId) -> Result<bool, SQLError> {
Ok(self.contains_change(id))
}
fn find_match(
&self,
columns: &[String],
values: &[Value],
presence: FieldPresence,
) -> Result<Option<DocId>, SQLError> {
for (id, present) in self.changes() {
if !present {
continue;
}
let mut matches = true;
for (column, expected) in columns.iter().zip(values) {
let actual = self
.get_field(id, column)
.map_err(|error| storage_error("read private exact key", &error))?;
if !matches_value(actual.as_ref(), expected, presence)? {
matches = false;
break;
}
}
if matches {
return Ok(Some(id));
}
}
Ok(None)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IndexConflictProbe {
Unanswerable,
NoConflict,
Conflict(DocId),
}
fn primary_key_doc_id(columns: &[ColumnDef], column: &str, value: &Value) -> Option<DocId> {
let Value::Int(id) = value else {
return None;
};
if *id < 0
|| !columns.iter().any(|candidate| {
candidate.name == column && candidate.primary_key && candidate.ty.is_integer()
})
{
return None;
}
Some(*id as DocId)
}
pub struct ExactLookup<'a> {
pub table: &'a dyn TableRead,
pub overlay: &'a dyn ExactLookupOverlay,
pub read: Option<&'a SerializableRelationRead>,
}
impl ExactLookup<'_> {
pub fn find_field(&self, field: &str, value: &Value) -> Result<Option<DocId>, SQLError> {
self.scan(
&[field.to_string()],
std::slice::from_ref(value),
FieldPresence::Required,
)
}
pub fn find_conflict(
&self,
schema_columns: &[ColumnDef],
columns: &[String],
values: &[Value],
scan: impl FnMut(&str, &Predicate) -> Result<Option<PostingList>, SQLError>,
) -> Result<Option<DocId>, SQLError> {
if columns.is_empty() || columns.len() != values.len() {
return Ok(None);
}
if columns.len() == 1 {
if let Some(id) = primary_key_doc_id(schema_columns, &columns[0], &values[0]) {
if let Some(read) = self.read {
read.observe_row(id)?;
}
if let Some(id) =
self.overlay
.find_match(columns, values, FieldPresence::MissingIsNull)?
{
return Ok(Some(id));
}
if self.overlay.masks(id)? {
return Ok(None);
}
return self
.table
.read_documents()
.contains_doc_id(id)
.map(|exists| exists.then_some(id))
.map_err(|error| storage_error("check conflicting document", &error));
}
}
match self.indexed_conflict(columns, values, scan)? {
IndexConflictProbe::Conflict(id) => Ok(Some(id)),
IndexConflictProbe::NoConflict => Ok(None),
IndexConflictProbe::Unanswerable => {
self.scan(columns, values, FieldPresence::MissingIsNull)
}
}
}
fn indexed_conflict(
&self,
columns: &[String],
values: &[Value],
mut scan: impl FnMut(&str, &Predicate) -> Result<Option<PostingList>, SQLError>,
) -> Result<IndexConflictProbe, SQLError> {
for (pivot, (column, value)) in columns.iter().zip(values).enumerate() {
let predicate = if matches!(value, Value::Null) {
Predicate::IsNull
} else {
Predicate::Equals(value.clone())
};
let Some(candidates) = scan(column, &predicate)? else {
continue;
};
if let Some(id) =
self.overlay
.find_match(columns, values, FieldPresence::MissingIsNull)?
{
return Ok(IndexConflictProbe::Conflict(id));
}
let documents = self.table.read_documents();
for entry in candidates.entries() {
if self.overlay.masks(entry.doc_id)? {
continue;
}
if columns.len() > 1 {
if let Some(read) = self.read {
read.observe_row(entry.doc_id)?;
}
}
let mut matches = true;
for (index, (column, expected)) in columns.iter().zip(values).enumerate() {
if index == pivot {
continue;
}
let actual = documents
.get_field(entry.doc_id, column)
.map_err(|error| storage_error("verify conflicting document", &error))?;
if !matches_value(actual.as_ref(), expected, FieldPresence::MissingIsNull)? {
matches = false;
break;
}
}
if matches {
return Ok(IndexConflictProbe::Conflict(entry.doc_id));
}
}
return Ok(IndexConflictProbe::NoConflict);
}
Ok(IndexConflictProbe::Unanswerable)
}
fn scan(
&self,
columns: &[String],
values: &[Value],
presence: FieldPresence,
) -> Result<Option<DocId>, SQLError> {
if let Some(read) = self.read {
read.observe_scan()?;
}
if let Some(id) = self.overlay.find_match(columns, values, presence)? {
return Ok(Some(id));
}
let documents = self.table.read_documents();
let comparison_can_fail = values.iter().any(uqa_sql::expr::value_comparison_can_fail)
|| self.table.column_definitions().iter().any(|column| {
columns.contains(&column.name)
&& uqa_sql::expr::type_comparison_can_fail(&column.ty)
});
if self.overlay.is_empty()? && !comparison_can_fail {
return match presence {
FieldPresence::Required => documents.find_doc_id_by_field(&columns[0], &values[0]),
FieldPresence::MissingIsNull => documents.find_doc_id_by_fields(columns, values),
}
.map_err(|error| storage_error("find matching document", &error));
}
let mut after = None;
loop {
let ids = documents
.next_doc_ids(after, crate::DEFAULT_BATCH_SIZE)
.map_err(|error| storage_error("scan command-visible document fields", &error))?;
let Some(last) = ids.last().copied() else {
return Ok(None);
};
after = Some(last);
for id in ids {
if self.overlay.masks(id)? {
continue;
}
let mut matches = true;
for (column, expected) in columns.iter().zip(values) {
let actual = documents.get_field(id, column).map_err(|error| {
storage_error("read command-visible document field", &error)
})?;
if !matches_value(actual.as_ref(), expected, presence)? {
matches = false;
break;
}
}
if matches {
return Ok(Some(id));
}
}
}
}
}
#[cfg(test)]
mod tests;