use gnitz_wire::schema_block::SchemaBlockCol;
use gnitz_wire::ColType;
pub(crate) fn oob_col(what: &str, c: u32, schema: &SchemaDescriptor) -> String {
format!("{what} {c} out of range ({} cols)", schema.num_columns())
}
pub(crate) use gnitz_wire::ReduceOutKey;
pub(crate) use gnitz_wire::TypeCode;
pub(crate) use gnitz_wire::MAX_COLUMNS;
pub(crate) use gnitz_wire::{MAX_PK_BYTES, MAX_PK_COLUMNS};
pub(crate) use gnitz_expr::ColumnLocator;
pub(crate) use gnitz_expr::ColumnTable;
pub use gnitz_expr::SchemaFacts;
pub mod key;
pub(crate) mod payload_order;
pub use key::KeySpec;
mod route;
pub use route::{ground_owner, Placement, Slot};
pub(crate) use route::{worker_for_key, worker_for_pk_bytes};
pub(crate) struct DerivedSchema {
cols: Vec<SchemaColumn>,
pk_len: usize,
}
impl DerivedSchema {
pub(crate) fn new() -> Self {
DerivedSchema { cols: Vec::new(), pk_len: 0 }
}
pub(crate) fn push(&mut self, col: SchemaColumn) {
self.cols.push(col);
}
pub(crate) fn push_pk(&mut self, col: SchemaColumn) {
assert_eq!(
self.pk_len,
self.cols.len(),
"DerivedSchema: key column pushed behind a payload column"
);
self.cols.push(col);
self.pk_len += 1;
}
pub(crate) fn push_pk_of(&mut self, schema: &SchemaDescriptor) {
schema.pk_columns().for_each(|(_, c)| self.push_pk(*c));
}
pub(crate) fn push_payload_of(&mut self, schema: &SchemaDescriptor) {
schema.payload_columns().for_each(|(_, c)| self.push(*c));
}
pub(crate) fn finish(&self) -> Result<SchemaDescriptor, String> {
let pk: Vec<u32> = (0..self.pk_len as u32).collect();
SchemaDescriptor::try_new(&self.cols, &pk)
}
}
#[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 {
pub 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) -> u8 {
self.size
}
#[inline(always)]
pub(crate) const fn is_signed(&self) -> bool {
self.is_signed != 0
}
#[inline]
pub(crate) 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],
pub(crate) payload_cmp: payload_order::PayloadCmpKind,
cap_mask: u8,
pub 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);
};
impl SchemaDescriptor {
pub fn try_new(cols: &[SchemaColumn], pk_indices: &[u32]) -> Result<Self, String> {
if cols.len() > MAX_COLUMNS {
return Err(format!(
"column count {} exceeds MAX_COLUMNS ({MAX_COLUMNS})",
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(|rule| rule.for_role(gnitz_wire::PkListRole::PrimaryKey))?;
let pk_stride: usize = pk_indices.iter().map(|&c| cols[c as usize].size() as usize).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() as usize;
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,
payload_cmp: payload_order::PayloadCmpKind::of(payload),
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(crate) 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
}
pub fn format_pk_bytes(&self, pk_bytes: &[u8]) -> String {
let mut off = 0usize;
self.pk_columns()
.map(|(_, col)| {
let cell = &pk_bytes[off..off + col.size() as usize];
off += cell.len();
let tc = col.type_code;
match gnitz_wire::FixedInt::from_type_code(tc) {
Some(fi) if fi.is_signed() => format!("{}", gnitz_wire::decode_opk_i64(cell, fi)),
Some(fi) => format!("{}", gnitz_wire::decode_opk_i64(cell, fi) as u64),
None => {
let native = gnitz_wire::key_image(tc, gnitz_wire::widen_pk_be(cell));
match tc {
TypeCode::UUID => gnitz_wire::format_uuid(native),
TypeCode::I128 => format!("{}", native as i128),
_ => format!("{native}"),
}
}
}
})
.collect::<Vec<_>>()
.join(", ")
}
#[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(crate) const fn region_start(&self, r: usize, cap: usize) -> usize {
cap * self.region_off[r] as usize
}
#[inline(always)]
pub(crate) const fn region_stride(&self, r: usize) -> usize {
(self.region_off[r + 1] - self.region_off[r]) as usize
}
pub(crate) fn same_regions(&self, other: &SchemaDescriptor) -> bool {
self.region_off[..=self.num_regions()] == other.region_off[..=other.num_regions()]
}
#[inline]
pub(crate) 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 column(&self, ci: usize) -> Option<SchemaColumn> {
(ci < self.num_columns()).then(|| self.columns[ci])
}
pub(crate) fn check_cols<'a>(&self, cols: impl IntoIterator<Item = (&'a str, u32)>) -> Result<(), String> {
for (what, c) in cols {
if self.column(c as usize).is_none() {
return Err(oob_col(what, c, self));
}
}
Ok(())
}
pub fn covers_pk(&self, cols: &[u32]) -> bool {
self.pk_cols().iter().all(|p| cols.contains(p))
}
pub fn pk_only(&self) -> SchemaDescriptor {
let mut b = DerivedSchema::new();
b.push_pk_of(self);
b.finish().expect("a schema's own PK is admissible")
}
#[inline]
pub fn payload_col_idx(&self, pi: usize) -> usize {
debug_assert!(pi < self.num_payload_cols(), "payload_col_idx: pi out of range");
self.payload_to_ci[pi] as usize
}
}
impl ColumnTable for SchemaDescriptor {
#[inline]
fn pk_cols(&self) -> &[u32] {
&self.pk_indices[..self.pk_count as usize]
}
#[inline]
fn num_columns(&self) -> usize {
self.num_columns as usize
}
#[inline]
fn col_type_code(&self, ci: usize) -> TypeCode {
self.columns[ci].type_code
}
#[inline]
fn col_nullable(&self, ci: usize) -> bool {
self.columns[ci].nullable
}
}
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 index_spec_and_schema(
source_cols: &[u32],
source: &SchemaDescriptor,
) -> Result<(KeySpec, SchemaDescriptor), String> {
let spec = KeySpec::new(source_cols, source)?;
Ok((spec, spec.output_schema(source)))
}
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(())
})?;
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())
}
pub fn project_schema(schema: &SchemaDescriptor, project: &[u32]) -> Result<SchemaDescriptor, String> {
let mut b = DerivedSchema::new();
b.push_pk_of(schema);
for &p in project {
if schema.payload_slot(p as usize).is_none() {
return Err(format!(
"column {p} is not a payload column of a {}-column schema",
schema.num_columns()
));
}
b.push(schema.columns[p as usize]);
}
b.finish()
}
pub fn key_prefixed_schema(prefix: SchemaColumn, schema: &SchemaDescriptor) -> Option<SchemaDescriptor> {
let mut b = DerivedSchema::new();
b.push_pk(prefix);
b.push_pk_of(schema);
b.push_payload_of(schema);
b.finish().ok()
}
#[cfg(test)]
#[path = "tests/schema.rs"]
mod tests;