use super::{identity::SCHEMA_CATALOG_CLASS_ID, SchemaSecurityCatalog};
use crate::row_locks::{
binding::{acquire_relation, RelationLockSession},
session::RowLockSession,
shared_objects::{SharedCatalogLock, SharedObjectLockSession},
LockAcquire, RelationLockMode,
};
use uqa_core::catalog_schema::SchemaTupleIdentity;
use uqa_sql::{
ast::{LockStrength, LockWait},
catalog::security::BoundSchemaSecurity,
SQLError,
};
pub struct SchemaLockContext<'a> {
pub objects: &'a dyn SharedObjectLockSession,
pub relations: &'a dyn RelationLockSession,
pub rows: &'a dyn RowLockSession,
pub catalog: &'a dyn SchemaSecurityCatalog,
}
impl SchemaLockContext<'_> {
pub fn bind_lifetime(
&self,
name: &str,
mode: RelationLockMode,
) -> Result<Option<BoundSchemaSecurity>, SQLError> {
bind_namespace_lifetime(self.objects, mode, || {
self.catalog
.schema_security(name)
.map(|security| Ok((tuple(&security)?, security)))
.transpose()
})
}
pub fn catalog_write(&self) -> Result<(), SQLError> {
acquire_relation(
self.relations,
"pg_catalog.pg_namespace",
RelationLockMode::RowExclusive,
false,
)?
.retain();
self.objects.refresh_shared_catalog()
}
pub fn replace(&self, name: &str, before: SchemaTupleIdentity) -> Result<(), SQLError> {
match self.rows.lock_row(
"pg_catalog.pg_namespace",
before.oid as u64,
LockStrength::ForNoKeyUpdate,
LockWait::Block,
name,
)? {
LockAcquire::Granted { .. } => {}
LockAcquire::Skipped => {
return Err(SQLError::Internal(
"blocking schema tuple lock was skipped".into(),
));
}
}
self.objects.refresh_shared_catalog()?;
let current = self
.catalog
.schema_security(name)
.map(|row| tuple(&row))
.transpose()?;
let Some(current) = current
.filter(|current| current.oid == before.oid && current.object_id == before.object_id)
else {
return Err(concurrent_tuple("deleted"));
};
if current.revision != before.revision {
return Err(concurrent_tuple("updated"));
}
Ok(())
}
}
pub(super) fn bind_namespace_lifetime<T>(
objects: &dyn SharedObjectLockSession,
mode: RelationLockMode,
mut resolve: impl FnMut() -> Result<Option<(SchemaTupleIdentity, T)>, SQLError>,
) -> Result<Option<T>, SQLError> {
loop {
let Some((before, _)) = resolve()? else {
return Ok(None);
};
let guard = objects.acquire_shared_catalog(
SharedCatalogLock::Object {
class_id: SCHEMA_CATALOG_CLASS_ID,
oid: u32::try_from(before.oid).expect("validated schema OID"),
},
mode,
)?;
objects.refresh_shared_catalog()?;
let Some((current, value)) = resolve()? else {
return Ok(None);
};
if current.oid != before.oid || current.object_id != before.object_id {
continue;
}
guard.retain();
return Ok(Some(value));
}
}
pub(super) fn tuple(security: &BoundSchemaSecurity) -> Result<SchemaTupleIdentity, SQLError> {
security
.tuple
.filter(|tuple| tuple.is_valid())
.ok_or_else(|| SQLError::Internal("schema has no valid catalog tuple identity".into()))
}
pub(super) fn missing(name: &str) -> SQLError {
SQLError::Routine {
sqlstate: "3F000".into(),
message: format!("schema \"{name}\" does not exist"),
}
}
fn concurrent_tuple(action: &str) -> SQLError {
SQLError::Routine {
sqlstate: "XX000".into(),
message: format!("tuple concurrently {action}"),
}
}
#[cfg(test)]
mod tests;