Expose foreign key data to alter table UI

Include current foreign key metadata in the alter table page data and allow the foreign-key-targets endpoint to be read by actors with alter-table permission for a specific table.

Add API and HTML data tests for the new alter-table foreign key support.
This commit is contained in:
Simon Willison 2026-06-22 12:47:02 -07:00
commit 063b04ad83
4 changed files with 46 additions and 2 deletions

View file

@ -382,6 +382,19 @@ async def _table_alter_ui(
return None
column_types_map = await datasette.get_column_types(database_name, table_name)
foreign_keys_by_column = {}
for fk in await db.foreign_keys_for_table(table_name):
other_column = fk["other_column"]
if other_column is None and await db.table_exists(fk["other_table"]):
other_pks = await db.primary_keys(fk["other_table"])
if len(other_pks) == 1:
other_column = other_pks[0]
if other_column is None:
continue
foreign_keys_by_column[fk["column"]] = {
"fk_table": fk["other_table"],
"fk_column": other_column,
}
columns = []
for column in await db.table_column_details(table_name):
if column.hidden:
@ -397,6 +410,7 @@ async def _table_alter_ui(
"default": column.default_value,
"has_default": column.default_value is not None,
"is_pk": column.name in pks,
"foreign_key": foreign_keys_by_column.get(column.name),
"column_type": (
{"type": column_type.name, "config": column_type.config}
if column_type is not None
@ -412,6 +426,10 @@ async def _table_alter_ui(
"primaryKeys": pks,
"columnTypes": ALTER_TABLE_COLUMN_TYPES,
"defaultExpressions": list(DEFAULT_EXPR_SQL),
"foreignKeyTargetsPath": "{}/-/foreign-key-targets?table={}".format(
datasette.urls.database(database_name),
urllib.parse.quote(table_name, safe=""),
),
}
can_set_column_type = await datasette.allowed(
action="set-column-type",

View file

@ -867,11 +867,20 @@ class DatabaseForeignKeyTargetsView(BaseView):
db = await self.ds.resolve_database(request)
database_name = db.name
if not await self.ds.allowed(
table_name = request.args.get("table")
can_create_table = await self.ds.allowed(
action="create-table",
resource=DatabaseResource(database=database_name),
actor=request.actor,
):
)
can_alter_table = False
if table_name and await db.table_exists(table_name):
can_alter_table = await self.ds.allowed(
action="alter-table",
resource=TableResource(database=database_name, table=table_name),
actor=request.actor,
)
if not (can_create_table or can_alter_table):
return _error(["Permission denied: need create-table"], 403)
hidden_tables = await db.execute_fn(

View file

@ -1252,6 +1252,17 @@ async def test_foreign_key_targets_permission_denied(ds_write):
}
@pytest.mark.asyncio
async def test_foreign_key_targets_allowed_for_alter_table(ds_write):
token = write_token(ds_write, permissions=["at"])
response = await ds_write.client.get(
"/data/-/foreign-key-targets?table=docs",
headers=_headers(token),
)
assert response.status_code == 200, response.text
assert response.json()["ok"] is True
@pytest.mark.asyncio
async def test_alter_table_permission_denied(ds_write):
token = write_token(ds_write, permissions=["ir"])

View file

@ -1137,6 +1137,9 @@ async def test_table_alter_action_button_and_data():
assert alter_data["tableName"] == "items"
assert alter_data["primaryKeys"] == ["id"]
assert alter_data["columnTypes"] == ["text", "integer", "float", "blob"]
assert alter_data["foreignKeyTargetsPath"] == (
"/data/-/foreign-key-targets?table=items"
)
assert alter_data["defaultExpressions"] == [
"current_timestamp",
"current_date",
@ -1158,6 +1161,7 @@ async def test_table_alter_action_button_and_data():
"default": None,
"has_default": False,
"is_pk": True,
"foreign_key": None,
"column_type": None,
},
{
@ -1168,6 +1172,7 @@ async def test_table_alter_action_button_and_data():
"default": None,
"has_default": False,
"is_pk": False,
"foreign_key": None,
"column_type": {"type": "textarea", "config": None},
},
{
@ -1178,6 +1183,7 @@ async def test_table_alter_action_button_and_data():
"default": "5",
"has_default": True,
"is_pk": False,
"foreign_key": None,
"column_type": None,
},
]