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
94/// A function of the table's row type, read as though it were a column.
95#[derive(Clone, Debug, Serialize, Deserialize)]
96pub struct ComputedColumn {
97    /// The function to call.
98    pub function: QualifiedIdentifier,
99    /// What it returns.
100    pub data_type: String,
101    /// The comment on the function, which is the field's description.
102    ///
103    /// A column's description comes from a comment on the column; a computed
104    /// column has no column to comment on, so it comes from the function --
105    /// which is where a schema author would write it and where Hasura reads
106    /// it from.
107    #[serde(default)]
108    pub description: Option<String>,
109    /// The name of the parameter that takes the table's row.
110    ///
111    /// Usually the first and usually the only one, but not always: Hasura
112    /// records a `table_argument` because a function may take the row second,
113    /// after the session. Where it is not first the call has to be written in
114    /// named notation, which is why the name is kept rather than the position.
115    #[serde(default)]
116    pub row_argument: Option<String>,
117    /// The name of the parameter filled from the caller's session, if any.
118    ///
119    /// A function with one is not callable from the REST surface, which has no
120    /// session document to give it, and is skipped there.
121    #[serde(default)]
122    pub session_argument: Option<String>,
123    /// Whether the function takes anything besides the row.
124    ///
125    /// The REST surface has nowhere to write an argument and no session to
126    /// supply, so it reads only the fields that take neither: a field it could
127    /// only ever call wrongly is worse than a field that is not there.
128    #[serde(default)]
129    pub takes_arguments: bool,
130}
131
132/// A table column.
133#[derive(Clone, Debug, Serialize, Deserialize)]
134pub struct Column {
135    /// Column name
136    pub name: String,
137    /// Description from comment
138    pub description: Option<String>,
139    /// Whether NULL is allowed
140    pub nullable: bool,
141    /// PostgreSQL data type
142    pub data_type: String,
143    /// Base type (for domains)
144    pub nominal_type: String,
145    /// The domain this column is declared over, where it is declared over one.
146    ///
147    /// `information_schema` reports the type *underneath* a domain in
148    /// `data_type` and `udt_name`, so nothing else says that a value is a
149    /// `color` rather than an integer -- and a data representation is declared
150    /// on the domain.
151    #[serde(default)]
152    pub domain_type: Option<String>,
153    /// Maximum length (for varchar, etc.)
154    pub max_len: Option<i32>,
155    /// Default value expression
156    pub default: Option<String>,
157    /// Enum values (for enum types)
158    pub enum_values: Vec<String>,
159    /// Whether this is part of the primary key
160    pub is_pk: bool,
161    /// Column position (1-based)
162    pub position: i32,
163    /// Whether PostgreSQL generates this column's value and refuses any other.
164    ///
165    /// `GENERATED ALWAYS AS IDENTITY` and `GENERATED ALWAYS AS (...) STORED`
166    /// both mean the same thing to a writer: naming the column at all is an
167    /// error, not merely unnecessary. A `BY DEFAULT` identity is not this --
168    /// it has a default and accepts a value -- which is why the flag is about
169    /// what a write may say rather than about how a value is produced.
170    #[serde(default)]
171    pub always_generated: bool,
172}
173
174impl Column {
175    /// Check if this column has a default value.
176    pub fn has_default(&self) -> bool {
177        self.default.is_some()
178    }
179
180    /// Check if this is an auto-generated column.
181    pub fn is_auto(&self) -> bool {
182        self.default
183            .as_ref()
184            .map(|d| d.contains("nextval(") || d.contains("gen_random_uuid()"))
185            .unwrap_or(false)
186    }
187
188    /// Check if this is a JSON/JSONB column.
189    pub fn is_json(&self) -> bool {
190        self.data_type == "json" || self.data_type == "jsonb"
191    }
192
193    /// Check if this is an array type.
194    pub fn is_array(&self) -> bool {
195        self.data_type.starts_with('_') || self.data_type.ends_with("[]")
196    }
197
198    /// Check if this is a range type.
199    pub fn is_range(&self) -> bool {
200        self.data_type.ends_with("range")
201    }
202}
203
204/// Map of column name to column.
205pub type ColumnMap = IndexMap<String, Column>;
206
207/// Map of qualified identifier to table.
208pub type TablesMap = HashMap<QualifiedIdentifier, Table>;
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_table_qualified_identifier() {
216        let table = Table {
217            schema: "public".into(),
218            name: "users".into(),
219            description: None,
220            is_view: false,
221            insertable: true,
222            updatable: true,
223            deletable: true,
224            pk_cols: vec!["id".into()],
225            unique_constraints: Vec::new(),
226            columns: IndexMap::new(),
227            computed_columns: Default::default(),
228            is_partitioned: false,
229        };
230
231        let qi = table.qualified_identifier();
232        assert_eq!(qi.schema, "public");
233        assert_eq!(qi.name, "users");
234    }
235
236    #[test]
237    fn test_column_is_auto() {
238        let col1 = Column {
239            name: "id".into(),
240            description: None,
241            nullable: false,
242            data_type: "integer".into(),
243            nominal_type: "integer".into(),
244            max_len: None,
245            default: Some("nextval('users_id_seq'::regclass)".into()),
246            enum_values: vec![],
247            is_pk: true,
248            position: 1,
249            always_generated: false,
250            domain_type: None,
251        };
252        assert!(col1.is_auto());
253
254        let col2 = Column {
255            name: "uuid".into(),
256            description: None,
257            nullable: false,
258            data_type: "uuid".into(),
259            nominal_type: "uuid".into(),
260            max_len: None,
261            default: Some("gen_random_uuid()".into()),
262            enum_values: vec![],
263            is_pk: false,
264            position: 2,
265            always_generated: false,
266            domain_type: None,
267        };
268        assert!(col2.is_auto());
269
270        let col3 = Column {
271            name: "name".into(),
272            description: None,
273            nullable: false,
274            data_type: "text".into(),
275            nominal_type: "text".into(),
276            max_len: None,
277            default: None,
278            enum_values: vec![],
279            is_pk: false,
280            position: 3,
281            always_generated: false,
282            domain_type: None,
283        };
284        assert!(!col3.is_auto());
285    }
286}