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