spacetimedb_schema/
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, ColSet};
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: Box<str> },
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("Hash indexes are not supported: `{index}` is a hash index")]
61    HashIndexUnsupported { index: RawIdentifier },
62    #[error("No index found to support unique constraint `{constraint}` for columns `{columns:?}`")]
63    UniqueConstraintWithoutIndex { constraint: Box<str>, columns: ColSet },
64    #[error("Direct index does not support type `{ty}` in column `{column}` in index `{index}`")]
65    DirectIndexOnBadType {
66        index: RawIdentifier,
67        column: RawIdentifier,
68        ty: PrettyAlgebraicType,
69    },
70    #[error("def `{def}` has duplicate columns: {columns:?}")]
71    DuplicateColumns { def: RawIdentifier, columns: ColList },
72    #[error("invalid sequence column type: {column} with type `{column_type:?}` in sequence `{sequence}`")]
73    InvalidSequenceColumnType {
74        sequence: RawIdentifier,
75        column: RawColumnName,
76        column_type: PrettyAlgebraicType,
77    },
78    #[error("invalid sequence range information: expected {min_value:?} <= {start:?} <= {max_value:?} in sequence `{sequence}`")]
79    InvalidSequenceRange {
80        sequence: RawIdentifier,
81        min_value: Option<i128>,
82        start: Option<i128>,
83        max_value: Option<i128>,
84    },
85    #[error("Table {table} has invalid product_type_ref {ref_}")]
86    InvalidProductTypeRef {
87        table: RawIdentifier,
88        ref_: AlgebraicTypeRef,
89    },
90    #[error("Type {type_name:?} has invalid ref: {ref_}")]
91    InvalidTypeRef {
92        type_name: RawScopedTypeNameV9,
93        ref_: AlgebraicTypeRef,
94    },
95    #[error("A scheduled table must have columns `scheduled_id: u64` and `scheduled_at: ScheduledAt`, but table `{table}` has columns {columns:?}")]
96    ScheduledIncorrectColumns { table: RawIdentifier, columns: ProductType },
97    #[error("error at {location}: {error}")]
98    ClientCodegenError {
99        location: TypeLocation<'static>,
100        error: ClientCodegenError,
101    },
102    #[error("Missing type definition for ref: {ref_}, holds type: {ty}")]
103    MissingTypeDef {
104        ref_: AlgebraicTypeRef,
105        ty: PrettyAlgebraicType,
106    },
107    #[error("{column} is primary key but has no unique constraint")]
108    MissingPrimaryKeyUniqueConstraint { column: RawColumnName },
109    #[error("Table {table} should have a type definition for its product_type_element, but does not")]
110    TableTypeNameMismatch { table: Identifier },
111    #[error("Schedule {schedule} refers to a scheduled reducer {reducer} that does not exist")]
112    MissingScheduledReducer { schedule: Box<str>, reducer: Identifier },
113    #[error("Scheduled reducer {reducer} expected to have type {expected}, but has type {actual}")]
114    IncorrectScheduledReducerParams {
115        reducer: RawIdentifier,
116        expected: PrettyAlgebraicType,
117        actual: PrettyAlgebraicType,
118    },
119    #[error("Table name is reserved for system use: {table}")]
120    TableNameReserved { table: Identifier },
121    #[error("Row-level security invalid: `{error}`, query: `{sql}")]
122    InvalidRowLevelQuery { sql: String, error: String },
123}
124
125/// A wrapper around an `AlgebraicType` that implements `fmt::Display`.
126#[derive(PartialOrd, Ord, PartialEq, Eq)]
127pub struct PrettyAlgebraicType(pub AlgebraicType);
128
129impl fmt::Display for PrettyAlgebraicType {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        fmt_algebraic_type(&self.0).fmt(f)
132    }
133}
134impl fmt::Debug for PrettyAlgebraicType {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        <Self as fmt::Display>::fmt(self, f)
137    }
138}
139impl From<AlgebraicType> for PrettyAlgebraicType {
140    fn from(ty: AlgebraicType) -> Self {
141        Self(ty)
142    }
143}
144impl From<ProductType> for PrettyAlgebraicType {
145    fn from(ty: ProductType) -> Self {
146        let ty: AlgebraicType = ty.into();
147        Self(ty)
148    }
149}
150impl From<SumType> for PrettyAlgebraicType {
151    fn from(ty: SumType) -> Self {
152        let ty: AlgebraicType = ty.into();
153        Self(ty)
154    }
155}
156
157/// A place a type can be located in a module.
158#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
159pub enum TypeLocation<'a> {
160    /// A reducer argument.
161    ReducerArg {
162        reducer_name: Cow<'a, str>,
163        position: usize,
164        arg_name: Option<Cow<'a, str>>,
165    },
166    /// A type in the typespace.
167    InTypespace {
168        /// The reference to the type within the typespace.
169        ref_: AlgebraicTypeRef,
170    },
171}
172impl TypeLocation<'_> {
173    /// Make the lifetime of the location `'static`.
174    /// This allocates.
175    pub fn make_static(self) -> TypeLocation<'static> {
176        match self {
177            TypeLocation::ReducerArg {
178                reducer_name,
179                position,
180                arg_name,
181            } => TypeLocation::ReducerArg {
182                reducer_name: reducer_name.to_string().into(),
183                position,
184                arg_name: arg_name.map(|s| s.to_string().into()),
185            },
186            // needed to convince rustc this is allowed.
187            TypeLocation::InTypespace { ref_ } => TypeLocation::InTypespace { ref_ },
188        }
189    }
190}
191
192impl fmt::Display for TypeLocation<'_> {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            TypeLocation::ReducerArg {
196                reducer_name,
197                position,
198                arg_name,
199            } => {
200                write!(f, "reducer `{}` argument {}", reducer_name, position)?;
201                if let Some(arg_name) = arg_name {
202                    write!(f, " (`{}`)", arg_name)?;
203                }
204                Ok(())
205            }
206            TypeLocation::InTypespace { ref_ } => {
207                write!(f, "typespace ref `{}`", ref_)
208            }
209        }
210    }
211}
212
213/// The name of a column.
214#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
215pub struct RawColumnName {
216    /// The table the column is in.
217    pub table: RawIdentifier,
218    /// The name of the column. This may be an integer if the column is unnamed.
219    pub column: RawIdentifier,
220}
221
222impl RawColumnName {
223    /// Create a new `RawColumnName`.
224    pub fn new(table: impl Into<RawIdentifier>, column: impl Into<RawIdentifier>) -> Self {
225        Self {
226            table: table.into(),
227            column: column.into(),
228        }
229    }
230}
231
232impl fmt::Display for RawColumnName {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        write!(f, "table `{}` column `{}`", self.table, self.column)
235    }
236}
237
238/// A reason that a string the user used is not allowed.
239#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
240pub enum IdentifierError {
241    /// The identifier is not in Unicode Normalization Form C.
242    ///
243    /// TODO(1.0): We should *canonicalize* identifiers,
244    /// rather than simply *rejecting* non-canonicalized identifiers.
245    /// However, this will require careful testing of codegen in both modules and clients,
246    /// to ensure that the canonicalization is done consistently.
247    /// Otherwise, strange name errors will result.
248    #[error(
249        "Identifier `{name}` is not in normalization form C according to Unicode Standard Annex 15 \
250        (http://www.unicode.org/reports/tr15/) and cannot be used for entities in a module."
251    )]
252    NotCanonicalized { name: RawIdentifier },
253
254    /// The identifier is reserved.
255    #[error("Identifier `{name}` is reserved by spacetimedb and cannot be used for entities in a module.")]
256    Reserved { name: RawIdentifier },
257
258    #[error(
259        "Identifier `{name}`'s starting character '{invalid_start}' is neither an underscore ('_') nor a \
260        Unicode XID_start character (according to Unicode Standard Annex 31, https://www.unicode.org/reports/tr31/) \
261        and cannot be used for entities in a module."
262    )]
263    InvalidStart { name: RawIdentifier, invalid_start: char },
264
265    #[error(
266        "Identifier `{name}` contains a character '{invalid_continue}' that is not an XID_continue character \
267        (according to Unicode Standard Annex 31, https://www.unicode.org/reports/tr31/) \
268        and cannot be used for entities in a module."
269    )]
270    InvalidContinue {
271        name: RawIdentifier,
272        invalid_continue: char,
273    },
274    // This is not a particularly useful error without a link to WHICH identifier is empty.
275    #[error("Empty identifiers are forbidden.")]
276    Empty {},
277}