use vibesql_ast::{ColumnDef, CreateTableStmt};
use vibesql_storage::Database;
use vibesql_types::DataType;
use crate::CreateTableExecutor;
#[test]
fn test_create_simple_table() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "users".to_string(),
columns: vec![
ColumnDef {
name: "id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "name".to_string(),
data_type: DataType::Varchar { max_length: Some(255) },
nullable: true,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
format!(
"Table 'users' created successfully in schema '{}'",
vibesql_catalog::DEFAULT_SCHEMA
)
);
assert!(db.catalog.table_exists("users"));
assert!(db.get_table("users").is_some());
}
#[test]
fn test_create_table_with_multiple_types() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "products".to_string(),
columns: vec![
ColumnDef {
name: "product_id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "name".to_string(),
data_type: DataType::Varchar { max_length: Some(100) },
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "price".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "in_stock".to_string(),
data_type: DataType::Boolean,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "description".to_string(),
data_type: DataType::Varchar { max_length: Some(500) },
nullable: true, constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
let schema = db.catalog.get_table("products");
assert!(schema.is_some());
let schema = schema.unwrap();
assert_eq!(schema.column_count(), 5);
assert!(!schema.get_column("product_id").unwrap().nullable);
assert!(schema.get_column("description").unwrap().nullable);
}
#[test]
fn test_create_table_already_exists() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "users".to_string(),
columns: vec![ColumnDef {
name: "id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
}],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_err());
assert!(matches!(result, Err(crate::errors::ExecutorError::TableAlreadyExists(_))));
}
fn parse_create_table(sql: &str) -> CreateTableStmt {
match vibesql_parser::Parser::parse_sql(sql).expect("parse failed") {
vibesql_ast::Statement::CreateTable(stmt) => stmt,
other => panic!("Expected CreateTable, got {:?}", other),
}
}
#[test]
fn test_duplicate_table_already_exists_echoes_source_quoting() {
let cases = [
("CREATE TABLE tbl1 (a)", "CREATE TABLE tbl1 (a)", "Table 'tbl1' already exists"),
("CREATE TABLE tbl1 (a)", "CREATE TABLE \"tbl1\" (a)", "Table '\"tbl1\"' already exists"),
("CREATE TABLE tbl1 (a)", "CREATE TABLE [tbl1] (a)", "Table '[tbl1]' already exists"),
("CREATE TABLE tbl1 (a)", "CREATE TABLE `tbl1` (a)", "Table '`tbl1`' already exists"),
(
"CREATE TABLE \"TBL1\" (a)",
"CREATE TABLE \"TBL1\" (a)",
"Table '\"TBL1\"' already exists",
),
];
for (first_sql, dup_sql, expected) in cases {
let mut db = Database::new();
let first = parse_create_table(first_sql);
CreateTableExecutor::execute(&first, &mut db).expect("first create should succeed");
let dup = parse_create_table(dup_sql);
let err = CreateTableExecutor::execute(&dup, &mut db)
.expect_err("duplicate table must be rejected");
assert_eq!(err.to_string(), expected, "for SQL: {dup_sql}");
}
}
#[test]
fn test_duplicate_table_as_select_echoes_source_quoting() {
let mut db = Database::new();
let first = parse_create_table("CREATE TABLE tbl1 (a)");
CreateTableExecutor::execute(&first, &mut db).expect("first create should succeed");
let dup = parse_create_table("CREATE TABLE \"tbl1\" AS SELECT 1");
let err =
CreateTableExecutor::execute(&dup, &mut db).expect_err("duplicate CTAS must be rejected");
assert_eq!(err.to_string(), "Table '\"tbl1\"' already exists");
}
#[test]
fn test_duplicate_table_programmatic_ast_falls_back_to_normalized_name() {
let mut db = Database::new();
let mut stmt = parse_create_table("CREATE TABLE tbl1 (a)");
stmt.name_source = None;
CreateTableExecutor::execute(&stmt, &mut db).expect("first create should succeed");
let err =
CreateTableExecutor::execute(&stmt, &mut db).expect_err("duplicate must be rejected");
let msg = err.to_string();
assert!(msg.ends_with("tbl1' already exists"), "unexpected fallback message: {msg}");
assert!(msg.contains('.'), "expected schema-qualified fallback: {msg}");
}
#[test]
fn test_create_table_with_nullable_columns() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "employees".to_string(),
columns: vec![
ColumnDef {
name: "id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "middle_name".to_string(),
data_type: DataType::Varchar { max_length: Some(50) },
nullable: true, constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "manager_id".to_string(),
data_type: DataType::Integer,
nullable: true, constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
let schema = db.catalog.get_table("employees").unwrap();
assert!(!schema.get_column("id").unwrap().nullable);
assert!(schema.get_column("middle_name").unwrap().nullable);
assert!(schema.get_column("manager_id").unwrap().nullable);
}
#[test]
fn test_create_table_empty_columns_list() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "empty_table".to_string(),
columns: vec![], table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
let schema = db.catalog.get_table("empty_table").unwrap();
assert_eq!(schema.column_count(), 0);
}
#[test]
fn test_create_multiple_tables() {
let mut db = Database::new();
let stmt1 = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "customers".to_string(),
columns: vec![ColumnDef {
name: "customer_id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
}],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
CreateTableExecutor::execute(&stmt1, &mut db).unwrap();
let stmt2 = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "orders".to_string(),
columns: vec![ColumnDef {
name: "order_id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
}],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
CreateTableExecutor::execute(&stmt2, &mut db).unwrap();
assert!(db.catalog.table_exists("customers"));
assert!(db.catalog.table_exists("orders"));
assert_eq!(db.list_tables().len(), 2);
}
#[test]
fn test_create_table_with_special_characters_in_name() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "user_profiles".to_string(),
columns: vec![ColumnDef {
name: "profile_id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
}],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
assert!(db.catalog.table_exists("user_profiles"));
}
fn create_table_stmt(name: &str, quoted: bool) -> CreateTableStmt {
CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: name.to_string(),
columns: vec![ColumnDef {
name: "id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
}],
table_constraints: vec![],
table_options: vec![],
quoted,
name_source: None,
as_query: None,
without_rowid: false,
}
}
#[test]
fn test_create_table_case_sensitivity() {
let mut db = Database::new();
CreateTableExecutor::execute(&create_table_stmt("Users", true), &mut db).unwrap();
let result = CreateTableExecutor::execute(&create_table_stmt("users", false), &mut db);
assert!(result.is_err(), "differing-case unquoted CREATE should collide with quoted table");
let result = CreateTableExecutor::execute(&create_table_stmt("USERS", true), &mut db);
assert!(result.is_err(), "differing-case quoted CREATE should collide too");
assert!(db.catalog.table_exists("users"));
assert!(db.catalog.table_exists("USERS"));
assert!(db.catalog.table_exists("Users"));
assert_eq!(db.catalog.list_tables().len(), 1);
}
#[test]
fn test_create_table_quoted_mixed_case_resolves_and_preserves_spelling() {
let mut db = Database::new();
CreateTableExecutor::execute(&create_table_stmt("MixedCase", true), &mut db).unwrap();
assert!(db.catalog.table_exists("mixedcase"));
assert!(db.catalog.table_exists("MIXEDCASE"));
assert!(db.catalog.get_table("MixedCase").is_some());
let table = db.catalog.get_table("mixedcase").expect("table resolvable lowercase");
assert_eq!(table.name, "MixedCase", "original declared case must be preserved");
}
#[test]
fn test_create_table_with_spatial_types() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "spatial_table".to_string(),
columns: vec![
ColumnDef {
name: "id".to_string(),
data_type: DataType::Integer,
nullable: false,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "location".to_string(),
data_type: DataType::UserDefined { type_name: "POINT".to_string() },
nullable: true,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "area".to_string(),
data_type: DataType::UserDefined { type_name: "POLYGON".to_string() },
nullable: true,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "regions".to_string(),
data_type: DataType::UserDefined { type_name: "MULTIPOLYGON".to_string() },
nullable: true,
constraints: vec![],
default_value: None,
comment: None,
generated_expr: None,
is_exact_integer_type: false,
},
],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(result.is_ok(), "Should accept spatial types as UserDefined types");
let schema = db.catalog.get_table("spatial_table");
assert!(schema.is_some());
let schema = schema.unwrap();
assert_eq!(schema.column_count(), 4);
assert!(schema.get_column("location").is_some());
assert!(schema.get_column("area").is_some());
assert!(schema.get_column("regions").is_some());
}
#[test]
fn test_create_table_multipolygon_sqllogictest() {
let mut db = Database::new();
let stmt = CreateTableStmt {
temporary: false,
if_not_exists: false,
table_name: "t1710a".to_string(),
columns: vec![
ColumnDef {
name: "c1".to_string(),
data_type: DataType::UserDefined { type_name: "MULTIPOLYGON".to_string() },
nullable: true,
constraints: vec![],
default_value: None,
comment: Some("text155459".to_string()),
generated_expr: None,
is_exact_integer_type: false,
},
ColumnDef {
name: "c2".to_string(),
data_type: DataType::UserDefined { type_name: "MULTIPOLYGON".to_string() },
nullable: true,
constraints: vec![],
default_value: None,
comment: Some("text155461".to_string()),
generated_expr: None,
is_exact_integer_type: false,
},
],
table_constraints: vec![],
table_options: vec![],
quoted: false,
name_source: None,
as_query: None,
without_rowid: false,
};
let result = CreateTableExecutor::execute(&stmt, &mut db);
assert!(
result.is_ok(),
"Should create table with MULTIPOLYGON columns (SQLLogicTest conformance)"
);
assert!(db.catalog.table_exists("t1710a"));
let schema = db.catalog.get_table("t1710a").unwrap();
assert_eq!(schema.column_count(), 2);
}
fn exec_sql_collision(db: &mut Database, sql: &str) -> Result<String, String> {
let stmt =
vibesql_parser::Parser::parse_sql(sql).map_err(|e| format!("Parse error: {:?}", e))?;
match stmt {
vibesql_ast::Statement::CreateTable(s) => {
CreateTableExecutor::execute(&s, db).map_err(|e| e.to_string())
}
vibesql_ast::Statement::CreateIndex(s) => crate::CreateIndexExecutor::execute(&s, db)
.map(|_| "ok".to_string())
.map_err(|e| e.to_string()),
vibesql_ast::Statement::DropTable(s) => crate::DropTableExecutor::execute(&s, db)
.map(|_| "ok".to_string())
.map_err(|e| e.to_string()),
other => Err(format!("Unsupported statement type: {:?}", other)),
}
}
#[test]
fn create_table_rejects_existing_index_name() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE test2(one text)").unwrap();
exec_sql_collision(&mut db, "CREATE INDEX test3 ON test2(one)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE TABLE test3(two text)").unwrap_err();
assert_eq!(err, "there is already an index named test3");
assert!(!db.catalog.table_exists("test3"));
}
#[test]
fn create_table_if_not_exists_still_rejects_index_name() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE t2(a)").unwrap();
exec_sql_collision(&mut db, "CREATE INDEX t3 ON t2(a)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE TABLE IF NOT EXISTS t3(b)").unwrap_err();
assert_eq!(err, "there is already an index named t3");
}
#[test]
fn create_table_index_collision_is_case_insensitive() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE t2(a)").unwrap();
exec_sql_collision(&mut db, "CREATE INDEX t3 ON t2(a)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE TABLE T3(b)").unwrap_err();
assert_eq!(err, "there is already an index named T3");
}
#[test]
fn create_index_rejects_existing_table_name() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE t1(a)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE INDEX t1 ON t1(a)").unwrap_err();
assert_eq!(err, "there is already a table named t1");
}
#[test]
fn create_table_allows_distinct_names() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE a(x)").unwrap();
exec_sql_collision(&mut db, "CREATE INDEX idx_a ON a(x)").unwrap();
exec_sql_collision(&mut db, "CREATE TABLE b(y)").unwrap();
assert!(db.catalog.table_exists("a"));
assert!(db.catalog.table_exists("b"));
}
#[test]
fn create_table_as_select_rejects_existing_index_name() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE src(one text)").unwrap();
exec_sql_collision(&mut db, "CREATE INDEX ix ON src(one)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE TABLE ix AS SELECT * FROM src").unwrap_err();
assert_eq!(err, "there is already an index named ix");
assert!(!db.catalog.table_exists("ix"));
}
#[test]
fn create_table_rejects_reserved_sqlite_master_name() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE sqlite_master(two text)").unwrap_err();
assert_eq!(err, "object name reserved for internal use: sqlite_master");
assert!(!db.catalog.table_exists("sqlite_master"));
}
#[test]
fn create_table_rejects_any_sqlite_prefixed_name() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE sqlite_foo(x)").unwrap_err();
assert_eq!(err, "object name reserved for internal use: sqlite_foo");
}
#[test]
fn create_table_reserved_name_echoes_original_case() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE SQLITE_foo(x)").unwrap_err();
assert_eq!(err, "object name reserved for internal use: SQLITE_foo");
}
#[test]
fn create_table_reserved_name_quoted_is_dequoted() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE \"sqlite_quoted\"(x)").unwrap_err();
assert_eq!(err, "object name reserved for internal use: sqlite_quoted");
}
#[test]
fn create_index_rejects_reserved_sqlite_name() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE realt(x)").unwrap();
let err = exec_sql_collision(&mut db, "CREATE INDEX sqlite_idx ON realt(x)").unwrap_err();
assert_eq!(err, "object name reserved for internal use: sqlite_idx");
assert!(!db.index_exists("sqlite_idx"));
}
#[test]
fn create_table_rejects_duplicate_column_names() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE t9(a, a)").unwrap_err();
assert_eq!(err, "duplicate column name: a");
assert!(!db.catalog.table_exists("t9"));
}
#[test]
fn create_table_duplicate_column_detection_is_case_insensitive() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE t9b(a, A)").unwrap_err();
assert_eq!(err, "duplicate column name: A");
}
#[test]
fn create_table_duplicate_column_quoted_variant() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "CREATE TABLE t9c(a, [a])").unwrap_err();
assert_eq!(err, "duplicate column name: a");
}
#[test]
fn create_table_allows_distinct_columns() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE okcols(a, ab, abc)").unwrap();
assert!(db.catalog.table_exists("okcols"));
}
#[test]
fn drop_table_sqlite_master_is_rejected() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "DROP TABLE sqlite_master").unwrap_err();
assert_eq!(err, "table sqlite_master may not be dropped");
}
#[test]
fn drop_table_sqlite_master_rejected_regardless_of_case() {
let mut db = Database::new();
let err = exec_sql_collision(&mut db, "DROP TABLE SQLITE_MASTER").unwrap_err();
assert_eq!(err, "table sqlite_master may not be dropped");
}
#[test]
fn user_table_named_sqlitewithoutunderscore_is_allowed() {
let mut db = Database::new();
exec_sql_collision(&mut db, "CREATE TABLE sqlitefoo(x)").unwrap();
assert!(db.catalog.table_exists("sqlitefoo"));
}