ignore=True argument for add_foreign_key, closes #112

Also --ignore for add-foreign-key command

Plus table.add_foreign_key(...) now returns self, allowing more chaining
This commit is contained in:
Simon Willison 2020-09-20 15:17:25 -07:00
commit e23eedb4ce
6 changed files with 48 additions and 9 deletions

View file

@ -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 $ 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. 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: .. _cli_add_foreign_keys:

View file

@ -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. 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: .. _python_api_add_foreign_keys:
Adding multiple foreign key constraints at once Adding multiple foreign key constraints at once

View file

@ -308,7 +308,12 @@ def add_column(path, table, col_name, col_type, fk, fk_col, not_null_default):
@click.argument("column") @click.argument("column")
@click.argument("other_table", required=False) @click.argument("other_table", required=False)
@click.argument("other_column", 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: 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) db = sqlite_utils.Database(path)
try: 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: except AlterError as e:
raise click.ClickException(e) raise click.ClickException(e)

View file

@ -781,7 +781,9 @@ class Table(Queryable):
else: else:
return pks[0].name 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 # Ensure column exists
if column not in self.columns_dict: if column not in self.columns_dict:
raise AlterError("No such column: {}".format(column)) raise AlterError("No such column: {}".format(column))
@ -806,12 +808,16 @@ class Table(Queryable):
and fk.other_table == other_table and fk.other_table == other_table
and fk.other_column == other_column and fk.other_column == other_column
): ):
raise AlterError( if ignore:
"Foreign key already exists for {} => {}.{}".format( return self
column, other_table, other_column 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)]) self.db.add_foreign_keys([(self.name, column, other_table, other_column)])
return self
def enable_fts( def enable_fts(
self, self,

View file

@ -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" table="books", column="author_id", other_table="authors", other_column="id"
) )
] == db["books"].foreign_keys ] == db["books"].foreign_keys
# Error if we try to add it twice: # Error if we try to add it twice:
result = CliRunner().invoke( result = CliRunner().invoke(
cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "id"] cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "id"]
) )
assert 0 != result.exit_code assert 0 != result.exit_code
assert ( assert (
"Error: Foreign key already exists for author_id => authors.id" "Error: Foreign key already exists for author_id => authors.id"
== result.output.strip() == 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 # Error if we try against an invalid column
result = CliRunner().invoke( result = CliRunner().invoke(
cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "bad"] cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "bad"]

View file

@ -5,6 +5,7 @@ from sqlite_utils.db import (
AlterError, AlterError,
NoObviousTable, NoObviousTable,
ForeignKey, ForeignKey,
Table,
) )
from sqlite_utils.utils import sqlite3 from sqlite_utils.utils import sqlite3
import collections import collections
@ -348,7 +349,9 @@ def test_add_foreign_key(fresh_db):
] ]
) )
assert [] == fresh_db["books"].foreign_keys 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 [ assert [
ForeignKey( ForeignKey(
table="books", column="author_id", other_table="authors", other_column="id" 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] 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): def test_add_foreign_keys(fresh_db):
fresh_db["authors"].insert_all( fresh_db["authors"].insert_all(
[{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id" [{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id"