-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(torii-sqlite): support enum upgrade of variants #2930
base: main
Are you sure you want to change the base?
Changes from 8 commits
d44e11b
f7c1ce8
8087be0
9423c97
6e2c333
353804d
415daca
95cebf7
4bc0b36
f993071
1df0eaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -847,20 +847,44 @@ | |
)); | ||
}; | ||
|
||
let modify_column = | ||
|alter_table_queries: &mut Vec<String>, name: &str, sql_type: &str, sql_value: &str| { | ||
// SQLite doesn't support ALTER COLUMN directly, so we need to: | ||
// 1. Create a temporary table to store the current values | ||
// 2. Drop the old column & index | ||
// 3. Create new column with new type/constraint | ||
// 4. Copy values back & create new index | ||
alter_table_queries.push(format!( | ||
"CREATE TEMPORARY TABLE tmp_values_{name} AS SELECT internal_id, [{name}] FROM \ | ||
[{table_id}]" | ||
)); | ||
alter_table_queries.push(format!("DROP INDEX IF EXISTS [idx_{table_id}_{name}]")); | ||
alter_table_queries.push(format!("ALTER TABLE [{table_id}] DROP COLUMN [{name}]")); | ||
alter_table_queries | ||
.push(format!("ALTER TABLE [{table_id}] ADD COLUMN [{name}] {sql_type}")); | ||
alter_table_queries.push(format!("UPDATE [{table_id}] SET [{name}] = {sql_value}")); | ||
alter_table_queries.push(format!("DROP TABLE tmp_values_{name}")); | ||
alter_table_queries.push(format!( | ||
"CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] ([{name}]);" | ||
)); | ||
}; | ||
|
||
match ty { | ||
Ty::Struct(s) => { | ||
let struct_diff = | ||
if let Some(upgrade_diff) = upgrade_diff { upgrade_diff.as_struct() } else { None }; | ||
|
||
for member in &s.children { | ||
if let Some(upgrade_diff) = upgrade_diff { | ||
if !upgrade_diff | ||
.as_struct() | ||
.unwrap() | ||
.children | ||
.iter() | ||
.any(|m| m.name == member.name) | ||
{ | ||
let member_diff = if let Some(diff) = struct_diff { | ||
if let Some(m) = diff.children.iter().find(|m| m.name == member.name) { | ||
Some(&m.ty) | ||
} else { | ||
// If the member is not in the diff, skip it | ||
continue; | ||
} | ||
} | ||
} else { | ||
None | ||
}; | ||
|
||
let mut new_path = path.to_vec(); | ||
new_path.push(member.name.clone()); | ||
|
@@ -872,23 +896,36 @@ | |
alter_table_queries, | ||
indices, | ||
table_id, | ||
None, | ||
member_diff, | ||
)?; | ||
} | ||
} | ||
Ty::Tuple(tuple) => { | ||
let tuple_diff = | ||
if let Some(upgrade_diff) = upgrade_diff { upgrade_diff.as_tuple() } else { None }; | ||
|
||
for (idx, member) in tuple.iter().enumerate() { | ||
let mut new_path = path.to_vec(); | ||
new_path.push(idx.to_string()); | ||
|
||
let member_diff = if let Some(diff) = tuple_diff { | ||
if let Some((_, m)) = diff.iter().enumerate().find(|(i, _)| *i == idx) { | ||
Some(m) | ||
} else { | ||
continue; | ||
} | ||
} else { | ||
None | ||
}; | ||
|
||
add_columns_recursive( | ||
&new_path, | ||
member, | ||
columns, | ||
alter_table_queries, | ||
indices, | ||
table_id, | ||
None, | ||
member_diff, | ||
)?; | ||
} | ||
} | ||
|
@@ -899,17 +936,45 @@ | |
add_column(&column_name, "TEXT"); | ||
} | ||
Ty::Enum(e) => { | ||
// The variant of the enum | ||
let enum_diff = | ||
if let Some(upgrade_diff) = upgrade_diff { upgrade_diff.as_enum() } else { None }; | ||
|
||
let column_name = | ||
if column_prefix.is_empty() { "option".to_string() } else { column_prefix }; | ||
|
||
let all_options = | ||
e.options.iter().map(|c| format!("'{}'", c.name)).collect::<Vec<_>>().join(", "); | ||
|
||
let sql_type = format!("TEXT CHECK([{column_name}] IN ({all_options}))"); | ||
add_column(&column_name, &sql_type); | ||
let sql_type = format!( | ||
"TEXT CONSTRAINT [{column_name}_check] CHECK([{column_name}] IN ({all_options}))" | ||
); | ||
if enum_diff.is_some() { | ||
// For upgrades, modify the existing option column to add the new options to the | ||
// CHECK constraint We need to drop the old column and create a new | ||
// one with the new CHECK constraint | ||
modify_column( | ||
alter_table_queries, | ||
&column_name, | ||
&sql_type, | ||
&format!("[{column_name}]"), | ||
); | ||
} else { | ||
// For new tables, create the column directly | ||
add_column(&column_name, &sql_type); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification 🛠️ Refactor suggestion Ohayo sensei! The review comment is spot-on about enum variant removal! The current implementation is indeed unsafe when removing enum variants. The code modifies the CHECK constraint without validating if any rows contain the removed variants, which could lead to invalid data states. The suggested validation using 🔗 Analysis chainOhayo sensei! Handle removed enum variants. The current implementation only handles new variants but doesn't account for variants that might have been removed in the upgrade. This could lead to invalid data if variants are removed. Run this script to check if any enums have removed variants: Consider adding validation to handle removed variants: let sql_type = format!(
"TEXT CONSTRAINT [{column_name}_check] CHECK([{column_name}] IN ({all_options}))"
);
if enum_diff.is_some() {
+ // Validate existing data before modifying the constraint
+ alter_table_queries.push(format!(
+ "SELECT CASE WHEN EXISTS (
+ SELECT 1 FROM [{table_id}]
+ WHERE [{column_name}] NOT IN ({all_options})
+ )
+ THEN RAISE(ABORT, 'Found invalid enum variants')
+ END"
+ ));
+
modify_column(
alter_table_queries,
&column_name, 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Description: Check for removed enum variants in the codebase
# Search for enum definitions and their variants
ast-grep --pattern 'enum $name {
$$$
$variant($$$)
$$$
}'
Length of output: 65 Script: #!/bin/bash
# Search for enum definitions in Rust
ast-grep --pattern 'enum $name { $$$ }'
# Search for enum-related tests and migration code
rg -l "enum.*diff|enum.*migration" --type rust
# Search for EnumDiff implementation
rg "impl.*EnumDiff" -A 5 --type rust
Length of output: 192 Script: #!/bin/bash
# Find EnumDiff type definition and usage
rg "struct EnumDiff|type EnumDiff" -A 5 --type rust
# Look for enum migration tests
rg "test.*enum.*migration|test.*enum.*upgrade" -B 2 -A 10 --type rust
# Search for variant removal handling
rg "enum.*remove|variant.*remove|DROP.*CONSTRAINT" -A 5 --type rust
Length of output: 1181 Script: #!/bin/bash
# Search for CHECK constraint and column modification logic
rg "CHECK.*IN|modify_column|alter_table.*enum" -B 2 -A 5 --type rust
# Look for SQL migration validation
rg "RAISE.*ABORT|SELECT.*EXISTS.*WHERE" -A 5 --type rust
# Find column modification implementation
rg "fn modify_column" -B 2 -A 10 --type rust
Length of output: 1868 |
||
|
||
for child in &e.options { | ||
// If we have a diff, only process new variants that aren't in the original enum | ||
let variant_diff = if let Some(diff) = enum_diff { | ||
if let Some(v) = diff.options.iter().find(|v| v.name == child.name) { | ||
Some(&v.ty) | ||
} else { | ||
continue; | ||
} | ||
} else { | ||
None | ||
}; | ||
|
||
if let Ty::Tuple(tuple) = &child.ty { | ||
if tuple.is_empty() { | ||
continue; | ||
|
@@ -926,7 +991,7 @@ | |
alter_table_queries, | ||
indices, | ||
table_id, | ||
None, | ||
variant_diff, | ||
)?; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohayo sensei! Fix potential data loss in column modification.
The
modify_column
closure creates a temporary table but doesn't copy the values back to the new column. TheUPDATE
statement on line 865 sets a staticsql_value
instead of restoring the original values.Apply this diff to fix the data loss:
📝 Committable suggestion