mod lifecycle;
use uqa_core::memory::BudgetedVec;
use crate::catalog::new_nonzero_catalog_identity;
use crate::document_store::identifiers::{
inherit_document_ids, legacy_document_id_metadata_key, observe_document_id,
restored_document_id_watermark,
};
use crate::read_control::StorageReadControl;
use crate::{KeyValueBatch, RelationIdentity, StorageBackendResult, TableSchema};
use super::catalog::keys::relation_key;
use super::codec::{decode_value, document_key_prefix, encode_value, other_error, single_str_key};
use super::{KeyValueRead, TAG_METADATA, TAG_TABLE};
pub(super) use lifecycle::{drop_binding, prepare_table, rename_bindings};
const TAG: u8 = b'O';
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Owner {
pub(super) object: [u8; 16],
pub(super) generation: [u8; 16],
pub(super) catalog: bool,
}
impl Owner {
fn new(object: [u8; 16], generation: [u8; 16], catalog: bool) -> StorageBackendResult<Self> {
Ok(Self {
object: if object == [0; 16] {
new_nonzero_catalog_identity("table", "object")?
} else {
object
},
generation: if generation == [0; 16] {
new_nonzero_catalog_identity("table", "storage generation")?
} else {
generation
},
catalog,
})
}
fn decode(value: &[u8]) -> StorageBackendResult<Self> {
if value.len() != 34 || value[0] != 1 || value[1] > 1 {
return Err(other_error("invalid KeyValue table owner encoding"));
}
let object: [u8; 16] = value[2..18].try_into().expect("checked owner length");
let generation: [u8; 16] = value[18..34].try_into().expect("checked owner length");
if object == [0; 16] || generation == [0; 16] {
return Err(other_error(
"KeyValue table owner identities must not be zero",
));
}
Ok(Self {
object,
generation,
catalog: value[1] == 1,
})
}
fn encode(self) -> [u8; 34] {
let mut value = [0; 34];
value[0] = 1;
value[1] = u8::from(self.catalog);
value[2..18].copy_from_slice(&self.object);
value[18..].copy_from_slice(&self.generation);
value
}
fn marker(self) -> [u8; 34] {
let mut key = self.encode();
key[..2].copy_from_slice(&[TAG, 1]);
key
}
fn identity_key(self) -> [u8; 18] {
let mut key = [0; 18];
key[..2].copy_from_slice(&[TAG, 2]);
key[2..].copy_from_slice(&self.object);
key
}
pub(super) fn observe(
self,
batch: &mut dyn KeyValueBatch,
id: u64,
) -> StorageBackendResult<()> {
observe_document_id(batch, self.object, self.generation, id)
}
pub(super) fn fence(self, batch: &mut dyn KeyValueBatch) -> StorageBackendResult<()> {
batch.fence_record(&self.marker())
}
fn inherit(self, batch: &mut dyn KeyValueBatch, from: Self) -> StorageBackendResult<()> {
if (self.object, self.generation) != (from.object, from.generation) {
inherit_document_ids(
batch,
(from.object, from.generation),
(self.object, self.generation),
)?;
}
Ok(())
}
}
fn binding_key(name: &str, control: &StorageReadControl) -> StorageBackendResult<BudgetedVec<u8>> {
let mut key = BudgetedVec::new(control.memory());
key.extend_from_slice(&[TAG, 0])?;
key.extend_from_slice(name.as_bytes())?;
Ok(key)
}
fn catalog_key(name: &str) -> StorageBackendResult<Option<Vec<u8>>> {
let Ok(relation) = RelationIdentity::from_legacy_name(name) else {
return Ok(None);
};
if relation.qualified_name() != name {
return Ok(None);
}
relation_key(TAG_TABLE, &relation).map(Some)
}
fn lookup(read: &dyn KeyValueRead, name: &str) -> StorageBackendResult<Option<Owner>> {
let key = binding_key(name, read.control())?;
let mut owner = None;
read.visit_value(&key, &mut |value| {
owner = value.map(Owner::decode).transpose()?;
Ok(())
})?;
if let Some(owner) = owner {
let indexed = read.get(&owner.identity_key())?;
if indexed.as_deref() != Some(name.as_bytes()) {
return Err(other_error(
"table owner identity index disagrees with its binding",
));
}
let schema = catalog_key(name)?
.map(|key| read.get(&key))
.transpose()?
.flatten();
if owner.catalog != schema.is_some() {
return Err(other_error(
"table owner binding disagrees with its catalog definition",
));
}
if let Some(bytes) = schema {
let schema: TableSchema = decode_value(&bytes)?;
if schema.relation.qualified_name() != name
|| schema.object_id != owner.object
|| schema.storage_generation != owner.generation
{
return Err(other_error(
"table definition disagrees with its owner identities",
));
}
}
}
Ok(owner)
}
pub(super) fn validate_table(
read: &dyn KeyValueRead,
schema: &TableSchema,
) -> StorageBackendResult<()> {
lookup(read, &schema.relation.qualified_name()).map(|_| ())
}
fn claim(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
owner: Owner,
previous_name: Option<&str>,
) -> StorageBackendResult<()> {
let key = owner.identity_key();
if let Some(existing) = read.get(&key)? {
if &*existing != name.as_bytes()
&& previous_name.is_none_or(|old| &*existing != old.as_bytes())
{
return Err(other_error(
"table object identity already belongs to another storage name",
));
}
} else {
read.visit_prefix(&[TAG_TABLE], &mut |_, value| {
let _decoded = read.control().memory().reserve(
value
.len()
.checked_mul(16)
.ok_or(uqa_core::memory::MemoryError::SizeOverflow)?,
)?;
let schema: TableSchema = decode_value(value)?;
let existing = schema.relation.qualified_name();
if schema.object_id == owner.object
&& existing != name
&& previous_name.is_none_or(|old| old != existing)
{
return Err(other_error(
"table object identity already belongs to another catalog relation",
));
}
Ok(())
})?;
}
batch.put(&key, name.as_bytes())?;
batch.put(&binding_key(name, read.control())?, &owner.encode())
}
fn document_maximum(read: &dyn KeyValueRead, name: &str) -> StorageBackendResult<u64> {
let prefix = document_key_prefix(name)?;
let mut maximum = 0;
read.visit_keys_after(&prefix, None, usize::MAX, read.control(), &mut |key| {
let id: [u8; 8] = key[prefix.len()..]
.try_into()
.map_err(|_| other_error("invalid document identity key"))?;
maximum = maximum.max(u64::from_be_bytes(id));
Ok(())
})?;
Ok(maximum)
}
fn seed_generation(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
owner: Owner,
) -> StorageBackendResult<()> {
owner.observe(batch, document_maximum(read, name)?)
}
fn seed(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
owner: Owner,
) -> StorageBackendResult<()> {
let maximum = document_maximum(read, name)?;
let legacy_key = single_str_key(TAG_METADATA, &legacy_document_id_metadata_key(name))?;
let mut legacy = None;
read.visit_value(&legacy_key, &mut |value| {
if let Some(value) = value.filter(|value| !value.is_empty()) {
legacy = Some(
std::str::from_utf8(value)
.map_err(|error| {
other_error(format!(
"invalid persisted next id for table `{name}`: {error}"
))
})?
.parse::<u128>()
.map_err(|error| {
other_error(format!(
"invalid persisted next id for table `{name}`: {error}"
))
})?,
);
}
Ok(())
})?;
let maximum = u64::try_from(restored_document_id_watermark(maximum, legacy) - 1)
.map_err(|_| other_error("invalid document id watermark"))?;
owner.observe(batch, maximum)?;
if legacy.is_some() {
batch.put(&legacy_key, b"")?;
}
Ok(())
}
pub(super) fn document_owner(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
) -> StorageBackendResult<Owner> {
let binding = binding_key(name, read.control())?;
let definition = catalog_key(name)?;
let owner = if let Some(owner) = lookup(read, name)? {
batch.require_unchanged(&owner.identity_key())?;
owner
} else {
let schema = definition
.as_ref()
.map(|key| read.get(key))
.transpose()?
.flatten();
let owner = if let Some(bytes) = schema {
let mut schema: TableSchema = decode_value(&bytes)?;
if schema.relation.qualified_name() != name {
return Err(other_error(
"table definition disagrees with its storage name",
));
}
let owner = Owner::new(schema.object_id, schema.storage_generation, true)?;
if schema.object_id != owner.object || schema.storage_generation != owner.generation {
schema.object_id = owner.object;
schema.storage_generation = owner.generation;
batch.put(
definition.as_ref().expect("schema has a definition key"),
&encode_value(&schema)?,
)?;
}
owner
} else {
Owner::new([0; 16], [0; 16], false)?
};
seed(read, batch, name, owner)?;
claim(read, batch, name, owner, None)?;
owner
};
batch.require_unchanged(&binding)?;
if let Some(key) = definition {
batch.require_unchanged(&key)?;
} else if owner.catalog {
return Err(other_error("catalog-owned storage name is not canonical"));
}
batch.touch_marker(&owner.marker(), &[1])?;
Ok(owner)
}
pub(super) fn fence_data(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
) -> StorageBackendResult<Option<Owner>> {
let owner = lookup(read, name)?;
batch.fence_record(&binding_key(name, read.control())?)?;
if let Some(owner) = owner {
batch.fence_record(&owner.marker())?;
}
Ok(owner)
}
pub(super) fn preserve_and_fence(
read: &dyn KeyValueRead,
batch: &mut dyn KeyValueBatch,
name: &str,
) -> StorageBackendResult<()> {
let schema_exists = catalog_key(name)?
.map(|key| read.get(&key))
.transpose()?
.flatten()
.is_some();
if lookup(read, name)?.is_some()
|| schema_exists
|| read.contains_prefix_budgeted(&document_key_prefix(name)?, read.control())?
|| read
.get(&single_str_key(
TAG_METADATA,
&legacy_document_id_metadata_key(name),
)?)?
.is_some_and(|value| !value.is_empty())
{
document_owner(read, batch, name)?.fence(batch)?;
}
batch.fence_record(&binding_key(name, read.control())?)
}