diff --git a/docs/cli.rst b/docs/cli.rst index 9c5344c..bd73eff 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -619,6 +619,10 @@ If you omit the other table and other column references ``sqlite-utils`` will at $ sqlite-utils add-foreign-key books.db books author_id +Add ``--ignore`` to ignore an existing foreign key (as opposed to returning an error):: + + $ sqlite-utils add-foreign-key books.db books author_id --ignore + See :ref:`python_api_add_foreign_key` in the Python API documentation for further details, including how the automatic table guessing mechanism works. .. _cli_add_foreign_keys: diff --git a/docs/python-api.rst b/docs/python-api.rst index e6a0dd1..fae8cee 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -858,6 +858,12 @@ The ``table.add_foreign_key(column, other_table, other_column)`` method takes th This method first checks that the specified foreign key references tables and columns that exist and does not clash with an existing foreign key. It will raise a ``sqlite_utils.db.AlterError`` exception if these checks fail. +To ignore the case where the key already exists, use ``ignore=True``: + +.. code-block:: python + + db["books"].add_foreign_key("author_id", "authors", "id", ignore=True) + .. _python_api_add_foreign_keys: Adding multiple foreign key constraints at once diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index eb44a8e..cfdffed 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -308,7 +308,12 @@ def add_column(path, table, col_name, col_type, fk, fk_col, not_null_default): @click.argument("column") @click.argument("other_table", required=False) @click.argument("other_column", required=False) -def add_foreign_key(path, table, column, other_table, other_column): +@click.option( + "--ignore", + is_flag=True, + help="If foreign key already exists, do nothing", +) +def add_foreign_key(path, table, column, other_table, other_column, ignore): """ Add a new foreign key constraint to an existing table. Example usage: @@ -318,7 +323,7 @@ def add_foreign_key(path, table, column, other_table, other_column): """ db = sqlite_utils.Database(path) try: - db[table].add_foreign_key(column, other_table, other_column) + db[table].add_foreign_key(column, other_table, other_column, ignore=ignore) except AlterError as e: raise click.ClickException(e) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 7a3e4d6..ebffabf 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -781,7 +781,9 @@ class Table(Queryable): else: return pks[0].name - def add_foreign_key(self, column, other_table=None, other_column=None): + def add_foreign_key( + self, column, other_table=None, other_column=None, ignore=False + ): # Ensure column exists if column not in self.columns_dict: raise AlterError("No such column: {}".format(column)) @@ -806,12 +808,16 @@ class Table(Queryable): and fk.other_table == other_table and fk.other_column == other_column ): - raise AlterError( - "Foreign key already exists for {} => {}.{}".format( - column, other_table, other_column + if ignore: + return self + else: + raise AlterError( + "Foreign key already exists for {} => {}.{}".format( + column, other_table, other_column + ) ) - ) self.db.add_foreign_keys([(self.name, column, other_table, other_column)]) + return self def enable_fts( self, diff --git a/tests/test_cli.py b/tests/test_cli.py index dba182b..7d8ccf4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -272,16 +272,24 @@ def test_add_foreign_key(db_path, args, assert_message): table="books", column="author_id", other_table="authors", other_column="id" ) ] == db["books"].foreign_keys + # Error if we try to add it twice: result = CliRunner().invoke( cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "id"] ) - assert 0 != result.exit_code assert ( "Error: Foreign key already exists for author_id => authors.id" == result.output.strip() ) + + # No error if we add it twice with --ignore + result = CliRunner().invoke( + cli.cli, + ["add-foreign-key", db_path, "books", "author_id", "authors", "id", "--ignore"], + ) + assert 0 == result.exit_code + # Error if we try against an invalid column result = CliRunner().invoke( cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "bad"] diff --git a/tests/test_create.py b/tests/test_create.py index 1dfc541..9e30d2b 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -5,6 +5,7 @@ from sqlite_utils.db import ( AlterError, NoObviousTable, ForeignKey, + Table, ) from sqlite_utils.utils import sqlite3 import collections @@ -348,7 +349,9 @@ def test_add_foreign_key(fresh_db): ] ) assert [] == fresh_db["books"].foreign_keys - fresh_db["books"].add_foreign_key("author_id", "authors", "id") + t = fresh_db["books"].add_foreign_key("author_id", "authors", "id") + # Ensure it returned self: + assert isinstance(t, Table) and t.name == "books" assert [ ForeignKey( table="books", column="author_id", other_table="authors", other_column="id" @@ -379,6 +382,13 @@ def test_add_foreign_key_error_if_already_exists(fresh_db): assert "Foreign key already exists for author_id => authors.id" == ex.value.args[0] +def test_add_foreign_key_no_error_if_exists_and_ignore_true(fresh_db): + fresh_db["books"].insert({"title": "Hedgehogs of the world", "author_id": 1}) + fresh_db["authors"].insert({"id": 1, "name": "Sally"}, pk="id") + fresh_db["books"].add_foreign_key("author_id", "authors", "id") + fresh_db["books"].add_foreign_key("author_id", "authors", "id", ignore=True) + + def test_add_foreign_keys(fresh_db): fresh_db["authors"].insert_all( [{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id"