Skip to main content

surrealdb_expr/expr/
table_type.rs

1//! The kind of a table: schemafull document, relation, or any.
2//!
3//! `TableType` and its `Relation` payload are shared vocabulary between the
4//! DEFINE/ALTER TABLE statements and the catalog's table definitions; both
5//! planes persist them, so the revisioned shapes here are storage-stable.
6
7use common::fmt::EscapeKwFreeIdent;
8use revision::revisioned;
9use surrealdb_types::{SqlFormat, ToSql, write_sql};
10
11use crate::expr::Kind;
12use crate::expr::statements::info::InfoStructure;
13use crate::sql;
14use crate::val::{TableName, Value};
15
16/// The type of records stored by a table
17#[revisioned(revision = 1)]
18#[derive(Debug, Default, Hash, Clone, Eq, PartialEq)]
19pub enum TableType {
20	#[default]
21	Any,
22	Normal,
23	Relation(Relation),
24}
25
26impl ToSql for TableType {
27	fn fmt_sql(&self, f: &mut String, sql_fmt: SqlFormat) {
28		match self {
29			TableType::Any => f.push_str("ANY"),
30			TableType::Normal => f.push_str("NORMAL"),
31			TableType::Relation(rel) => {
32				f.push_str("RELATION");
33				if !rel.from.is_empty() {
34					f.push_str(" IN ");
35					for (idx, k) in rel.from.iter().enumerate() {
36						if idx != 0 {
37							f.push_str(" | ");
38						}
39						write_sql!(f, sql_fmt, "{}", EscapeKwFreeIdent(k.as_str()));
40					}
41				}
42				if !rel.to.is_empty() {
43					f.push_str(" OUT ");
44					for (idx, k) in rel.to.iter().enumerate() {
45						if idx != 0 {
46							f.push_str(" | ");
47						}
48						write_sql!(f, sql_fmt, "{}", EscapeKwFreeIdent(k.as_str()));
49					}
50				}
51				if rel.enforced {
52					f.push_str(" ENFORCED");
53				}
54				if rel.lightweight {
55					f.push_str(" LIGHTWEIGHT");
56				}
57			}
58		}
59	}
60}
61
62impl InfoStructure for TableType {
63	fn structure(self) -> Value {
64		match self {
65			Self::Any => Value::from(map! {
66				"kind" => "ANY".into(),
67			}),
68			Self::Normal => Value::from(map! {
69				"kind" => "NORMAL".into(),
70			}),
71			Self::Relation(rel) => Value::from(map! {
72				"kind" => "RELATION".into(),
73				"in", if !rel.from.is_empty() =>
74					rel.from.into_iter().map(Value::Table).collect::<Vec<_>>().into(),
75				"out", if !rel.to.is_empty() =>
76					rel.to.into_iter().map(Value::Table).collect::<Vec<_>>().into(),
77				"enforced" => rel.enforced.into(),
78				"lightweight", if rel.lightweight => true.into()
79			}),
80		}
81	}
82}
83
84impl From<sql::table_type::TableType> for TableType {
85	fn from(v: sql::table_type::TableType) -> Self {
86		match v {
87			sql::table_type::TableType::Any => Self::Any,
88			sql::table_type::TableType::Normal => Self::Normal,
89			sql::table_type::TableType::Relation(rel) => Self::Relation(rel.into()),
90		}
91	}
92}
93
94impl From<TableType> for sql::table_type::TableType {
95	fn from(v: TableType) -> Self {
96		match v {
97			TableType::Any => Self::Any,
98			TableType::Normal => Self::Normal,
99			TableType::Relation(rel) => Self::Relation(rel.into()),
100		}
101	}
102}
103
104#[revisioned(revision = 3)]
105#[derive(Debug, Hash, Clone, Eq, PartialEq)]
106pub struct Relation {
107	#[revision(end = 2, convert_fn = "rev_convert_from")]
108	pub old_from: Option<Kind>,
109	/// Contains the tables the relation originates from,
110	/// if empty then there was no `IN` clause
111	#[revision(start = 2)]
112	pub from: Vec<TableName>,
113	#[revision(end = 2, convert_fn = "rev_convert_to")]
114	pub old_to: Option<Kind>,
115	/// Contains the tables the relation goes to,
116	/// if empty then there was no `OUT` clause
117	#[revision(start = 2)]
118	pub to: Vec<TableName>,
119	pub enforced: bool,
120	/// A lightweight relation stores each edge as its two vertex-side
121	/// adjacency keys alone — no record, no edge-side keys, no index keys.
122	/// Its edges are synthesized on read from their canonical
123	/// `[in, out]` ids, so the table can hold no data beyond `id`, `in`
124	/// and `out`, and its endpoints must always be enforced (vertex
125	/// deletion is the only cleanup path).
126	#[revision(start = 3)]
127	pub lightweight: bool,
128}
129
130impl Relation {
131	fn rev_convert_from(&mut self, _rev: u16, value: Option<Kind>) -> Result<(), revision::Error> {
132		if let Some(x) = value {
133			let Kind::Record(x) = x else {
134				return Err(revision::Error::Conversion(format!(
135					"Invalid kind within table relation, should have been a record, found: {:#?}",
136					x,
137				)));
138			};
139			self.from = x
140		}
141		Ok(())
142	}
143	fn rev_convert_to(&mut self, _rev: u16, value: Option<Kind>) -> Result<(), revision::Error> {
144		if let Some(x) = value {
145			let Kind::Record(x) = x else {
146				return Err(revision::Error::Conversion(format!(
147					"Invalid kind within table relation, should have been a record, found: {:#?}",
148					x,
149				)));
150			};
151			self.to = x
152		}
153		Ok(())
154	}
155}
156
157impl From<sql::table_type::Relation> for Relation {
158	fn from(v: sql::table_type::Relation) -> Self {
159		Self {
160			from: v.from.into_iter().map(Into::into).collect(),
161			to: v.to.into_iter().map(Into::into).collect(),
162			enforced: v.enforced,
163			lightweight: v.lightweight,
164		}
165	}
166}
167
168impl From<Relation> for sql::table_type::Relation {
169	fn from(v: Relation) -> Self {
170		Self {
171			from: v.from.into_iter().map(Into::into).collect(),
172			to: v.to.into_iter().map(Into::into).collect(),
173			enforced: v.enforced,
174			lightweight: v.lightweight,
175		}
176	}
177}