use std::num::NonZeroU64;
use crate::{ColType, PkListRole, PkRule, TypeCode, WireProbeMode};
pub struct WireSysCol {
pub name: &'static str,
pub type_code: TypeCode,
}
const fn col(name: &'static str, type_code: TypeCode) -> WireSysCol {
WireSysCol { name, type_code }
}
const fn col_index_in(cols: &[WireSysCol], name: &str) -> usize {
let mut i = 0;
while i < cols.len() {
let a = cols[i].name.as_bytes();
let b = name.as_bytes();
if a.len() == b.len() {
let mut j = 0;
let mut matched = true;
while j < a.len() {
if a[j] != b[j] {
matched = false;
break;
}
j += 1;
}
if matched {
return i;
}
}
i += 1;
}
panic!("column not found")
}
const fn pay_index_in_fam(id: u64, name: &str) -> usize {
let f = &SYS_FAMILIES[match sys_family_index(id) {
Some(i) => i,
None => panic!("not a system family"),
}];
match payload_slot(f.pk_cols, col_index_in(f.cols, name)) {
Some(pi) => pi,
None => panic!("a PK column has no payload slot"),
}
}
#[inline]
pub const fn unpack_pair_pk(pk: u128) -> (u64, u64) {
((pk >> 64) as u64, pk as u64)
}
const SCHEMA_TAB_COLS: &[WireSysCol] = &[col("schema_id", TypeCode::U64), col("name", TypeCode::String)];
const TABLE_TAB_COLS: &[WireSysCol] = &[
col("table_id", TypeCode::U64),
col("schema_id", TypeCode::U64),
col("name", TypeCode::String),
col("pk_col_idx", TypeCode::U64),
col("flags", TypeCode::U64),
];
const VIEW_TAB_COLS: &[WireSysCol] = &[
col("view_id", TypeCode::U64),
col("schema_id", TypeCode::U64),
col("name", TypeCode::String),
col("pk_col_idx", TypeCode::U64),
col("capacity_bytes", TypeCode::U64),
col("delta_bytes", TypeCode::U64),
col("owner_view_id", TypeCode::U64),
col("pk_repeats", TypeCode::U64),
];
const COL_TAB_COLS: &[WireSysCol] = &[
col("owner_id", TypeCode::U64),
col("col_idx", TypeCode::U64),
col("name", TypeCode::String),
col("type_code", TypeCode::U64),
col("is_nullable", TypeCode::U64),
col("fk_table_id", TypeCode::U64),
col("fk_col_idx", TypeCode::U64),
col("is_hidden", TypeCode::U64),
col("scale", TypeCode::U64),
];
const IDX_TAB_COLS: &[WireSysCol] = &[
col("index_id", TypeCode::U64),
col("owner_id", TypeCode::U64),
col("source_col_idx", TypeCode::U64),
col("name", TypeCode::String),
col("is_unique", TypeCode::U64),
];
const SEQ_TAB_COLS: &[WireSysCol] = &[col("seq_id", TypeCode::U64), col("next_val", TypeCode::U64)];
const CIRCUIT_TAB_COLS: &[WireSysCol] = &[col("view_id", TypeCode::U64), col("circuit", TypeCode::Blob)];
pub const SCHEMATAB_PAY_NAME: usize = pay_index_in_fam(SCHEMA_TAB, "name");
pub const TABTAB_PAY_PK_COL_IDX: usize = pay_index_in_fam(TABLE_TAB, "pk_col_idx");
pub const TABTAB_PAY_FLAGS: usize = pay_index_in_fam(TABLE_TAB, "flags");
pub const VIEWTAB_PAY_PK_COL_IDX: usize = pay_index_in_fam(VIEW_TAB, "pk_col_idx");
pub const VIEWTAB_PAY_CAPACITY: usize = pay_index_in_fam(VIEW_TAB, "capacity_bytes");
pub const VIEWTAB_PAY_DELTA: usize = pay_index_in_fam(VIEW_TAB, "delta_bytes");
pub const VIEWTAB_PAY_OWNER_VIEW_ID: usize = pay_index_in_fam(VIEW_TAB, "owner_view_id");
pub const VIEWTAB_PAY_PK_REPEATS: usize = pay_index_in_fam(VIEW_TAB, "pk_repeats");
const fn shared_pay_index(a: u64, b: u64, name: &str) -> usize {
let i = pay_index_in_fam(a, name);
assert!(
i == pay_index_in_fam(b, name),
"the two families disagree on this column"
);
i
}
pub const RELTAB_PAY_SCHEMA_ID: usize = shared_pay_index(TABLE_TAB, VIEW_TAB, "schema_id");
pub const RELTAB_PAY_NAME: usize = shared_pay_index(TABLE_TAB, VIEW_TAB, "name");
pub const COLTAB_PAY_NAME: usize = pay_index_in_fam(COL_TAB, "name");
pub const COLTAB_PAY_TYPE_CODE: usize = pay_index_in_fam(COL_TAB, "type_code");
pub const COLTAB_PAY_FK_TABLE_ID: usize = pay_index_in_fam(COL_TAB, "fk_table_id");
pub const COLTAB_PAY_FK_COL_IDX: usize = pay_index_in_fam(COL_TAB, "fk_col_idx");
pub const COLTAB_PAY_IS_NULLABLE: usize = pay_index_in_fam(COL_TAB, "is_nullable");
pub const COLTAB_PAY_IS_HIDDEN: usize = pay_index_in_fam(COL_TAB, "is_hidden");
pub const COLTAB_PAY_SCALE: usize = pay_index_in_fam(COL_TAB, "scale");
pub const CIRCTAB_PAY_CIRCUIT: usize = pay_index_in_fam(CIRCUIT_TAB, "circuit");
pub const IDXTAB_PAY_OWNER_ID: usize = pay_index_in_fam(IDX_TAB, "owner_id");
pub const IDXTAB_PAY_SOURCE_COLS: usize = pay_index_in_fam(IDX_TAB, "source_col_idx");
pub const IDXTAB_PAY_NAME: usize = pay_index_in_fam(IDX_TAB, "name");
pub const IDXTAB_PAY_IS_UNIQUE: usize = pay_index_in_fam(IDX_TAB, "is_unique");
pub const SEQTAB_PAY_VALUE: usize = pay_index_in_fam(SEQ_TAB, "next_val");
const FNV_PRIME: u64 = 0x100_0000_01b3;
const fn fnv_bytes(mut h: u64, b: &[u8]) -> u64 {
let mut i = 0;
while i < b.len() {
h = (h ^ b[i] as u64).wrapping_mul(FNV_PRIME);
i += 1;
}
h
}
const fn fold_family(mut h: u64, f: &WireSysFamily) -> u64 {
let (cols, pk) = (f.cols, f.pk_cols);
h = (h ^ f.id).wrapping_mul(FNV_PRIME);
h = fnv_bytes(h, f.name.as_bytes());
let mut i = 0;
while i < cols.len() {
h = fnv_bytes(h, cols[i].name.as_bytes());
h = (h ^ cols[i].type_code as u64).wrapping_mul(FNV_PRIME);
i += 1;
}
let mut k = 0;
while k < pk.len() {
h = (h ^ pk[k] as u64).wrapping_mul(FNV_PRIME);
k += 1;
}
h
}
pub const SYS_SCHEMA_DIGEST: u64 = {
let mut h = 0xcbf2_9ce4_8422_2325;
let mut i = 0;
while i < SYS_FAMILIES.len() {
h = fold_family(h, &SYS_FAMILIES[i]);
i += 1;
}
h = (h ^ crate::circuit::CIRCUIT_VERSION as u64).wrapping_mul(FNV_PRIME);
h = (h ^ EXPR_BLOB_VERSION as u64).wrapping_mul(FNV_PRIME);
h
};
pub const EXPR_BLOB_VERSION: u8 = 7;
pub const SCHEMA_TAB: u64 = 1;
pub const TABLE_TAB: u64 = 2;
pub const VIEW_TAB: u64 = 3;
pub const COL_TAB: u64 = 4;
pub const IDX_TAB: u64 = 5;
pub const SEQ_TAB: u64 = 7;
pub const CIRCUIT_TAB: u64 = 11;
pub struct WireSysFamily {
pub id: u64,
pub name: &'static str,
pub cols: &'static [WireSysCol],
pub pk_cols: &'static [u32],
}
const fn fam(id: u64, name: &'static str, cols: &'static [WireSysCol], pk_cols: &'static [u32]) -> WireSysFamily {
WireSysFamily { id, name, cols, pk_cols }
}
pub const SYS_FAMILIES: &[WireSysFamily] = &[
fam(SCHEMA_TAB, "_schemas", SCHEMA_TAB_COLS, &[0]),
fam(TABLE_TAB, "_tables", TABLE_TAB_COLS, &[0]),
fam(VIEW_TAB, "_views", VIEW_TAB_COLS, &[0]),
fam(COL_TAB, "_columns", COL_TAB_COLS, &[0, 1]),
fam(IDX_TAB, "_indices", IDX_TAB_COLS, &[0]),
fam(SEQ_TAB, "_sequences", SEQ_TAB_COLS, &[0]),
fam(CIRCUIT_TAB, "_circuits", CIRCUIT_TAB_COLS, &[0]),
];
const _: () = {
let mut f = 0;
while f < SYS_FAMILIES.len() {
let (cols, pk) = (SYS_FAMILIES[f].cols, SYS_FAMILIES[f].pk_cols);
let mut i = 0;
while i < pk.len() {
assert!(
matches!(cols[pk[i] as usize].type_code, TypeCode::U64),
"a system family's key column is not U64"
);
i += 1;
}
f += 1;
}
};
pub const fn sys_family_index(id: u64) -> Option<usize> {
let mut i = 0;
while i < SYS_FAMILIES.len() {
if SYS_FAMILIES[i].id == id {
return Some(i);
}
i += 1;
}
None
}
pub const FIRST_USER_TABLE_ID: u64 = 16;
pub const CATALOG_ID_CEILING: u64 = 1 << 31;
pub fn is_valid_ident_char(ch: u8) -> bool {
ch.is_ascii_alphanumeric() || ch == b'_'
}
pub fn validate_user_identifier(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("Identifier cannot be empty".into());
}
if name.as_bytes()[0] == b'_' {
return Err(format!(
"User identifiers cannot start with '_' (reserved for system prefix): {name}"
));
}
for &ch in name.as_bytes() {
if !is_valid_ident_char(ch) {
return Err(format!("Identifier contains invalid characters: {name}"));
}
}
Ok(())
}
pub fn canonical_identifier(name: &str) -> Result<String, String> {
validate_user_identifier(name)?;
Ok(name.to_ascii_lowercase())
}
pub fn qualified_key(schema_name: &str, name: &str) -> String {
let mut q = String::with_capacity(schema_name.len() + 1 + name.len());
q.push_str(schema_name);
q.push('.');
q.push_str(name);
q
}
pub const MAX_COLUMNS: usize = 65;
pub const fn payload_slot(pk: &[u32], ci: usize) -> Option<usize> {
if contains_col(pk, ci) {
return None;
}
let mut below = 0;
let mut k = 0;
while k < pk.len() {
below += ((pk[k] as usize) < ci) as usize;
k += 1;
}
Some(ci - below)
}
pub const fn payload_col_idx(pk: &[u32], pi: usize) -> usize {
let mut ci = 0;
let mut slot = 0;
loop {
if !contains_col(pk, ci) {
if slot == pi {
return ci;
}
slot += 1;
}
ci += 1;
}
}
const fn contains_col(cols: &[u32], ci: usize) -> bool {
let mut k = 0;
while k < cols.len() {
if cols[k] as usize == ci {
return true;
}
k += 1;
}
false
}
pub const MAX_PK_COLUMNS: usize = PK_LIST_MAX_COLS + 1;
pub const MAX_PK_BYTES: usize = MAX_PK_COLUMNS * 16;
pub const PK_LIST_MAX_COLS: usize = 4;
const PK_LIST_COUNT_BITS: u32 = 4;
const PK_LIST_COL_BITS: u32 = 7;
const PK_LIST_COL_MAX: u32 = (1 << PK_LIST_COL_BITS) - 1;
const _: () = assert!(PK_LIST_MAX_COLS >= 1);
const _: () = assert!(
PK_LIST_MAX_COLS < (1 << PK_LIST_COUNT_BITS),
"PK_LIST_MAX_COLS overflows the packed count field"
);
const _: () = assert!(
PK_LIST_COUNT_BITS as usize + PK_LIST_COL_BITS as usize * PK_LIST_MAX_COLS <= 62,
"PK_LIST_MAX_COLS overflows the packed u64 column region" );
const _: () = assert!(
MAX_COLUMNS <= 1 << PK_LIST_COL_BITS,
"a schema column index no longer fits the packed 7-bit field"
);
pub const PK_LIST_PACKED_FLAG: u64 = 1 << 63;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Probe {
Pk,
PkColumn(u32),
Index(PkColList, NonZeroU64),
}
impl Probe {
pub fn wire(self) -> (WireProbeMode, u64, u64) {
match self {
Probe::Pk => (WireProbeMode::Pk, 0, 0),
Probe::PkColumn(col) => (WireProbeMode::PkColumn, col as u64, 0),
Probe::Index(cols, cap) => (WireProbeMode::Index, cap.get(), cols.pack()),
}
}
pub fn from_wire(mode: WireProbeMode, arg0: u64, arg1: u64) -> Result<Self, String> {
let refused = || format!("no probe is carried as ({mode:?}, {arg0}, {arg1:#x})");
match (mode, arg0, arg1) {
(WireProbeMode::Pk, 0, 0) => Ok(Probe::Pk),
(WireProbeMode::PkColumn, col, 0) => Ok(Probe::PkColumn(u32::try_from(col).map_err(|_| refused())?)),
(WireProbeMode::Index, cap, cols) => {
let cap = NonZeroU64::new(cap).ok_or_else(refused)?;
let cols = PkColList::unpack(cols).map_err(|e| e.for_role(PkListRole::ColumnList))?;
Ok(Probe::Index(cols, cap))
}
_ => Err(refused()),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct PkColList {
cols: [u32; PK_LIST_MAX_COLS],
len: usize,
}
impl PkColList {
pub fn checked(cols: &[u32], ncols: usize) -> Result<Self, PkRule> {
crate::validate_pk_indices(cols, ncols.min(MAX_COLUMNS), PK_LIST_MAX_COLS)?;
let mut arr = [0u32; PK_LIST_MAX_COLS];
arr[..cols.len()].copy_from_slice(cols);
Ok(PkColList { cols: arr, len: cols.len() })
}
pub fn from_slice(cols: &[u32]) -> Self {
Self::checked(cols, MAX_COLUMNS).unwrap_or_else(|rule| {
panic!(
"PkColList::from_slice({cols:?}): {}",
rule.for_role(PkListRole::ColumnList)
)
})
}
pub fn as_slice(&self) -> &[u32] {
&self.cols[..self.len]
}
pub fn pack(self) -> u64 {
let mut w = PK_LIST_PACKED_FLAG | self.len as u64;
for (i, &c) in self.as_slice().iter().enumerate() {
w |= (c as u64) << (PK_LIST_COUNT_BITS + PK_LIST_COL_BITS * i as u32);
}
w
}
pub fn unpack(word: u64) -> Result<Self, PkRule> {
if word & PK_LIST_PACKED_FLAG == 0 {
return Err(PkRule::NotPacked);
}
let n = (word & ((1 << PK_LIST_COUNT_BITS) - 1)) as usize;
if n > PK_LIST_MAX_COLS {
return Err(PkRule::TooManyColumns { count: n, max: PK_LIST_MAX_COLS });
}
let mut cols = [0u32; PK_LIST_MAX_COLS];
for (i, slot) in cols[..n].iter_mut().enumerate() {
*slot = ((word >> (PK_LIST_COUNT_BITS + PK_LIST_COL_BITS * i as u32)) & PK_LIST_COL_MAX as u64) as u32;
}
Self::checked(&cols[..n], MAX_COLUMNS)
}
}
const TABLE_FLAG_REPLICATED: u64 = 1;
const TABLE_FLAG_STREAM: u64 = 1 << 1;
const TABLE_FLAG_SERIAL: u64 = 1 << 2;
const TABLE_FLAG_DIST_SHIFT: u32 = 8;
const TABLE_FLAG_DIST_MASK: u64 = 0xFF;
const TABLE_FLAGS_DEFINED: u64 =
TABLE_FLAG_REPLICATED | TABLE_FLAG_STREAM | TABLE_FLAG_SERIAL | (TABLE_FLAG_DIST_MASK << TABLE_FLAG_DIST_SHIFT);
pub fn bool_word(w: u64) -> Result<bool, String> {
match w {
0 => Ok(false),
1 => Ok(true),
_ => Err(format!("boolean word {w} is neither 0 nor 1")),
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum TableDistribution {
Keyed { prefix_len: u8 },
Replicated,
}
impl Default for TableDistribution {
fn default() -> Self {
TableDistribution::Keyed { prefix_len: 0 }
}
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct TableProps {
pub stream: bool,
pub serial: bool,
pub distribution: TableDistribution,
}
impl TableProps {
#[inline]
pub fn pack(self) -> u64 {
let dist = match self.distribution {
TableDistribution::Keyed { prefix_len } => (prefix_len as u64) << TABLE_FLAG_DIST_SHIFT,
TableDistribution::Replicated => TABLE_FLAG_REPLICATED,
};
dist | if self.stream { TABLE_FLAG_STREAM } else { 0 } | if self.serial { TABLE_FLAG_SERIAL } else { 0 }
}
pub fn from_flags(flags: u64) -> Result<TableProps, String> {
if flags & !TABLE_FLAGS_DEFINED != 0 {
return Err(format!("table flags {flags:#x} carry unknown bits"));
}
let prefix_len = ((flags >> TABLE_FLAG_DIST_SHIFT) & TABLE_FLAG_DIST_MASK) as u8;
let distribution = if flags & TABLE_FLAG_REPLICATED == 0 {
TableDistribution::Keyed { prefix_len }
} else if prefix_len == 0 {
TableDistribution::Replicated
} else {
return Err(replicated_with_prefix(prefix_len));
};
Ok(TableProps {
stream: flags & TABLE_FLAG_STREAM != 0,
serial: flags & TABLE_FLAG_SERIAL != 0,
distribution,
})
}
pub fn validate(&self, pk_len: usize) -> Result<(), String> {
if let TableDistribution::Keyed { prefix_len } = self.distribution {
if prefix_len as usize > pk_len {
return Err(format!(
"distribution prefix length {prefix_len} exceeds PK column count {pk_len}"
));
}
}
if self.serial && self.stream {
return Err("a stream cannot be SERIAL: it holds no rows to seed the generator from".to_string());
}
if self.serial && pk_len != 1 {
return Err("a SERIAL table's primary key is its one SERIAL column".to_string());
}
Ok(())
}
}
pub fn validate_serial_key<'a>(pk: impl IntoIterator<Item = (&'a str, ColType)>) -> Result<(), String> {
match pk.into_iter().find(|(_, ty)| crate::FixedInt::exact(ty.tc).is_none()) {
Some((name, ty)) => Err(format!(
"SERIAL primary key column '{name}' is {ty}; SERIAL needs an integer of at most 8 bytes"
)),
None => Ok(()),
}
}
pub fn replicated_with_prefix(prefix_len: u8) -> String {
format!(
"REPLICATED and CLUSTER BY are mutually exclusive: a replicated table keeps \
a full copy on every worker, so a hash-distribution prefix (k={prefix_len}) is meaningless"
)
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum ViewProps {
#[default]
Plain,
Bounded { capacity_bytes: NonZeroU64 },
Fed { delta_bytes: NonZeroU64 },
}
impl ViewProps {
pub fn from_budgets(
capacity_bytes: Option<NonZeroU64>,
delta_bytes: Option<NonZeroU64>,
) -> Result<ViewProps, String> {
match (capacity_bytes, delta_bytes) {
(None, None) => Ok(ViewProps::Plain),
(Some(capacity_bytes), None) => Ok(ViewProps::Bounded { capacity_bytes }),
(None, Some(delta_bytes)) => Ok(ViewProps::Fed { delta_bytes }),
(Some(_), Some(_)) => Err("a capacity-bounded view cannot carry a delta feed: its bootstrap read \
hydrates from the source relation's live store, which no tick round governs"
.to_string()),
}
}
pub fn from_row(capacity: u64, delta: u64) -> Result<ViewProps, String> {
Self::from_budgets(NonZeroU64::new(capacity), NonZeroU64::new(delta))
}
pub(crate) fn row_words(self) -> (u64, u64) {
(self.capacity_bytes().unwrap_or(0), self.delta_bytes().unwrap_or(0))
}
pub fn capacity_bytes(self) -> Option<u64> {
match self {
ViewProps::Bounded { capacity_bytes } => Some(capacity_bytes.get()),
_ => None,
}
}
pub fn delta_bytes(self) -> Option<u64> {
match self {
ViewProps::Fed { delta_bytes } => Some(delta_bytes.get()),
_ => None,
}
}
}
#[cfg(test)]
#[path = "tests/catalog.rs"]
mod tests;