Skip to main content

icydb_schema/
expression.rs

1//! Bounded source-level constraint expressions.
2
3use std::collections::BTreeSet;
4
5use crate::{
6    Account, Blob, Date, Decimal, Duration, FieldSourceKey, Float32, Float64, IntBig,
7    MAX_PROPOSAL_LITERAL_BYTES, MAX_SOURCE_CHECK_INSTRUCTIONS, NatBig, Principal, ScalarKind,
8    SchemaContractError, Subaccount, Timestamp, TypeSourceKey, Ulid, Unit,
9};
10
11/// One canonical scalar literal carried by a schema proposal.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum ScalarLiteral {
14    /// Account identifier.
15    Account(Account),
16    /// Bounded binary value.
17    Blob(Blob),
18    /// Boolean value.
19    Bool(bool),
20    /// Days since the Unix epoch.
21    Date(Date),
22    /// Canonical fixed-point decimal.
23    Decimal(Decimal),
24    /// Millisecond duration.
25    Duration(Duration),
26    /// Source-keyed unit-enum variant.
27    EnumUnit {
28        /// Immutable enum type identity.
29        enum_type: TypeSourceKey,
30        /// Immutable unit-variant identity.
31        variant: TypeSourceKey,
32    },
33    /// Finite 32-bit float.
34    Float32(Float32),
35    /// Finite 64-bit float.
36    Float64(Float64),
37    /// Signed fixed-width integer.
38    Int(i128),
39    /// Bounded canonical signed big-endian integer bytes.
40    IntBig(IntBig),
41    /// Unsigned fixed-width integer.
42    Nat(u128),
43    /// Bounded canonical unsigned big-endian integer bytes.
44    NatBig(NatBig),
45    /// Principal value.
46    Principal(Principal),
47    /// Fixed-width subaccount.
48    Subaccount(Subaccount),
49    /// Bounded text value.
50    Text(String),
51    /// Unix-millisecond timestamp.
52    Timestamp(Timestamp),
53    /// Canonical ULID.
54    Ulid(Ulid),
55    /// Explicit unit value.
56    Unit(Unit),
57}
58
59impl ScalarLiteral {
60    /// Return the declared scalar kind represented by this literal.
61    #[must_use]
62    pub const fn kind(&self) -> ScalarKind {
63        match self {
64            Self::Account(_) => ScalarKind::Account,
65            Self::Blob(_) => ScalarKind::Blob,
66            Self::Bool(_) => ScalarKind::Bool,
67            Self::Date(_) => ScalarKind::Date,
68            Self::Decimal(_) => ScalarKind::Decimal,
69            Self::Duration(_) => ScalarKind::Duration,
70            Self::EnumUnit { .. } => ScalarKind::Enum,
71            Self::Float32(_) => ScalarKind::Float32,
72            Self::Float64(_) => ScalarKind::Float64,
73            Self::Int(_) => ScalarKind::Int128,
74            Self::IntBig(_) => ScalarKind::IntBig,
75            Self::Nat(_) => ScalarKind::Nat128,
76            Self::NatBig(_) => ScalarKind::NatBig,
77            Self::Principal(_) => ScalarKind::Principal,
78            Self::Subaccount(_) => ScalarKind::Subaccount,
79            Self::Text(_) => ScalarKind::Text,
80            Self::Timestamp(_) => ScalarKind::Timestamp,
81            Self::Ulid(_) => ScalarKind::Ulid,
82            Self::Unit(_) => ScalarKind::Unit,
83        }
84    }
85
86    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
87        match self {
88            Self::Blob(value) if value.len() > MAX_PROPOSAL_LITERAL_BYTES => {
89                Err(SchemaContractError::InvalidLiteral)
90            }
91            Self::Text(value) if value.len() > MAX_PROPOSAL_LITERAL_BYTES => {
92                Err(SchemaContractError::InvalidLiteral)
93            }
94            Self::IntBig(value) if value.to_leb128().len() > MAX_PROPOSAL_LITERAL_BYTES => {
95                Err(SchemaContractError::InvalidLiteral)
96            }
97            Self::NatBig(value) if value.to_leb128().len() > MAX_PROPOSAL_LITERAL_BYTES => {
98                Err(SchemaContractError::InvalidLiteral)
99            }
100            _ => Ok(()),
101        }
102    }
103}
104
105/// One instruction in a bounded source-level postfix check expression.
106///
107/// This is an AST transport, not accepted bytecode: field references remain
108/// typed current-name keys and IcyDB still owns accepted binding and compilation.
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub enum SourceCheckInstruction {
111    /// Push one field value.
112    Field(FieldSourceKey),
113    /// Push one admitted proposal literal.
114    Literal(ScalarLiteral),
115    /// SQL equality.
116    Equal,
117    /// SQL inequality.
118    NotEqual,
119    /// SQL less-than.
120    LessThan,
121    /// SQL less-than-or-equal.
122    LessThanOrEqual,
123    /// SQL greater-than.
124    GreaterThan,
125    /// SQL greater-than-or-equal.
126    GreaterThanOrEqual,
127    /// SQL three-valued conjunction.
128    And,
129    /// SQL three-valued disjunction.
130    Or,
131    /// SQL three-valued negation.
132    Not,
133    /// Two-valued null test.
134    IsNull,
135    /// Two-valued non-null test.
136    IsNotNull,
137    /// Bounded scalar/collection length.
138    Length,
139}
140
141/// Bounded canonical source expression.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct SourceCheckExpr {
144    instructions: Vec<SourceCheckInstruction>,
145}
146
147impl SourceCheckExpr {
148    /// Construct and validate one source expression.
149    ///
150    /// # Errors
151    ///
152    /// Returns a typed expression error for empty, oversized, malformed-stack,
153    /// or invalid-literal input.
154    pub fn try_new(instructions: Vec<SourceCheckInstruction>) -> Result<Self, SchemaContractError> {
155        let expression = Self { instructions };
156        expression.validate()?;
157        Ok(expression)
158    }
159
160    /// Borrow canonical postfix instructions.
161    #[must_use]
162    pub fn instructions(&self) -> &[SourceCheckInstruction] {
163        &self.instructions
164    }
165
166    /// Derive referenced field source keys from the expression.
167    #[must_use]
168    pub fn dependencies(&self) -> BTreeSet<FieldSourceKey> {
169        self.instructions
170            .iter()
171            .filter_map(|instruction| match instruction {
172                SourceCheckInstruction::Field(field) => Some(field.clone()),
173                _ => None,
174            })
175            .collect()
176    }
177
178    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
179        if self.instructions.is_empty() || self.instructions.len() > MAX_SOURCE_CHECK_INSTRUCTIONS {
180            return Err(SchemaContractError::InvalidExpression);
181        }
182        let mut stack_depth = 0usize;
183        for instruction in &self.instructions {
184            match instruction {
185                SourceCheckInstruction::Field(_) => {
186                    stack_depth = stack_depth
187                        .checked_add(1)
188                        .ok_or(SchemaContractError::InvalidExpression)?;
189                }
190                SourceCheckInstruction::Literal(literal) => {
191                    literal.validate()?;
192                    stack_depth = stack_depth
193                        .checked_add(1)
194                        .ok_or(SchemaContractError::InvalidExpression)?;
195                }
196                SourceCheckInstruction::Not
197                | SourceCheckInstruction::IsNull
198                | SourceCheckInstruction::IsNotNull
199                | SourceCheckInstruction::Length => {
200                    if stack_depth < 1 {
201                        return Err(SchemaContractError::InvalidExpression);
202                    }
203                }
204                SourceCheckInstruction::Equal
205                | SourceCheckInstruction::NotEqual
206                | SourceCheckInstruction::LessThan
207                | SourceCheckInstruction::LessThanOrEqual
208                | SourceCheckInstruction::GreaterThan
209                | SourceCheckInstruction::GreaterThanOrEqual
210                | SourceCheckInstruction::And
211                | SourceCheckInstruction::Or => {
212                    if stack_depth < 2 {
213                        return Err(SchemaContractError::InvalidExpression);
214                    }
215                    stack_depth -= 1;
216                }
217            }
218        }
219        if stack_depth != 1 {
220            return Err(SchemaContractError::InvalidExpression);
221        }
222        Ok(())
223    }
224}