Skip to main content

spacetimedb_schema/
error.rs

1use spacetimedb_data_structures::error_stream::ErrorStream;
2use spacetimedb_lib::db::raw_def::v9::{Lifecycle, RawScopedTypeNameV9};
3use spacetimedb_lib::{ProductType, SumType};
4use spacetimedb_primitives::{ColId, ColList, ColSet};
5use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type;
6use spacetimedb_sats::{bsatn::DecodeError, raw_identifier::RawIdentifier, AlgebraicType, AlgebraicTypeRef};
7use std::fmt;
8
9use crate::def::{FunctionKind, ScopedTypeName};
10use crate::identifier::Identifier;
11use crate::type_for_generate::ClientCodegenError;
12
13/// A stream of validation errors, defined using the `ErrorStream` type.
14pub type ValidationErrors = ErrorStream<ValidationError>;
15
16/// A single validation error.
17///
18/// Many variants of this enum store `RawIdentifier`s rather than `Identifier`s.
19/// This is because we want to support reporting errors about module entities with invalid names.
20#[derive(thiserror::Error, Debug, PartialOrd, Ord, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum ValidationError {
23    #[error("name `{name}` is used for multiple entities")]
24    DuplicateName { name: RawIdentifier },
25    #[error("name `{name}` is used for multiple types")]
26    DuplicateTypeName { name: ScopedTypeName },
27    #[error("Multiple reducers defined for lifecycle event {lifecycle:?}")]
28    DuplicateLifecycle { lifecycle: Lifecycle },
29    #[error("module contains invalid identifier: {error}")]
30    IdentifierError { error: IdentifierError },
31    #[error("table `{}` has unnamed column `{}`, which is forbidden.", column.table, column.column)]
32    UnnamedColumn { column: RawColumnName },
33    #[error("type `{type_name:?}` is not annotated with a custom ordering, but uses one: {bad_type}")]
34    TypeHasIncorrectOrdering {
35        type_name: RawScopedTypeNameV9,
36        ref_: AlgebraicTypeRef,
37        /// Could be a sum or product.
38        bad_type: PrettyAlgebraicType,
39    },
40    #[error("column `{column}` referenced by def {def} not found in table `{table}`")]
41    ColumnNotFound {
42        table: RawIdentifier,
43        def: RawIdentifier,
44        column: ColId,
45    },
46    #[error(
47        "{column} type {ty} does not match corresponding element at position {pos} in product type `{product_type}`"
48    )]
49    ColumnDefMalformed {
50        column: RawColumnName,
51        ty: PrettyAlgebraicType,
52        pos: ColId,
53        product_type: PrettyAlgebraicType,
54    },
55    #[error("table `{table}` has multiple primary key annotations")]
56    RepeatedPrimaryKey { table: RawIdentifier },
57    #[error("Attempt to define {column} with more than 1 auto_inc sequence")]
58    OneAutoInc { column: RawColumnName },
59    #[error("No index found to support unique constraint `{constraint}` for columns `{columns:?}`")]
60    UniqueConstraintWithoutIndex { constraint: RawIdentifier, columns: ColSet },
61    #[error("Direct index does not support type `{ty}` in column `{column}` in index `{index}`")]
62    DirectIndexOnBadType {
63        index: RawIdentifier,
64        column: RawIdentifier,
65        ty: PrettyAlgebraicType,
66    },
67    #[error("def `{def}` has duplicate columns: {columns:?}")]
68    DuplicateColumns { def: RawIdentifier, columns: ColList },
69    #[error("invalid sequence column type: {column} with type `{column_type:?}` in sequence `{sequence}`")]
70    InvalidSequenceColumnType {
71        sequence: RawIdentifier,
72        column: RawColumnName,
73        column_type: PrettyAlgebraicType,
74    },
75    #[error("invalid sequence range information: expected {min_value:?} <= {start:?} <= {max_value:?} in sequence `{sequence}`")]
76    InvalidSequenceRange {
77        sequence: RawIdentifier,
78        min_value: Option<i128>,
79        start: Option<i128>,
80        max_value: Option<i128>,
81    },
82    #[error("View {view} has invalid return type {ty}")]
83    InvalidViewReturnType {
84        view: RawIdentifier,
85        ty: PrettyAlgebraicType,
86    },
87    #[error("Table {table} has invalid product_type_ref {ref_}")]
88    InvalidProductTypeRef {
89        table: RawIdentifier,
90        ref_: AlgebraicTypeRef,
91    },
92    #[error("Type {type_name:?} has invalid ref: {ref_}")]
93    InvalidTypeRef {
94        type_name: RawScopedTypeNameV9,
95        ref_: AlgebraicTypeRef,
96    },
97    #[error("A scheduled table must have columns `scheduled_id: u64` and `scheduled_at: ScheduledAt`, but table `{table}` has columns {columns:?}")]
98    ScheduledIncorrectColumns { table: RawIdentifier, columns: ProductType },
99    #[error("error at {location}: {error}")]
100    ClientCodegenError {
101        location: TypeLocation,
102        error: ClientCodegenError,
103    },
104    #[error("Missing type definition for ref: {ref_}, holds type: {ty}")]
105    MissingTypeDef {
106        ref_: AlgebraicTypeRef,
107        ty: PrettyAlgebraicType,
108    },
109    #[error("{column} is primary key but has no unique constraint")]
110    MissingPrimaryKeyUniqueConstraint { column: RawColumnName },
111    #[error("Table {table} should have a type definition for its product_type_element, but does not")]
112    TableTypeNameMismatch { table: Identifier },
113    #[error("Schedule {schedule} refers to a scheduled reducer or procedure {function} that does not exist")]
114    MissingScheduledFunction { schedule: Identifier, function: Identifier },
115    #[error("Scheduled {function_kind} {function_name} expected to have type {expected}, but has type {actual}")]
116    IncorrectScheduledFunctionParams {
117        function_name: RawIdentifier,
118        function_kind: FunctionKind,
119        expected: PrettyAlgebraicType,
120        actual: PrettyAlgebraicType,
121    },
122    #[error("Table name is reserved for system use: {table}")]
123    TableNameReserved { table: Identifier },
124    #[error("Row-level security invalid: `{error}`, query: `{sql}")]
125    InvalidRowLevelQuery { sql: String, error: String },
126    #[error("Failed to deserialize default value for table {table} column {col_id}: {err}")]
127    ColumnDefaultValueMalformed {
128        table: RawIdentifier,
129        col_id: ColId,
130        err: DecodeError,
131    },
132    #[error("Multiple default values for table {table} column {col_id}")]
133    MultipleColumnDefaultValues { table: RawIdentifier, col_id: ColId },
134    #[error("Table {table} not found")]
135    TableNotFound { table: RawIdentifier },
136    #[error("Name {name} is used for multiple reducers, procedures and/or views")]
137    DuplicateFunctionName { name: Identifier },
138    #[error("lifecycle event {lifecycle:?} without reducer")]
139    LifecycleWithoutReducer { lifecycle: Lifecycle },
140    #[error("lifecycle event {lifecycle:?} assigned multiple reducers")]
141    DuplicateLifeCycle { lifecycle: Lifecycle },
142    #[error("table {table} is assigned in multiple schedules")]
143    DuplicateSchedule { table: Identifier },
144    #[error("table {} corresponding to schedule {} not found", table_name, schedule_name)]
145    MissingScheduleTable {
146        table_name: RawIdentifier,
147        schedule_name: Identifier,
148    },
149    #[error("reducer {reducer_name} has invalid return type: found Result<{ok_type}, {err_type}>")]
150    InvalidReducerReturnType {
151        reducer_name: RawIdentifier,
152        ok_type: PrettyAlgebraicType,
153        err_type: PrettyAlgebraicType,
154    },
155}
156
157/// A wrapper around an `AlgebraicType` that implements `fmt::Display`.
158#[derive(PartialOrd, Ord, PartialEq, Eq)]
159pub struct PrettyAlgebraicType(pub AlgebraicType);
160
161impl fmt::Display for PrettyAlgebraicType {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        fmt_algebraic_type(&self.0).fmt(f)
164    }
165}
166impl fmt::Debug for PrettyAlgebraicType {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        <Self as fmt::Display>::fmt(self, f)
169    }
170}
171impl From<AlgebraicType> for PrettyAlgebraicType {
172    fn from(ty: AlgebraicType) -> Self {
173        Self(ty)
174    }
175}
176impl From<ProductType> for PrettyAlgebraicType {
177    fn from(ty: ProductType) -> Self {
178        let ty: AlgebraicType = ty.into();
179        Self(ty)
180    }
181}
182impl From<SumType> for PrettyAlgebraicType {
183    fn from(ty: SumType) -> Self {
184        let ty: AlgebraicType = ty.into();
185        Self(ty)
186    }
187}
188
189/// A place a type can be located in a module.
190#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
191pub enum TypeLocation {
192    /// A reducer argument.
193    ReducerArg {
194        reducer_name: RawIdentifier,
195        position: usize,
196        arg_name: Option<RawIdentifier>,
197    },
198    /// A procedure argument.
199    ProcedureArg {
200        procedure_name: RawIdentifier,
201        position: usize,
202        arg_name: Option<RawIdentifier>,
203    },
204    /// A view argument.
205    ViewArg {
206        view_name: RawIdentifier,
207        position: usize,
208        arg_name: Option<RawIdentifier>,
209    },
210    /// A procedure return type.
211    ProcedureReturn { procedure_name: RawIdentifier },
212    /// A view return type.
213    ViewReturn { view_name: RawIdentifier },
214    /// A type in the typespace.
215    InTypespace {
216        /// The reference to the type within the typespace.
217        ref_: AlgebraicTypeRef,
218    },
219}
220
221impl fmt::Display for TypeLocation {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            TypeLocation::ReducerArg {
225                reducer_name,
226                position,
227                arg_name,
228            } => {
229                write!(f, "reducer `{reducer_name}` argument {position}")?;
230                if let Some(arg_name) = arg_name {
231                    write!(f, " (`{arg_name}`)")?;
232                }
233                Ok(())
234            }
235            TypeLocation::ProcedureArg {
236                procedure_name,
237                position,
238                arg_name,
239            } => {
240                write!(f, "procedure `{procedure_name}` argument {position}")?;
241                if let Some(arg_name) = arg_name {
242                    write!(f, " (`{arg_name}`)")?;
243                }
244                Ok(())
245            }
246            TypeLocation::ViewArg {
247                view_name,
248                position,
249                arg_name,
250            } => {
251                write!(f, "view `{view_name}` argument {position}")?;
252                if let Some(arg_name) = arg_name {
253                    write!(f, " (`{arg_name}`)")?;
254                }
255                Ok(())
256            }
257            TypeLocation::ProcedureReturn { procedure_name } => {
258                write!(f, "procedure `{procedure_name}` return value")
259            }
260            TypeLocation::ViewReturn { view_name } => {
261                write!(f, "view `{view_name}` return value")
262            }
263            TypeLocation::InTypespace { ref_ } => {
264                write!(f, "typespace ref `{ref_}`")
265            }
266        }
267    }
268}
269
270/// The name of a column.
271#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
272pub struct RawColumnName {
273    /// The table the column is in.
274    pub table: RawIdentifier,
275    /// The name of the column. This may be an integer if the column is unnamed.
276    pub column: RawIdentifier,
277}
278
279impl RawColumnName {
280    /// Create a new `RawColumnName`.
281    pub fn new(table: impl Into<RawIdentifier>, column: impl Into<RawIdentifier>) -> Self {
282        Self {
283            table: table.into(),
284            column: column.into(),
285        }
286    }
287}
288
289impl fmt::Display for RawColumnName {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(f, "table `{}` column `{}`", self.table, self.column)
292    }
293}
294
295/// A reason that a string the user used is not allowed.
296#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
297pub enum IdentifierError {
298    /// The identifier is not in Unicode Normalization Form C.
299    ///
300    /// TODO(1.0): We should *canonicalize* identifiers,
301    /// rather than simply *rejecting* non-canonicalized identifiers.
302    /// However, this will require careful testing of codegen in both modules and clients,
303    /// to ensure that the canonicalization is done consistently.
304    /// Otherwise, strange name errors will result.
305    #[error(
306        "Identifier `{name}` is not in normalization form C according to Unicode Standard Annex 15 \
307        (http://www.unicode.org/reports/tr15/) and cannot be used for entities in a module."
308    )]
309    NotCanonicalized { name: RawIdentifier },
310
311    /// The identifier is reserved.
312    #[error("Identifier `{name}` is reserved by spacetimedb and cannot be used for entities in a module.")]
313    Reserved { name: RawIdentifier },
314
315    #[error(
316        "Identifier `{name}`'s starting character '{invalid_start}' is neither an underscore ('_') nor a \
317        Unicode XID_start character (according to Unicode Standard Annex 31, https://www.unicode.org/reports/tr31/) \
318        and cannot be used for entities in a module."
319    )]
320    InvalidStart { name: RawIdentifier, invalid_start: char },
321
322    #[error(
323        "Identifier `{name}` contains a character '{invalid_continue}' that is not an XID_continue character \
324        (according to Unicode Standard Annex 31, https://www.unicode.org/reports/tr31/) \
325        and cannot be used for entities in a module."
326    )]
327    InvalidContinue {
328        name: RawIdentifier,
329        invalid_continue: char,
330    },
331    // This is not a particularly useful error without a link to WHICH identifier is empty.
332    #[error("Empty identifiers are forbidden.")]
333    Empty {},
334}