Skip to main content

icydb_schema/
expression.rs

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