alter table support for /db/-/create API, refs #2101

pull/2266/head
Simon Willison 2024-02-08 12:21:13 -08:00
rodzic 569aacd39b
commit 900d15bcb8
5 zmienionych plików z 174 dodań i 8 usunięć

Wyświetl plik

@ -8,7 +8,6 @@ from typing import Union, Tuple
@hookimpl
def register_permissions():
return (
# name, abbr, description, takes_database, takes_resource, default
Permission(
name="view-instance",
abbr="vi",
@ -109,6 +108,14 @@ def register_permissions():
takes_resource=False,
default=False,
),
Permission(
name="alter-table",
abbr="at",
description="Alter tables",
takes_database=True,
takes_resource=True,
default=False,
),
Permission(
name="drop-table",
abbr="dt",
@ -129,6 +136,7 @@ def permission_allowed_default(datasette, actor, action, resource):
"debug-menu",
"insert-row",
"create-table",
"alter-table",
"drop-table",
"delete-row",
"update-row",

Wyświetl plik

@ -108,6 +108,30 @@ class DropTableEvent(Event):
table: str
@dataclass
class AlterTableEvent(Event):
"""
Event name: ``alter-table``
A table has been altered.
:ivar database: The name of the database where the table was altered
:type database: str
:ivar table: The name of the table that was altered
:type table: str
:ivar before_schema: The table's SQL schema before the alteration
:type before_schema: str
:ivar after_schema: The table's SQL schema after the alteration
:type after_schema: str
"""
name = "alter-table"
database: str
table: str
before_schema: str
after_schema: str
@dataclass
class InsertRowsEvent(Event):
"""
@ -203,6 +227,7 @@ def register_events():
LogoutEvent,
CreateTableEvent,
CreateTokenEvent,
AlterTableEvent,
DropTableEvent,
InsertRowsEvent,
UpsertRowsEvent,

Wyświetl plik

@ -10,7 +10,7 @@ import re
import sqlite_utils
import textwrap
from datasette.events import CreateTableEvent
from datasette.events import AlterTableEvent, CreateTableEvent
from datasette.database import QueryInterrupted
from datasette.utils import (
add_cors_headers,
@ -792,7 +792,17 @@ class MagicParameters(dict):
class TableCreateView(BaseView):
name = "table-create"
_valid_keys = {"table", "rows", "row", "columns", "pk", "pks", "ignore", "replace"}
_valid_keys = {
"table",
"rows",
"row",
"columns",
"pk",
"pks",
"ignore",
"replace",
"alter",
}
_supported_column_types = {
"text",
"integer",
@ -876,6 +886,20 @@ class TableCreateView(BaseView):
):
return _error(["Permission denied - need insert-row"], 403)
alter = False
if rows or row:
if not table_exists:
# if table is being created for the first time, alter=True
alter = True
else:
# alter=True only if they request it AND they have permission
if data.get("alter"):
if not await self.ds.permission_allowed(
request.actor, "alter-table", resource=database_name
):
return _error(["Permission denied - need alter-table"], 403)
alter = True
if columns:
if rows or row:
return _error(["Cannot specify columns with rows or row"])
@ -939,10 +963,18 @@ class TableCreateView(BaseView):
return _error(["pk cannot be changed for existing table"])
pks = actual_pks
initial_schema = None
if table_exists:
initial_schema = await db.execute_fn(
lambda conn: sqlite_utils.Database(conn)[table_name].schema
)
def create_table(conn):
table = sqlite_utils.Database(conn)[table_name]
if rows:
table.insert_all(rows, pk=pks or pk, ignore=ignore, replace=replace)
table.insert_all(
rows, pk=pks or pk, ignore=ignore, replace=replace, alter=alter
)
else:
table.create(
{c["name"]: c["type"] for c in columns},
@ -954,6 +986,18 @@ class TableCreateView(BaseView):
schema = await db.execute_write_fn(create_table)
except Exception as e:
return _error([str(e)])
if initial_schema is not None and initial_schema != schema:
await self.ds.track_event(
AlterTableEvent(
request.actor,
database=database_name,
table=table_name,
before_schema=initial_schema,
after_schema=schema,
)
)
table_url = self.ds.absolute_url(
request, self.ds.urls.table(db.name, table_name)
)
@ -970,11 +1014,14 @@ class TableCreateView(BaseView):
}
if rows:
details["row_count"] = len(rows)
await self.ds.track_event(
CreateTableEvent(
request.actor, database=db.name, table=table_name, schema=schema
if not table_exists:
# Only log creation if we created a table
await self.ds.track_event(
CreateTableEvent(
request.actor, database=db.name, table=table_name, schema=schema
)
)
)
return Response.json(details, status=201)

Wyświetl plik

@ -1217,6 +1217,18 @@ Actor is allowed to create a database table.
Default *deny*.
.. _permissions_alter_table:
alter-table
-----------
Actor is allowed to alter a database table.
``resource`` - tuple: (string, string)
The name of the database, then the name of the table
Default *deny*.
.. _permissions_drop_table:
drop-table

Wyświetl plik

@ -1349,3 +1349,77 @@ async def test_method_not_allowed(ds_write, path):
"ok": False,
"error": "Method not allowed",
}
@pytest.mark.asyncio
async def test_create_uses_alter_by_default_for_new_table(ds_write):
token = write_token(ds_write)
response = await ds_write.client.post(
"/data/-/create",
json={
"table": "new_table",
"rows": [
{
"name": "Row 1",
}
]
* 100
+ [
{"name": "Row 2", "extra": "Extra"},
],
"pk": "id",
},
headers=_headers(token),
)
assert response.status_code == 201
event = last_event(ds_write)
assert event.name == "create-table"
@pytest.mark.asyncio
@pytest.mark.parametrize("has_alter_permission", (True,)) # False))
async def test_create_using_alter_against_existing_table(
ds_write, has_alter_permission
):
token = write_token(
ds_write, permissions=["ir", "ct"] + (["at"] if has_alter_permission else [])
)
# First create the table
response = await ds_write.client.post(
"/data/-/create",
json={
"table": "new_table",
"rows": [
{
"name": "Row 1",
}
],
"pk": "id",
},
headers=_headers(token),
)
assert response.status_code == 201
# Now try to insert more rows using /-/create with alter=True
response2 = await ds_write.client.post(
"/data/-/create",
json={
"table": "new_table",
"rows": [{"name": "Row 2", "extra": "extra"}],
"pk": "id",
"alter": True,
},
headers=_headers(token),
)
if not has_alter_permission:
assert response2.status_code == 403
assert response2.json() == {
"ok": False,
"errors": ["Permission denied - need alter-table"],
}
else:
assert response2.status_code == 201
# It should have altered the table
event = last_event(ds_write)
assert event.name == "alter-table"
assert "extra" not in event.before_schema
assert "extra" in event.after_schema