diff --git a/docs/python-api.rst b/docs/python-api.rst index 1ffc94d..d3d05d3 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -125,6 +125,43 @@ After inserting a row like this, the ``dogs.last_rowid`` property will return th The ``dogs.last_pk`` property will return the last inserted primary key value, if you specified one. This can be very useful when writing code that creates foreign key or many-to-many relationships. +Explicitly creating a table +--------------------------- + +You can directly create a new table without inserting any data into it using the ``.create()`` method:: + + db["cats"].create({ + "id": int, + "name": str, + "weight": float, + }, pk="id") + +The first argument here is a dictionary specifying the columns you would like to create. Each column is paired with a Python type indicating the type of column. See :ref:`python_api_add_column` for full details on how these types work. + +This method takes optional arguments ``pk=``, ``column_order=`` and ``foreign_keys=``. + +Specifying foreign keys +----------------------- + +Any operation that can create a table (``.create()``, ``.insert()``, ``.insert_all()``, ``.upsert()`` and ``.upsert_all()``) accepts an optional ``foreign_keys=`` argument which can be used to set up foreign key constraints for the table that is being created. + +If you are using your database with `Datasette `__, Datasette will detect these constraints and use them to generate hyperlinks to associated records. + +The ``foreign_keys`` argument takes a sequence of three-tuples, each one specifying the column, other table and other column that should be used to create the relationship. For example: + +.. code-block:: python + + 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}, + ], foreign_keys=[ + ("author_id", "authors", "id") + ]) + Bulk inserts ============ diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index c30de6f..725ba53 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -109,6 +109,14 @@ class Database: if hash_id: column_items.insert(0, (hash_id, str)) pk = hash_id + # Sanity check foreign_keys point to existing tables + for _, fk_other_table, fk_other_column in foreign_keys: + if not any( + c for c in self[fk_other_table].columns if c.name == fk_other_column + ): + raise AlterError( + "No such column: {}.{}".format(fk_other_table, fk_other_column) + ) extra = "" columns_sql = ",\n".join( " [{col_name}] {col_type}{primary_key}{references}".format( @@ -117,8 +125,8 @@ class Database: primary_key=" PRIMARY KEY" if (pk == col_name) else "", references=( " REFERENCES [{other_table}]([{other_column}])".format( - other_table=foreign_keys_by_name[col_name][2], - other_column=foreign_keys_by_name[col_name][3], + other_table=foreign_keys_by_name[col_name][1], + other_column=foreign_keys_by_name[col_name][2], ) if col_name in foreign_keys_by_name else "" diff --git a/tests/test_create.py b/tests/test_create.py index 4ccfcc0..7aaae5d 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -96,10 +96,7 @@ def test_create_table_works_for_m2m_with_only_foreign_keys(fresh_db): fresh_db["two"].insert({"id": 1}, pk="id") fresh_db["m2m"].insert( {"one_id": 1, "two_id": 1}, - foreign_keys=( - ("one_id", "INTEGER", "one", "id"), - ("two_id", "INTEGER", "two", "id"), - ), + foreign_keys=(("one_id", "one", "id"), ("two_id", "two", "id")), ) assert [ {"name": "one_id", "type": "INTEGER"}, @@ -124,6 +121,15 @@ def test_create_table_works_for_m2m_with_only_foreign_keys(fresh_db): ) +def test_create_error_if_invalid_foreign_keys(fresh_db): + with pytest.raises(AlterError): + fresh_db["one"].insert( + {"id": 1, "ref_id": 3}, + pk="id", + foreign_keys=(("ref_id", "bad_table", "bad_column"),), + ) + + @pytest.mark.parametrize( "col_name,col_type,expected_schema", (