dbrest_core/schema_cache/
table.rs1use compact_str::CompactString;
7use indexmap::IndexMap;
8use smallvec::SmallVec;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::types::QualifiedIdentifier;
13
14#[derive(Debug, Clone)]
19pub struct Table {
20 pub schema: CompactString,
22 pub name: CompactString,
24 pub description: Option<String>,
26 pub is_view: bool,
28 pub insertable: bool,
30 pub updatable: bool,
32 pub deletable: bool,
34 pub readable: bool,
36 pub pk_cols: SmallVec<[CompactString; 2]>,
38 pub columns: Arc<IndexMap<CompactString, Column>>,
40 pub computed_fields: HashMap<CompactString, ComputedField>,
43}
44
45impl Table {
46 pub fn qi(&self) -> QualifiedIdentifier {
48 QualifiedIdentifier::new(self.schema.clone(), self.name.clone())
49 }
50
51 pub fn columns_list(&self) -> impl Iterator<Item = &Column> {
53 self.columns.values()
54 }
55
56 pub fn get_column(&self, name: &str) -> Option<&Column> {
58 self.columns.get(name)
59 }
60
61 pub fn has_pk(&self) -> bool {
63 !self.pk_cols.is_empty()
64 }
65
66 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 pub fn column_count(&self) -> usize {
73 self.columns.len()
74 }
75
76 pub fn is_read_only(&self) -> bool {
78 !self.insertable && !self.updatable && !self.deletable
79 }
80
81 pub fn insertable_columns(&self) -> impl Iterator<Item = &Column> {
83 self.columns.values().filter(|c| !c.is_generated())
84 }
85
86 pub fn updatable_columns(&self) -> impl Iterator<Item = &Column> {
88 self.columns.values().filter(|c| !c.is_generated())
89 }
90
91 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 pub fn get_computed_field(&self, name: &str) -> Option<&ComputedField> {
100 self.computed_fields.get(name)
101 }
102}
103
104#[derive(Debug, Clone)]
108pub struct Column {
109 pub name: CompactString,
111 pub description: Option<String>,
113 pub nullable: bool,
115 pub data_type: CompactString,
117 pub nominal_type: CompactString,
119 pub max_length: Option<i32>,
121 pub default: Option<String>,
123 pub enum_values: SmallVec<[String; 8]>,
125 pub is_composite: bool,
127 pub composite_type_schema: Option<CompactString>,
129 pub composite_type_name: Option<CompactString>,
131}
132
133impl Column {
134 pub fn has_default(&self) -> bool {
136 self.default.is_some()
137 }
138
139 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 pub fn is_enum(&self) -> bool {
150 !self.enum_values.is_empty()
151 }
152
153 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 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 pub fn is_boolean_type(&self) -> bool {
183 self.data_type.as_str() == "boolean" || self.data_type.as_str() == "bool"
184 }
185
186 pub fn is_json_type(&self) -> bool {
188 self.data_type.as_str() == "json" || self.data_type.as_str() == "jsonb"
189 }
190
191 pub fn is_array_type(&self) -> bool {
193 self.data_type.ends_with("[]") || self.data_type.starts_with("ARRAY")
194 }
195
196 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 pub fn is_uuid_type(&self) -> bool {
215 self.data_type.as_str() == "uuid"
216 }
217
218 pub fn is_composite_type(&self) -> bool {
220 self.is_composite
221 }
222}
223
224#[derive(Debug, Clone)]
229pub struct ComputedField {
230 pub function: QualifiedIdentifier,
232 pub return_type: CompactString,
234 pub returns_set: bool,
236}
237
238#[cfg(test)]
239mod tests {
240 use crate::test_helpers::*;
241
242 #[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 #[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 #[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 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}