spacetimedb_lib/db/raw_def/v8.rs
1//! Database definitions v8, the last version before they were wrapped in an enum.
2//!
3//! Nothing to do with Chrome.
4
5use crate::db::auth::{StAccess, StTableType};
6use crate::relation::FieldName;
7use crate::{AlgebraicType, ProductType, SpacetimeType};
8use derive_more::Display;
9use spacetimedb_data_structures::map::HashSet;
10use spacetimedb_primitives::*;
11
12// TODO(1.0): move these definitions into this file,
13// along with the other structs contained in it,
14// which are currently in the crate root.
15pub use crate::ModuleDefBuilder as RawModuleDefV8Builder;
16pub use crate::RawModuleDefV8;
17
18/// The amount sequences allocate each time they over-run their allocation.
19///
20/// Note that we do not perform an initial allocation during `create_sequence` or at startup.
21/// Newly-created sequences will allocate the first time they are advanced.
22pub const SEQUENCE_ALLOCATION_STEP: i128 = 4096;
23
24/// Represents a sequence definition for a database table column.
25#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
26#[sats(crate = crate)]
27pub struct RawSequenceDefV8 {
28 /// The name of the sequence.
29 pub sequence_name: Box<str>,
30 /// The position of the column associated with this sequence.
31 pub col_pos: ColId,
32 /// The increment value for the sequence.
33 pub increment: i128,
34 /// The starting value for the sequence.
35 pub start: Option<i128>,
36 /// The minimum value for the sequence.
37 pub min_value: Option<i128>,
38 /// The maximum value for the sequence.
39 pub max_value: Option<i128>,
40 /// The number of values to preallocate for the sequence.
41 /// Deprecated, in the future this concept will no longer exist.
42 pub allocated: i128,
43}
44
45impl RawSequenceDefV8 {
46 /// Creates a new [RawSequenceDefV8] instance for a specific table and column.
47 ///
48 /// # Parameters
49 ///
50 /// * `table` - The name of the table.
51 /// * `seq_name` - The name of the sequence.
52 /// * `col_pos` - The position of the column in the `table`.
53 ///
54 /// # Example
55 ///
56 /// ```
57 /// use spacetimedb_lib::db::raw_def::*;
58 ///
59 /// let sequence_def = RawSequenceDefV8::for_column("my_table", "my_sequence", 1.into());
60 /// assert_eq!(&*sequence_def.sequence_name, "seq_my_table_my_sequence");
61 /// ```
62 pub fn for_column(table: &str, column_or_name: &str, col_pos: ColId) -> Self {
63 //removes the auto-generated suffix...
64 let seq_name = column_or_name.trim_start_matches(&format!("ct_{}_", table));
65
66 RawSequenceDefV8 {
67 sequence_name: format!("seq_{}_{}", table, seq_name).into(),
68 col_pos,
69 increment: 1,
70 start: None,
71 min_value: None,
72 max_value: None,
73 // Start with no values allocated. The first time we advance the sequence,
74 // we will allocate [`SEQUENCE_ALLOCATION_STEP`] values.
75 allocated: 0,
76 }
77 }
78}
79
80/// Which type of index to create.
81///
82/// Currently only `IndexType::BTree` is allowed.
83#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Display, SpacetimeType)]
84#[sats(crate = crate)]
85pub enum IndexType {
86 /// A BTree index.
87 BTree = 0,
88 /// A Hash index.
89 Hash = 1,
90}
91
92impl From<IndexType> for u8 {
93 fn from(value: IndexType) -> Self {
94 value as u8
95 }
96}
97
98impl TryFrom<u8> for IndexType {
99 type Error = ();
100 fn try_from(v: u8) -> Result<Self, Self::Error> {
101 match v {
102 0 => Ok(IndexType::BTree),
103 1 => Ok(IndexType::Hash),
104 _ => Err(()),
105 }
106 }
107}
108
109/// A struct representing the definition of a database index.
110#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
111#[sats(crate = crate)]
112pub struct RawIndexDefV8 {
113 /// The name of the index.
114 /// This should not be assumed to follow any particular format.
115 pub index_name: Box<str>,
116 /// Whether the index is unique.
117 pub is_unique: bool,
118 /// The type of the index.
119 pub index_type: IndexType,
120 /// List of column positions that compose the index.
121 pub columns: ColList,
122}
123
124impl RawIndexDefV8 {
125 /// Creates a new [RawIndexDefV8] with the provided parameters.
126 ///
127 /// # Parameters
128 ///
129 /// * `index_name`: The name of the index.
130 /// * `columns`: List of column positions that compose the index.
131 /// * `is_unique`: Indicates whether the index enforces uniqueness.
132 pub fn btree(index_name: Box<str>, columns: impl Into<ColList>, is_unique: bool) -> Self {
133 Self {
134 columns: columns.into(),
135 index_name,
136 is_unique,
137 index_type: IndexType::BTree,
138 }
139 }
140
141 /// Creates an [RawIndexDefV8] for a specific column of a table.
142 ///
143 /// This method generates an index name based on the table name, index name, column positions, and uniqueness constraint.
144 ///
145 /// # Example
146 ///
147 /// ```
148 /// use spacetimedb_primitives::ColList;
149 /// use spacetimedb_lib::db::raw_def::*;
150 ///
151 /// let index_def = RawIndexDefV8::for_column("my_table", "test", 1, true);
152 /// assert_eq!(&*index_def.index_name, "idx_my_table_test_unique");
153 /// ```
154 pub fn for_column(table: &str, index_or_name: &str, columns: impl Into<ColList>, is_unique: bool) -> Self {
155 let unique = if is_unique { "unique" } else { "non_unique" };
156
157 // Removes the auto-generated suffix from the index name.
158 let name = index_or_name.trim_start_matches(&format!("ct_{}_", table));
159
160 // Constructs the index name using a predefined format.
161 // No duplicate the `kind_name` that was added by an constraint
162 let name = if name.ends_with(&unique) {
163 format!("idx_{table}_{name}")
164 } else {
165 format!("idx_{table}_{name}_{unique}")
166 };
167 Self::btree(name.into(), columns, is_unique)
168 }
169}
170
171/// A struct representing the definition of a database column.
172#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
173#[sats(crate = crate)]
174pub struct RawColumnDefV8 {
175 /// The name of the column.
176 pub col_name: Box<str>,
177 /// The type of the column.
178 ///
179 /// Must satisfy [AlgebraicType::is_valid_for_client_type_use].
180 pub col_type: AlgebraicType,
181}
182
183impl RawColumnDefV8 {
184 /// Convert a product type to a list of column definitions.
185 pub fn from_product_type(value: ProductType) -> Vec<RawColumnDefV8> {
186 Vec::from(value.elements)
187 .into_iter()
188 .enumerate()
189 .map(|(pos, col)| {
190 let col_name = if let Some(name) = col.name {
191 name
192 } else {
193 format!("col_{pos}").into()
194 };
195
196 RawColumnDefV8 {
197 col_name,
198 col_type: col.algebraic_type,
199 }
200 })
201 .collect()
202 }
203}
204
205impl RawColumnDefV8 {
206 /// Creates a new [RawColumnDefV8] for a system field with the specified data type.
207 ///
208 /// This method is typically used to define system columns with predefined names and data types.
209 ///
210 /// # Parameters
211 ///
212 /// * `field_name`: The name for which to create a column definition.
213 /// * `col_type`: The [AlgebraicType] of the column.
214 ///
215 /// If `type_` is not `AlgebraicType::Builtin` or `AlgebraicType::Ref`, an error will result at validation time.
216 pub fn sys(field_name: &str, col_type: AlgebraicType) -> Self {
217 Self {
218 col_name: field_name.into(),
219 col_type,
220 }
221 }
222}
223
224/// A struct representing the definition of a database constraint.
225/// Associated with a unique `TableDef`, the one that contains it.
226#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
227#[sats(crate = crate)]
228pub struct RawConstraintDefV8 {
229 /// The name of the constraint.
230 pub constraint_name: Box<str>,
231 /// The constraints applied to the columns.
232 pub constraints: Constraints,
233 /// List of column positions associated with the constraint.
234 pub columns: ColList,
235}
236
237impl RawConstraintDefV8 {
238 /// Creates a new [RawConstraintDefV8] with the specified parameters.
239 ///
240 /// # Parameters
241 ///
242 /// * `constraint_name`: The name of the constraint.
243 /// * `constraints`: The constraints.
244 /// * `columns`: List of column positions associated with the constraint.
245 pub fn new(constraint_name: Box<str>, constraints: Constraints, columns: impl Into<ColList>) -> Self {
246 Self {
247 constraint_name,
248 constraints,
249 columns: columns.into(),
250 }
251 }
252
253 /// Creates a `ConstraintDef` for a specific column of a table.
254 ///
255 /// This method generates a constraint name based on the table name, column name, and constraint type.
256 ///
257 /// # Parameters
258 ///
259 /// * `table`: The name of the table to which the constraint belongs.
260 /// * `column_name`: The name of the column associated with the constraint.
261 /// * `constraints`: The constraints.
262 /// * `columns`: List of column positions associated with the constraint.
263 ///
264 /// # Example
265 ///
266 /// ```
267 /// use spacetimedb_primitives::{Constraints, ColList};
268 /// use spacetimedb_lib::db::raw_def::*;
269 ///
270 /// let constraint_def = RawConstraintDefV8::for_column("my_table", "test", Constraints::identity(), 1);
271 /// assert_eq!(&*constraint_def.constraint_name, "ct_my_table_test_identity");
272 /// ```
273 pub fn for_column(
274 table: &str,
275 column_or_name: &str,
276 constraints: Constraints,
277 columns: impl Into<ColList>,
278 ) -> Self {
279 //removes the auto-generated suffix...
280 let name = column_or_name.trim_start_matches(&format!("idx_{}_", table));
281
282 let kind_name = format!("{:?}", constraints.kind()).to_lowercase();
283 // No duplicate the `kind_name` that was added by an index
284 if name.ends_with(&kind_name) {
285 Self::new(format!("ct_{table}_{name}").into(), constraints, columns)
286 } else {
287 Self::new(format!("ct_{table}_{name}_{kind_name}").into(), constraints, columns)
288 }
289 }
290}
291
292/// Concatenate the column names from the `columns`
293///
294/// WARNING: If the `ColId` not exist, is skipped.
295/// TODO(Tyler): This should return an error and not allow this to be constructed
296/// if there is an invalid `ColId`
297pub fn generate_cols_name<'a>(columns: &ColList, col_name: impl Fn(ColId) -> Option<&'a str>) -> String {
298 let mut column_name = Vec::with_capacity(columns.len() as usize);
299 column_name.extend(columns.iter().filter_map(col_name));
300 column_name.join("_")
301}
302
303/// A data structure representing the definition of a database table.
304///
305/// This struct holds information about the table, including its name, columns, indexes,
306/// constraints, sequences, type, and access rights.
307#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
308#[sats(crate = crate)]
309pub struct RawTableDefV8 {
310 /// The name of the table.
311 pub table_name: Box<str>,
312 /// The columns of the table.
313 /// The ordering of the columns is significant. Columns are frequently identified by `ColId`, that is, position in this list.
314 pub columns: Vec<RawColumnDefV8>,
315 /// The indexes on the table.
316 pub indexes: Vec<RawIndexDefV8>,
317 /// The constraints on the table.
318 pub constraints: Vec<RawConstraintDefV8>,
319 /// The sequences attached to the table.
320 pub sequences: Vec<RawSequenceDefV8>,
321 /// Whether the table was created by a user or by the system.
322 pub table_type: StTableType,
323 /// The visibility of the table.
324 pub table_access: StAccess,
325 /// If this is a schedule table, the reducer it is scheduled for.
326 pub scheduled: Option<Box<str>>,
327}
328
329impl RawTableDefV8 {
330 /// Create a new `TableDef` instance with the specified `table_name` and `columns`.
331 ///
332 /// # Parameters
333 ///
334 /// - `table_name`: The name of the table.
335 /// - `columns`: A `vec` of `ColumnDef` instances representing the columns of the table.
336 ///
337 pub fn new(table_name: Box<str>, columns: Vec<RawColumnDefV8>) -> Self {
338 Self {
339 table_name,
340 columns,
341 indexes: vec![],
342 constraints: vec![],
343 sequences: vec![],
344 table_type: StTableType::User,
345 table_access: StAccess::Public,
346 scheduled: None,
347 }
348 }
349
350 #[cfg(feature = "test")]
351 pub fn new_for_tests(table_name: impl Into<Box<str>>, columns: ProductType) -> Self {
352 Self::new(table_name.into(), RawColumnDefV8::from_product_type(columns))
353 }
354
355 /// Set the type of the table and return a new `TableDef` instance with the updated type.
356 pub fn with_type(self, table_type: StTableType) -> Self {
357 let mut x = self;
358 x.table_type = table_type;
359 x
360 }
361
362 /// Set the access rights for the table and return a new `TableDef` instance with the updated access rights.
363 pub fn with_access(self, table_access: StAccess) -> Self {
364 let mut x = self;
365 x.table_access = table_access;
366 x
367 }
368
369 /// Set the constraints for the table and return a new `TableDef` instance with the updated constraints.
370 pub fn with_constraints(self, constraints: Vec<RawConstraintDefV8>) -> Self {
371 let mut x = self;
372 x.constraints = constraints;
373 x
374 }
375
376 /// Concatenate the column names from the `columns`
377 ///
378 /// WARNING: If the `ColId` not exist, is skipped.
379 /// TODO(Tyler): This should return an error and not allow this to be constructed
380 /// if there is an invalid `ColId`
381 fn generate_cols_name(&self, columns: &ColList) -> String {
382 generate_cols_name(columns, |p| self.get_column(p.idx()).map(|c| &*c.col_name))
383 }
384
385 /// Generate a [RawConstraintDefV8] using the supplied `columns`.
386 pub fn with_column_constraint(mut self, kind: Constraints, columns: impl Into<ColList>) -> Self {
387 self.constraints.push(self.gen_constraint_def(kind, columns));
388 self
389 }
390
391 fn gen_constraint_def(&self, kind: Constraints, columns: impl Into<ColList>) -> RawConstraintDefV8 {
392 let columns = columns.into();
393 RawConstraintDefV8::for_column(&self.table_name, &self.generate_cols_name(&columns), kind, columns)
394 }
395
396 /// Set the indexes for the table and return a new `TableDef` instance with the updated indexes.
397 pub fn with_indexes(self, indexes: Vec<RawIndexDefV8>) -> Self {
398 let mut x = self;
399 x.indexes = indexes;
400 x
401 }
402
403 /// Generate a [RawIndexDefV8] using the supplied `columns`.
404 pub fn with_column_index(self, columns: impl Into<ColList>, is_unique: bool) -> Self {
405 let mut x = self;
406 let columns = columns.into();
407 x.indexes.push(RawIndexDefV8::for_column(
408 &x.table_name,
409 &x.generate_cols_name(&columns),
410 columns,
411 is_unique,
412 ));
413 x
414 }
415
416 /// Set the sequences for the table and return a new `TableDef` instance with the updated sequences.
417 pub fn with_sequences(self, sequences: Vec<RawSequenceDefV8>) -> Self {
418 let mut x = self;
419 x.sequences = sequences;
420 x
421 }
422
423 /// Generate a [RawSequenceDefV8] using the supplied `columns`.
424 pub fn with_column_sequence(self, columns: ColId) -> Self {
425 let mut x = self;
426
427 x.sequences.push(RawSequenceDefV8::for_column(
428 &x.table_name,
429 &x.generate_cols_name(&ColList::new(columns)),
430 columns,
431 ));
432 x
433 }
434
435 /// Set the reducer name for scheduled tables and return updated `TableDef`.
436 pub fn with_scheduled(mut self, scheduled: Option<Box<str>>) -> Self {
437 self.scheduled = scheduled;
438 self
439 }
440
441 /// Create a `TableDef` from a product type and table name.
442 ///
443 /// NOTE: If the [ProductType.name] is `None` then it auto-generate a name like `col_{col_pos}`
444 pub fn from_product(table_name: &str, row: ProductType) -> Self {
445 Self::new(
446 table_name.into(),
447 Vec::from(row.elements)
448 .into_iter()
449 .enumerate()
450 .map(|(col_pos, e)| RawColumnDefV8 {
451 col_name: e.name.unwrap_or_else(|| format!("col_{col_pos}").into()),
452 col_type: e.algebraic_type,
453 })
454 .collect::<Vec<_>>(),
455 )
456 }
457
458 /// Get an iterator deriving [RawIndexDefV8]s from the constraints that require them like `UNIQUE`.
459 ///
460 /// It looks into [Self::constraints] for possible duplicates and remove them from the result
461 pub fn generated_indexes(&self) -> impl Iterator<Item = RawIndexDefV8> + '_ {
462 self.constraints
463 .iter()
464 // We are only interested in constraints implying an index.
465 .filter(|x| x.constraints.has_indexed())
466 // Create the `IndexDef`.
467 .map(|x| {
468 let is_unique = x.constraints.has_unique();
469 RawIndexDefV8::for_column(&self.table_name, &x.constraint_name, x.columns.clone(), is_unique)
470 })
471 // Only keep those we don't yet have in the list of indices (checked by name).
472 .filter(|idx| self.indexes.iter().all(|x| x.index_name != idx.index_name))
473 }
474
475 /// Get an iterator deriving [RawSequenceDefV8] from the constraints that require them like `IDENTITY`.
476 ///
477 /// It looks into [Self::constraints] for possible duplicates and remove them from the result
478 pub fn generated_sequences(&self) -> impl Iterator<Item = RawSequenceDefV8> + '_ {
479 let cols: HashSet<_> = self.sequences.iter().map(|seq| ColList::new(seq.col_pos)).collect();
480
481 self.constraints
482 .iter()
483 // We are only interested in constraints implying a sequence.
484 .filter(move |x| !cols.contains(&x.columns) && x.constraints.has_autoinc())
485 // Create the `SequenceDef`.
486 .map(|x| RawSequenceDefV8::for_column(&self.table_name, &x.constraint_name, x.columns.head().unwrap()))
487 // Only keep those we don't yet have in the list of sequences (checked by name).
488 .filter(|seq| self.sequences.iter().all(|x| x.sequence_name != seq.sequence_name))
489 }
490
491 /// Get an iterator deriving [RawConstraintDefV8] from the indexes that require them like `UNIQUE`.
492 ///
493 /// It looks into Self::constraints for possible duplicates and remove them from the result
494 pub fn generated_constraints(&self) -> impl Iterator<Item = RawConstraintDefV8> + '_ {
495 // Collect the set of all col-lists with a constraint.
496 let cols: HashSet<_> = self
497 .constraints
498 .iter()
499 .filter(|x| x.constraints.kind() != ConstraintKind::UNSET)
500 .map(|x| &x.columns)
501 .collect();
502
503 // Those indices that are not present in the constraints above
504 // have constraints generated for them.
505 // When `idx.is_unique`, a unique constraint is generated rather than an indexed one.
506 self.indexes
507 .iter()
508 .filter(move |idx| !cols.contains(&idx.columns))
509 .map(|idx| self.gen_constraint_def(Constraints::from_is_unique(idx.is_unique), idx.columns.clone()))
510 }
511
512 /// Check if the `name` of the [FieldName] exist on this [RawTableDefV8]
513 ///
514 /// Warning: It ignores the `table_id`
515 pub fn get_column_by_field(&self, field: FieldName) -> Option<&RawColumnDefV8> {
516 self.get_column(field.col.idx())
517 }
518
519 /// Get a column by its position in the table.
520 pub fn get_column(&self, pos: usize) -> Option<&RawColumnDefV8> {
521 self.columns.get(pos)
522 }
523
524 /// Check if the `col_name` exist on this [RawTableDefV8]
525 ///
526 /// Warning: It ignores the `table_name`
527 pub fn get_column_by_name(&self, col_name: &str) -> Option<&RawColumnDefV8> {
528 self.columns.iter().find(|x| &*x.col_name == col_name)
529 }
530}