Skip to main content

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}