use gnitz_wire::schema_block::SchemaBlockCol;
use gnitz_wire::{ColType, PkListRole, PkRule, TypeCode, MAX_COLUMNS, MAX_PK_BYTES, MAX_PK_COLUMNS};
use crate::ColumnLocator;
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct SchemaColumn {
pub type_code: TypeCode,
size: u8,
pub nullable: bool,
is_signed: u8,
}
const _: () = assert!(std::mem::size_of::<SchemaColumn>() == 4);
impl SchemaColumn {
const EMPTY: SchemaColumn = SchemaColumn {
type_code: TypeCode::U8,
size: 0,
nullable: false,
is_signed: 0,
};
pub const fn new(type_code: TypeCode, nullable: bool) -> Self {
SchemaColumn {
type_code,
size: type_code.wire_stride() as u8,
nullable,
is_signed: type_code.is_signed_int() as u8,
}
}
#[inline(always)]
pub const fn size(&self) -> usize {
self.size as usize
}
#[inline(always)]
pub const fn is_signed(&self) -> bool {
self.is_signed != 0
}
#[inline]
pub fn fixed_int(&self) -> Option<gnitz_wire::FixedInt> {
gnitz_wire::FixedInt::from_type_code(self.type_code)
}
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SchemaDescriptor {
string_slots: u64,
nullable_slots: u64,
region_off: [u16; gnitz_wire::MAX_WIRE_REGIONS],
num_columns: u32,
pk_count: u32,
pk_indices: [u32; MAX_PK_COLUMNS],
pk_stride: u8,
payload_to_ci: [u8; MAX_COLUMNS],
fixed_int_nonnull: bool,
cap_mask: u8,
columns: [SchemaColumn; MAX_COLUMNS],
}
const _: () = assert!(std::mem::size_of::<SchemaDescriptor>() <= 512);
const _: () = {
let mut i = 0;
while i < TypeCode::ALL.len() {
assert!(TypeCode::ALL[i].wire_stride() <= 16);
i += 1;
}
assert!(MAX_PK_COLUMNS * 16 <= MAX_PK_BYTES && MAX_PK_BYTES <= u8::MAX as usize);
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaRefusal {
TooManyColumns(usize),
Pk(PkRule),
}
impl std::fmt::Display for SchemaRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaRefusal::TooManyColumns(n) => write!(f, "column count {n} exceeds MAX_COLUMNS ({MAX_COLUMNS})"),
SchemaRefusal::Pk(rule) => f.write_str(&rule.for_role(PkListRole::PrimaryKey)),
}
}
}
impl From<SchemaRefusal> for String {
fn from(refusal: SchemaRefusal) -> String {
refusal.to_string()
}
}
impl SchemaDescriptor {
pub fn try_new(cols: &[SchemaColumn], pk_indices: &[u32]) -> Result<Self, SchemaRefusal> {
if cols.len() > MAX_COLUMNS {
return Err(SchemaRefusal::TooManyColumns(cols.len()));
}
gnitz_wire::validate_pk_tuple(pk_indices, cols.len(), MAX_PK_COLUMNS, |c| {
let col = &cols[c as usize];
(col.type_code, col.nullable)
})
.map_err(SchemaRefusal::Pk)?;
let pk_stride: usize = pk_indices.iter().map(|&c| cols[c as usize].size()).sum();
let mut columns = [SchemaColumn::EMPTY; MAX_COLUMNS];
columns[..cols.len()].copy_from_slice(cols);
let mut pk = [0u32; MAX_PK_COLUMNS];
pk[..pk_indices.len()].copy_from_slice(pk_indices);
let mut payload_to_ci = [0u8; MAX_COLUMNS];
for ci in 0..cols.len() {
if let Some(pi) = gnitz_wire::payload_slot(pk_indices, ci) {
payload_to_ci[pi] = ci as u8;
}
}
let num_payload = cols.len() - pk_indices.len();
let payload = payload_to_ci[..num_payload].iter().map(|&ci| cols[ci as usize]);
let mut region_off = [0u16; gnitz_wire::MAX_WIRE_REGIONS];
let (mut string_slots, mut nullable_slots) = (0u64, 0u64);
let mut off = pk_stride + 16;
region_off[gnitz_wire::REG_WEIGHT] = pk_stride as u16;
region_off[gnitz_wire::REG_NULL_BMP] = pk_stride as u16 + 8;
for (pi, col) in payload.clone().enumerate() {
region_off[gnitz_wire::REG_PAYLOAD_START + pi] = off as u16;
off += col.size();
string_slots |= u64::from(col.type_code.is_german_string()) << pi;
nullable_slots |= u64::from(col.nullable) << pi;
}
region_off[gnitz_wire::REG_PAYLOAD_START + num_payload] = off as u16;
let low = region_off[..gnitz_wire::REG_PAYLOAD_START + num_payload]
.iter()
.fold(8u16, |g, &o| g | o);
let cap_mask = (8u8 >> low.trailing_zeros().min(3)) - 1;
Ok(SchemaDescriptor {
string_slots,
nullable_slots,
region_off,
num_columns: cols.len() as u32,
pk_count: pk_indices.len() as u32,
pk_indices: pk,
pk_stride: pk_stride as u8,
payload_to_ci,
fixed_int_nonnull: payload.clone().all(|c| !c.nullable && c.type_code.is_fixed_int()),
cap_mask,
columns,
})
}
#[track_caller]
pub fn new(cols: &[SchemaColumn], pk_indices: &[u32]) -> Self {
match Self::try_new(cols, pk_indices) {
Ok(schema) => schema,
Err(e) => panic!("SchemaDescriptor::new: {e}"),
}
}
#[inline]
pub const fn num_columns(&self) -> usize {
self.num_columns as usize
}
pub fn is_trailing_append_of(&self, prev: &SchemaDescriptor) -> bool {
self.pk_cols() == prev.pk_cols()
&& self.num_columns() >= prev.num_columns()
&& (0..prev.num_columns()).all(|i| self.columns[i].type_code == prev.columns[i].type_code)
}
#[inline]
pub fn pk_columns(&self) -> impl Iterator<Item = (usize, &SchemaColumn)> {
self.pk_cols()
.iter()
.map(move |&ci| (ci as usize, &self.columns[ci as usize]))
}
#[inline]
pub const fn pk_stride(&self) -> usize {
self.pk_stride as usize
}
#[inline(always)]
pub const fn num_payload_cols(&self) -> usize {
self.num_columns as usize - self.pk_count as usize
}
#[inline]
pub fn payload_columns(&self) -> impl Iterator<Item = (usize, &SchemaColumn)> {
(0..self.num_payload_cols()).map(move |pi| (pi, &self.columns[self.payload_to_ci[pi] as usize]))
}
#[inline]
pub fn has_german_string(&self) -> bool {
self.string_slots != 0
}
#[inline]
pub fn string_payload_slots(&self) -> u64 {
self.string_slots
}
#[inline]
pub fn nullable_payload_slots(&self) -> u64 {
self.nullable_slots
}
#[inline]
pub fn not_null_payload_slots(&self) -> u64 {
!self.nullable_slots
}
#[inline(always)]
pub const fn num_regions(&self) -> usize {
gnitz_wire::REG_PAYLOAD_START + self.num_payload_cols()
}
#[inline(always)]
pub const fn row_width(&self) -> usize {
self.region_off[self.num_regions()] as usize
}
#[inline(always)]
pub const fn region_start(&self, r: usize, cap: usize) -> usize {
cap * self.region_off[r] as usize
}
#[inline(always)]
pub const fn region_stride(&self, r: usize) -> usize {
(self.region_off[r + 1] - self.region_off[r]) as usize
}
pub fn same_regions(&self, other: &SchemaDescriptor) -> bool {
self.region_off[..=self.num_regions()] == other.region_off[..=other.num_regions()]
}
#[inline]
pub const fn arena_rows(&self, rows: usize) -> usize {
let mask = self.cap_mask as usize;
if rows < 8 {
rows
} else {
(rows + mask) & !mask
}
}
#[inline]
pub fn columns(&self) -> &[SchemaColumn] {
&self.columns[..self.num_columns as usize]
}
pub fn wire_col(&self, what: impl std::fmt::Display, c: u32) -> Result<(SchemaColumn, ColumnLocator), String> {
match self.try_locate(c as usize) {
Some(loc) => Ok((self.columns[c as usize], loc)),
None => Err(format!("{what} {c} out of range ({} cols)", self.num_columns())),
}
}
pub fn covers_pk(&self, cols: &[u32]) -> bool {
self.pk_cols().iter().all(|p| cols.contains(p))
}
pub fn pk_only(&self) -> SchemaDescriptor {
let cols: Vec<SchemaColumn> = self.pk_columns().map(|(_, c)| *c).collect();
let pk: Vec<u32> = (0..cols.len() as u32).collect();
SchemaDescriptor::new(&cols, &pk)
}
#[inline]
pub fn payload_col_idx(&self, pi: usize) -> usize {
debug_assert!(pi < self.num_payload_cols(), "payload_col_idx: pi {pi} out of range");
self.payload_to_ci[pi] as usize
}
#[inline]
pub fn pk_cols(&self) -> &[u32] {
&self.pk_indices[..self.pk_count as usize]
}
#[inline(always)]
pub fn payload_is_fixed_int_nonnull(&self) -> bool {
self.fixed_int_nonnull
}
pub fn pk_prefix_stride(&self, k: usize) -> usize {
self.pk_cols()[..k]
.iter()
.map(|&c| self.columns[c as usize].size())
.sum()
}
pub fn try_locate(&self, ci: usize) -> Option<ColumnLocator> {
if ci >= self.num_columns() {
return None;
}
let SchemaColumn { type_code, size, .. } = self.columns[ci];
Some(match gnitz_wire::payload_slot(self.pk_cols(), ci) {
Some(slot) => ColumnLocator::Payload { slot: slot as u8, size, type_code },
None => {
let k = self
.pk_cols()
.iter()
.position(|&p| p as usize == ci)
.expect("a PK column");
ColumnLocator::Pk {
byte_off: self.pk_prefix_stride(k) as u8,
size,
type_code,
}
}
})
}
pub fn locate(&self, ci: usize) -> ColumnLocator {
self.try_locate(ci).unwrap_or_else(|| {
panic!(
"locate: col_idx {ci} out of bounds (num_columns = {})",
self.num_columns()
)
})
}
pub fn is_pk_col(&self, ci: usize) -> bool {
self.pk_cols().iter().any(|&p| p as usize == ci)
}
pub fn payload_slot(&self, ci: usize) -> Option<usize> {
self.try_locate(ci)?.payload_slot()
}
pub fn payload_locators(&self) -> Vec<ColumnLocator> {
self.payload_columns()
.map(|(pi, c)| ColumnLocator::Payload {
slot: pi as u8,
size: c.size,
type_code: c.type_code,
})
.collect()
}
pub fn lone_pk_col(&self) -> Option<usize> {
match self.pk_cols() {
[c] => Some(*c as usize),
_ => None,
}
}
fn region_types(&self) -> impl Iterator<Item = TypeCode> + '_ {
let pk = self.pk_columns().map(|(_, c)| c.type_code);
pk.chain(self.payload_columns().map(|(_, c)| c.type_code))
}
pub fn layout_digest(&self) -> u64 {
gnitz_wire::layout_digest(self.pk_cols().len(), self.region_types())
}
pub fn same_layout(&self, other: &SchemaDescriptor) -> bool {
self.num_columns() == other.num_columns() && self.is_trailing_append_of(other)
}
pub fn same_region_types(&self, other: &SchemaDescriptor) -> bool {
self.pk_cols().len() == other.pk_cols().len() && self.region_types().eq(other.region_types())
}
pub fn reduce_out_key(&self, group: &[u32]) -> gnitz_wire::ReduceOutKey {
gnitz_wire::ReduceOutKey::for_group_cols(self.pk_cols(), group, |c| {
let col = self.columns[c as usize];
(col.type_code, col.nullable)
})
}
pub fn opk_key_cols(&self, natives: &[u128]) -> gnitz_wire::PkBuf {
debug_assert_eq!(
natives.len(),
self.pk_cols().len(),
"opk_key_cols: one native value per PK column",
);
let mut key = gnitz_wire::PkBuf::zeroed(0);
for ((_, col), &v) in self.pk_columns().zip(natives) {
let (tc, w) = (col.type_code, col.size());
debug_assert!(
{
let dropped = v.checked_shr(w as u32 * 8).unwrap_or(0);
dropped == 0 || dropped == u128::MAX >> (w * 8)
},
"opk_key_cols: {v:#x} does not fit a {tc:?} column",
);
key.push(w, v, tc.is_signed_int());
}
key
}
}
impl std::fmt::Debug for SchemaDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SchemaDescriptor {{ columns: [")?;
for ci in 0..self.num_columns() {
if ci > 0 {
write!(f, ", ")?;
}
let col = self.columns[ci];
write!(f, "{:?}", col.type_code)?;
if col.nullable {
write!(f, "?")?;
}
if self.is_pk_col(ci) {
write!(f, " pk")?;
}
}
write!(f, "], pk_indices: {:?} }}", self.pk_cols())
}
}
impl PartialEq for SchemaDescriptor {
fn eq(&self, other: &Self) -> bool {
if self.num_columns() != other.num_columns() || self.pk_cols() != other.pk_cols() {
return false;
}
self.columns[..self.num_columns()] == other.columns[..other.num_columns()]
}
}
impl Eq for SchemaDescriptor {}
pub fn decode_schema_block(data: &[u8]) -> Result<SchemaDescriptor, String> {
let mut cols = [SchemaColumn::EMPTY; MAX_COLUMNS];
let mut n = 0;
let pk = gnitz_wire::schema_block::decode(data, |c| {
cols[n] = SchemaColumn::new(c.ty.tc, c.nullable);
n += 1;
Ok(())
})?;
Ok(SchemaDescriptor::try_new(&cols[..n], pk.as_slice())?)
}
pub fn encode_schema_block(schema: &SchemaDescriptor) -> Vec<u8> {
let cols = schema.columns[..schema.num_columns()].iter().map(|c| SchemaBlockCol {
ty: ColType::of(c.type_code),
nullable: c.nullable,
hidden: false,
name: b"",
});
gnitz_wire::schema_block::encode(cols, schema.pk_cols())
}
#[cfg(test)]
#[path = "tests/schema.rs"]
mod tests;