Skip to main content

dbrest_core/schema_cache/
table.rs

1//! Table and Column types for schema cache
2//!
3//! This module defines the types for representing PostgreSQL tables, views,
4//! and their columns in the schema cache.
5
6use compact_str::CompactString;
7use indexmap::IndexMap;
8use smallvec::SmallVec;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::types::QualifiedIdentifier;
13
14/// Table/View metadata
15///
16/// Represents a PostgreSQL table, view, materialized view, or foreign table
17/// with its metadata and columns.
18#[derive(Debug, Clone)]
19pub struct Table {
20    /// Schema name
21    pub schema: CompactString,
22    /// Table/view name
23    pub name: CompactString,
24    /// Description from pg_description
25    pub description: Option<String>,
26    /// Whether this is a view (or materialized view)
27    pub is_view: bool,
28    /// Whether INSERT is allowed
29    pub insertable: bool,
30    /// Whether UPDATE is allowed
31    pub updatable: bool,
32    /// Whether DELETE is allowed
33    pub deletable: bool,
34    /// Whether SELECT is allowed (for current role)
35    pub readable: bool,
36    /// Primary key column names (sorted)
37    pub pk_cols: SmallVec<[CompactString; 2]>,
38    /// Columns indexed by name (preserves insertion order)
39    pub columns: Arc<IndexMap<CompactString, Column>>,
40    /// Computed fields available on this table
41    /// Maps function name -> ComputedField
42    pub computed_fields: HashMap<CompactString, ComputedField>,
43}
44
45impl Table {
46    /// Get the qualified identifier for this table
47    pub fn qi(&self) -> QualifiedIdentifier {
48        QualifiedIdentifier::new(self.schema.clone(), self.name.clone())
49    }
50
51    /// Iterate over all columns
52    pub fn columns_list(&self) -> impl Iterator<Item = &Column> {
53        self.columns.values()
54    }
55
56    /// Get a column by name
57    pub fn get_column(&self, name: &str) -> Option<&Column> {
58        self.columns.get(name)
59    }
60
61    /// Check if this table has a primary key
62    pub fn has_pk(&self) -> bool {
63        !self.pk_cols.is_empty()
64    }
65
66    /// Check if a column is part of the primary key
67    pub fn is_pk_column(&self, col_name: &str) -> bool {
68        self.pk_cols.iter().any(|pk| pk.as_str() == col_name)
69    }
70
71    /// Get the number of columns
72    pub fn column_count(&self) -> usize {
73        self.columns.len()
74    }
75
76    /// Check if the table is read-only (no insert, update, or delete)
77    pub fn is_read_only(&self) -> bool {
78        !self.insertable && !self.updatable && !self.deletable
79    }
80
81    /// Get all insertable columns (non-generated, or with defaults)
82    pub fn insertable_columns(&self) -> impl Iterator<Item = &Column> {
83        self.columns.values().filter(|c| !c.is_generated())
84    }
85
86    /// Get all updatable columns (non-generated)
87    pub fn updatable_columns(&self) -> impl Iterator<Item = &Column> {
88        self.columns.values().filter(|c| !c.is_generated())
89    }
90
91    /// Get required columns for INSERT (non-nullable, no default, not generated)
92    pub fn required_columns(&self) -> impl Iterator<Item = &Column> {
93        self.columns
94            .values()
95            .filter(|c| !c.nullable && !c.has_default() && !c.is_generated())
96    }
97
98    /// Get a computed field by function name
99    pub fn get_computed_field(&self, name: &str) -> Option<&ComputedField> {
100        self.computed_fields.get(name)
101    }
102}
103
104/// Column metadata
105///
106/// Represents a PostgreSQL column with its type and constraints.
107#[derive(Debug, Clone)]
108pub struct Column {
109    /// Column name
110    pub name: CompactString,
111    /// Description from pg_description
112    pub description: Option<String>,
113    /// Whether NULL values are allowed
114    pub nullable: bool,
115    /// The "coerced" data type (e.g., "character varying" instead of "varchar")
116    pub data_type: CompactString,
117    /// The actual declared type as written
118    pub nominal_type: CompactString,
119    /// Maximum length for character types
120    pub max_length: Option<i32>,
121    /// Default value expression
122    pub default: Option<String>,
123    /// Enum values if this is an enum type
124    pub enum_values: SmallVec<[String; 8]>,
125    /// Whether this is a composite type
126    pub is_composite: bool,
127    /// Composite type schema (if composite)
128    pub composite_type_schema: Option<CompactString>,
129    /// Composite type name (if composite)
130    pub composite_type_name: Option<CompactString>,
131}
132
133impl Column {
134    /// Check if the column has a default value
135    pub fn has_default(&self) -> bool {
136        self.default.is_some()
137    }
138
139    /// Check if this is an auto-generated column (serial, identity, generated)
140    pub fn is_generated(&self) -> bool {
141        if let Some(ref def) = self.default {
142            def.starts_with("nextval(") || def.contains("generated")
143        } else {
144            false
145        }
146    }
147
148    /// Check if this is an enum column
149    pub fn is_enum(&self) -> bool {
150        !self.enum_values.is_empty()
151    }
152
153    /// Check if this is a text/character type
154    pub fn is_text_type(&self) -> bool {
155        matches!(
156            self.data_type.as_str(),
157            "text" | "character varying" | "character" | "varchar" | "char" | "name"
158        )
159    }
160
161    /// Check if this is a numeric type
162    pub fn is_numeric_type(&self) -> bool {
163        matches!(
164            self.data_type.as_str(),
165            "integer"
166                | "bigint"
167                | "smallint"
168                | "numeric"
169                | "decimal"
170                | "real"
171                | "double precision"
172                | "int"
173                | "int4"
174                | "int8"
175                | "int2"
176                | "float4"
177                | "float8"
178        )
179    }
180
181    /// Check if this is a boolean type
182    pub fn is_boolean_type(&self) -> bool {
183        self.data_type.as_str() == "boolean" || self.data_type.as_str() == "bool"
184    }
185
186    /// Check if this is a JSON type
187    pub fn is_json_type(&self) -> bool {
188        self.data_type.as_str() == "json" || self.data_type.as_str() == "jsonb"
189    }
190
191    /// Check if this is an array type
192    pub fn is_array_type(&self) -> bool {
193        self.data_type.ends_with("[]") || self.data_type.starts_with("ARRAY")
194    }
195
196    /// Check if this is a timestamp/date type
197    pub fn is_temporal_type(&self) -> bool {
198        matches!(
199            self.data_type.as_str(),
200            "timestamp without time zone"
201                | "timestamp with time zone"
202                | "timestamptz"
203                | "timestamp"
204                | "date"
205                | "time without time zone"
206                | "time with time zone"
207                | "timetz"
208                | "time"
209                | "interval"
210        )
211    }
212
213    /// Check if this is a UUID type
214    pub fn is_uuid_type(&self) -> bool {
215        self.data_type.as_str() == "uuid"
216    }
217
218    /// Check if this is a composite type
219    pub fn is_composite_type(&self) -> bool {
220        self.is_composite
221    }
222}
223
224/// Computed field metadata
225///
226/// Represents a function that can be used as a computed field on a table.
227/// The function takes a table row type as the first parameter and returns a scalar value.
228#[derive(Debug, Clone)]
229pub struct ComputedField {
230    /// Function qualified identifier
231    pub function: QualifiedIdentifier,
232    /// Return type (scalar)
233    pub return_type: CompactString,
234    /// Whether function returns a set
235    pub returns_set: bool,
236}
237
238#[cfg(test)]
239mod tests {
240    use crate::test_helpers::*;
241
242    // ========================================================================
243    // Table Tests
244    // ========================================================================
245
246    #[test]
247    fn test_table_qi() {
248        let table = test_table().schema("api").name("users").build();
249
250        let qi = table.qi();
251        assert_eq!(qi.schema.as_str(), "api");
252        assert_eq!(qi.name.as_str(), "users");
253    }
254
255    #[test]
256    fn test_table_get_column() {
257        let col1 = test_column().name("id").data_type("integer").build();
258        let col2 = test_column().name("name").data_type("text").build();
259
260        let table = test_table().column(col1).column(col2).build();
261
262        assert!(table.get_column("id").is_some());
263        assert!(table.get_column("name").is_some());
264        assert!(table.get_column("nonexistent").is_none());
265    }
266
267    #[test]
268    fn test_table_has_pk() {
269        let table_with_pk = test_table().pk_col("id").build();
270        assert!(table_with_pk.has_pk());
271
272        let table_without_pk = test_table().build();
273        assert!(!table_without_pk.has_pk());
274    }
275
276    #[test]
277    fn test_table_is_pk_column() {
278        let table = test_table().pk_cols(["id", "tenant_id"]).build();
279
280        assert!(table.is_pk_column("id"));
281        assert!(table.is_pk_column("tenant_id"));
282        assert!(!table.is_pk_column("name"));
283    }
284
285    #[test]
286    fn test_table_column_count() {
287        let col1 = test_column().name("id").build();
288        let col2 = test_column().name("name").build();
289        let col3 = test_column().name("email").build();
290
291        let table = test_table().column(col1).column(col2).column(col3).build();
292
293        assert_eq!(table.column_count(), 3);
294    }
295
296    #[test]
297    fn test_table_is_read_only() {
298        let rw_table = test_table()
299            .insertable(true)
300            .updatable(true)
301            .deletable(true)
302            .build();
303        assert!(!rw_table.is_read_only());
304
305        let ro_table = test_table()
306            .insertable(false)
307            .updatable(false)
308            .deletable(false)
309            .build();
310        assert!(ro_table.is_read_only());
311
312        let partial_table = test_table()
313            .insertable(false)
314            .updatable(true)
315            .deletable(false)
316            .build();
317        assert!(!partial_table.is_read_only());
318    }
319
320    #[test]
321    fn test_table_columns_list() {
322        let col1 = test_column().name("a").build();
323        let col2 = test_column().name("b").build();
324
325        let table = test_table().column(col1).column(col2).build();
326
327        let names: Vec<_> = table.columns_list().map(|c| c.name.as_str()).collect();
328        assert_eq!(names, vec!["a", "b"]);
329    }
330
331    #[test]
332    fn test_table_insertable_columns() {
333        let regular_col = test_column().name("name").build();
334        let generated_col = test_column()
335            .name("id")
336            .default_value("nextval('users_id_seq')")
337            .build();
338
339        let table = test_table()
340            .column(regular_col)
341            .column(generated_col)
342            .build();
343
344        let insertable: Vec<_> = table
345            .insertable_columns()
346            .map(|c| c.name.as_str())
347            .collect();
348        assert_eq!(insertable, vec!["name"]);
349    }
350
351    #[test]
352    fn test_table_required_columns() {
353        let required_col = test_column().name("name").nullable(false).build();
354        let optional_col = test_column().name("bio").nullable(true).build();
355        let defaulted_col = test_column()
356            .name("status")
357            .nullable(false)
358            .default_value("'active'")
359            .build();
360        let generated_col = test_column()
361            .name("id")
362            .nullable(false)
363            .default_value("nextval('seq')")
364            .build();
365
366        let table = test_table()
367            .column(required_col)
368            .column(optional_col)
369            .column(defaulted_col)
370            .column(generated_col)
371            .build();
372
373        let required: Vec<_> = table.required_columns().map(|c| c.name.as_str()).collect();
374        assert_eq!(required, vec!["name"]);
375    }
376
377    #[test]
378    fn test_table_is_view() {
379        let table = test_table().is_view(false).build();
380        assert!(!table.is_view);
381
382        let view = test_table().is_view(true).build();
383        assert!(view.is_view);
384    }
385
386    // ========================================================================
387    // Column Tests
388    // ========================================================================
389
390    #[test]
391    fn test_column_has_default() {
392        let col_with_default = test_column().default_value("now()").build();
393        assert!(col_with_default.has_default());
394
395        let col_without_default = test_column().build();
396        assert!(!col_without_default.has_default());
397    }
398
399    #[test]
400    fn test_column_is_generated_nextval() {
401        let serial_col = test_column()
402            .name("id")
403            .default_value("nextval('users_id_seq'::regclass)")
404            .build();
405        assert!(serial_col.is_generated());
406    }
407
408    #[test]
409    fn test_column_is_generated_identity() {
410        let identity_col = test_column()
411            .name("id")
412            .default_value("generated always as identity")
413            .build();
414        assert!(identity_col.is_generated());
415    }
416
417    #[test]
418    fn test_column_is_generated_regular_default() {
419        let col = test_column()
420            .name("created_at")
421            .default_value("now()")
422            .build();
423        assert!(!col.is_generated());
424    }
425
426    #[test]
427    fn test_column_is_enum() {
428        let enum_col = test_column()
429            .name("status")
430            .enum_values(["active", "inactive", "pending"])
431            .build();
432        assert!(enum_col.is_enum());
433        assert_eq!(enum_col.enum_values.len(), 3);
434
435        let regular_col = test_column().name("name").build();
436        assert!(!regular_col.is_enum());
437    }
438
439    #[test]
440    fn test_column_is_text_type() {
441        assert!(test_column().data_type("text").build().is_text_type());
442        assert!(
443            test_column()
444                .data_type("character varying")
445                .build()
446                .is_text_type()
447        );
448        assert!(test_column().data_type("varchar").build().is_text_type());
449        assert!(test_column().data_type("char").build().is_text_type());
450        assert!(!test_column().data_type("integer").build().is_text_type());
451    }
452
453    #[test]
454    fn test_column_is_numeric_type() {
455        assert!(test_column().data_type("integer").build().is_numeric_type());
456        assert!(test_column().data_type("bigint").build().is_numeric_type());
457        assert!(test_column().data_type("numeric").build().is_numeric_type());
458        assert!(
459            test_column()
460                .data_type("double precision")
461                .build()
462                .is_numeric_type()
463        );
464        assert!(!test_column().data_type("text").build().is_numeric_type());
465    }
466
467    #[test]
468    fn test_column_is_boolean_type() {
469        assert!(test_column().data_type("boolean").build().is_boolean_type());
470        assert!(test_column().data_type("bool").build().is_boolean_type());
471        assert!(!test_column().data_type("text").build().is_boolean_type());
472    }
473
474    #[test]
475    fn test_column_is_json_type() {
476        assert!(test_column().data_type("json").build().is_json_type());
477        assert!(test_column().data_type("jsonb").build().is_json_type());
478        assert!(!test_column().data_type("text").build().is_json_type());
479    }
480
481    #[test]
482    fn test_column_is_array_type() {
483        assert!(test_column().data_type("integer[]").build().is_array_type());
484        assert!(test_column().data_type("text[]").build().is_array_type());
485        assert!(!test_column().data_type("integer").build().is_array_type());
486    }
487
488    #[test]
489    fn test_column_is_temporal_type() {
490        assert!(
491            test_column()
492                .data_type("timestamp with time zone")
493                .build()
494                .is_temporal_type()
495        );
496        assert!(
497            test_column()
498                .data_type("timestamp without time zone")
499                .build()
500                .is_temporal_type()
501        );
502        assert!(test_column().data_type("date").build().is_temporal_type());
503        assert!(
504            test_column()
505                .data_type("interval")
506                .build()
507                .is_temporal_type()
508        );
509        assert!(!test_column().data_type("text").build().is_temporal_type());
510    }
511
512    #[test]
513    fn test_column_is_uuid_type() {
514        assert!(test_column().data_type("uuid").build().is_uuid_type());
515        assert!(!test_column().data_type("text").build().is_uuid_type());
516    }
517
518    #[test]
519    fn test_column_max_length() {
520        let col = test_column()
521            .data_type("character varying")
522            .max_length(255)
523            .build();
524        assert_eq!(col.max_length, Some(255));
525
526        let col_no_limit = test_column().data_type("text").build();
527        assert_eq!(col_no_limit.max_length, None);
528    }
529
530    #[test]
531    fn test_column_nullable() {
532        let nullable_col = test_column().nullable(true).build();
533        assert!(nullable_col.nullable);
534
535        let non_nullable_col = test_column().nullable(false).build();
536        assert!(!non_nullable_col.nullable);
537    }
538
539    // ========================================================================
540    // ComputedField Tests
541    // ========================================================================
542
543    #[test]
544    fn test_computed_field_structure() {
545        use super::ComputedField;
546        use crate::types::QualifiedIdentifier;
547
548        let func_qi = QualifiedIdentifier::new("test_api", "full_name");
549        let computed = ComputedField {
550            function: func_qi.clone(),
551            return_type: "text".into(),
552            returns_set: false,
553        };
554
555        assert_eq!(computed.function.schema.as_str(), "test_api");
556        assert_eq!(computed.function.name.as_str(), "full_name");
557        assert_eq!(computed.return_type.as_str(), "text");
558        assert!(!computed.returns_set);
559    }
560
561    #[test]
562    fn test_table_get_computed_field() {
563        use super::ComputedField;
564        use crate::types::QualifiedIdentifier;
565
566        let mut table = test_table().schema("test_api").name("users").build();
567
568        // Add a computed field manually for testing
569        let func_qi = QualifiedIdentifier::new("test_api", "full_name");
570        let computed = ComputedField {
571            function: func_qi,
572            return_type: "text".into(),
573            returns_set: false,
574        };
575        table.computed_fields.insert("full_name".into(), computed);
576
577        assert!(table.get_computed_field("full_name").is_some());
578        assert!(table.get_computed_field("nonexistent").is_none());
579
580        let cf = table.get_computed_field("full_name").unwrap();
581        assert_eq!(cf.return_type.as_str(), "text");
582    }
583}