1use vibesql_ast::{
7 DescribeStmt, ShowColumnsStmt, ShowCreateTableStmt, ShowDatabasesStmt, ShowIndexStmt,
8 ShowTablesStmt,
9};
10use vibesql_catalog::{IndexType, ReferentialAction, SortOrder};
11use vibesql_storage::{Database, Row};
12use vibesql_types::SqlValue;
13
14use crate::{errors::ExecutorError, select::SelectResult};
15
16pub struct IntrospectionExecutor<'a> {
18 db: &'a Database,
19}
20
21impl<'a> IntrospectionExecutor<'a> {
22 pub fn new(db: &'a Database) -> Self {
24 Self { db }
25 }
26
27 pub fn execute_show_tables(
32 &self,
33 stmt: &ShowTablesStmt,
34 ) -> Result<SelectResult, ExecutorError> {
35 let schema_name =
37 stmt.database.as_deref().unwrap_or_else(|| self.db.catalog.get_current_schema());
38
39 let tables = if let Some(schema) = self.db.catalog.get_schema(schema_name) {
41 schema.list_tables()
42 } else {
43 let upper_name = schema_name.to_uppercase();
45 self.db
46 .catalog
47 .list_schemas()
48 .into_iter()
49 .find(|s| s.to_uppercase() == upper_name)
50 .and_then(|actual_name| self.db.catalog.get_schema(&actual_name))
51 .map(|schema| schema.list_tables())
52 .unwrap_or_default()
53 };
54
55 let filtered_tables: Vec<String> = if let Some(ref pattern) = stmt.like_pattern {
57 tables.into_iter().filter(|t| like_match(pattern, t)).collect()
58 } else {
59 tables
60 };
61
62 let column_name = format!("Tables_in_{}", schema_name);
64 let rows: Vec<Row> = filtered_tables
65 .into_iter()
66 .map(|name| Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from(name))]))
67 .collect();
68
69 Ok(SelectResult { columns: vec![column_name], rows })
70 }
71
72 pub fn execute_show_databases(
77 &self,
78 stmt: &ShowDatabasesStmt,
79 ) -> Result<SelectResult, ExecutorError> {
80 let schemas = self.db.catalog.list_schemas();
81
82 let filtered_schemas: Vec<String> = if let Some(ref pattern) = stmt.like_pattern {
84 schemas.into_iter().filter(|s| like_match(pattern, s)).collect()
85 } else {
86 schemas
87 };
88
89 let rows: Vec<Row> = filtered_schemas
90 .into_iter()
91 .map(|name| Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from(name))]))
92 .collect();
93
94 Ok(SelectResult { columns: vec!["Database".to_string()], rows })
95 }
96
97 pub fn execute_show_columns(
101 &self,
102 stmt: &ShowColumnsStmt,
103 ) -> Result<SelectResult, ExecutorError> {
104 let table_name = if let Some(ref db) = stmt.database {
106 format!("{}.{}", db, stmt.table_name)
107 } else {
108 stmt.table_name.clone()
109 };
110
111 let table = self
112 .db
113 .catalog
114 .get_table(&table_name)
115 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
116
117 let pk_columns: std::collections::HashSet<String> =
119 table.primary_key.as_ref().map(|pk| pk.iter().cloned().collect()).unwrap_or_default();
120
121 let unique_columns: std::collections::HashSet<String> =
123 table.unique_constraints.iter().flatten().cloned().collect();
124
125 let mut rows: Vec<Row> = Vec::with_capacity(table.columns.len());
126
127 for col in &table.columns {
128 if let Some(ref pattern) = stmt.like_pattern {
130 if !like_match(pattern, &col.name) {
131 continue;
132 }
133 }
134
135 let field = SqlValue::Varchar(arcstr::ArcStr::from(col.name.clone()));
137
138 let type_str = format_data_type(&col.data_type);
140 let col_type = SqlValue::Varchar(arcstr::ArcStr::from(type_str));
141
142 let nullable = SqlValue::Varchar(if col.nullable { "YES" } else { "NO" }.into());
144
145 let key = if pk_columns.contains(&col.name) {
147 SqlValue::Varchar("PRI".into())
148 } else if unique_columns.contains(&col.name) {
149 SqlValue::Varchar("UNI".into())
150 } else {
151 SqlValue::Varchar("".into())
152 };
153
154 let default = col
156 .default_value
157 .as_ref()
158 .map(|expr| SqlValue::Varchar(arcstr::ArcStr::from(format!("{:?}", expr))))
159 .unwrap_or(SqlValue::Null);
160
161 let extra = SqlValue::Varchar("".into());
163
164 if stmt.full {
165 rows.push(Row::new(vec![
167 field,
168 col_type,
169 SqlValue::Null, nullable,
171 key,
172 default,
173 extra,
174 SqlValue::Varchar("select,insert,update,references".into()), SqlValue::Varchar("".into()), ]));
177 } else {
178 rows.push(Row::new(vec![field, col_type, nullable, key, default, extra]));
179 }
180 }
181
182 let columns = if stmt.full {
183 vec![
184 "Field".to_string(),
185 "Type".to_string(),
186 "Collation".to_string(),
187 "Null".to_string(),
188 "Key".to_string(),
189 "Default".to_string(),
190 "Extra".to_string(),
191 "Privileges".to_string(),
192 "Comment".to_string(),
193 ]
194 } else {
195 vec![
196 "Field".to_string(),
197 "Type".to_string(),
198 "Null".to_string(),
199 "Key".to_string(),
200 "Default".to_string(),
201 "Extra".to_string(),
202 ]
203 };
204
205 Ok(SelectResult { columns, rows })
206 }
207
208 pub fn execute_describe(&self, stmt: &DescribeStmt) -> Result<SelectResult, ExecutorError> {
210 let show_columns = ShowColumnsStmt {
212 table_name: stmt.table_name.clone(),
213 database: None,
214 full: false,
215 like_pattern: stmt.column_pattern.clone(),
216 where_clause: None,
217 };
218 self.execute_show_columns(&show_columns)
219 }
220
221 pub fn execute_show_index(&self, stmt: &ShowIndexStmt) -> Result<SelectResult, ExecutorError> {
225 let table_name = if let Some(ref db) = stmt.database {
227 format!("{}.{}", db, stmt.table_name)
228 } else {
229 stmt.table_name.clone()
230 };
231
232 let table = self
234 .db
235 .catalog
236 .get_table(&table_name)
237 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
238
239 let mut rows: Vec<Row> = Vec::new();
240
241 if let Some(ref pk_cols) = table.primary_key {
243 for (seq, col_name) in pk_cols.iter().enumerate() {
244 rows.push(Row::new(vec![
245 SqlValue::Varchar(arcstr::ArcStr::from(stmt.table_name.clone())), SqlValue::Integer(0), SqlValue::Varchar("PRIMARY".into()), SqlValue::Integer((seq + 1) as i64), SqlValue::Varchar(arcstr::ArcStr::from(col_name.to_string())), SqlValue::Varchar("A".into()), SqlValue::Null, SqlValue::Null, SqlValue::Null, SqlValue::Varchar("".into()), SqlValue::Varchar("BTREE".into()), SqlValue::Varchar("".into()), SqlValue::Varchar("".into()), SqlValue::Varchar("YES".into()), ]));
260 }
261 }
262
263 let indexes = self.db.catalog.get_table_indexes(&stmt.table_name);
265 for index in indexes {
266 for (seq, col) in index.columns.iter().enumerate() {
267 let non_unique = if index.is_unique { 0 } else { 1 };
268 let collation = match col.order() {
269 SortOrder::Ascending => "A",
270 SortOrder::Descending => "D",
271 };
272 let index_type = match index.index_type {
273 IndexType::BTree => "BTREE",
274 IndexType::Hash => "HASH",
275 IndexType::RTree => "RTREE",
276 IndexType::Fulltext => "FULLTEXT",
277 IndexType::IVFFlat { .. } => "IVFFLAT",
278 IndexType::Hnsw { .. } => "HNSW",
279 };
280
281 let column_name = col
283 .column_name()
284 .map(|s| s.to_string())
285 .unwrap_or_else(|| "(expression)".to_string());
286
287 rows.push(Row::new(vec![
288 SqlValue::Varchar(arcstr::ArcStr::from(stmt.table_name.clone())), SqlValue::Integer(non_unique), SqlValue::Varchar(arcstr::ArcStr::from(index.name.clone())), SqlValue::Integer((seq + 1) as i64), SqlValue::Varchar(arcstr::ArcStr::from(column_name)), SqlValue::Varchar(collation.into()), SqlValue::Null, col.prefix_length()
296 .map(|l| SqlValue::Integer(l as i64))
297 .unwrap_or(SqlValue::Null), SqlValue::Null, SqlValue::Varchar("".into()), SqlValue::Varchar(index_type.into()), SqlValue::Varchar("".into()), SqlValue::Varchar("".into()), SqlValue::Varchar("YES".into()), ]));
305 }
306 }
307
308 Ok(SelectResult {
309 columns: vec![
310 "Table".to_string(),
311 "Non_unique".to_string(),
312 "Key_name".to_string(),
313 "Seq_in_index".to_string(),
314 "Column_name".to_string(),
315 "Collation".to_string(),
316 "Cardinality".to_string(),
317 "Sub_part".to_string(),
318 "Packed".to_string(),
319 "Null".to_string(),
320 "Index_type".to_string(),
321 "Comment".to_string(),
322 "Index_comment".to_string(),
323 "Visible".to_string(),
324 ],
325 rows,
326 })
327 }
328
329 pub fn execute_show_create_table(
333 &self,
334 stmt: &ShowCreateTableStmt,
335 ) -> Result<SelectResult, ExecutorError> {
336 let table = self
337 .db
338 .catalog
339 .get_table(&stmt.table_name)
340 .ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
341
342 let mut sql = format!("CREATE TABLE {} (\n", table.name);
344 let mut definitions: Vec<String> = Vec::new();
345
346 for col in &table.columns {
348 let mut col_def = format!(" {} {}", col.name, format_data_type(&col.data_type));
349
350 if !col.nullable {
351 col_def.push_str(" NOT NULL");
352 }
353
354 if let Some(ref default) = col.default_value {
355 col_def.push_str(&format!(" DEFAULT {:?}", default));
356 }
357
358 definitions.push(col_def);
359 }
360
361 if let Some(ref pk_cols) = table.primary_key {
363 definitions.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
364 }
365
366 for unique_cols in &table.unique_constraints {
368 definitions.push(format!(" UNIQUE ({})", unique_cols.join(", ")));
369 }
370
371 for fk in &table.foreign_keys {
373 let mut fk_def = format!(
374 " FOREIGN KEY ({}) REFERENCES {} ({})",
375 fk.column_names.join(", "),
376 fk.parent_table,
377 fk.parent_column_names.join(", ")
378 );
379
380 fk_def.push_str(&format!(" ON DELETE {}", format_referential_action(&fk.on_delete)));
382
383 fk_def.push_str(&format!(" ON UPDATE {}", format_referential_action(&fk.on_update)));
385
386 if fk.is_deferrable {
393 if fk.initially_deferred {
394 fk_def.push_str(" DEFERRABLE INITIALLY DEFERRED");
395 } else {
396 fk_def.push_str(" DEFERRABLE INITIALLY IMMEDIATE");
397 }
398 }
399
400 definitions.push(fk_def);
401 }
402
403 for (name, _expr) in &table.check_constraints {
405 definitions.push(format!(" CONSTRAINT {} CHECK (...)", name));
406 }
407
408 sql.push_str(&definitions.join(",\n"));
409 sql.push_str("\n)");
410
411 let rows = vec![Row::new(vec![
412 SqlValue::Varchar(arcstr::ArcStr::from(table.name.clone())),
413 SqlValue::Varchar(arcstr::ArcStr::from(sql)),
414 ])];
415
416 Ok(SelectResult { columns: vec!["Table".to_string(), "Create Table".to_string()], rows })
417 }
418}
419
420fn like_match(pattern: &str, text: &str) -> bool {
424 let pattern = pattern.to_lowercase();
425 let text = text.to_lowercase();
426
427 let pattern_chars: Vec<char> = pattern.chars().collect();
428 let text_chars: Vec<char> = text.chars().collect();
429
430 like_match_impl(&pattern_chars, &text_chars)
431}
432
433fn like_match_impl(pattern: &[char], text: &[char]) -> bool {
435 match (pattern.first(), text.first()) {
436 (None, None) => true,
437 (None, Some(_)) => false,
438 (Some('%'), _) => {
439 like_match_impl(&pattern[1..], text)
442 || (!text.is_empty() && like_match_impl(pattern, &text[1..]))
443 }
444 (Some('_'), Some(_)) => {
445 like_match_impl(&pattern[1..], &text[1..])
447 }
448 (Some('_'), None) => false,
449 (Some('\\'), _) if pattern.len() > 1 => {
450 if text.first() == Some(&pattern[1]) {
452 like_match_impl(&pattern[2..], &text[1..])
453 } else {
454 false
455 }
456 }
457 (Some(p), Some(t)) if *p == *t => like_match_impl(&pattern[1..], &text[1..]),
458 _ => false,
459 }
460}
461
462fn format_data_type(dt: &vibesql_types::DataType) -> String {
464 use vibesql_types::DataType;
465
466 match dt {
467 DataType::Integer => "INT".to_string(),
468 DataType::Smallint => "SMALLINT".to_string(),
469 DataType::Bigint => "BIGINT".to_string(),
470 DataType::Unsigned => "UNSIGNED BIGINT".to_string(),
471 DataType::Real => "REAL".to_string(),
472 DataType::Float { precision } => format!("FLOAT({})", precision),
473 DataType::DoublePrecision => "DOUBLE PRECISION".to_string(),
474 DataType::Numeric { precision, scale } => {
475 format!("NUMERIC({}, {})", precision, scale)
476 }
477 DataType::Decimal { precision, scale } => {
478 format!("DECIMAL({}, {})", precision, scale)
479 }
480 DataType::Varchar { max_length } => {
481 if let Some(len) = max_length {
482 format!("VARCHAR({})", len)
483 } else {
484 "VARCHAR".to_string()
485 }
486 }
487 DataType::Character { length } => {
488 format!("CHAR({})", length)
489 }
490 DataType::CharacterLargeObject => "CLOB".to_string(),
491 DataType::Name => "NAME".to_string(),
492 DataType::Boolean => "BOOLEAN".to_string(),
493 DataType::Date => "DATE".to_string(),
494 DataType::Time { with_timezone } => {
495 if *with_timezone {
496 "TIME WITH TIME ZONE".to_string()
497 } else {
498 "TIME".to_string()
499 }
500 }
501 DataType::Timestamp { with_timezone } => {
502 if *with_timezone {
503 "TIMESTAMP WITH TIME ZONE".to_string()
504 } else {
505 "TIMESTAMP".to_string()
506 }
507 }
508 DataType::Interval { start_field, end_field } => {
509 if let Some(end) = end_field {
510 format!("INTERVAL {:?} TO {:?}", start_field, end)
511 } else {
512 format!("INTERVAL {:?}", start_field)
513 }
514 }
515 DataType::BinaryLargeObject => "BLOB".to_string(),
516 DataType::Bit { length } => {
517 if let Some(len) = length {
518 format!("BIT({})", len)
519 } else {
520 "BIT".to_string()
521 }
522 }
523 DataType::UserDefined { type_name } => type_name.clone(),
524 DataType::Vector { dimensions } => format!("VECTOR({})", dimensions),
525 DataType::Null => "NULL".to_string(),
526 }
527}
528
529fn format_referential_action(action: &ReferentialAction) -> &'static str {
531 match action {
532 ReferentialAction::NoAction => "NO ACTION",
533 ReferentialAction::Restrict => "RESTRICT",
534 ReferentialAction::Cascade => "CASCADE",
535 ReferentialAction::SetNull => "SET NULL",
536 ReferentialAction::SetDefault => "SET DEFAULT",
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use vibesql_catalog::{ColumnSchema, TableSchema};
543 use vibesql_types::DataType;
544
545 use super::*;
546
547 fn create_test_db() -> Database {
548 let mut db = Database::new();
549 db.catalog.set_case_sensitive_identifiers(false);
550
551 let columns = vec![
553 ColumnSchema::new("id".to_string(), DataType::Integer, false),
554 ColumnSchema::new(
555 "name".to_string(),
556 DataType::Varchar { max_length: Some(100) },
557 true,
558 ),
559 ColumnSchema::new(
560 "email".to_string(),
561 DataType::Varchar { max_length: Some(255) },
562 false,
563 ),
564 ];
565 let schema =
566 TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
567 db.create_table(schema).unwrap();
568
569 let order_columns = vec![
571 ColumnSchema::new("id".to_string(), DataType::Integer, false),
572 ColumnSchema::new("user_id".to_string(), DataType::Integer, false),
573 ColumnSchema::new(
574 "total".to_string(),
575 DataType::Decimal { precision: 10, scale: 2 },
576 true,
577 ),
578 ];
579 let order_schema = TableSchema::with_primary_key(
580 "orders".to_string(),
581 order_columns,
582 vec!["id".to_string()],
583 );
584 db.create_table(order_schema).unwrap();
585
586 db
587 }
588
589 #[test]
590 fn test_show_tables() {
591 let db = create_test_db();
592 let executor = IntrospectionExecutor::new(&db);
593
594 let stmt = ShowTablesStmt { database: None, like_pattern: None, where_clause: None };
595
596 let result = executor.execute_show_tables(&stmt).unwrap();
597 assert_eq!(result.columns.len(), 1);
598 assert!(result.columns[0].starts_with("Tables_in_"));
599
600 assert_eq!(result.rows.len(), 2);
602
603 let table_names: Vec<&str> = result
605 .rows
606 .iter()
607 .filter_map(|r| match &r.values[0] {
608 SqlValue::Varchar(s) => Some(s.as_ref()),
609 _ => None,
610 })
611 .collect();
612
613 assert!(table_names.contains(&"users") || table_names.contains(&"USERS"));
614 assert!(table_names.contains(&"orders") || table_names.contains(&"ORDERS"));
615 }
616
617 #[test]
618 fn test_show_tables_with_like() {
619 let db = create_test_db();
620 let executor = IntrospectionExecutor::new(&db);
621
622 let stmt = ShowTablesStmt {
623 database: None,
624 like_pattern: Some("user%".to_string()),
625 where_clause: None,
626 };
627
628 let result = executor.execute_show_tables(&stmt).unwrap();
629 assert_eq!(result.rows.len(), 1);
631 }
632
633 #[test]
634 fn test_show_databases() {
635 let db = create_test_db();
636 let executor = IntrospectionExecutor::new(&db);
637
638 let stmt = ShowDatabasesStmt { like_pattern: None, where_clause: None };
639
640 let result = executor.execute_show_databases(&stmt).unwrap();
641 assert_eq!(result.columns, vec!["Database"]);
642
643 assert!(!result.rows.is_empty());
645
646 let schema_names: Vec<&str> = result
647 .rows
648 .iter()
649 .filter_map(|r| match &r.values[0] {
650 SqlValue::Varchar(s) => Some(s.as_ref()),
651 _ => None,
652 })
653 .collect();
654
655 assert!(schema_names.contains(&vibesql_catalog::DEFAULT_SCHEMA));
656 }
657
658 #[test]
659 fn test_show_columns() {
660 let db = create_test_db();
661 let executor = IntrospectionExecutor::new(&db);
662
663 let stmt = ShowColumnsStmt {
664 table_name: "users".to_string(),
665 database: None,
666 full: false,
667 like_pattern: None,
668 where_clause: None,
669 };
670
671 let result = executor.execute_show_columns(&stmt).unwrap();
672 assert_eq!(result.columns, vec!["Field", "Type", "Null", "Key", "Default", "Extra"]);
673 assert_eq!(result.rows.len(), 3); }
675
676 #[test]
677 fn test_show_columns_full() {
678 let db = create_test_db();
679 let executor = IntrospectionExecutor::new(&db);
680
681 let stmt = ShowColumnsStmt {
682 table_name: "users".to_string(),
683 database: None,
684 full: true,
685 like_pattern: None,
686 where_clause: None,
687 };
688
689 let result = executor.execute_show_columns(&stmt).unwrap();
690 assert_eq!(result.columns.len(), 9);
691 assert!(result.columns.contains(&"Privileges".to_string()));
692 assert!(result.columns.contains(&"Comment".to_string()));
693 }
694
695 #[test]
696 fn test_describe() {
697 let db = create_test_db();
698 let executor = IntrospectionExecutor::new(&db);
699
700 let stmt = DescribeStmt { table_name: "users".to_string(), column_pattern: None };
701
702 let result = executor.execute_describe(&stmt).unwrap();
703 assert_eq!(result.columns, vec!["Field", "Type", "Null", "Key", "Default", "Extra"]);
704 assert_eq!(result.rows.len(), 3);
705 }
706
707 #[test]
708 fn test_show_index() {
709 let db = create_test_db();
710 let executor = IntrospectionExecutor::new(&db);
711
712 let stmt = ShowIndexStmt { table_name: "users".to_string(), database: None };
713
714 let result = executor.execute_show_index(&stmt).unwrap();
715
716 assert!(!result.rows.is_empty());
718
719 let has_primary = result.rows.iter().any(|r| {
721 match &r.values[2] {
722 SqlValue::Varchar(s) => s.as_str() == "PRIMARY",
724 _ => false,
725 }
726 });
727 assert!(has_primary);
728 }
729
730 #[test]
731 fn test_show_create_table() {
732 let db = create_test_db();
733 let executor = IntrospectionExecutor::new(&db);
734
735 let stmt = ShowCreateTableStmt { table_name: "users".to_string() };
736
737 let result = executor.execute_show_create_table(&stmt).unwrap();
738 assert_eq!(result.columns, vec!["Table", "Create Table"]);
739 assert_eq!(result.rows.len(), 1);
740
741 if let SqlValue::Varchar(sql) = &result.rows[0].values[1] {
743 assert!(sql.contains("CREATE TABLE"));
744 assert!(sql.contains("users"));
745 assert!(sql.contains("id"));
746 assert!(sql.contains("name"));
747 assert!(sql.contains("email"));
748 assert!(sql.contains("PRIMARY KEY"));
749 } else {
750 panic!("Expected VARCHAR for Create Table");
751 }
752 }
753
754 #[test]
755 fn test_like_match() {
756 assert!(like_match("test", "test"));
758 assert!(like_match("test", "TEST")); assert!(!like_match("test", "testing"));
760
761 assert!(like_match("test%", "test"));
763 assert!(like_match("test%", "testing"));
764 assert!(like_match("%test", "test"));
765 assert!(like_match("%test", "mytest"));
766 assert!(like_match("%test%", "test"));
767 assert!(like_match("%test%", "mytesting"));
768
769 assert!(like_match("te_t", "test"));
771 assert!(!like_match("te_t", "tet"));
772 assert!(!like_match("te_t", "testt"));
773
774 assert!(like_match("te\\_t", "te_t"));
776 assert!(!like_match("te\\_t", "test"));
777 }
778}