postrust_core/schema_cache/table.rs
1//! Table and column types.
2
3use crate::api_request::QualifiedIdentifier;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// A database table or view.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct Table {
11 /// Schema name
12 pub schema: String,
13 /// Table/view name
14 pub name: String,
15 /// Description from comment
16 pub description: Option<String>,
17 /// Whether this is a view (vs a table)
18 pub is_view: bool,
19 /// Whether this is a partitioned table.
20 ///
21 /// It behaves as a table for reading and writing, but not for everything:
22 /// a system column such as `xmax` cannot be read back from one.
23 #[serde(default)]
24 pub is_partitioned: bool,
25 /// Whether INSERT is allowed
26 pub insertable: bool,
27 /// Whether UPDATE is allowed
28 pub updatable: bool,
29 /// Whether DELETE is allowed
30 pub deletable: bool,
31 /// Primary key column names
32 pub pk_cols: Vec<String>,
33 /// Unique constraints, as `(name, columns)`, the primary key among them.
34 ///
35 /// Named because `ON CONFLICT ON CONSTRAINT` takes a name: an upsert has
36 /// to say which uniqueness it is resolving against, and a table may have
37 /// several.
38 #[serde(default)]
39 pub unique_constraints: Vec<(String, Vec<String>)>,
40 /// Columns indexed by name
41 pub columns: ColumnMap,
42 /// Computed columns, indexed by name.
43 ///
44 /// Not part of the table, and never returned by `select=*`: a function of
45 /// one argument of the table's own row type, which PostgreSQL lets a query
46 /// read as though it were a column. PostgREST exposes them under the name
47 /// the function has, so `?select=id,always_true` and `?always_true=is.true`
48 /// both work on a table that has no such column.
49 #[serde(default)]
50 pub computed_columns: HashMap<String, ComputedColumn>,
51}
52
53impl Column {
54 /// The type a data representation would be declared on.
55 ///
56 /// The domain where the column has one, and the type itself otherwise.
57 pub fn representation_type(&self) -> &str {
58 self.domain_type.as_deref().unwrap_or(&self.nominal_type)
59 }
60}
61
62impl Table {
63 /// Get a column by name.
64 pub fn get_column(&self, name: &str) -> Option<&Column> {
65 self.columns.get(name)
66 }
67
68 /// Get a computed column by name.
69 pub fn get_computed_column(&self, name: &str) -> Option<&ComputedColumn> {
70 self.computed_columns.get(name)
71 }
72
73 /// Check if the table has a column.
74 pub fn has_column(&self, name: &str) -> bool {
75 self.columns.contains_key(name)
76 }
77
78 /// Get the qualified identifier for this table.
79 pub fn qualified_identifier(&self) -> QualifiedIdentifier {
80 QualifiedIdentifier::new(&self.schema, &self.name)
81 }
82
83 /// Get column names in order.
84 pub fn column_names(&self) -> impl Iterator<Item = &str> {
85 self.columns.keys().map(|s| s.as_str())
86 }
87
88 /// Check if this is a read-only view.
89 pub fn is_readonly(&self) -> bool {
90 !self.insertable && !self.updatable && !self.deletable
91 }
92
93 /// The methods this relation answers, as an `Allow` header lists them.
94 ///
95 /// Reading is always offered. Writing follows what the relation can
96 /// actually do, which for a view is what its triggers give it: a view with
97 /// only an INSERT trigger takes a POST and nothing else.
98 ///
99 /// PUT is the exception that is not simply a permission. It replaces one
100 /// row named by its primary key, so a relation without a primary key
101 /// cannot answer it however writable it otherwise is -- which is why an
102 /// auto-updatable view without a key offers POST, PATCH and DELETE but not
103 /// PUT.
104 pub fn allow(&self) -> String {
105 let mut methods = vec!["OPTIONS", "GET", "HEAD"];
106 if self.insertable {
107 methods.push("POST");
108 }
109 if self.insertable && self.updatable && !self.pk_cols.is_empty() {
110 methods.push("PUT");
111 }
112 if self.updatable {
113 methods.push("PATCH");
114 }
115 if self.deletable {
116 methods.push("DELETE");
117 }
118 methods.join(",")
119 }
120}
121
122/// A function of the table's row type, read as though it were a column.
123#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct ComputedColumn {
125 /// The function to call.
126 pub function: QualifiedIdentifier,
127 /// What it returns.
128 pub data_type: String,
129 /// The comment on the function, which is the field's description.
130 ///
131 /// A column's description comes from a comment on the column; a computed
132 /// column has no column to comment on, so it comes from the function --
133 /// which is where a schema author would write it and where Hasura reads
134 /// it from.
135 #[serde(default)]
136 pub description: Option<String>,
137 /// The name of the parameter that takes the table's row.
138 ///
139 /// Usually the first and usually the only one, but not always: Hasura
140 /// records a `table_argument` because a function may take the row second,
141 /// after the session. Where it is not first the call has to be written in
142 /// named notation, which is why the name is kept rather than the position.
143 #[serde(default)]
144 pub row_argument: Option<String>,
145 /// The name of the parameter filled from the caller's session, if any.
146 ///
147 /// A function with one is not callable from the REST surface, which has no
148 /// session document to give it, and is skipped there.
149 #[serde(default)]
150 pub session_argument: Option<String>,
151 /// Whether the function takes anything besides the row.
152 ///
153 /// The REST surface has nowhere to write an argument and no session to
154 /// supply, so it reads only the fields that take neither: a field it could
155 /// only ever call wrongly is worse than a field that is not there.
156 #[serde(default)]
157 pub takes_arguments: bool,
158}
159
160/// A table column.
161#[derive(Clone, Debug, Serialize, Deserialize)]
162pub struct Column {
163 /// Column name
164 pub name: String,
165 /// Description from comment
166 pub description: Option<String>,
167 /// Whether NULL is allowed
168 pub nullable: bool,
169 /// PostgreSQL data type
170 pub data_type: String,
171 /// Base type (for domains)
172 pub nominal_type: String,
173 /// The domain this column is declared over, where it is declared over one.
174 ///
175 /// `information_schema` reports the type *underneath* a domain in
176 /// `data_type` and `udt_name`, so nothing else says that a value is a
177 /// `color` rather than an integer -- and a data representation is declared
178 /// on the domain.
179 #[serde(default)]
180 pub domain_type: Option<String>,
181 /// Maximum length (for varchar, etc.)
182 pub max_len: Option<i32>,
183 /// Default value expression
184 pub default: Option<String>,
185 /// Enum values (for enum types)
186 pub enum_values: Vec<String>,
187 /// Whether this is part of the primary key
188 pub is_pk: bool,
189 /// Column position (1-based)
190 pub position: i32,
191 /// Whether PostgreSQL generates this column's value and refuses any other.
192 ///
193 /// `GENERATED ALWAYS AS IDENTITY` and `GENERATED ALWAYS AS (...) STORED`
194 /// both mean the same thing to a writer: naming the column at all is an
195 /// error, not merely unnecessary. A `BY DEFAULT` identity is not this --
196 /// it has a default and accepts a value -- which is why the flag is about
197 /// what a write may say rather than about how a value is produced.
198 #[serde(default)]
199 pub always_generated: bool,
200}
201
202impl Column {
203 /// Check if this column has a default value.
204 pub fn has_default(&self) -> bool {
205 self.default.is_some()
206 }
207
208 /// Check if this is an auto-generated column.
209 pub fn is_auto(&self) -> bool {
210 self.default
211 .as_ref()
212 .map(|d| d.contains("nextval(") || d.contains("gen_random_uuid()"))
213 .unwrap_or(false)
214 }
215
216 /// Check if this is a JSON/JSONB column.
217 pub fn is_json(&self) -> bool {
218 self.data_type == "json" || self.data_type == "jsonb"
219 }
220
221 /// Check if this is an array type.
222 pub fn is_array(&self) -> bool {
223 self.data_type.starts_with('_') || self.data_type.ends_with("[]")
224 }
225
226 /// Check if this is a range type.
227 pub fn is_range(&self) -> bool {
228 self.data_type.ends_with("range")
229 }
230}
231
232/// Map of column name to column.
233pub type ColumnMap = IndexMap<String, Column>;
234
235/// Map of qualified identifier to table.
236pub type TablesMap = HashMap<QualifiedIdentifier, Table>;
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_table_qualified_identifier() {
244 let table = Table {
245 schema: "public".into(),
246 name: "users".into(),
247 description: None,
248 is_view: false,
249 insertable: true,
250 updatable: true,
251 deletable: true,
252 pk_cols: vec!["id".into()],
253 unique_constraints: Vec::new(),
254 columns: IndexMap::new(),
255 computed_columns: Default::default(),
256 is_partitioned: false,
257 };
258
259 let qi = table.qualified_identifier();
260 assert_eq!(qi.schema, "public");
261 assert_eq!(qi.name, "users");
262 }
263
264 fn writable(insertable: bool, updatable: bool, deletable: bool, pk: bool) -> Table {
265 Table {
266 schema: "api".into(),
267 name: "t".into(),
268 description: None,
269 is_view: true,
270 insertable,
271 updatable,
272 deletable,
273 pk_cols: if pk { vec!["id".into()] } else { Vec::new() },
274 unique_constraints: Vec::new(),
275 columns: IndexMap::new(),
276 computed_columns: Default::default(),
277 is_partitioned: false,
278 }
279 }
280
281 #[test]
282 fn allow_lists_what_the_relation_takes() {
283 assert_eq!(
284 writable(false, false, false, true).allow(),
285 "OPTIONS,GET,HEAD"
286 );
287 assert_eq!(
288 writable(true, false, false, true).allow(),
289 "OPTIONS,GET,HEAD,POST"
290 );
291 assert_eq!(
292 writable(true, true, true, true).allow(),
293 "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
294 );
295 // PUT names a row by its key, so a keyless relation cannot take it.
296 assert_eq!(
297 writable(true, true, true, false).allow(),
298 "OPTIONS,GET,HEAD,POST,PATCH,DELETE"
299 );
300 }
301
302 #[test]
303 fn test_column_is_auto() {
304 let col1 = Column {
305 name: "id".into(),
306 description: None,
307 nullable: false,
308 data_type: "integer".into(),
309 nominal_type: "integer".into(),
310 max_len: None,
311 default: Some("nextval('users_id_seq'::regclass)".into()),
312 enum_values: vec![],
313 is_pk: true,
314 position: 1,
315 always_generated: false,
316 domain_type: None,
317 };
318 assert!(col1.is_auto());
319
320 let col2 = Column {
321 name: "uuid".into(),
322 description: None,
323 nullable: false,
324 data_type: "uuid".into(),
325 nominal_type: "uuid".into(),
326 max_len: None,
327 default: Some("gen_random_uuid()".into()),
328 enum_values: vec![],
329 is_pk: false,
330 position: 2,
331 always_generated: false,
332 domain_type: None,
333 };
334 assert!(col2.is_auto());
335
336 let col3 = Column {
337 name: "name".into(),
338 description: None,
339 nullable: false,
340 data_type: "text".into(),
341 nominal_type: "text".into(),
342 max_len: None,
343 default: None,
344 enum_values: vec![],
345 is_pk: false,
346 position: 3,
347 always_generated: false,
348 domain_type: None,
349 };
350 assert!(!col3.is_auto());
351 }
352}