spacetimedb_schema_2/
error.rs

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