Skip to content

Commit c527f9f

Browse files
committed
Support sqlite enum
1 parent 6b55d5b commit c527f9f

5 files changed

+108
-11
lines changed

crates/vespertide-query/src/sql/add_column.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ use sea_query::{Alias, Expr, Query, Table, TableAlterStatement};
33
use vespertide_core::{ColumnDef, TableDef};
44

55
use super::create_table::build_create_table_for_backend;
6-
use super::helpers::{build_create_enum_type_sql, build_sea_column_def};
6+
use super::helpers::{
7+
build_create_enum_type_sql, build_schema_statement, build_sea_column_def,
8+
collect_sqlite_enum_check_clauses,
9+
};
710
use super::rename_table::build_rename_table;
8-
use super::types::{BuiltQuery, DatabaseBackend};
11+
use super::types::{BuiltQuery, DatabaseBackend, RawSql};
912
use crate::error::QueryError;
1013

1114
fn build_add_column_alter_for_backend(
@@ -20,15 +23,27 @@ fn build_add_column_alter_for_backend(
2023
.to_owned()
2124
}
2225

26+
/// Check if the column type is an enum
27+
fn is_enum_column(column: &ColumnDef) -> bool {
28+
matches!(
29+
column.r#type,
30+
vespertide_core::ColumnType::Complex(vespertide_core::ComplexColumnType::Enum { .. })
31+
)
32+
}
33+
2334
pub fn build_add_column(
2435
backend: &DatabaseBackend,
2536
table: &str,
2637
column: &ColumnDef,
2738
fill_with: Option<&str>,
2839
current_schema: &[TableDef],
2940
) -> Result<Vec<BuiltQuery>, QueryError> {
30-
// SQLite: only NOT NULL additions require table recreation
31-
if *backend == DatabaseBackend::Sqlite && !column.nullable {
41+
// SQLite: NOT NULL additions or enum columns require table recreation
42+
// (enum columns need CHECK constraint which requires table recreation in SQLite)
43+
let sqlite_needs_recreation =
44+
*backend == DatabaseBackend::Sqlite && (!column.nullable || is_enum_column(column));
45+
46+
if sqlite_needs_recreation {
3247
let table_def = current_schema
3348
.iter()
3449
.find(|t| t.name == table)
@@ -47,7 +62,25 @@ pub fn build_add_column(
4762
&new_columns,
4863
&table_def.constraints,
4964
);
50-
let create_query = BuiltQuery::CreateTable(Box::new(create_temp));
65+
66+
// For SQLite, add CHECK constraints for enum columns
67+
// Use original table name for constraint naming (table will be renamed back)
68+
let enum_check_clauses = collect_sqlite_enum_check_clauses(table, &new_columns);
69+
let create_query = if !enum_check_clauses.is_empty() {
70+
let base_sql = build_schema_statement(&create_temp, *backend);
71+
let mut modified_sql = base_sql;
72+
if let Some(pos) = modified_sql.rfind(')') {
73+
let check_sql = enum_check_clauses.join(", ");
74+
modified_sql.insert_str(pos, &format!(", {}", check_sql));
75+
}
76+
BuiltQuery::Raw(RawSql::per_backend(
77+
modified_sql.clone(),
78+
modified_sql.clone(),
79+
modified_sql,
80+
))
81+
} else {
82+
BuiltQuery::CreateTable(Box::new(create_temp))
83+
};
5184

5285
// Copy existing data, filling new column
5386
let mut select_query = Query::select();

crates/vespertide-query/src/sql/create_table.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ use sea_query::{Alias, ForeignKey, Index, Table, TableCreateStatement};
22

33
use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, TableConstraint};
44

5-
use super::helpers::{build_create_enum_type_sql, build_sea_column_def, to_sea_fk_action};
6-
use super::types::{BuiltQuery, DatabaseBackend};
5+
use super::helpers::{
6+
build_create_enum_type_sql, build_schema_statement, build_sea_column_def,
7+
collect_sqlite_enum_check_clauses, to_sea_fk_action,
8+
};
9+
use super::types::{BuiltQuery, DatabaseBackend, RawSql};
710
use crate::error::QueryError;
811

912
pub(crate) fn build_create_table_for_backend(
@@ -145,7 +148,29 @@ pub fn build_create_table(
145148
table_constraints.iter().cloned().cloned().collect();
146149
build_create_table_for_backend(backend, table, columns, &table_constraints_owned)
147150
};
148-
queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
151+
152+
// For SQLite, add CHECK constraints for enum columns
153+
if matches!(backend, DatabaseBackend::Sqlite) {
154+
let enum_check_clauses = collect_sqlite_enum_check_clauses(table, columns);
155+
if !enum_check_clauses.is_empty() {
156+
// Embed CHECK constraints into CREATE TABLE statement
157+
let base_sql = build_schema_statement(&create_table_stmt, *backend);
158+
let mut modified_sql = base_sql;
159+
if let Some(pos) = modified_sql.rfind(')') {
160+
let check_sql = enum_check_clauses.join(", ");
161+
modified_sql.insert_str(pos, &format!(", {}", check_sql));
162+
}
163+
queries.push(BuiltQuery::Raw(RawSql::per_backend(
164+
modified_sql.clone(),
165+
modified_sql.clone(),
166+
modified_sql,
167+
)));
168+
} else {
169+
queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
170+
}
171+
} else {
172+
queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt)));
173+
}
149174

150175
// For Postgres and SQLite, add unique constraints as separate CREATE UNIQUE INDEX statements
151176
if matches!(backend, DatabaseBackend::Postgres | DatabaseBackend::Sqlite) {

crates/vespertide-query/src/sql/helpers.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,43 @@ pub fn is_enum_type(column_type: &ColumnType) -> bool {
228228
)
229229
}
230230

231+
/// Generate CHECK constraint name for SQLite enum column
232+
/// Format: chk_{table}_{column}
233+
pub fn build_sqlite_enum_check_name(table: &str, column: &str) -> String {
234+
format!("chk_{}_{}", table, column)
235+
}
236+
237+
/// Generate CHECK constraint expression for SQLite enum column
238+
/// Returns the constraint clause like: CONSTRAINT "chk_table_col" CHECK (col IN ('val1', 'val2'))
239+
pub fn build_sqlite_enum_check_clause(
240+
table: &str,
241+
column: &str,
242+
column_type: &ColumnType,
243+
) -> Option<String> {
244+
if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = column_type {
245+
let name = build_sqlite_enum_check_name(table, column);
246+
let values_sql = values
247+
.iter()
248+
.map(|v| format!("'{}'", v.replace('\'', "''")))
249+
.collect::<Vec<_>>()
250+
.join(", ");
251+
Some(format!(
252+
"CONSTRAINT \"{}\" CHECK (\"{}\" IN ({}))",
253+
name, column, values_sql
254+
))
255+
} else {
256+
None
257+
}
258+
}
259+
260+
/// Collect all CHECK constraints for enum columns in a table (for SQLite)
261+
pub fn collect_sqlite_enum_check_clauses(table: &str, columns: &[ColumnDef]) -> Vec<String> {
262+
columns
263+
.iter()
264+
.filter_map(|col| build_sqlite_enum_check_clause(table, &col.name, &col.r#type))
265+
.collect()
266+
}
267+
231268
/// Extract enum name from column type if it's an enum
232269
pub fn get_enum_name(column_type: &ColumnType) -> Option<&str> {
233270
if let ColumnType::Complex(ComplexColumnType::Enum { name, .. }) = column_type {

crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_enum_type@add_column_with_enum_type_Sqlite.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
source: crates/vespertide-query/src/sql/add_column.rs
33
expression: sql
44
---
5-
;
6-
ALTER TABLE "users" ADD COLUMN "status" enum_text
5+
CREATE TABLE "users_temp" ( "id" integer NOT NULL, "status" enum_text , CONSTRAINT "chk_users_status" CHECK ("status" IN ('active', 'inactive')));
6+
INSERT INTO "users_temp" ("id", "status") SELECT "id", NULL AS "status" FROM "users";
7+
DROP TABLE "users";
8+
ALTER TABLE "users_temp" RENAME TO "users"

crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_enum_column@create_table_with_enum_column_Sqlite.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs
33
expression: sql
44
---
55
;
6-
CREATE TABLE "users" ( "id" integer NOT NULL, "status" enum_text NOT NULL DEFAULT 'active', PRIMARY KEY ("id") )
6+
CREATE TABLE "users" ( "id" integer NOT NULL, "status" enum_text NOT NULL DEFAULT 'active', PRIMARY KEY ("id") , CONSTRAINT "chk_users_status" CHECK ("status" IN ('active', 'inactive', 'pending')))

0 commit comments

Comments
 (0)