Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show referring tables and rows when the referring foreign key is compound #2003

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,8 +899,11 @@ async def expand_foreign_keys(self, database, table, column, values):
fk = [
foreign_key
for foreign_key in foreign_keys
if foreign_key["column"] == column
if foreign_key["columns"][0] == column
Copy link
Contributor Author

@fgregg fgregg Jan 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because we don't have a strong idea of how to display compound foreign keys in a table view, we have to do this operation in a few places.

and len(foreign_key["columns"]) == 1
][0]
fk["column"] = fk["columns"][0]
fk["other_column"] = fk["other_columns"][0]
except IndexError:
return {}
label_column = await db.label_column_for_table(fk["other_table"])
Expand Down
6 changes: 5 additions & 1 deletion datasette/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ async def inner():
outgoing_foreign_keys = await db.foreign_keys_for_table(through_table)
try:
fk_to_us = [
fk for fk in outgoing_foreign_keys if fk["other_table"] == table
fk
for fk in outgoing_foreign_keys
if fk["other_table"] == table and len(fk["other_columns"]) == 1
][0]
fk_to_us["column"] = fk_to_us["columns"][0]
fk_to_us["other_column"] = fk_to_us["other_columns"][0]
except IndexError:
raise DatasetteError(
"Invalid _through - could not find corresponding foreign key"
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h2>Links from other tables</h2>
<li>
<a href="{{ other.link }}">
{{ "{:,}".format(other.count) }} row{% if other.count == 1 %}{% else %}s{% endif %}</a>
from {{ other.other_column }} in {{ other.other_table }}
from {{ other.other_columns_reference }} in {{ other.other_table }}
</li>
{% endfor %}
</ul>
Expand Down
37 changes: 14 additions & 23 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,30 +523,21 @@ def detect_primary_keys(conn, table):

def get_outbound_foreign_keys(conn, table):
infos = conn.execute(f"PRAGMA foreign_key_list([{table}])").fetchall()
fks = []
fks = {}
for info in infos:
if info is not None:
id, seq, table_name, from_, to_, on_update, on_delete, match = info
fks.append(
{
"column": from_,
if id in fks:
fk_info = fks[id]
fk_info["columns"] += (from_,)
fk_info["other_columns"] += (to_,)
else:
fks[id] = {
"other_table": table_name,
"other_column": to_,
"id": id,
"seq": seq,
"columns": (from_,),
"other_columns": (to_,),
}
)
# Filter out compound foreign keys by removing any where "id" is not unique
id_counts = Counter(fk["id"] for fk in fks)
return [
{
"column": fk["column"],
"other_table": fk["other_table"],
"other_column": fk["other_column"],
}
for fk in fks
if id_counts[fk["id"]] == 1
]
return list(fks.values())


def get_all_foreign_keys(conn):
Expand All @@ -560,17 +551,17 @@ def get_all_foreign_keys(conn):
fks = get_outbound_foreign_keys(conn, table)
for fk in fks:
table_name = fk["other_table"]
from_ = fk["column"]
to_ = fk["other_column"]
from_ = fk["columns"]
to_ = fk["other_columns"]
if table_name not in table_to_foreign_keys:
# Weird edge case where something refers to a table that does
# not actually exist
continue
table_to_foreign_keys[table_name]["incoming"].append(
{"other_table": table, "column": to_, "other_column": from_}
{"other_table": table, "columns": to_, "other_columns": from_}
)
table_to_foreign_keys[table]["outgoing"].append(
{"other_table": table_name, "column": from_, "other_column": to_}
{"other_table": table_name, "columns": from_, "other_columns": to_}
)

return table_to_foreign_keys
Expand Down
48 changes: 33 additions & 15 deletions datasette/views/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ async def template_data():
)

async def foreign_key_tables(self, database, table, pk_values):
if len(pk_values) != 1:
return []
db = self.ds.databases[database]
all_foreign_keys = await db.get_all_foreign_keys()
foreign_keys = all_foreign_keys[table]["incoming"]
Expand All @@ -107,39 +105,59 @@ async def foreign_key_tables(self, database, table, pk_values):

sql = "select " + ", ".join(
[
"(select count(*) from {table} where {column}=:id)".format(
"(select count(*) from {table} where {condition})".format(
table=escape_sqlite(fk["other_table"]),
column=escape_sqlite(fk["other_column"]),
condition=" and ".join(
"{column}=:id{i}".format(column=escape_sqlite(column), i=i)
for i, column in enumerate(fk["other_columns"])
),
)
for fk in foreign_keys
]
)
try:
rows = list(await db.execute(sql, {"id": pk_values[0]}))
rows = list(
await db.execute(
sql, {"id{i}".format(i=i): pk for i, pk in enumerate(pk_values)}
)
)
except QueryInterrupted:
# Almost certainly hit the timeout
return []

foreign_table_counts = dict(
zip(
[(fk["other_table"], fk["other_column"]) for fk in foreign_keys],
[(fk["other_table"], fk["other_columns"]) for fk in foreign_keys],
list(rows[0]),
)
)
foreign_key_tables = []
for fk in foreign_keys:
count = (
foreign_table_counts.get((fk["other_table"], fk["other_column"])) or 0
foreign_table_counts.get((fk["other_table"], fk["other_columns"])) or 0
)
query_pairs = zip(fk["other_columns"], pk_values)
query = "&".join(
"{}={}".format(col + "__exact" if col.startswith("_") else col, pk)
for col, pk in query_pairs
)
link = "{}?{}".format(
self.ds.urls.table(database, fk["other_table"]), query
)
key = fk["other_column"]
if key.startswith("_"):
key += "__exact"
link = "{}?{}={}".format(
self.ds.urls.table(database, fk["other_table"]),
key,
",".join(pk_values),
if len(pk_values) == 1:
other_columns_reference = fk["other_columns"][0]
else:
other_columns_reference = "({})".format(", ".join(fk["other_columns"]))
foreign_key_tables.append(
{
**fk,
**{
"count": count,
"link": link,
"other_columns_reference": other_columns_reference,
},
}
)
foreign_key_tables.append({**fk, **{"count": count, "link": link}})
return foreign_key_tables


Expand Down
12 changes: 10 additions & 2 deletions datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,15 @@ async def expandable_columns(self, database_name, table_name):
expandables = []
db = self.ds.databases[database_name]
for fk in await db.foreign_keys_for_table(table_name):
if len(fk["other_columns"]) > 1:
continue
label_column = await db.label_column_for_table(fk["other_table"])
expandables.append((fk, label_column))
singleton_fk = {
"other_table": fk["other_table"],
"other_column": fk["other_columns"][0],
"column": fk["columns"][0],
}
expandables.append((singleton_fk, label_column))
return expandables

async def post(self, request):
Expand Down Expand Up @@ -920,8 +927,9 @@ async def display_columns_and_rows(
)

column_to_foreign_key_table = {
fk["column"]: fk["other_table"]
fk["columns"][0]: fk["other_table"]
for fk in await db.foreign_keys_for_table(table_name)
if len(fk["columns"]) == 1
}

cell_rows = []
Expand Down
Loading