|
1 | 1 | import sqlite3 |
2 | 2 |
|
3 | | -from sqlite_utils.db import ForeignKey, TransformError |
| 3 | +from sqlite_utils.db import ForeignKey, TransactionError, TransformError |
4 | 4 | from sqlite_utils.utils import OperationalError |
5 | 5 | import pytest |
6 | 6 |
|
@@ -469,6 +469,128 @@ def test_transform_on_delete_cascade_does_not_delete_records( |
469 | 469 | assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0] |
470 | 470 |
|
471 | 471 |
|
| 472 | +@pytest.mark.parametrize("on_delete", ["CASCADE", "SET NULL", "SET DEFAULT", "cascade"]) |
| 473 | +def test_transform_in_transaction_refuses_destructive_on_delete(fresh_db, on_delete): |
| 474 | + # PRAGMA foreign_keys is a no-op inside a transaction, so transforming a |
| 475 | + # table referenced by ON DELETE CASCADE / SET NULL / SET DEFAULT foreign |
| 476 | + # keys inside an open transaction would fire those actions when the old |
| 477 | + # table is dropped - transform() should refuse instead |
| 478 | + fresh_db.conn.execute("PRAGMA foreign_keys=ON") |
| 479 | + fresh_db.executescript(""" |
| 480 | + CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT); |
| 481 | + CREATE TABLE books ( |
| 482 | + id INTEGER PRIMARY KEY, |
| 483 | + title TEXT, |
| 484 | + author_id INTEGER REFERENCES authors(id) ON DELETE {} |
| 485 | + ); |
| 486 | + """.format(on_delete)) |
| 487 | + fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"}) |
| 488 | + fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1}) |
| 489 | + previous_schema = fresh_db["authors"].schema |
| 490 | + with fresh_db.atomic(): |
| 491 | + with pytest.raises(TransactionError) as excinfo: |
| 492 | + fresh_db["authors"].transform(rename={"name": "author_name"}) |
| 493 | + message = str(excinfo.value) |
| 494 | + assert "books" in message |
| 495 | + assert "ON DELETE {}".format(on_delete.upper()) in message |
| 496 | + # Nothing should have changed |
| 497 | + assert fresh_db["authors"].schema == previous_schema |
| 498 | + assert list(fresh_db["books"].rows) == [ |
| 499 | + {"id": 1, "title": "The Dispossessed", "author_id": 1} |
| 500 | + ] |
| 501 | + assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0] |
| 502 | + |
| 503 | + |
| 504 | +def test_transform_in_transaction_refuses_self_referential_cascade(fresh_db): |
| 505 | + # The copied table carries a foreign key referencing the original table |
| 506 | + # name, so a self-referential cascade would wipe the copy too |
| 507 | + fresh_db.conn.execute("PRAGMA foreign_keys=ON") |
| 508 | + fresh_db.executescript(""" |
| 509 | + CREATE TABLE categories ( |
| 510 | + id INTEGER PRIMARY KEY, |
| 511 | + name TEXT, |
| 512 | + parent_id INTEGER REFERENCES categories(id) ON DELETE CASCADE |
| 513 | + ); |
| 514 | + """) |
| 515 | + fresh_db["categories"].insert_all( |
| 516 | + [ |
| 517 | + {"id": 1, "name": "Fiction", "parent_id": None}, |
| 518 | + {"id": 2, "name": "Science Fiction", "parent_id": 1}, |
| 519 | + ] |
| 520 | + ) |
| 521 | + with fresh_db.atomic(): |
| 522 | + with pytest.raises(TransactionError) as excinfo: |
| 523 | + fresh_db["categories"].transform(rename={"name": "title"}) |
| 524 | + assert "categories" in str(excinfo.value) |
| 525 | + assert fresh_db["categories"].count == 2 |
| 526 | + |
| 527 | + |
| 528 | +def test_transform_in_transaction_allowed_with_no_action_foreign_key(fresh_db): |
| 529 | + # An inbound foreign key without a destructive ON DELETE action is safe |
| 530 | + # inside a transaction thanks to PRAGMA defer_foreign_keys |
| 531 | + fresh_db.conn.execute("PRAGMA foreign_keys=ON") |
| 532 | + fresh_db.executescript(""" |
| 533 | + CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT); |
| 534 | + CREATE TABLE books ( |
| 535 | + id INTEGER PRIMARY KEY, |
| 536 | + title TEXT, |
| 537 | + author_id INTEGER REFERENCES authors(id) |
| 538 | + ); |
| 539 | + """) |
| 540 | + fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"}) |
| 541 | + fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1}) |
| 542 | + with fresh_db.atomic(): |
| 543 | + fresh_db["authors"].transform(rename={"name": "author_name"}) |
| 544 | + assert list(fresh_db["authors"].rows) == [ |
| 545 | + {"id": 1, "author_name": "Ursula K. Le Guin"} |
| 546 | + ] |
| 547 | + assert list(fresh_db["books"].rows) == [ |
| 548 | + {"id": 1, "title": "The Dispossessed", "author_id": 1} |
| 549 | + ] |
| 550 | + assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0] |
| 551 | + |
| 552 | + |
| 553 | +def test_transform_in_transaction_allowed_for_child_table(fresh_db): |
| 554 | + # The table being transformed only has an outbound foreign key - dropping |
| 555 | + # it fires no ON DELETE actions, so this is allowed inside a transaction |
| 556 | + fresh_db.conn.execute("PRAGMA foreign_keys=ON") |
| 557 | + fresh_db.executescript(""" |
| 558 | + CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT); |
| 559 | + CREATE TABLE books ( |
| 560 | + id INTEGER PRIMARY KEY, |
| 561 | + title TEXT, |
| 562 | + author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE |
| 563 | + ); |
| 564 | + """) |
| 565 | + fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"}) |
| 566 | + fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1}) |
| 567 | + with fresh_db.atomic(): |
| 568 | + fresh_db["books"].transform(rename={"title": "book_title"}) |
| 569 | + assert list(fresh_db["books"].rows) == [ |
| 570 | + {"id": 1, "book_title": "The Dispossessed", "author_id": 1} |
| 571 | + ] |
| 572 | + |
| 573 | + |
| 574 | +def test_transform_in_transaction_allowed_with_foreign_keys_off(fresh_db): |
| 575 | + # With PRAGMA foreign_keys off (the default) no cascades can fire, so |
| 576 | + # transform inside a transaction is safe even with a CASCADE schema |
| 577 | + fresh_db.executescript(""" |
| 578 | + CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT); |
| 579 | + CREATE TABLE books ( |
| 580 | + id INTEGER PRIMARY KEY, |
| 581 | + title TEXT, |
| 582 | + author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE |
| 583 | + ); |
| 584 | + """) |
| 585 | + fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"}) |
| 586 | + fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1}) |
| 587 | + with fresh_db.atomic(): |
| 588 | + fresh_db["authors"].transform(rename={"name": "author_name"}) |
| 589 | + assert list(fresh_db["books"].rows) == [ |
| 590 | + {"id": 1, "title": "The Dispossessed", "author_id": 1} |
| 591 | + ] |
| 592 | + |
| 593 | + |
472 | 594 | def test_transform_add_foreign_keys_from_scratch(fresh_db): |
473 | 595 | _add_country_city_continent(fresh_db) |
474 | 596 | fresh_db["places"].insert(_CAVEAU) |
|
0 commit comments