Improved foreign_keys= argument, closes #17

This commit is contained in:
Simon Willison 2019-02-24 14:12:45 -08:00
commit 557dc3f9a7
3 changed files with 57 additions and 6 deletions

View file

@ -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 <https://datasette.readthedocs.io/>`__, 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
============

View file

@ -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 ""

View file

@ -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",
(