1use vibesql_ast::pretty_print::ToSql;
20use vibesql_catalog::{ColumnSchema, ReferentialAction, SortOrder, TableSchema};
21use vibesql_storage::Row;
22use vibesql_types::{DataType, SqlValue};
23
24use crate::{errors::ExecutorError, select::SelectResult};
25
26pub fn is_sqlite_schema_table(table_name: &str) -> bool {
28 let normalized = table_name.to_lowercase();
29 matches!(normalized.as_str(), "sqlite_master" | "sqlite_schema")
30}
31
32pub fn is_reserved_object_name(name: &str) -> bool {
50 name.len() >= "sqlite_".len()
51 && name.as_bytes()[.."sqlite_".len()].eq_ignore_ascii_case(b"sqlite_")
52}
53
54pub fn is_sqlite_temp_schema_table(table_name: &str) -> bool {
60 let normalized = table_name.to_lowercase();
61 matches!(normalized.as_str(), "sqlite_temp_master" | "sqlite_temp_schema")
62}
63
64pub fn get_sqlite_schema_table_schema() -> TableSchema {
66 TableSchema::new(
67 "sqlite_master".to_string(),
68 vec![
69 ColumnSchema::new("type".to_string(), DataType::Varchar { max_length: None }, false),
70 ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: None }, false),
71 ColumnSchema::new(
72 "tbl_name".to_string(),
73 DataType::Varchar { max_length: None },
74 false,
75 ),
76 ColumnSchema::new("rootpage".to_string(), DataType::Integer, false),
77 ColumnSchema::new("sql".to_string(), DataType::Varchar { max_length: None }, true),
78 ],
79 )
80}
81
82fn schema_row(obj_type: &str, name: &str, tbl_name: &str, sql: String) -> Row {
84 Row::new(vec![
85 SqlValue::Varchar(arcstr::ArcStr::from(obj_type)),
86 SqlValue::Varchar(arcstr::ArcStr::from(name)),
87 SqlValue::Varchar(arcstr::ArcStr::from(tbl_name)),
88 SqlValue::Integer(0), SqlValue::Varchar(arcstr::ArcStr::from(sql)),
90 ])
91}
92
93pub fn execute_sqlite_schema_query(
100 catalog: &vibesql_catalog::Catalog,
101) -> Result<SelectResult, ExecutorError> {
102 let schema = get_sqlite_schema_table_schema();
103 let column_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
104 let mut rows = Vec::new();
105
106 for table_name in catalog.list_tables() {
109 if let Some(table) = catalog.get_table(&table_name) {
110 let sql = generate_create_table_sql(table);
111 rows.push(schema_row("table", &table.name, &table.name, sql));
115 }
116 }
117
118 for index in catalog.get_schema_indexes(vibesql_catalog::DEFAULT_SCHEMA) {
121 let sql = generate_create_index_sql(index);
122 rows.push(schema_row("index", &index.name, &index.table_name, sql));
123 }
124
125 for view_name in catalog.list_views() {
128 if let Some(view) = catalog.get_view(&view_name) {
129 if view.is_temp() {
130 continue;
131 }
132 let sql = generate_create_view_sql(view);
133 rows.push(schema_row("view", &view.name, &view.name, sql));
135 }
136 }
137
138 for trigger_name in catalog.list_triggers() {
141 if let Some(trigger) = catalog.get_trigger(&trigger_name) {
142 if trigger.is_temp() {
143 continue;
144 }
145 let sql = generate_create_trigger_sql(trigger);
146 rows.push(schema_row("trigger", &trigger.name, &trigger.table_name, sql));
147 }
148 }
149
150 Ok(SelectResult { columns: column_names, rows })
151}
152
153pub fn execute_sqlite_temp_schema_query(
163 catalog: &vibesql_catalog::Catalog,
164) -> Result<SelectResult, ExecutorError> {
165 let schema = get_sqlite_schema_table_schema();
166 let column_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
167 let mut rows = Vec::new();
168
169 let temp_schema = catalog.temp_schema_name();
170
171 for table_name in catalog.list_tables_in_schema(temp_schema) {
173 let qualified = format!("{}.{}", temp_schema, table_name);
176 if let Some(table) = catalog.get_table(&qualified) {
177 let sql = generate_create_table_sql(table);
178 rows.push(schema_row("table", &table.name, &table.name, sql));
181 }
182 }
183
184 for index in catalog.get_schema_indexes(temp_schema) {
186 let sql = generate_create_index_sql(index);
187 rows.push(schema_row("index", &index.name, &index.table_name, sql));
188 }
189
190 for view_name in catalog.list_views() {
194 if let Some(view) = catalog.get_view(&view_name) {
195 if !view.is_temp() {
196 continue;
197 }
198 let sql = generate_create_view_sql(view);
199 rows.push(schema_row("view", &view.name, &view.name, sql));
200 }
201 }
202
203 for trigger_name in catalog.list_triggers() {
206 if let Some(trigger) = catalog.get_trigger(&trigger_name) {
207 if !trigger.is_temp() {
208 continue;
209 }
210 let sql = generate_create_trigger_sql(trigger);
211 rows.push(schema_row("trigger", &trigger.name, &trigger.table_name, sql));
212 }
213 }
214
215 Ok(SelectResult { columns: column_names, rows })
216}
217
218fn generate_create_table_sql(table: &TableSchema) -> String {
220 if let Some(ref src) = table.sql_source {
227 return src.clone();
228 }
229
230 let mut sql = format!("CREATE TABLE {} (\n", table.name);
231 let mut definitions: Vec<String> = Vec::new();
232
233 for col in &table.columns {
235 let mut col_def = format!(" {} {}", col.name, format_data_type(&col.data_type));
236
237 if let Some(ref generated_expr) = col.generated_expr {
239 col_def.push_str(&format!(" AS ({})", format_expression(generated_expr)));
240 } else {
241 if !col.nullable {
243 col_def.push_str(" NOT NULL");
244 }
245
246 if let Some(ref default) = col.default_value {
247 col_def.push_str(&format!(" DEFAULT {}", format_expression(default)));
248 }
249 }
250
251 definitions.push(col_def);
252 }
253
254 if let Some(ref pk_cols) = table.primary_key {
256 definitions.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
257 }
258
259 for unique_cols in &table.unique_constraints {
261 definitions.push(format!(" UNIQUE ({})", unique_cols.join(", ")));
262 }
263
264 for fk in &table.foreign_keys {
266 let mut fk_def = format!(
267 " FOREIGN KEY ({}) REFERENCES {} ({})",
268 fk.column_names.join(", "),
269 fk.parent_table,
270 fk.parent_column_names.join(", ")
271 );
272
273 if fk.on_delete != ReferentialAction::NoAction {
275 fk_def.push_str(&format!(" ON DELETE {}", format_referential_action(&fk.on_delete)));
276 }
277
278 if fk.on_update != ReferentialAction::NoAction {
280 fk_def.push_str(&format!(" ON UPDATE {}", format_referential_action(&fk.on_update)));
281 }
282
283 if fk.is_deferrable {
287 if fk.initially_deferred {
288 fk_def.push_str(" DEFERRABLE INITIALLY DEFERRED");
289 } else {
290 fk_def.push_str(" DEFERRABLE INITIALLY IMMEDIATE");
291 }
292 }
293
294 definitions.push(fk_def);
295 }
296
297 for (name, expr) in &table.check_constraints {
299 definitions.push(format!(" CONSTRAINT {} CHECK ({})", name, format_expression(expr)));
300 }
301
302 sql.push_str(&definitions.join(",\n"));
303 sql.push_str("\n)");
304
305 sql
306}
307
308fn generate_create_index_sql(index: &vibesql_catalog::IndexMetadata) -> String {
310 let unique_str = if index.is_unique { "UNIQUE " } else { "" };
311
312 let columns: Vec<String> = index
313 .columns
314 .iter()
315 .map(|col| {
316 let order_str = match col.order() {
317 SortOrder::Ascending => "",
318 SortOrder::Descending => " DESC",
319 };
320 match col {
321 vibesql_catalog::IndexedColumn::Column { column_name, prefix_length, .. } => {
322 let prefix_str = prefix_length.map(|l| format!("({})", l)).unwrap_or_default();
323 format!("{}{}{}", column_name, prefix_str, order_str)
324 }
325 vibesql_catalog::IndexedColumn::Expression { expr, .. } => {
326 format!("({}){}", expr.to_sql(), order_str)
328 }
329 }
330 })
331 .collect();
332
333 format!(
334 "CREATE {}INDEX {} ON {}({})",
335 unique_str,
336 index.name,
337 index.table_name,
338 columns.join(", ")
339 )
340}
341
342fn generate_create_view_sql(view: &vibesql_catalog::ViewDefinition) -> String {
344 if let Some(ref sql) = view.sql_definition {
346 return sql.clone();
347 }
348
349 let columns_str =
351 view.columns.as_ref().map(|cols| format!(" ({})", cols.join(", "))).unwrap_or_default();
352
353 use vibesql_ast::pretty_print::ToSql;
355 format!("CREATE VIEW {}{} AS {}", view.name, columns_str, view.query.to_sql())
356}
357
358fn generate_create_trigger_sql(trigger: &vibesql_catalog::TriggerDefinition) -> String {
360 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
361
362 if let Some(ref sql) = trigger.sql_definition {
369 let trimmed = sql.trim_end();
370 let trimmed = trimmed.strip_suffix(';').unwrap_or(trimmed);
371 return trimmed.trim_end().to_string();
372 }
373
374 let timing = match trigger.timing {
375 TriggerTiming::Before => "BEFORE",
376 TriggerTiming::After => "AFTER",
377 TriggerTiming::InsteadOf => "INSTEAD OF",
378 };
379
380 let granularity = match trigger.granularity {
381 TriggerGranularity::Row => "ROW",
382 TriggerGranularity::Statement => "STATEMENT",
383 };
384
385 let event = match &trigger.event {
386 TriggerEvent::Insert => "INSERT".to_string(),
387 TriggerEvent::Update(None) => "UPDATE".to_string(),
388 TriggerEvent::Update(Some(cols)) => format!("UPDATE OF {}", cols.join(", ")),
389 TriggerEvent::Delete => "DELETE".to_string(),
390 };
391
392 let when_clause = trigger
394 .when_condition
395 .as_ref()
396 .map(|expr| format!(" WHEN ({})", format_expression(expr)))
397 .unwrap_or_default();
398
399 let body = match &trigger.triggered_action {
401 TriggerAction::RawSql(sql) => sql.clone(),
402 };
403
404 format!(
405 "CREATE TRIGGER {} {} {} ON {} FOR EACH {}{}{}",
406 trigger.name, timing, event, trigger.table_name, granularity, when_clause, body
407 )
408}
409
410fn format_data_type(dt: &DataType) -> String {
412 match dt {
413 DataType::Integer => "INTEGER".to_string(),
414 DataType::Smallint => "SMALLINT".to_string(),
415 DataType::Bigint => "BIGINT".to_string(),
416 DataType::Unsigned => "UNSIGNED BIGINT".to_string(),
417 DataType::Real => "REAL".to_string(),
418 DataType::Float { precision } => format!("FLOAT({})", precision),
419 DataType::DoublePrecision => "DOUBLE PRECISION".to_string(),
420 DataType::Numeric { precision, scale } => format!("NUMERIC({}, {})", precision, scale),
421 DataType::Decimal { precision, scale } => format!("DECIMAL({}, {})", precision, scale),
422 DataType::Varchar { max_length } => {
423 if let Some(len) = max_length {
424 format!("VARCHAR({})", len)
425 } else {
426 "TEXT".to_string()
427 }
428 }
429 DataType::Character { length } => format!("CHAR({})", length),
430 DataType::CharacterLargeObject => "TEXT".to_string(),
431 DataType::Name => "TEXT".to_string(),
432 DataType::Boolean => "BOOLEAN".to_string(),
433 DataType::Date => "DATE".to_string(),
434 DataType::Time { with_timezone } => {
435 if *with_timezone {
436 "TIME WITH TIME ZONE".to_string()
437 } else {
438 "TIME".to_string()
439 }
440 }
441 DataType::Timestamp { with_timezone } => {
442 if *with_timezone {
443 "TIMESTAMP WITH TIME ZONE".to_string()
444 } else {
445 "TIMESTAMP".to_string()
446 }
447 }
448 DataType::Interval { start_field, end_field } => {
449 if let Some(end) = end_field {
450 format!("INTERVAL {:?} TO {:?}", start_field, end)
451 } else {
452 format!("INTERVAL {:?}", start_field)
453 }
454 }
455 DataType::BinaryLargeObject => "BLOB".to_string(),
456 DataType::Bit { length } => {
457 if let Some(len) = length {
458 format!("BIT({})", len)
459 } else {
460 "BIT".to_string()
461 }
462 }
463 DataType::UserDefined { type_name } => type_name.clone(),
464 DataType::Vector { dimensions } => format!("VECTOR({})", dimensions),
465 DataType::Null => "NULL".to_string(),
466 }
467}
468
469fn format_referential_action(action: &ReferentialAction) -> &'static str {
471 match action {
472 ReferentialAction::NoAction => "NO ACTION",
473 ReferentialAction::Restrict => "RESTRICT",
474 ReferentialAction::Cascade => "CASCADE",
475 ReferentialAction::SetNull => "SET NULL",
476 ReferentialAction::SetDefault => "SET DEFAULT",
477 }
478}
479
480fn format_expression(expr: &vibesql_ast::Expression) -> String {
482 use vibesql_ast::pretty_print::ToSql;
483 expr.to_sql()
484}
485
486#[cfg(test)]
487mod tests {
488 use vibesql_catalog::{Catalog, IndexMetadata, IndexType, IndexedColumn};
489
490 use super::*;
491
492 #[test]
493 fn test_is_sqlite_schema_table() {
494 assert!(is_sqlite_schema_table("sqlite_master"));
495 assert!(is_sqlite_schema_table("sqlite_schema"));
496 assert!(is_sqlite_schema_table("SQLITE_MASTER"));
497 assert!(is_sqlite_schema_table("SQLITE_SCHEMA"));
498 assert!(is_sqlite_schema_table("Sqlite_Master"));
499 assert!(!is_sqlite_schema_table("users"));
500 assert!(!is_sqlite_schema_table("sqlite_stat1"));
501 assert!(!is_sqlite_schema_table("information_schema.tables"));
502 assert!(!is_sqlite_schema_table("sqlite_temp_master"));
504 }
505
506 #[test]
507 fn test_is_sqlite_temp_schema_table() {
508 assert!(is_sqlite_temp_schema_table("sqlite_temp_master"));
509 assert!(is_sqlite_temp_schema_table("sqlite_temp_schema"));
510 assert!(is_sqlite_temp_schema_table("SQLITE_TEMP_MASTER"));
511 assert!(is_sqlite_temp_schema_table("Sqlite_Temp_Schema"));
512 assert!(!is_sqlite_temp_schema_table("sqlite_master"));
513 assert!(!is_sqlite_temp_schema_table("users"));
514 }
515
516 #[test]
517 fn test_temp_index_separated_from_main_in_schema_views() {
518 let mut catalog = Catalog::new();
521
522 let main_table = TableSchema::new(
524 "t".to_string(),
525 vec![ColumnSchema::new("a".to_string(), DataType::Integer, true)],
526 );
527 catalog.create_table(main_table).unwrap();
528 catalog
529 .add_index(IndexMetadata::new(
530 "main_i".to_string(),
531 "t".to_string(),
532 IndexType::BTree,
533 vec![IndexedColumn::new_column("a".to_string(), SortOrder::Ascending)],
534 false,
535 ))
536 .unwrap();
537
538 let temp_schema = catalog.temp_schema_name().to_string();
540 let temp_table = TableSchema::new(
541 "tt".to_string(),
542 vec![ColumnSchema::new("a".to_string(), DataType::Integer, true)],
543 );
544 catalog.create_table_in_schema(&temp_schema, temp_table).unwrap();
545 catalog
546 .add_index(
547 IndexMetadata::new(
548 "temp_i".to_string(),
549 "tt".to_string(),
550 IndexType::BTree,
551 vec![IndexedColumn::new_column("a".to_string(), SortOrder::Ascending)],
552 false,
553 )
554 .with_schema(temp_schema.clone()),
555 )
556 .unwrap();
557
558 let master = execute_sqlite_schema_query(&catalog).unwrap();
559 let master_names: Vec<String> = master
560 .rows
561 .iter()
562 .map(|r| match &r.values[1] {
563 SqlValue::Varchar(s) => s.to_string(),
564 _ => String::new(),
565 })
566 .collect();
567 assert!(master_names.contains(&"main_i".to_string()), "master should list main index");
568 assert!(!master_names.contains(&"temp_i".to_string()), "master must exclude temp index");
569
570 let temp_master = execute_sqlite_temp_schema_query(&catalog).unwrap();
571 let temp_names: Vec<String> = temp_master
572 .rows
573 .iter()
574 .map(|r| match &r.values[1] {
575 SqlValue::Varchar(s) => s.to_string(),
576 _ => String::new(),
577 })
578 .collect();
579 assert!(temp_names.contains(&"temp_i".to_string()), "temp_master should list temp index");
580 assert!(temp_names.contains(&"tt".to_string()), "temp_master should list temp table");
581 assert!(!temp_names.contains(&"main_i".to_string()), "temp_master must exclude main index");
582 }
583
584 #[test]
585 fn test_temp_view_and_trigger_separated_from_main_in_schema_views() {
586 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
589 use vibesql_catalog::{TriggerDefinition, ViewDefinition};
590
591 let mut catalog = Catalog::new();
592
593 let base = TableSchema::new(
595 "t".to_string(),
596 vec![ColumnSchema::new("a".to_string(), DataType::Integer, true)],
597 );
598 catalog.create_table(base).unwrap();
599
600 catalog
602 .create_view(ViewDefinition::new(
603 "main_v".to_string(),
604 None,
605 create_minimal_select_stmt(),
606 false,
607 ))
608 .unwrap();
609 catalog
610 .create_view(
611 ViewDefinition::new("temp_v".to_string(), None, create_minimal_select_stmt(), false)
612 .with_schema(Some("temp".to_string())),
613 )
614 .unwrap();
615
616 let mk_trigger = |name: &str| {
618 TriggerDefinition::new(
619 name.to_string(),
620 TriggerTiming::After,
621 TriggerEvent::Insert,
622 "t".to_string(),
623 TriggerGranularity::Row,
624 None,
625 TriggerAction::RawSql("SELECT 1".to_string()),
626 )
627 };
628 catalog.create_trigger(mk_trigger("main_tr")).unwrap();
629 catalog
630 .create_trigger(mk_trigger("temp_tr").with_schema(Some("temp".to_string())))
631 .unwrap();
632
633 let names = |result: &SelectResult| -> Vec<String> {
634 result
635 .rows
636 .iter()
637 .map(|r| match &r.values[1] {
638 SqlValue::Varchar(s) => s.to_string(),
639 _ => String::new(),
640 })
641 .collect()
642 };
643
644 let master = execute_sqlite_schema_query(&catalog).unwrap();
645 let master_names = names(&master);
646 assert!(master_names.contains(&"main_v".to_string()), "master should list main view");
647 assert!(master_names.contains(&"main_tr".to_string()), "master should list main trigger");
648 assert!(!master_names.contains(&"temp_v".to_string()), "master must exclude temp view");
649 assert!(
650 !master_names.contains(&"temp_tr".to_string()),
651 "master must exclude temp trigger"
652 );
653
654 let temp_master = execute_sqlite_temp_schema_query(&catalog).unwrap();
655 let temp_names = names(&temp_master);
656 assert!(temp_names.contains(&"temp_v".to_string()), "temp_master should list temp view");
657 assert!(
658 temp_names.contains(&"temp_tr".to_string()),
659 "temp_master should list temp trigger"
660 );
661 assert!(!temp_names.contains(&"main_v".to_string()), "temp_master must exclude main view");
662 assert!(
663 !temp_names.contains(&"main_tr".to_string()),
664 "temp_master must exclude main trigger"
665 );
666 }
667
668 #[test]
669 fn test_sqlite_schema_empty_db() {
670 let catalog = Catalog::new();
671 let result = execute_sqlite_schema_query(&catalog).unwrap();
672
673 assert_eq!(result.columns, vec!["type", "name", "tbl_name", "rootpage", "sql"]);
674 assert!(result.rows.is_empty());
675 }
676
677 #[test]
678 fn test_get_sqlite_schema_table_schema() {
679 let schema = get_sqlite_schema_table_schema();
680
681 assert_eq!(schema.name, "sqlite_master");
682 assert_eq!(schema.columns.len(), 5);
683 assert_eq!(schema.columns[0].name, "type");
684 assert_eq!(schema.columns[1].name, "name");
685 assert_eq!(schema.columns[2].name, "tbl_name");
686 assert_eq!(schema.columns[3].name, "rootpage");
687 assert_eq!(schema.columns[4].name, "sql");
688 }
689
690 #[test]
691 fn test_format_data_type() {
692 assert_eq!(format_data_type(&DataType::Integer), "INTEGER");
693 assert_eq!(format_data_type(&DataType::Varchar { max_length: Some(255) }), "VARCHAR(255)");
694 assert_eq!(format_data_type(&DataType::Varchar { max_length: None }), "TEXT");
695 assert_eq!(format_data_type(&DataType::Boolean), "BOOLEAN");
696 assert_eq!(
697 format_data_type(&DataType::Numeric { precision: 10, scale: 2 }),
698 "NUMERIC(10, 2)"
699 );
700 }
701
702 #[test]
703 fn test_sqlite_schema_with_table() {
704 let mut catalog = Catalog::new();
705
706 let columns = vec![
708 ColumnSchema::new("id".to_string(), DataType::Integer, false),
709 ColumnSchema::new(
710 "name".to_string(),
711 DataType::Varchar { max_length: Some(100) },
712 true,
713 ),
714 ];
715 let table =
716 TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
717 catalog.create_table(table).unwrap();
718
719 let result = execute_sqlite_schema_query(&catalog).unwrap();
720
721 assert_eq!(result.rows.len(), 1);
723
724 let type_val = &result.rows[0].values[0];
726 assert_eq!(type_val, &SqlValue::Varchar(arcstr::ArcStr::from("table")));
727
728 let name_val = &result.rows[0].values[1];
730 assert_eq!(name_val, &SqlValue::Varchar(arcstr::ArcStr::from("users")));
731
732 let tbl_name_val = &result.rows[0].values[2];
734 assert_eq!(tbl_name_val, &SqlValue::Varchar(arcstr::ArcStr::from("users")));
735
736 let rootpage_val = &result.rows[0].values[3];
738 assert_eq!(rootpage_val, &SqlValue::Integer(0));
739
740 let sql_val = &result.rows[0].values[4];
742 if let SqlValue::Varchar(sql) = sql_val {
743 assert!(sql.contains("CREATE TABLE users"));
744 assert!(sql.contains("id INTEGER"));
745 assert!(sql.contains("name VARCHAR(100)"));
746 assert!(sql.contains("PRIMARY KEY"));
747 } else {
748 panic!("Expected VARCHAR for sql column");
749 }
750 }
751
752 #[test]
753 fn test_sqlite_schema_with_index() {
754 let mut catalog = Catalog::new();
755
756 let columns = vec![
758 ColumnSchema::new("id".to_string(), DataType::Integer, false),
759 ColumnSchema::new(
760 "email".to_string(),
761 DataType::Varchar { max_length: Some(255) },
762 false,
763 ),
764 ];
765 let table =
766 TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
767 catalog.create_table(table).unwrap();
768
769 let index = IndexMetadata::new(
771 "idx_email".to_string(),
772 "users".to_string(),
773 IndexType::BTree,
774 vec![IndexedColumn::new_column("email".to_string(), SortOrder::Ascending)],
775 true, );
777 catalog.add_index(index).unwrap();
778
779 let result = execute_sqlite_schema_query(&catalog).unwrap();
780
781 assert_eq!(result.rows.len(), 2);
783
784 let index_row = result
786 .rows
787 .iter()
788 .find(|r| matches!(&r.values[0], SqlValue::Varchar(s) if s.as_str() == "index"))
789 .expect("Should have an index row");
790
791 assert_eq!(index_row.values[1], SqlValue::Varchar(arcstr::ArcStr::from("idx_email")));
793
794 assert_eq!(index_row.values[2], SqlValue::Varchar(arcstr::ArcStr::from("users")));
796
797 if let SqlValue::Varchar(sql) = &index_row.values[4] {
799 assert!(sql.contains("CREATE UNIQUE INDEX idx_email ON users"));
800 } else {
801 panic!("Expected VARCHAR for sql column");
802 }
803 }
804
805 #[test]
806 fn test_generate_create_index_sql() {
807 let index = IndexMetadata::new(
808 "idx_name".to_string(),
809 "users".to_string(),
810 IndexType::BTree,
811 vec![
812 IndexedColumn::new_column("last_name".to_string(), SortOrder::Ascending),
813 IndexedColumn::Column {
814 column_name: "first_name".to_string(),
815 order: SortOrder::Descending,
816 prefix_length: None,
817 },
818 ],
819 false, );
821
822 let sql = generate_create_index_sql(&index);
823 assert_eq!(sql, "CREATE INDEX idx_name ON users(last_name, first_name DESC)");
824 }
825
826 #[test]
827 fn test_generate_create_index_sql_with_prefix() {
828 let index = IndexMetadata::new(
829 "idx_email".to_string(),
830 "users".to_string(),
831 IndexType::BTree,
832 vec![IndexedColumn::new_column_with_prefix(
833 "email".to_string(),
834 SortOrder::Ascending,
835 50,
836 )],
837 true, );
839
840 let sql = generate_create_index_sql(&index);
841 assert_eq!(sql, "CREATE UNIQUE INDEX idx_email ON users(email(50))");
842 }
843
844 #[test]
849 fn test_sqlite_schema_index_preserves_name_case() {
850 let mut catalog = Catalog::new();
851 catalog.set_case_sensitive_identifiers(false);
852
853 let columns = vec![ColumnSchema::new("a".to_string(), DataType::Integer, false)];
854 let table = TableSchema::new("t".to_string(), columns);
855 catalog.create_table(table).unwrap();
856
857 let index = IndexMetadata::new(
858 "MyIdx".to_string(),
859 "t".to_string(),
860 IndexType::BTree,
861 vec![IndexedColumn::new_column("a".to_string(), SortOrder::Ascending)],
862 false,
863 );
864 catalog.add_index(index).unwrap();
865
866 let result = execute_sqlite_schema_query(&catalog).unwrap();
867 let index_row = result
868 .rows
869 .iter()
870 .find(|r| matches!(&r.values[0], SqlValue::Varchar(s) if s.as_str() == "index"))
871 .expect("Should have an index row");
872
873 assert_eq!(index_row.values[1], SqlValue::Varchar(arcstr::ArcStr::from("MyIdx")));
875
876 if let SqlValue::Varchar(sql) = &index_row.values[4] {
878 assert!(
879 sql.contains("CREATE INDEX MyIdx ON t"),
880 "sqlite_master.sql must preserve original index-name case, got: {sql}"
881 );
882 assert!(!sql.contains("myidx"), "index name must not be lowercased, got: {sql}");
883 } else {
884 panic!("Expected VARCHAR for sql column");
885 }
886 }
887
888 #[test]
889 fn test_generate_create_trigger_sql_basic() {
890 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
891 use vibesql_catalog::TriggerDefinition;
892
893 let trigger = TriggerDefinition {
894 name: "audit_insert".to_string(),
895 schema: None,
896 table_name: "users".to_string(),
897 timing: TriggerTiming::After,
898 event: TriggerEvent::Insert,
899 granularity: TriggerGranularity::Row,
900 when_condition: None,
901 triggered_action: TriggerAction::RawSql(
902 " BEGIN INSERT INTO audit VALUES (NEW.id); END".to_string(),
903 ),
904 enabled: true,
905 sql_definition: None,
906 };
907
908 let sql = generate_create_trigger_sql(&trigger);
909 assert_eq!(
910 sql,
911 "CREATE TRIGGER audit_insert AFTER INSERT ON users FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END"
912 );
913 }
914
915 #[test]
916 fn test_generate_create_trigger_sql_with_update_of() {
917 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
918 use vibesql_catalog::TriggerDefinition;
919
920 let trigger = TriggerDefinition {
921 name: "track_status_change".to_string(),
922 schema: None,
923 table_name: "orders".to_string(),
924 timing: TriggerTiming::Before,
925 event: TriggerEvent::Update(Some(vec!["status".to_string(), "updated_at".to_string()])),
926 granularity: TriggerGranularity::Row,
927 when_condition: None,
928 triggered_action: TriggerAction::RawSql(" BEGIN SELECT 1; END".to_string()),
929 enabled: true,
930 sql_definition: None,
931 };
932
933 let sql = generate_create_trigger_sql(&trigger);
934 assert!(sql.contains("UPDATE OF status, updated_at"));
935 assert!(sql.contains("CREATE TRIGGER track_status_change BEFORE UPDATE OF"));
936 }
937
938 #[test]
939 fn test_generate_create_trigger_sql_instead_of() {
940 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
941 use vibesql_catalog::TriggerDefinition;
942
943 let trigger = TriggerDefinition {
944 name: "instead_delete".to_string(),
945 schema: None,
946 table_name: "my_view".to_string(),
947 timing: TriggerTiming::InsteadOf,
948 event: TriggerEvent::Delete,
949 granularity: TriggerGranularity::Row,
950 when_condition: None,
951 triggered_action: TriggerAction::RawSql(
952 " BEGIN DELETE FROM base_table WHERE id = OLD.id; END".to_string(),
953 ),
954 enabled: true,
955 sql_definition: None,
956 };
957
958 let sql = generate_create_trigger_sql(&trigger);
959 assert!(sql.contains("INSTEAD OF DELETE"));
960 assert!(sql.contains("FOR EACH ROW"));
961 }
962
963 fn create_minimal_select_stmt() -> vibesql_ast::SelectStmt {
965 vibesql_ast::SelectStmt {
966 with_clause: None,
967 distinct: false,
968 select_list: vec![vibesql_ast::SelectItem::Wildcard { alias: None }],
969 into_table: None,
970 into_variables: None,
971 from: None,
972 where_clause: None,
973 group_by: None,
974 having: None,
975 window_definitions: None,
976 order_by: None,
977 limit: None,
978 offset: None,
979 set_operation: None,
980 values: None,
981 }
982 }
983
984 #[test]
985 fn test_generate_create_view_sql_with_definition() {
986 use vibesql_catalog::ViewDefinition;
987
988 let view = ViewDefinition::new_with_sql(
990 "active_users".to_string(),
991 Some(vec!["id".to_string(), "name".to_string()]),
992 create_minimal_select_stmt(),
993 false,
994 "CREATE VIEW active_users (id, name) AS SELECT id, name FROM users WHERE active = 1"
995 .to_string(),
996 );
997
998 let sql = generate_create_view_sql(&view);
999 assert_eq!(
1000 sql,
1001 "CREATE VIEW active_users (id, name) AS SELECT id, name FROM users WHERE active = 1"
1002 );
1003 }
1004
1005 #[test]
1006 fn test_generate_create_view_sql_fallback() {
1007 use vibesql_catalog::ViewDefinition;
1008
1009 let view = ViewDefinition::new(
1011 "test_view".to_string(),
1012 Some(vec!["col1".to_string()]),
1013 create_minimal_select_stmt(),
1014 false,
1015 );
1016
1017 let sql = generate_create_view_sql(&view);
1018 assert!(sql.contains("CREATE VIEW test_view"));
1020 assert!(sql.contains("(col1)"));
1022 }
1023}