kopia lustrzana https://github.com/simonw/datasette
alter table support for /db/-/create API, refs #2101
rodzic
569aacd39b
commit
900d15bcb8
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue