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 get_sqlite_schema_table_schema() -> TableSchema {
34 TableSchema::new(
35 "sqlite_master".to_string(),
36 vec![
37 ColumnSchema::new("type".to_string(), DataType::Varchar { max_length: None }, false),
38 ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: None }, false),
39 ColumnSchema::new(
40 "tbl_name".to_string(),
41 DataType::Varchar { max_length: None },
42 false,
43 ),
44 ColumnSchema::new("rootpage".to_string(), DataType::Integer, false),
45 ColumnSchema::new("sql".to_string(), DataType::Varchar { max_length: None }, true),
46 ],
47 )
48}
49
50pub fn execute_sqlite_schema_query(
52 catalog: &vibesql_catalog::Catalog,
53) -> Result<SelectResult, ExecutorError> {
54 let schema = get_sqlite_schema_table_schema();
55 let column_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
56 let mut rows = Vec::new();
57
58 for table_name in catalog.list_tables() {
60 if let Some(table) = catalog.get_table(&table_name) {
61 let sql = generate_create_table_sql(table);
62 rows.push(Row::new(vec![
63 SqlValue::Varchar(arcstr::ArcStr::from("table")),
64 SqlValue::Varchar(arcstr::ArcStr::from(table_name.clone())),
65 SqlValue::Varchar(arcstr::ArcStr::from(table_name)),
66 SqlValue::Integer(0), SqlValue::Varchar(arcstr::ArcStr::from(sql)),
68 ]));
69 }
70 }
71
72 for index in catalog.list_all_indexes() {
74 let sql = generate_create_index_sql(index);
75 rows.push(Row::new(vec![
76 SqlValue::Varchar(arcstr::ArcStr::from("index")),
77 SqlValue::Varchar(arcstr::ArcStr::from(index.name.clone())),
78 SqlValue::Varchar(arcstr::ArcStr::from(index.table_name.clone())),
79 SqlValue::Integer(0), SqlValue::Varchar(arcstr::ArcStr::from(sql)),
81 ]));
82 }
83
84 for view_name in catalog.list_views() {
86 if let Some(view) = catalog.get_view(&view_name) {
87 let sql = generate_create_view_sql(view);
88 rows.push(Row::new(vec![
89 SqlValue::Varchar(arcstr::ArcStr::from("view")),
90 SqlValue::Varchar(arcstr::ArcStr::from(view.name.clone())),
91 SqlValue::Varchar(arcstr::ArcStr::from(view.name.clone())), SqlValue::Integer(0), SqlValue::Varchar(arcstr::ArcStr::from(sql)),
94 ]));
95 }
96 }
97
98 for trigger_name in catalog.list_triggers() {
100 if let Some(trigger) = catalog.get_trigger(&trigger_name) {
101 let sql = generate_create_trigger_sql(trigger);
102 rows.push(Row::new(vec![
103 SqlValue::Varchar(arcstr::ArcStr::from("trigger")),
104 SqlValue::Varchar(arcstr::ArcStr::from(trigger.name.clone())),
105 SqlValue::Varchar(arcstr::ArcStr::from(trigger.table_name.clone())),
106 SqlValue::Integer(0), SqlValue::Varchar(arcstr::ArcStr::from(sql)),
108 ]));
109 }
110 }
111
112 Ok(SelectResult { columns: column_names, rows })
113}
114
115fn generate_create_table_sql(table: &TableSchema) -> String {
117 let mut sql = format!("CREATE TABLE {} (\n", table.name);
118 let mut definitions: Vec<String> = Vec::new();
119
120 for col in &table.columns {
122 let mut col_def = format!(" {} {}", col.name, format_data_type(&col.data_type));
123
124 if let Some(ref generated_expr) = col.generated_expr {
126 col_def.push_str(&format!(" AS ({})", format_expression(generated_expr)));
127 } else {
128 if !col.nullable {
130 col_def.push_str(" NOT NULL");
131 }
132
133 if let Some(ref default) = col.default_value {
134 col_def.push_str(&format!(" DEFAULT {}", format_expression(default)));
135 }
136 }
137
138 definitions.push(col_def);
139 }
140
141 if let Some(ref pk_cols) = table.primary_key {
143 definitions.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
144 }
145
146 for unique_cols in &table.unique_constraints {
148 definitions.push(format!(" UNIQUE ({})", unique_cols.join(", ")));
149 }
150
151 for fk in &table.foreign_keys {
153 let mut fk_def = format!(
154 " FOREIGN KEY ({}) REFERENCES {} ({})",
155 fk.column_names.join(", "),
156 fk.parent_table,
157 fk.parent_column_names.join(", ")
158 );
159
160 if fk.on_delete != ReferentialAction::NoAction {
162 fk_def.push_str(&format!(" ON DELETE {}", format_referential_action(&fk.on_delete)));
163 }
164
165 if fk.on_update != ReferentialAction::NoAction {
167 fk_def.push_str(&format!(" ON UPDATE {}", format_referential_action(&fk.on_update)));
168 }
169
170 definitions.push(fk_def);
171 }
172
173 for (name, expr) in &table.check_constraints {
175 definitions.push(format!(" CONSTRAINT {} CHECK ({})", name, format_expression(expr)));
176 }
177
178 sql.push_str(&definitions.join(",\n"));
179 sql.push_str("\n)");
180
181 sql
182}
183
184fn generate_create_index_sql(index: &vibesql_catalog::IndexMetadata) -> String {
186 let unique_str = if index.is_unique { "UNIQUE " } else { "" };
187
188 let columns: Vec<String> = index
189 .columns
190 .iter()
191 .map(|col| {
192 let order_str = match col.order() {
193 SortOrder::Ascending => "",
194 SortOrder::Descending => " DESC",
195 };
196 match col {
197 vibesql_catalog::IndexedColumn::Column { column_name, prefix_length, .. } => {
198 let prefix_str = prefix_length.map(|l| format!("({})", l)).unwrap_or_default();
199 format!("{}{}{}", column_name, prefix_str, order_str)
200 }
201 vibesql_catalog::IndexedColumn::Expression { expr, .. } => {
202 format!("({}){}", expr.to_sql(), order_str)
204 }
205 }
206 })
207 .collect();
208
209 format!(
210 "CREATE {}INDEX {} ON {}({})",
211 unique_str,
212 index.name,
213 index.table_name,
214 columns.join(", ")
215 )
216}
217
218fn generate_create_view_sql(view: &vibesql_catalog::ViewDefinition) -> String {
220 if let Some(ref sql) = view.sql_definition {
222 return sql.clone();
223 }
224
225 let columns_str =
227 view.columns.as_ref().map(|cols| format!(" ({})", cols.join(", "))).unwrap_or_default();
228
229 use vibesql_ast::pretty_print::ToSql;
231 format!("CREATE VIEW {}{} AS {}", view.name, columns_str, view.query.to_sql())
232}
233
234fn generate_create_trigger_sql(trigger: &vibesql_catalog::TriggerDefinition) -> String {
236 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
237
238 let timing = match trigger.timing {
239 TriggerTiming::Before => "BEFORE",
240 TriggerTiming::After => "AFTER",
241 TriggerTiming::InsteadOf => "INSTEAD OF",
242 };
243
244 let granularity = match trigger.granularity {
245 TriggerGranularity::Row => "ROW",
246 TriggerGranularity::Statement => "STATEMENT",
247 };
248
249 let event = match &trigger.event {
250 TriggerEvent::Insert => "INSERT".to_string(),
251 TriggerEvent::Update(None) => "UPDATE".to_string(),
252 TriggerEvent::Update(Some(cols)) => format!("UPDATE OF {}", cols.join(", ")),
253 TriggerEvent::Delete => "DELETE".to_string(),
254 };
255
256 let when_clause = trigger
258 .when_condition
259 .as_ref()
260 .map(|expr| format!(" WHEN ({})", format_expression(expr)))
261 .unwrap_or_default();
262
263 let body = match &trigger.triggered_action {
265 TriggerAction::RawSql(sql) => sql.clone(),
266 };
267
268 format!(
269 "CREATE TRIGGER {} {} {} ON {} FOR EACH {}{}{}",
270 trigger.name, timing, event, trigger.table_name, granularity, when_clause, body
271 )
272}
273
274fn format_data_type(dt: &DataType) -> String {
276 match dt {
277 DataType::Integer => "INTEGER".to_string(),
278 DataType::Smallint => "SMALLINT".to_string(),
279 DataType::Bigint => "BIGINT".to_string(),
280 DataType::Unsigned => "UNSIGNED BIGINT".to_string(),
281 DataType::Real => "REAL".to_string(),
282 DataType::Float { precision } => format!("FLOAT({})", precision),
283 DataType::DoublePrecision => "DOUBLE PRECISION".to_string(),
284 DataType::Numeric { precision, scale } => format!("NUMERIC({}, {})", precision, scale),
285 DataType::Decimal { precision, scale } => format!("DECIMAL({}, {})", precision, scale),
286 DataType::Varchar { max_length } => {
287 if let Some(len) = max_length {
288 format!("VARCHAR({})", len)
289 } else {
290 "TEXT".to_string()
291 }
292 }
293 DataType::Character { length } => format!("CHAR({})", length),
294 DataType::CharacterLargeObject => "TEXT".to_string(),
295 DataType::Name => "TEXT".to_string(),
296 DataType::Boolean => "BOOLEAN".to_string(),
297 DataType::Date => "DATE".to_string(),
298 DataType::Time { with_timezone } => {
299 if *with_timezone {
300 "TIME WITH TIME ZONE".to_string()
301 } else {
302 "TIME".to_string()
303 }
304 }
305 DataType::Timestamp { with_timezone } => {
306 if *with_timezone {
307 "TIMESTAMP WITH TIME ZONE".to_string()
308 } else {
309 "TIMESTAMP".to_string()
310 }
311 }
312 DataType::Interval { start_field, end_field } => {
313 if let Some(end) = end_field {
314 format!("INTERVAL {:?} TO {:?}", start_field, end)
315 } else {
316 format!("INTERVAL {:?}", start_field)
317 }
318 }
319 DataType::BinaryLargeObject => "BLOB".to_string(),
320 DataType::Bit { length } => {
321 if let Some(len) = length {
322 format!("BIT({})", len)
323 } else {
324 "BIT".to_string()
325 }
326 }
327 DataType::UserDefined { type_name } => type_name.clone(),
328 DataType::Vector { dimensions } => format!("VECTOR({})", dimensions),
329 DataType::Null => "NULL".to_string(),
330 }
331}
332
333fn format_referential_action(action: &ReferentialAction) -> &'static str {
335 match action {
336 ReferentialAction::NoAction => "NO ACTION",
337 ReferentialAction::Restrict => "RESTRICT",
338 ReferentialAction::Cascade => "CASCADE",
339 ReferentialAction::SetNull => "SET NULL",
340 ReferentialAction::SetDefault => "SET DEFAULT",
341 }
342}
343
344fn format_expression(expr: &vibesql_ast::Expression) -> String {
346 use vibesql_ast::pretty_print::ToSql;
347 expr.to_sql()
348}
349
350#[cfg(test)]
351mod tests {
352 use vibesql_catalog::{Catalog, IndexMetadata, IndexType, IndexedColumn};
353
354 use super::*;
355
356 #[test]
357 fn test_is_sqlite_schema_table() {
358 assert!(is_sqlite_schema_table("sqlite_master"));
359 assert!(is_sqlite_schema_table("sqlite_schema"));
360 assert!(is_sqlite_schema_table("SQLITE_MASTER"));
361 assert!(is_sqlite_schema_table("SQLITE_SCHEMA"));
362 assert!(is_sqlite_schema_table("Sqlite_Master"));
363 assert!(!is_sqlite_schema_table("users"));
364 assert!(!is_sqlite_schema_table("sqlite_stat1"));
365 assert!(!is_sqlite_schema_table("information_schema.tables"));
366 }
367
368 #[test]
369 fn test_sqlite_schema_empty_db() {
370 let catalog = Catalog::new();
371 let result = execute_sqlite_schema_query(&catalog).unwrap();
372
373 assert_eq!(result.columns, vec!["type", "name", "tbl_name", "rootpage", "sql"]);
374 assert!(result.rows.is_empty());
375 }
376
377 #[test]
378 fn test_get_sqlite_schema_table_schema() {
379 let schema = get_sqlite_schema_table_schema();
380
381 assert_eq!(schema.name, "sqlite_master");
382 assert_eq!(schema.columns.len(), 5);
383 assert_eq!(schema.columns[0].name, "type");
384 assert_eq!(schema.columns[1].name, "name");
385 assert_eq!(schema.columns[2].name, "tbl_name");
386 assert_eq!(schema.columns[3].name, "rootpage");
387 assert_eq!(schema.columns[4].name, "sql");
388 }
389
390 #[test]
391 fn test_format_data_type() {
392 assert_eq!(format_data_type(&DataType::Integer), "INTEGER");
393 assert_eq!(format_data_type(&DataType::Varchar { max_length: Some(255) }), "VARCHAR(255)");
394 assert_eq!(format_data_type(&DataType::Varchar { max_length: None }), "TEXT");
395 assert_eq!(format_data_type(&DataType::Boolean), "BOOLEAN");
396 assert_eq!(
397 format_data_type(&DataType::Numeric { precision: 10, scale: 2 }),
398 "NUMERIC(10, 2)"
399 );
400 }
401
402 #[test]
403 fn test_sqlite_schema_with_table() {
404 let mut catalog = Catalog::new();
405
406 let columns = vec![
408 ColumnSchema::new("id".to_string(), DataType::Integer, false),
409 ColumnSchema::new(
410 "name".to_string(),
411 DataType::Varchar { max_length: Some(100) },
412 true,
413 ),
414 ];
415 let table =
416 TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
417 catalog.create_table(table).unwrap();
418
419 let result = execute_sqlite_schema_query(&catalog).unwrap();
420
421 assert_eq!(result.rows.len(), 1);
423
424 let type_val = &result.rows[0].values[0];
426 assert_eq!(type_val, &SqlValue::Varchar(arcstr::ArcStr::from("table")));
427
428 let name_val = &result.rows[0].values[1];
430 assert_eq!(name_val, &SqlValue::Varchar(arcstr::ArcStr::from("users")));
431
432 let tbl_name_val = &result.rows[0].values[2];
434 assert_eq!(tbl_name_val, &SqlValue::Varchar(arcstr::ArcStr::from("users")));
435
436 let rootpage_val = &result.rows[0].values[3];
438 assert_eq!(rootpage_val, &SqlValue::Integer(0));
439
440 let sql_val = &result.rows[0].values[4];
442 if let SqlValue::Varchar(sql) = sql_val {
443 assert!(sql.contains("CREATE TABLE users"));
444 assert!(sql.contains("id INTEGER"));
445 assert!(sql.contains("name VARCHAR(100)"));
446 assert!(sql.contains("PRIMARY KEY"));
447 } else {
448 panic!("Expected VARCHAR for sql column");
449 }
450 }
451
452 #[test]
453 fn test_sqlite_schema_with_index() {
454 let mut catalog = Catalog::new();
455
456 let columns = vec![
458 ColumnSchema::new("id".to_string(), DataType::Integer, false),
459 ColumnSchema::new(
460 "email".to_string(),
461 DataType::Varchar { max_length: Some(255) },
462 false,
463 ),
464 ];
465 let table =
466 TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
467 catalog.create_table(table).unwrap();
468
469 let index = IndexMetadata::new(
471 "idx_email".to_string(),
472 "users".to_string(),
473 IndexType::BTree,
474 vec![IndexedColumn::new_column("email".to_string(), SortOrder::Ascending)],
475 true, );
477 catalog.add_index(index).unwrap();
478
479 let result = execute_sqlite_schema_query(&catalog).unwrap();
480
481 assert_eq!(result.rows.len(), 2);
483
484 let index_row = result
486 .rows
487 .iter()
488 .find(|r| matches!(&r.values[0], SqlValue::Varchar(s) if s.as_str() == "index"))
489 .expect("Should have an index row");
490
491 assert_eq!(index_row.values[1], SqlValue::Varchar(arcstr::ArcStr::from("idx_email")));
493
494 assert_eq!(index_row.values[2], SqlValue::Varchar(arcstr::ArcStr::from("users")));
496
497 if let SqlValue::Varchar(sql) = &index_row.values[4] {
499 assert!(sql.contains("CREATE UNIQUE INDEX idx_email ON users"));
500 } else {
501 panic!("Expected VARCHAR for sql column");
502 }
503 }
504
505 #[test]
506 fn test_generate_create_index_sql() {
507 let index = IndexMetadata::new(
508 "idx_name".to_string(),
509 "users".to_string(),
510 IndexType::BTree,
511 vec![
512 IndexedColumn::new_column("last_name".to_string(), SortOrder::Ascending),
513 IndexedColumn::Column {
514 column_name: "first_name".to_string(),
515 order: SortOrder::Descending,
516 prefix_length: None,
517 },
518 ],
519 false, );
521
522 let sql = generate_create_index_sql(&index);
523 assert_eq!(sql, "CREATE INDEX idx_name ON users(last_name, first_name DESC)");
524 }
525
526 #[test]
527 fn test_generate_create_index_sql_with_prefix() {
528 let index = IndexMetadata::new(
529 "idx_email".to_string(),
530 "users".to_string(),
531 IndexType::BTree,
532 vec![IndexedColumn::new_column_with_prefix(
533 "email".to_string(),
534 SortOrder::Ascending,
535 50,
536 )],
537 true, );
539
540 let sql = generate_create_index_sql(&index);
541 assert_eq!(sql, "CREATE UNIQUE INDEX idx_email ON users(email(50))");
542 }
543
544 #[test]
545 fn test_generate_create_trigger_sql_basic() {
546 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
547 use vibesql_catalog::TriggerDefinition;
548
549 let trigger = TriggerDefinition {
550 name: "audit_insert".to_string(),
551 table_name: "users".to_string(),
552 timing: TriggerTiming::After,
553 event: TriggerEvent::Insert,
554 granularity: TriggerGranularity::Row,
555 when_condition: None,
556 triggered_action: TriggerAction::RawSql(
557 " BEGIN INSERT INTO audit VALUES (NEW.id); END".to_string(),
558 ),
559 enabled: true,
560 };
561
562 let sql = generate_create_trigger_sql(&trigger);
563 assert_eq!(
564 sql,
565 "CREATE TRIGGER audit_insert AFTER INSERT ON users FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END"
566 );
567 }
568
569 #[test]
570 fn test_generate_create_trigger_sql_with_update_of() {
571 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
572 use vibesql_catalog::TriggerDefinition;
573
574 let trigger = TriggerDefinition {
575 name: "track_status_change".to_string(),
576 table_name: "orders".to_string(),
577 timing: TriggerTiming::Before,
578 event: TriggerEvent::Update(Some(vec!["status".to_string(), "updated_at".to_string()])),
579 granularity: TriggerGranularity::Row,
580 when_condition: None,
581 triggered_action: TriggerAction::RawSql(" BEGIN SELECT 1; END".to_string()),
582 enabled: true,
583 };
584
585 let sql = generate_create_trigger_sql(&trigger);
586 assert!(sql.contains("UPDATE OF status, updated_at"));
587 assert!(sql.contains("CREATE TRIGGER track_status_change BEFORE UPDATE OF"));
588 }
589
590 #[test]
591 fn test_generate_create_trigger_sql_instead_of() {
592 use vibesql_ast::{TriggerAction, TriggerEvent, TriggerGranularity, TriggerTiming};
593 use vibesql_catalog::TriggerDefinition;
594
595 let trigger = TriggerDefinition {
596 name: "instead_delete".to_string(),
597 table_name: "my_view".to_string(),
598 timing: TriggerTiming::InsteadOf,
599 event: TriggerEvent::Delete,
600 granularity: TriggerGranularity::Row,
601 when_condition: None,
602 triggered_action: TriggerAction::RawSql(
603 " BEGIN DELETE FROM base_table WHERE id = OLD.id; END".to_string(),
604 ),
605 enabled: true,
606 };
607
608 let sql = generate_create_trigger_sql(&trigger);
609 assert!(sql.contains("INSTEAD OF DELETE"));
610 assert!(sql.contains("FOR EACH ROW"));
611 }
612
613 fn create_minimal_select_stmt() -> vibesql_ast::SelectStmt {
615 vibesql_ast::SelectStmt {
616 with_clause: None,
617 distinct: false,
618 select_list: vec![vibesql_ast::SelectItem::Wildcard { alias: None }],
619 into_table: None,
620 into_variables: None,
621 from: None,
622 where_clause: None,
623 group_by: None,
624 having: None,
625 order_by: None,
626 limit: None,
627 offset: None,
628 set_operation: None,
629 values: None,
630 }
631 }
632
633 #[test]
634 fn test_generate_create_view_sql_with_definition() {
635 use vibesql_catalog::ViewDefinition;
636
637 let view = ViewDefinition::new_with_sql(
639 "active_users".to_string(),
640 Some(vec!["id".to_string(), "name".to_string()]),
641 create_minimal_select_stmt(),
642 false,
643 "CREATE VIEW active_users (id, name) AS SELECT id, name FROM users WHERE active = 1"
644 .to_string(),
645 );
646
647 let sql = generate_create_view_sql(&view);
648 assert_eq!(
649 sql,
650 "CREATE VIEW active_users (id, name) AS SELECT id, name FROM users WHERE active = 1"
651 );
652 }
653
654 #[test]
655 fn test_generate_create_view_sql_fallback() {
656 use vibesql_catalog::ViewDefinition;
657
658 let view = ViewDefinition::new(
660 "test_view".to_string(),
661 Some(vec!["col1".to_string()]),
662 create_minimal_select_stmt(),
663 false,
664 );
665
666 let sql = generate_create_view_sql(&view);
667 assert!(sql.contains("CREATE VIEW test_view"));
669 assert!(sql.contains("(col1)"));
671 }
672}