diff --git a/docs/cli.rst b/docs/cli.rst index 806b416..addf7c6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -232,6 +232,19 @@ You can add a column using the ``add-column`` command:: The last argument here is the type of the column to be created. You can use one of ``text``, ``integer``, ``float`` or ``blob``. +.. _cli_add_foreign_key: + +Adding foreign key constraints +============================== + +The ``add-foreign-key`` command can be used to add new foreign key references to an existing table - something which SQLite's ``ALTER TABLE`` command does not support. + +See :ref:`python_api_add_foreign_key` in the Python API documentation for further details and warnings (this could corrupt your database). + +To add a foreign key constraint pointing the ``books.author_id`` column to ``authors.id`` in another table, do this:: + + $ sqlite-utils add-foreign-key books.db books author_id authors id + .. _cli_create_index: Creating indexes diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 7b7501a..665aa32 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1,6 +1,7 @@ import click from click_default_group import DefaultGroup import sqlite_utils +from sqlite_utils.db import AlterError import itertools import json import sys @@ -154,6 +155,31 @@ def add_column(path, table, col_name, col_type): db[table].add_column(col_name, col_type) +@cli.command(name="add-foreign-key") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +@click.argument("column") +@click.argument("other_table") +@click.argument("other_column") +def add_foreign_key(path, table, column, other_table, other_column): + """ + Add a new foreign key constraint to an existing table. Example usage: + + $ sqlite-utils add-foreign-key my.db books author_id authors id + + WARNING: Could corrupt your database! Back up your database file first. + """ + db = sqlite_utils.Database(path) + try: + db[table].add_foreign_key(column, other_table, other_column) + except AlterError as e: + raise click.ClickException(e) + + @cli.command(name="create-index") @click.argument( "path", diff --git a/tests/test_cli.py b/tests/test_cli.py index dc8096e..7f2d17f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,5 @@ from sqlite_utils import cli, Database -from sqlite_utils.db import Index +from sqlite_utils.db import Index, ForeignKey from click.testing import CliRunner import json import os @@ -200,14 +200,46 @@ def test_add_column(db_path, col_name, col_type, expected_schema): assert expected_schema == collapse_whitespace(db["dogs"].schema) -def test_add_column_error_invalid_type(db_path): +def test_add_foreign_key(db_path): db = Database(db_path) - db.create_table("dogs", {"name": str}) + db["authors"].insert_all( + [{"id": 1, "name": "Sally"}, {"id": 2, "name": "Asheesh"}], pk="id" + ) + db["books"].insert_all( + [ + {"title": "Hedgehogs of the world", "author_id": 1}, + {"title": "How to train your wolf", "author_id": 2}, + ] + ) + assert ( + 0 + == CliRunner() + .invoke( + cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "id"] + ) + .exit_code + ) + assert [ + ForeignKey( + 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-column", db_path, "dogs", "blah", "badtype"] + 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() + ) + # Error if we try against an invalid cgolumn + result = CliRunner().invoke( + cli.cli, ["add-foreign-key", db_path, "books", "author_id", "authors", "bad"] ) assert 0 != result.exit_code - assert "invalid choice: badtype" in result.output + assert "Error: No such column: authors.bad" == result.output.strip() def test_enable_fts(db_path):