mirror of
https://github.com/simonw/datasette.git
synced 2026-06-23 09:14:34 +02:00
Alter table API can now rename tables, refs #2788
Refs https://github.com/simonw/datasette/pull/2789#issuecomment-4771774289
This commit is contained in:
parent
87354cf94e
commit
4b219be8bd
3 changed files with 110 additions and 13 deletions
|
|
@ -476,6 +476,20 @@ class RenameColumnArgs(_StrictPydanticModel):
|
|||
to: str
|
||||
|
||||
|
||||
class RenameTableArgs(_StrictPydanticModel):
|
||||
to: str
|
||||
|
||||
@field_validator("to")
|
||||
@classmethod
|
||||
def validate_table_name(cls, v):
|
||||
if not TABLE_NAME_RE.match(v):
|
||||
raise PydanticCustomError(
|
||||
"alter_table_rename_table",
|
||||
"Invalid table name",
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class AlterColumnArgs(_DefaultArgsMixin):
|
||||
name: str
|
||||
type: SqliteApiType | None = None
|
||||
|
|
@ -548,6 +562,11 @@ class RenameColumnOperation(_StrictPydanticModel):
|
|||
args: RenameColumnArgs
|
||||
|
||||
|
||||
class RenameTableOperation(_StrictPydanticModel):
|
||||
op: Literal["rename_table"]
|
||||
args: RenameTableArgs
|
||||
|
||||
|
||||
class AlterColumnOperation(_StrictPydanticModel):
|
||||
op: Literal["alter_column"]
|
||||
args: AlterColumnArgs
|
||||
|
|
@ -587,6 +606,7 @@ AlterTableOperation = Annotated[
|
|||
Union[
|
||||
AddColumnOperation,
|
||||
RenameColumnOperation,
|
||||
RenameTableOperation,
|
||||
AlterColumnOperation,
|
||||
DropColumnOperation,
|
||||
SetPrimaryKeyOperation,
|
||||
|
|
@ -1042,10 +1062,12 @@ class TableAlterView(BaseView):
|
|||
def apply_operations(operation_conn):
|
||||
db_for_write = sqlite_utils.Database(operation_conn)
|
||||
table = db_for_write[table_name]
|
||||
current_table_name = table_name
|
||||
|
||||
add_columns = []
|
||||
types = {}
|
||||
rename = {}
|
||||
rename_table_to = None
|
||||
drop = set()
|
||||
not_null = {}
|
||||
defaults = {}
|
||||
|
|
@ -1080,6 +1102,8 @@ class TableAlterView(BaseView):
|
|||
defaults[args.name] = _default_expression_sql(
|
||||
args.default_expr
|
||||
)
|
||||
elif operation.op == "rename_table":
|
||||
rename_table_to = args.to
|
||||
elif operation.op == "rename_column":
|
||||
rename[args.name] = args.to
|
||||
elif operation.op == "alter_column":
|
||||
|
|
@ -1155,14 +1179,27 @@ class TableAlterView(BaseView):
|
|||
drop_foreign_keys=drop_foreign_keys or None,
|
||||
foreign_keys=foreign_keys,
|
||||
)
|
||||
if (
|
||||
rename_table_to is not None
|
||||
and rename_table_to != current_table_name
|
||||
):
|
||||
operation_conn.execute(
|
||||
"alter table {} rename to {}".format(
|
||||
escape_sqlite(current_table_name),
|
||||
escape_sqlite(rename_table_to),
|
||||
)
|
||||
)
|
||||
current_table_name = rename_table_to
|
||||
|
||||
return _table_schema_from_conn(operation_conn, table_name)
|
||||
return current_table_name, _table_schema_from_conn(
|
||||
operation_conn, current_table_name
|
||||
)
|
||||
|
||||
after_schema = apply_operations(conn)
|
||||
return before_schema, after_schema
|
||||
after_table_name, after_schema = apply_operations(conn)
|
||||
return before_schema, after_schema, after_table_name
|
||||
|
||||
try:
|
||||
before_schema, after_schema = await db.execute_write_fn(
|
||||
before_schema, after_schema, after_table_name = await db.execute_write_fn(
|
||||
alter_table, request=request
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -1174,23 +1211,23 @@ class TableAlterView(BaseView):
|
|||
AlterTableEvent(
|
||||
request.actor,
|
||||
database=database_name,
|
||||
table=table_name,
|
||||
table=after_table_name,
|
||||
before_schema=before_schema,
|
||||
after_schema=after_schema,
|
||||
)
|
||||
)
|
||||
|
||||
table_url = self.ds.absolute_url(
|
||||
request, self.ds.urls.table(database_name, table_name)
|
||||
request, self.ds.urls.table(database_name, after_table_name)
|
||||
)
|
||||
table_api_url = self.ds.absolute_url(
|
||||
request, self.ds.urls.table(database_name, table_name, format="json")
|
||||
request, self.ds.urls.table(database_name, after_table_name, format="json")
|
||||
)
|
||||
return Response.json(
|
||||
{
|
||||
"ok": True,
|
||||
"database": database_name,
|
||||
"table": table_name,
|
||||
"table": after_table_name,
|
||||
"table_url": table_url,
|
||||
"table_api_url": table_api_url,
|
||||
"altered": altered,
|
||||
|
|
|
|||
|
|
@ -2242,6 +2242,12 @@ The request body should include an ``operations`` array. Each operation has the
|
|||
"to": "headline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "rename_table",
|
||||
"args": {
|
||||
"to": "published_posts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "alter_column",
|
||||
"args": {
|
||||
|
|
@ -2299,6 +2305,7 @@ Supported operations:
|
|||
|
||||
* ``add_column`` adds a new column. ``args`` accepts ``name``, optional ``type`` of ``text``, ``integer``, ``float`` or ``blob``, optional ``not_null``, optional literal ``default`` and optional ``default_expr``. If ``not_null`` is ``true`` either a non-null ``default`` or ``default_expr`` is required.
|
||||
* ``rename_column`` renames a column. ``args`` accepts ``name`` and ``to``.
|
||||
* ``rename_table`` renames the table. ``args`` accepts ``to``, the new table name. If combined with other operations, Datasette applies the column, primary key, foreign key and column order changes before renaming the table.
|
||||
* ``alter_column`` changes column properties. ``args`` accepts ``name`` and at least one of ``type``, ``not_null``, literal ``default`` or ``default_expr``. Passing ``"default": null`` removes an existing default.
|
||||
* ``drop_column`` drops a column. ``args`` accepts ``name``.
|
||||
* ``set_primary_key`` changes the table primary key. ``args`` accepts ``columns``, a list of one or more column names.
|
||||
|
|
@ -2311,20 +2318,20 @@ Supported operations:
|
|||
|
||||
For foreign key operations that omit ``fk_column``, the referenced ``fk_table`` must have a single-column primary key. Datasette will return an error if it cannot identify a single primary key column for that table.
|
||||
|
||||
A successful response returns the new schema and the previous schema:
|
||||
A successful response returns the new schema and the previous schema. If the request used ``rename_table``, ``table``, ``table_url`` and ``table_api_url`` will use the new table name. Renaming a table through this endpoint triggers the :class:`~datasette.events.RenameTableEvent` event.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"database": "data",
|
||||
"table": "posts",
|
||||
"table_url": "http://127.0.0.1:8001/data/posts",
|
||||
"table_api_url": "http://127.0.0.1:8001/data/posts.json",
|
||||
"table": "published_posts",
|
||||
"table_url": "http://127.0.0.1:8001/data/published_posts",
|
||||
"table_api_url": "http://127.0.0.1:8001/data/published_posts.json",
|
||||
"altered": true,
|
||||
"schema": "CREATE TABLE ...",
|
||||
"before_schema": "CREATE TABLE ...",
|
||||
"operations_applied": 7
|
||||
"operations_applied": 11
|
||||
}
|
||||
|
||||
Any errors will return ``{"errors": ["... descriptive message ..."], "ok": false}``, and a ``400`` status code for a bad input or a ``403`` status code for an authentication or permission error.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from datasette.app import Datasette
|
||||
from datasette.events import RenameTableEvent
|
||||
from datasette.utils import sqlite3
|
||||
from .utils import last_event
|
||||
import pytest
|
||||
|
|
@ -898,6 +899,58 @@ async def test_alter_table_operations(ds_write):
|
|||
assert event.after_schema == data["schema"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alter_table_rename_table(ds_write):
|
||||
token = write_token(ds_write, permissions=["at"])
|
||||
db = ds_write.get_database("data")
|
||||
before_schema = await db.execute_fn(
|
||||
lambda conn: conn.execute(
|
||||
"select sql from sqlite_master where type = 'table' and name = 'docs'"
|
||||
).fetchone()[0]
|
||||
)
|
||||
|
||||
response = await ds_write.client.post(
|
||||
"/data/docs/-/alter",
|
||||
json={
|
||||
"operations": [
|
||||
{"op": "rename_table", "args": {"to": "documents"}},
|
||||
]
|
||||
},
|
||||
headers=_headers(token),
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
assert data["database"] == "data"
|
||||
assert data["table"] == "documents"
|
||||
assert data["table_url"].endswith("/data/documents")
|
||||
assert data["table_api_url"].endswith("/data/documents.json")
|
||||
assert data["altered"] is True
|
||||
assert data["operations_applied"] == 1
|
||||
assert data["before_schema"] == before_schema
|
||||
assert 'CREATE TABLE "documents"' in data["schema"]
|
||||
|
||||
tables = (
|
||||
await db.execute(
|
||||
"select name from sqlite_master where type = 'table' order by name"
|
||||
)
|
||||
).dicts()
|
||||
table_names = [table["name"] for table in tables]
|
||||
assert "docs" not in table_names
|
||||
assert "documents" in table_names
|
||||
|
||||
rename_events = [
|
||||
event
|
||||
for event in ds_write._tracked_events
|
||||
if isinstance(event, RenameTableEvent)
|
||||
]
|
||||
assert len(rename_events) == 1
|
||||
assert rename_events[0].database == "data"
|
||||
assert rename_events[0].old_table == "docs"
|
||||
assert rename_events[0].new_table == "documents"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alter_table_foreign_key_operations(ds_write):
|
||||
token = write_token(ds_write, permissions=["at"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue