mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-10-25 02:37:07 +02:00
fix: reject transform renames that collide column names
Renaming a column onto the name of another existing column silently collapsed the two into one and discarded the source column's entire dataset, because the new CREATE TABLE column list was built with dict(new_column_pairs) and the earlier entry was overwritten. Detect duplicate target names and raise TransformError before any SQL is executed, leaving the table untouched.
This commit is contained in:
parent
6bc1d33d58
commit
4487c99f89
2 changed files with 48 additions and 0 deletions
|
|
@ -2998,6 +2998,26 @@ class Table(Queryable):
|
|||
new_column_pairs.append((new_name, type_))
|
||||
copy_from_to[name] = new_name
|
||||
|
||||
duplicate_columns = sorted(
|
||||
{
|
||||
column_name
|
||||
for column_name, _ in new_column_pairs
|
||||
if sum(
|
||||
1 for other_name, _ in new_column_pairs if other_name == column_name
|
||||
)
|
||||
> 1
|
||||
}
|
||||
)
|
||||
if duplicate_columns:
|
||||
raise TransformError(
|
||||
"Cannot transform table '{}': renaming or retyping columns would result "
|
||||
"in duplicate column name{} '{}'. No changes have been applied to this table.".format(
|
||||
self.name,
|
||||
"s" if len(duplicate_columns) > 1 else "",
|
||||
"', '".join(duplicate_columns),
|
||||
)
|
||||
)
|
||||
|
||||
if existing_autoincrement:
|
||||
existing_autoincrement = resolve_casing(
|
||||
existing_autoincrement, existing_columns
|
||||
|
|
|
|||
|
|
@ -254,6 +254,34 @@ def test_transform_rename_pk(fresh_db):
|
|||
)
|
||||
|
||||
|
||||
def test_transform_rename_cannot_lose_data_by_colliding_columns(fresh_db):
|
||||
# Renaming one column onto the name of another used to silently collapse
|
||||
# the two columns into one, discarding the source column's data entirely.
|
||||
table = fresh_db.table("items")
|
||||
table.insert({"a": 1, "b": 2})
|
||||
with pytest.raises(TransformError, match="duplicate column name"):
|
||||
table.transform(rename={"b": "a"})
|
||||
# The failed transform must leave the table and its data untouched.
|
||||
assert table.count == 1
|
||||
assert [(r["a"], r["b"]) for r in table.rows_where(order_by="rowid")] == [(1, 2)]
|
||||
assert '"b"' in table.schema
|
||||
|
||||
|
||||
def test_transform_rename_two_columns_to_same_name_errors(fresh_db):
|
||||
table = fresh_db.table("items")
|
||||
table.insert({"a": 1, "b": 2, "c": 3})
|
||||
with pytest.raises(TransformError, match="duplicate column name 'x'"):
|
||||
table.transform(rename={"b": "x", "c": "x"})
|
||||
|
||||
|
||||
def test_transform_can_swap_two_column_names(fresh_db):
|
||||
# A straight swap a<->b is legal and must not be blocked.
|
||||
table = fresh_db.table("items")
|
||||
table.insert({"a": 1, "b": 2})
|
||||
table.transform(rename={"a": "b", "b": "a"})
|
||||
assert [(r["a"], r["b"]) for r in table.rows_where(order_by="rowid")] == [(2, 1)]
|
||||
|
||||
|
||||
def test_transform_preserves_keyword_literal_defaults(fresh_db):
|
||||
# transform() used to requote keyword-literal defaults (DEFAULT TRUE became
|
||||
# DEFAULT 'TRUE'), so a default insert stored the text 'TRUE' instead of the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue