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
|
@hookimpl
|
||||||
def register_permissions():
|
def register_permissions():
|
||||||
return (
|
return (
|
||||||
# name, abbr, description, takes_database, takes_resource, default
|
|
||||||
Permission(
|
Permission(
|
||||||
name="view-instance",
|
name="view-instance",
|
||||||
abbr="vi",
|
abbr="vi",
|
||||||
|
@ -109,6 +108,14 @@ def register_permissions():
|
||||||
takes_resource=False,
|
takes_resource=False,
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
|
Permission(
|
||||||
|
name="alter-table",
|
||||||
|
abbr="at",
|
||||||
|
description="Alter tables",
|
||||||
|
takes_database=True,
|
||||||
|
takes_resource=True,
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
Permission(
|
Permission(
|
||||||
name="drop-table",
|
name="drop-table",
|
||||||
abbr="dt",
|
abbr="dt",
|
||||||
|
@ -129,6 +136,7 @@ def permission_allowed_default(datasette, actor, action, resource):
|
||||||
"debug-menu",
|
"debug-menu",
|
||||||
"insert-row",
|
"insert-row",
|
||||||
"create-table",
|
"create-table",
|
||||||
|
"alter-table",
|
||||||
"drop-table",
|
"drop-table",
|
||||||
"delete-row",
|
"delete-row",
|
||||||
"update-row",
|
"update-row",
|
||||||
|
|
|
@ -108,6 +108,30 @@ class DropTableEvent(Event):
|
||||||
table: str
|
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
|
@dataclass
|
||||||
class InsertRowsEvent(Event):
|
class InsertRowsEvent(Event):
|
||||||
"""
|
"""
|
||||||
|
@ -203,6 +227,7 @@ def register_events():
|
||||||
LogoutEvent,
|
LogoutEvent,
|
||||||
CreateTableEvent,
|
CreateTableEvent,
|
||||||
CreateTokenEvent,
|
CreateTokenEvent,
|
||||||
|
AlterTableEvent,
|
||||||
DropTableEvent,
|
DropTableEvent,
|
||||||
InsertRowsEvent,
|
InsertRowsEvent,
|
||||||
UpsertRowsEvent,
|
UpsertRowsEvent,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import re
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from datasette.events import CreateTableEvent
|
from datasette.events import AlterTableEvent, CreateTableEvent
|
||||||
from datasette.database import QueryInterrupted
|
from datasette.database import QueryInterrupted
|
||||||
from datasette.utils import (
|
from datasette.utils import (
|
||||||
add_cors_headers,
|
add_cors_headers,
|
||||||
|
@ -792,7 +792,17 @@ class MagicParameters(dict):
|
||||||
class TableCreateView(BaseView):
|
class TableCreateView(BaseView):
|
||||||
name = "table-create"
|
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 = {
|
_supported_column_types = {
|
||||||
"text",
|
"text",
|
||||||
"integer",
|
"integer",
|
||||||
|
@ -876,6 +886,20 @@ class TableCreateView(BaseView):
|
||||||
):
|
):
|
||||||
return _error(["Permission denied - need insert-row"], 403)
|
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 columns:
|
||||||
if rows or row:
|
if rows or row:
|
||||||
return _error(["Cannot specify columns with 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"])
|
return _error(["pk cannot be changed for existing table"])
|
||||||
pks = actual_pks
|
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):
|
def create_table(conn):
|
||||||
table = sqlite_utils.Database(conn)[table_name]
|
table = sqlite_utils.Database(conn)[table_name]
|
||||||
if rows:
|
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:
|
else:
|
||||||
table.create(
|
table.create(
|
||||||
{c["name"]: c["type"] for c in columns},
|
{c["name"]: c["type"] for c in columns},
|
||||||
|
@ -954,6 +986,18 @@ class TableCreateView(BaseView):
|
||||||
schema = await db.execute_write_fn(create_table)
|
schema = await db.execute_write_fn(create_table)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _error([str(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(
|
table_url = self.ds.absolute_url(
|
||||||
request, self.ds.urls.table(db.name, table_name)
|
request, self.ds.urls.table(db.name, table_name)
|
||||||
)
|
)
|
||||||
|
@ -970,6 +1014,9 @@ class TableCreateView(BaseView):
|
||||||
}
|
}
|
||||||
if rows:
|
if rows:
|
||||||
details["row_count"] = len(rows)
|
details["row_count"] = len(rows)
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
# Only log creation if we created a table
|
||||||
await self.ds.track_event(
|
await self.ds.track_event(
|
||||||
CreateTableEvent(
|
CreateTableEvent(
|
||||||
request.actor, database=db.name, table=table_name, schema=schema
|
request.actor, database=db.name, table=table_name, schema=schema
|
||||||
|
|
|
@ -1217,6 +1217,18 @@ Actor is allowed to create a database table.
|
||||||
|
|
||||||
Default *deny*.
|
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:
|
.. _permissions_drop_table:
|
||||||
|
|
||||||
drop-table
|
drop-table
|
||||||
|
|
|
@ -1349,3 +1349,77 @@ async def test_method_not_allowed(ds_write, path):
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"error": "Method not allowed",
|
"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