llkv_table/constraints/
types.rs

1//! Constraint metadata definitions and helpers for persistence.
2//!
3//! This module defines the storage-friendly representations of constraints
4//! that are persisted through the system catalog. The structures intentionally
5//! avoid heavyweight data (like table names) to keep serialized blobs small and
6//! cache friendly.
7
8#![forbid(unsafe_code)]
9
10use bitcode::{Decode, Encode};
11
12use crate::types::{FieldId, RowId, TableId};
13
14/// Identifier assigned to a persisted constraint.
15///
16/// Constraint IDs are scoped per-table and limited to 32 bits so they can be
17/// embedded into catalog row IDs without collisions.
18pub type ConstraintId = u32;
19
20const TABLE_ID_BITS: u32 = (std::mem::size_of::<TableId>() * 8) as u32;
21const CONSTRAINT_ID_BITS: u32 = 64 - TABLE_ID_BITS;
22const CONSTRAINT_ID_MASK: u64 = (1u64 << CONSTRAINT_ID_BITS) - 1;
23
24const _: () = assert!(
25    std::mem::size_of::<ConstraintId>() * 8 <= CONSTRAINT_ID_BITS as usize,
26    "ConstraintId does not fit within allocated row id bits"
27);
28
29/// Pack a table/constraint pair into a catalog row identifier.
30#[inline]
31pub fn encode_constraint_row_id(table_id: TableId, constraint_id: ConstraintId) -> RowId {
32    ((table_id as u64) << CONSTRAINT_ID_BITS) | (constraint_id as u64 & CONSTRAINT_ID_MASK)
33}
34
35/// Decode a catalog row identifier into its table/constraint components.
36#[inline]
37pub fn decode_constraint_row_id(row_id: RowId) -> (TableId, ConstraintId) {
38    let constraint_id = (row_id & CONSTRAINT_ID_MASK) as ConstraintId;
39    let table_id = (row_id >> CONSTRAINT_ID_BITS) as TableId;
40    (table_id, constraint_id)
41}
42
43/// Persisted state of a constraint.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode)]
45pub enum ConstraintState {
46    Active,
47    Dropped,
48}
49
50impl ConstraintState {
51    #[inline]
52    pub fn is_active(self) -> bool {
53        matches!(self, ConstraintState::Active)
54    }
55}
56
57/// Persisted constraint record stored in the system catalog.
58#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
59pub struct ConstraintRecord {
60    pub constraint_id: ConstraintId,
61    pub kind: ConstraintKind,
62    pub state: ConstraintState,
63    /// Monotonic revision counter maintained by the metadata manager.
64    pub revision: u64,
65    /// Timestamp (microseconds since epoch) when this record was last updated.
66    pub last_modified_micros: u64,
67}
68
69impl ConstraintRecord {
70    pub fn is_active(&self) -> bool {
71        self.state.is_active()
72    }
73}
74
75/// Logical description of a constraint.
76#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
77pub enum ConstraintKind {
78    PrimaryKey(PrimaryKeyConstraint),
79    Unique(UniqueConstraint),
80    ForeignKey(ForeignKeyConstraint),
81    Check(CheckConstraint),
82}
83
84impl ConstraintKind {
85    /// Return the field IDs participating on the referencing side of the constraint.
86    pub fn field_ids(&self) -> &[FieldId] {
87        match self {
88            ConstraintKind::PrimaryKey(payload) => &payload.field_ids,
89            ConstraintKind::Unique(payload) => &payload.field_ids,
90            ConstraintKind::ForeignKey(payload) => &payload.referencing_field_ids,
91            ConstraintKind::Check(payload) => &payload.field_ids,
92        }
93    }
94}
95
96/// Primary key definition.
97#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
98pub struct PrimaryKeyConstraint {
99    pub field_ids: Vec<FieldId>,
100}
101
102/// Unique constraint definition (single or multi-column).
103#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
104pub struct UniqueConstraint {
105    pub field_ids: Vec<FieldId>,
106}
107
108/// Foreign key definition referencing another table.
109#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
110pub struct ForeignKeyConstraint {
111    pub referencing_field_ids: Vec<FieldId>,
112    pub referenced_table: TableId,
113    pub referenced_field_ids: Vec<FieldId>,
114    pub on_delete: ForeignKeyAction,
115    pub on_update: ForeignKeyAction,
116}
117
118/// Foreign key referential action.
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode)]
120pub enum ForeignKeyAction {
121    NoAction,
122    Restrict,
123}
124
125/// Serialized check constraint definition.
126///
127/// The expression payload is referenced indirectly via an identifier so the
128/// metadata layer can resolve the actual expression lazily.
129#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
130pub struct CheckConstraint {
131    pub field_ids: Vec<FieldId>,
132    pub expression_ref: ConstraintExpressionRef,
133}
134
135/// Reference to a stored constraint expression payload.
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Encode, Decode)]
137pub struct ConstraintExpressionRef(pub u64);
138
139impl ConstraintExpressionRef {
140    pub const NONE: ConstraintExpressionRef = ConstraintExpressionRef(0);
141
142    #[inline]
143    pub fn is_none(self) -> bool {
144        self.0 == 0
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn constraint_row_id_round_trip() {
154        let samples: &[(TableId, ConstraintId)] = &[
155            (1, 1),
156            (42, 7),
157            (TableId::MAX, ConstraintId::MAX),
158            (123, 987_654),
159        ];
160
161        for &(table_id, constraint_id) in samples {
162            let row_id = encode_constraint_row_id(table_id, constraint_id);
163            let (decoded_table, decoded_constraint) = decode_constraint_row_id(row_id);
164            assert_eq!(decoded_table, table_id);
165            assert_eq!(decoded_constraint, constraint_id);
166        }
167    }
168
169    #[test]
170    fn constraint_record_active_state() {
171        let record = ConstraintRecord {
172            constraint_id: 1,
173            kind: ConstraintKind::PrimaryKey(PrimaryKeyConstraint {
174                field_ids: vec![1, 2],
175            }),
176            state: ConstraintState::Active,
177            revision: 5,
178            last_modified_micros: 123,
179        };
180        assert!(record.is_active());
181    }
182}