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 INSERT is allowed
20    pub insertable: bool,
21    /// Whether UPDATE is allowed
22    pub updatable: bool,
23    /// Whether DELETE is allowed
24    pub deletable: bool,
25    /// Primary key column names
26    pub pk_cols: Vec<String>,
27    /// Columns indexed by name
28    pub columns: ColumnMap,
29}
30
31impl Table {
32    /// Get a column by name.
33    pub fn get_column(&self, name: &str) -> Option<&Column> {
34        self.columns.get(name)
35    }
36
37    /// Check if the table has a column.
38    pub fn has_column(&self, name: &str) -> bool {
39        self.columns.contains_key(name)
40    }
41
42    /// Get the qualified identifier for this table.
43    pub fn qualified_identifier(&self) -> QualifiedIdentifier {
44        QualifiedIdentifier::new(&self.schema, &self.name)
45    }
46
47    /// Get column names in order.
48    pub fn column_names(&self) -> impl Iterator<Item = &str> {
49        self.columns.keys().map(|s| s.as_str())
50    }
51
52    /// Check if this is a read-only view.
53    pub fn is_readonly(&self) -> bool {
54        !self.insertable && !self.updatable && !self.deletable
55    }
56}
57
58/// A table column.
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct Column {
61    /// Column name
62    pub name: String,
63    /// Description from comment
64    pub description: Option<String>,
65    /// Whether NULL is allowed
66    pub nullable: bool,
67    /// PostgreSQL data type
68    pub data_type: String,
69    /// Base type (for domains)
70    pub nominal_type: String,
71    /// Maximum length (for varchar, etc.)
72    pub max_len: Option<i32>,
73    /// Default value expression
74    pub default: Option<String>,
75    /// Enum values (for enum types)
76    pub enum_values: Vec<String>,
77    /// Whether this is part of the primary key
78    pub is_pk: bool,
79    /// Column position (1-based)
80    pub position: i32,
81}
82
83impl Column {
84    /// Check if this column has a default value.
85    pub fn has_default(&self) -> bool {
86        self.default.is_some()
87    }
88
89    /// Check if this is an auto-generated column.
90    pub fn is_auto(&self) -> bool {
91        self.default
92            .as_ref()
93            .map(|d| d.contains("nextval(") || d.contains("gen_random_uuid()"))
94            .unwrap_or(false)
95    }
96
97    /// Check if this is a JSON/JSONB column.
98    pub fn is_json(&self) -> bool {
99        self.data_type == "json" || self.data_type == "jsonb"
100    }
101
102    /// Check if this is an array type.
103    pub fn is_array(&self) -> bool {
104        self.data_type.starts_with('_') || self.data_type.ends_with("[]")
105    }
106
107    /// Check if this is a range type.
108    pub fn is_range(&self) -> bool {
109        self.data_type.ends_with("range")
110    }
111}
112
113/// Map of column name to column.
114pub type ColumnMap = IndexMap<String, Column>;
115
116/// Map of qualified identifier to table.
117pub type TablesMap = HashMap<QualifiedIdentifier, Table>;
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_table_qualified_identifier() {
125        let table = Table {
126            schema: "public".into(),
127            name: "users".into(),
128            description: None,
129            is_view: false,
130            insertable: true,
131            updatable: true,
132            deletable: true,
133            pk_cols: vec!["id".into()],
134            columns: IndexMap::new(),
135        };
136
137        let qi = table.qualified_identifier();
138        assert_eq!(qi.schema, "public");
139        assert_eq!(qi.name, "users");
140    }
141
142    #[test]
143    fn test_column_is_auto() {
144        let col1 = Column {
145            name: "id".into(),
146            description: None,
147            nullable: false,
148            data_type: "integer".into(),
149            nominal_type: "integer".into(),
150            max_len: None,
151            default: Some("nextval('users_id_seq'::regclass)".into()),
152            enum_values: vec![],
153            is_pk: true,
154            position: 1,
155        };
156        assert!(col1.is_auto());
157
158        let col2 = Column {
159            name: "uuid".into(),
160            description: None,
161            nullable: false,
162            data_type: "uuid".into(),
163            nominal_type: "uuid".into(),
164            max_len: None,
165            default: Some("gen_random_uuid()".into()),
166            enum_values: vec![],
167            is_pk: false,
168            position: 2,
169        };
170        assert!(col2.is_auto());
171
172        let col3 = Column {
173            name: "name".into(),
174            description: None,
175            nullable: false,
176            data_type: "text".into(),
177            nominal_type: "text".into(),
178            max_len: None,
179            default: None,
180            enum_values: vec![],
181            is_pk: false,
182            position: 3,
183        };
184        assert!(!col3.is_auto());
185    }
186}