mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-23 09:24:31 +02:00
Fix .transform() raising TransformError on column-level UNIQUE constraints
SQLite backs a column-level UNIQUE constraint with an auto-index whose sql in sqlite_master is NULL. The index-rebuild step in transform_sql() treated any NULL-sql index as an error, causing TransformError on tables with UNIQUE columns. The fix adds a 'unique' parameter to create_table_sql so the constraint is reproduced as an inline UNIQUE column attribute in the new CREATE TABLE statement. In transform_sql(), auto-UNIQUE indices (origin='u') are collected before the CREATE TABLE call and passed via unique=; the index rebuild loop then skips them with a continue instead of raising. Column renames are applied to the constraint; dropping a UNIQUE column drops its constraint silently. Fixes #762
This commit is contained in:
parent
7a52214624
commit
4d6d51ad02
2 changed files with 37 additions and 12 deletions
|
|
@ -1400,6 +1400,7 @@ class Database:
|
|||
extracts: Optional[Union[Dict[str, str], List[str]]] = None,
|
||||
if_not_exists: bool = False,
|
||||
strict: bool = False,
|
||||
unique: Optional[Iterable[str]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Returns the SQL ``CREATE TABLE`` statement for creating the specified table.
|
||||
|
|
@ -1442,6 +1443,7 @@ class Database:
|
|||
)
|
||||
# Soundness check not_null, and defaults if provided
|
||||
not_null = {resolve_casing(n, columns) for n in not_null or set()}
|
||||
unique = {resolve_casing(n, columns) for n in unique or set()}
|
||||
defaults = {resolve_casing(n, columns): v for n, v in (defaults or {}).items()}
|
||||
if column_order is not None:
|
||||
column_order = [resolve_casing(c, columns) for c in column_order]
|
||||
|
|
@ -1498,6 +1500,8 @@ class Database:
|
|||
column_extras.append("PRIMARY KEY")
|
||||
if column_name in not_null:
|
||||
column_extras.append("NOT NULL")
|
||||
if column_name in unique and column_name != single_pk:
|
||||
column_extras.append("UNIQUE")
|
||||
if column_name in defaults and defaults[column_name] is not None:
|
||||
column_extras.append(
|
||||
"DEFAULT {}".format(self.quote_default_value(defaults[column_name]))
|
||||
|
|
@ -2796,6 +2800,16 @@ class Table(Queryable):
|
|||
if column_order is not None:
|
||||
column_order = [rename.get(col) or col for col in column_order]
|
||||
|
||||
# Collect column-level UNIQUE constraints from auto-indices (origin="u").
|
||||
# These have no CREATE INDEX SQL, so they must be reproduced as inline UNIQUE
|
||||
# column attributes in the new CREATE TABLE statement.
|
||||
create_table_unique = set()
|
||||
for index in self.indexes:
|
||||
if index.origin == "u":
|
||||
for col in index.columns:
|
||||
if col not in drop:
|
||||
create_table_unique.add(rename.get(col, col))
|
||||
|
||||
sqls = []
|
||||
sqls.append(
|
||||
self.db.create_table_sql(
|
||||
|
|
@ -2807,6 +2821,7 @@ class Table(Queryable):
|
|||
foreign_keys=create_table_foreign_keys,
|
||||
column_order=column_order,
|
||||
strict=self.strict,
|
||||
unique=create_table_unique,
|
||||
).strip()
|
||||
)
|
||||
|
||||
|
|
@ -2850,6 +2865,11 @@ class Table(Queryable):
|
|||
{"index_name": index.name},
|
||||
).fetchall()[0][0]
|
||||
if index_sql is None:
|
||||
if index.origin == "u":
|
||||
# Auto-index backing a column-level UNIQUE constraint: already
|
||||
# reproduced as an inline UNIQUE attribute in the new table's
|
||||
# CREATE TABLE statement (see create_table_sql call above).
|
||||
continue
|
||||
raise TransformError(
|
||||
f"Index '{index.name}' on table '{self.name}' does not have a "
|
||||
"CREATE INDEX statement. You must manually drop this index prior to running this "
|
||||
|
|
|
|||
|
|
@ -681,15 +681,20 @@ def test_transform_with_unique_constraint_implicit_index(fresh_db):
|
|||
""")
|
||||
dogs.insert({"id": 1, "name": "Cleo", "age": 5})
|
||||
|
||||
# Attempt to transform the table without modifying 'name'
|
||||
with pytest.raises(TransformError) as excinfo:
|
||||
dogs.transform(types={"age": str})
|
||||
# Transform should succeed and preserve the UNIQUE constraint
|
||||
dogs.transform(types={"age": str})
|
||||
assert "UNIQUE" in dogs.schema
|
||||
|
||||
assert (
|
||||
"Index 'sqlite_autoindex_dogs_1' on table 'dogs' does not have a CREATE INDEX statement."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
assert (
|
||||
"You must manually drop this index prior to running this transformation and manually recreate the new index after running this transformation."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
# Duplicate name insert should still be rejected
|
||||
import pytest as _pytest
|
||||
with _pytest.raises(Exception):
|
||||
fresh_db.execute("INSERT INTO dogs VALUES (2, 'Cleo', '6')")
|
||||
|
||||
# Rename the UNIQUE column: constraint follows the new name
|
||||
dogs.transform(rename={"name": "dog_name"})
|
||||
assert "UNIQUE" in dogs.schema
|
||||
assert [c.name for c in dogs.columns] == ["id", "dog_name", "age"]
|
||||
|
||||
# Drop the UNIQUE column: transform completes without error
|
||||
dogs.transform(drop={"dog_name"})
|
||||
assert "dog_name" not in [c.name for c in dogs.columns]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue