datasette-edit-schema 0.7__py3-none-any.whl → 0.8a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of datasette-edit-schema might be problematic. Click here for more details.
- datasette_edit_schema/__init__.py +177 -41
- datasette_edit_schema/templates/edit_schema_create_table.html +6 -3
- datasette_edit_schema/templates/edit_schema_table.html +19 -11
- datasette_edit_schema/utils.py +9 -5
- {datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/METADATA +47 -6
- datasette_edit_schema-0.8a1.dist-info/RECORD +14 -0
- {datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/WHEEL +1 -1
- datasette_edit_schema-0.7.dist-info/RECORD +0 -14
- {datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/LICENSE +0 -0
- {datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/entry_points.txt +0 -0
- {datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datasette import hookimpl
|
|
2
|
+
from datasette.events import CreateTableEvent, AlterTableEvent, DropTableEvent
|
|
2
3
|
from datasette.utils.asgi import Response, NotFound, Forbidden
|
|
3
4
|
from datasette.utils import sqlite3
|
|
4
5
|
from urllib.parse import quote_plus, unquote_plus
|
|
@@ -11,6 +12,11 @@ from .utils import (
|
|
|
11
12
|
potential_primary_keys,
|
|
12
13
|
)
|
|
13
14
|
|
|
15
|
+
try:
|
|
16
|
+
from datasette import events
|
|
17
|
+
except ImportError: # Pre Datasette 1.0a8
|
|
18
|
+
events = None
|
|
19
|
+
|
|
14
20
|
# Don't attempt to detect foreign keys on tables larger than this:
|
|
15
21
|
FOREIGN_KEY_DETECTION_LIMIT = 10_000
|
|
16
22
|
|
|
@@ -29,9 +35,7 @@ def permission_allowed(actor, action, resource):
|
|
|
29
35
|
@hookimpl
|
|
30
36
|
def table_actions(datasette, actor, database, table):
|
|
31
37
|
async def inner():
|
|
32
|
-
if not await datasette
|
|
33
|
-
actor, "edit-schema", resource=database, default=False
|
|
34
|
-
):
|
|
38
|
+
if not await can_alter_table(datasette, actor, database, table):
|
|
35
39
|
return []
|
|
36
40
|
return [
|
|
37
41
|
{
|
|
@@ -39,18 +43,63 @@ def table_actions(datasette, actor, database, table):
|
|
|
39
43
|
"/-/edit-schema/{}/{}".format(database, quote_plus(table))
|
|
40
44
|
),
|
|
41
45
|
"label": "Edit table schema",
|
|
46
|
+
"description": "Rename the table, add and remove columns...",
|
|
42
47
|
}
|
|
43
48
|
]
|
|
44
49
|
|
|
45
50
|
return inner
|
|
46
51
|
|
|
47
52
|
|
|
53
|
+
async def can_create_table(datasette, actor, database):
|
|
54
|
+
if await datasette.permission_allowed(
|
|
55
|
+
actor, "edit-schema", resource=database, default=False
|
|
56
|
+
):
|
|
57
|
+
return True
|
|
58
|
+
# Or maybe they have create-table
|
|
59
|
+
if await datasette.permission_allowed(
|
|
60
|
+
actor, "create-table", resource=database, default=False
|
|
61
|
+
):
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def can_alter_table(datasette, actor, database, table):
|
|
67
|
+
if await datasette.permission_allowed(
|
|
68
|
+
actor, "edit-schema", resource=database, default=False
|
|
69
|
+
):
|
|
70
|
+
return True
|
|
71
|
+
if await datasette.permission_allowed(
|
|
72
|
+
actor, "alter-table", resource=(database, table), default=False
|
|
73
|
+
):
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def can_rename_table(datasette, actor, database, table):
|
|
79
|
+
if not await can_drop_table(datasette, actor, database, table):
|
|
80
|
+
return False
|
|
81
|
+
if not await can_create_table(datasette, actor, database):
|
|
82
|
+
return False
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def can_drop_table(datasette, actor, database, table):
|
|
87
|
+
if await datasette.permission_allowed(
|
|
88
|
+
actor, "edit-schema", resource=database, default=False
|
|
89
|
+
):
|
|
90
|
+
return True
|
|
91
|
+
# Or maybe they have drop-table
|
|
92
|
+
if await datasette.permission_allowed(
|
|
93
|
+
actor, "drop-table", resource=(database, table), default=False
|
|
94
|
+
):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
48
99
|
@hookimpl
|
|
49
100
|
def database_actions(datasette, actor, database):
|
|
50
101
|
async def inner():
|
|
51
|
-
if not await datasette
|
|
52
|
-
actor, "edit-schema", resource=database, default=False
|
|
53
|
-
):
|
|
102
|
+
if not await can_create_table(datasette, actor, database):
|
|
54
103
|
return []
|
|
55
104
|
return [
|
|
56
105
|
{
|
|
@@ -58,6 +107,7 @@ def database_actions(datasette, actor, database):
|
|
|
58
107
|
"/-/edit-schema/{}/-/create".format(database)
|
|
59
108
|
),
|
|
60
109
|
"label": "Create a table",
|
|
110
|
+
"description": "Define a new table with specified columns",
|
|
61
111
|
}
|
|
62
112
|
]
|
|
63
113
|
|
|
@@ -173,9 +223,9 @@ async def edit_schema_database(request, datasette):
|
|
|
173
223
|
|
|
174
224
|
|
|
175
225
|
async def edit_schema_create_table(request, datasette):
|
|
176
|
-
databases = get_databases(datasette)
|
|
177
226
|
database_name = request.url_vars["database"]
|
|
178
|
-
await
|
|
227
|
+
if not await can_create_table(datasette, request.actor, database_name):
|
|
228
|
+
raise Forbidden("Permission denied for create-table")
|
|
179
229
|
try:
|
|
180
230
|
db = datasette.get_database(database_name)
|
|
181
231
|
except KeyError:
|
|
@@ -210,17 +260,18 @@ async def edit_schema_create_table(request, datasette):
|
|
|
210
260
|
def create_the_table(conn):
|
|
211
261
|
db = sqlite_utils.Database(conn)
|
|
212
262
|
if not table_name.strip():
|
|
213
|
-
return "Table name is required"
|
|
263
|
+
return None, "Table name is required"
|
|
214
264
|
if db[table_name].exists():
|
|
215
|
-
return "Table already exists"
|
|
265
|
+
return None, "Table already exists"
|
|
216
266
|
try:
|
|
217
267
|
db[table_name].create(
|
|
218
268
|
create, pk=primary_key_name, not_null=(primary_key_name,)
|
|
219
269
|
)
|
|
270
|
+
return db[table_name].schema, None
|
|
220
271
|
except Exception as e:
|
|
221
|
-
return str(e)
|
|
272
|
+
return None, str(e)
|
|
222
273
|
|
|
223
|
-
error = await db.execute_write_fn(create_the_table, block=True)
|
|
274
|
+
schema, error = await db.execute_write_fn(create_the_table, block=True)
|
|
224
275
|
|
|
225
276
|
if error:
|
|
226
277
|
datasette.add_message(request, str(error), datasette.ERROR)
|
|
@@ -228,6 +279,14 @@ async def edit_schema_create_table(request, datasette):
|
|
|
228
279
|
else:
|
|
229
280
|
datasette.add_message(request, "Table has been created")
|
|
230
281
|
path = datasette.urls.table(database_name, table_name)
|
|
282
|
+
await datasette.track_event(
|
|
283
|
+
CreateTableEvent(
|
|
284
|
+
actor=request.actor,
|
|
285
|
+
database=database_name,
|
|
286
|
+
table=table_name,
|
|
287
|
+
schema=schema,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
231
290
|
|
|
232
291
|
return Response.redirect(path)
|
|
233
292
|
|
|
@@ -251,7 +310,10 @@ async def edit_schema_table(request, datasette):
|
|
|
251
310
|
table = unquote_plus(request.url_vars["table"])
|
|
252
311
|
databases = get_databases(datasette)
|
|
253
312
|
database_name = request.url_vars["database"]
|
|
254
|
-
|
|
313
|
+
|
|
314
|
+
if not await can_alter_table(datasette, request.actor, database_name, table):
|
|
315
|
+
raise Forbidden("Permission denied for alter-table")
|
|
316
|
+
|
|
255
317
|
try:
|
|
256
318
|
database = [db for db in databases if db.name == database_name][0]
|
|
257
319
|
except IndexError:
|
|
@@ -260,6 +322,29 @@ async def edit_schema_table(request, datasette):
|
|
|
260
322
|
raise NotFound("Table not found")
|
|
261
323
|
|
|
262
324
|
if request.method == "POST":
|
|
325
|
+
|
|
326
|
+
def get_schema(conn):
|
|
327
|
+
table_obj = sqlite_utils.Database(conn)[table]
|
|
328
|
+
if not table_obj.exists():
|
|
329
|
+
return None
|
|
330
|
+
return table_obj.schema
|
|
331
|
+
|
|
332
|
+
before_schema = await database.execute_fn(get_schema)
|
|
333
|
+
|
|
334
|
+
async def track_analytics():
|
|
335
|
+
after_schema = await database.execute_fn(get_schema)
|
|
336
|
+
# Don't track drop tables, which happen when after_schema is None
|
|
337
|
+
if after_schema is not None and after_schema != before_schema:
|
|
338
|
+
await datasette.track_event(
|
|
339
|
+
AlterTableEvent(
|
|
340
|
+
actor=request.actor,
|
|
341
|
+
database=database_name,
|
|
342
|
+
table=table,
|
|
343
|
+
before_schema=before_schema,
|
|
344
|
+
after_schema=after_schema,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
263
348
|
formdata = await request.post_vars()
|
|
264
349
|
if formdata.get("action") == "update_columns":
|
|
265
350
|
types = {}
|
|
@@ -315,31 +400,35 @@ async def edit_schema_table(request, datasette):
|
|
|
315
400
|
await database.execute_write_fn(transform_the_table, block=True)
|
|
316
401
|
|
|
317
402
|
datasette.add_message(request, "Changes to table have been saved")
|
|
318
|
-
|
|
403
|
+
await track_analytics()
|
|
319
404
|
return Response.redirect(request.path)
|
|
320
405
|
|
|
321
406
|
if formdata.get("action") == "update_foreign_keys":
|
|
322
|
-
|
|
407
|
+
response = await update_foreign_keys(
|
|
323
408
|
request, datasette, database, table, formdata
|
|
324
409
|
)
|
|
325
410
|
elif formdata.get("action") == "update_primary_key":
|
|
326
|
-
|
|
411
|
+
response = await update_primary_key(
|
|
327
412
|
request, datasette, database, table, formdata
|
|
328
413
|
)
|
|
329
|
-
elif "
|
|
330
|
-
|
|
414
|
+
elif "drop_table" in formdata:
|
|
415
|
+
response = await drop_table(request, datasette, database, table)
|
|
331
416
|
elif "add_column" in formdata:
|
|
332
|
-
|
|
417
|
+
response = await add_column(request, datasette, database, table, formdata)
|
|
333
418
|
elif "rename_table" in formdata:
|
|
334
|
-
|
|
419
|
+
response = await rename_table(request, datasette, database, table, formdata)
|
|
335
420
|
elif "add_index" in formdata:
|
|
336
421
|
column = formdata.get("add_index_column") or ""
|
|
337
422
|
unique = formdata.get("add_index_unique")
|
|
338
|
-
|
|
423
|
+
response = await add_index(
|
|
424
|
+
request, datasette, database, table, column, unique
|
|
425
|
+
)
|
|
339
426
|
elif any(key.startswith("drop_index_") for key in formdata.keys()):
|
|
340
|
-
|
|
427
|
+
response = await drop_index(request, datasette, database, table, formdata)
|
|
341
428
|
else:
|
|
342
|
-
|
|
429
|
+
response = Response.html("Unknown operation", status=400)
|
|
430
|
+
await track_analytics()
|
|
431
|
+
return response
|
|
343
432
|
|
|
344
433
|
def get_columns_and_schema_and_fks_and_pks_and_indexes(conn):
|
|
345
434
|
db = sqlite_utils.Database(conn)
|
|
@@ -354,10 +443,11 @@ async def edit_schema_table(request, datasette):
|
|
|
354
443
|
textwrap.dedent(
|
|
355
444
|
"""
|
|
356
445
|
select group_concat(sql, ';
|
|
357
|
-
') from sqlite_master where tbl_name =
|
|
446
|
+
') from sqlite_master where tbl_name = ?
|
|
358
447
|
order by type desc
|
|
359
448
|
"""
|
|
360
|
-
)
|
|
449
|
+
),
|
|
450
|
+
[table],
|
|
361
451
|
).fetchone()[0]
|
|
362
452
|
return columns, schema, t.foreign_keys, t.pks, t.indexes
|
|
363
453
|
|
|
@@ -394,19 +484,20 @@ async def edit_schema_table(request, datasette):
|
|
|
394
484
|
(pair[0], pair[1]) for pair in other_primary_keys if pair[2] is str
|
|
395
485
|
]
|
|
396
486
|
|
|
397
|
-
|
|
487
|
+
all_columns_to_manage_foreign_keys = [
|
|
398
488
|
{
|
|
399
489
|
"name": column["name"],
|
|
400
|
-
"foreign_key":
|
|
401
|
-
|
|
402
|
-
|
|
490
|
+
"foreign_key": (
|
|
491
|
+
foreign_keys_by_column.get(column["name"])[0]
|
|
492
|
+
if foreign_keys_by_column.get(column["name"])
|
|
493
|
+
else None
|
|
494
|
+
),
|
|
403
495
|
"suggestions": [],
|
|
404
|
-
"options":
|
|
405
|
-
|
|
406
|
-
|
|
496
|
+
"options": (
|
|
497
|
+
integer_primary_keys if column["type"] is int else string_primary_keys
|
|
498
|
+
),
|
|
407
499
|
}
|
|
408
500
|
for column in columns
|
|
409
|
-
if not column["is_pk"]
|
|
410
501
|
]
|
|
411
502
|
|
|
412
503
|
# Anything not a float or an existing PK could be the next PK, but
|
|
@@ -433,7 +524,7 @@ async def edit_schema_table(request, datasette):
|
|
|
433
524
|
other_primary_keys,
|
|
434
525
|
)
|
|
435
526
|
)
|
|
436
|
-
for info in
|
|
527
|
+
for info in all_columns_to_manage_foreign_keys:
|
|
437
528
|
info["suggestions"] = potential_fks.get(info["name"], [])
|
|
438
529
|
# Now do potential primary keys against non-float columns
|
|
439
530
|
non_float_columns = [
|
|
@@ -444,7 +535,7 @@ async def edit_schema_table(request, datasette):
|
|
|
444
535
|
)
|
|
445
536
|
|
|
446
537
|
# Add 'options' to those
|
|
447
|
-
for info in
|
|
538
|
+
for info in all_columns_to_manage_foreign_keys:
|
|
448
539
|
options = []
|
|
449
540
|
seen = set()
|
|
450
541
|
info["html_options"] = options
|
|
@@ -516,29 +607,49 @@ async def edit_schema_table(request, datasette):
|
|
|
516
607
|
for value in TYPES.values()
|
|
517
608
|
],
|
|
518
609
|
"foreign_keys": foreign_keys,
|
|
519
|
-
"
|
|
610
|
+
"all_columns_to_manage_foreign_keys": all_columns_to_manage_foreign_keys,
|
|
520
611
|
"potential_pks": potential_pks,
|
|
521
612
|
"is_rowid_table": bool(pks == ["rowid"]),
|
|
522
613
|
"current_pk": pks[0] if len(pks) == 1 else None,
|
|
523
614
|
"existing_indexes": existing_indexes,
|
|
524
615
|
"non_primary_key_columns": non_primary_key_columns,
|
|
616
|
+
"can_drop_table": await can_drop_table(
|
|
617
|
+
datasette, request.actor, database_name, table
|
|
618
|
+
),
|
|
619
|
+
"can_rename_table": await can_rename_table(
|
|
620
|
+
datasette, request.actor, database_name, table
|
|
621
|
+
),
|
|
525
622
|
},
|
|
526
623
|
request=request,
|
|
527
624
|
)
|
|
528
625
|
)
|
|
529
626
|
|
|
530
627
|
|
|
531
|
-
async def
|
|
532
|
-
|
|
628
|
+
async def drop_table(request, datasette, database, table):
|
|
629
|
+
if not await can_drop_table(datasette, request.actor, database.name, table):
|
|
630
|
+
raise Forbidden("Permission denied for drop-table")
|
|
631
|
+
|
|
632
|
+
def do_drop_table(conn):
|
|
533
633
|
db = sqlite_utils.Database(conn)
|
|
534
634
|
db[table].disable_fts()
|
|
535
635
|
db[table].drop()
|
|
536
636
|
db.vacuum()
|
|
537
637
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
638
|
+
if hasattr(database, "execute_isolated_fn"):
|
|
639
|
+
await database.execute_isolated_fn(do_drop_table)
|
|
640
|
+
# For the tests
|
|
641
|
+
datasette._datasette_edit_schema_used_execute_isolated_fn = True
|
|
642
|
+
else:
|
|
643
|
+
await database.execute_write_fn(do_drop_table)
|
|
644
|
+
|
|
541
645
|
datasette.add_message(request, "Table has been deleted")
|
|
646
|
+
await datasette.track_event(
|
|
647
|
+
DropTableEvent(
|
|
648
|
+
actor=request.actor,
|
|
649
|
+
database=database.name,
|
|
650
|
+
table=table,
|
|
651
|
+
)
|
|
652
|
+
)
|
|
542
653
|
return Response.redirect("/-/edit-schema/" + database.name)
|
|
543
654
|
|
|
544
655
|
|
|
@@ -601,7 +712,19 @@ async def rename_table(request, datasette, database, table, formdata):
|
|
|
601
712
|
)
|
|
602
713
|
return redirect
|
|
603
714
|
|
|
715
|
+
# User must have drop-table permission on old table and create-table on new table
|
|
716
|
+
if not await can_rename_table(datasette, request.actor, database.name, table):
|
|
717
|
+
datasette.add_message(
|
|
718
|
+
request,
|
|
719
|
+
"Permission denied to rename table '{}'".format(table),
|
|
720
|
+
datasette.ERROR,
|
|
721
|
+
)
|
|
722
|
+
return redirect
|
|
723
|
+
|
|
604
724
|
try:
|
|
725
|
+
before_schema = await database.execute_fn(
|
|
726
|
+
lambda conn: sqlite_utils.Database(conn)[table].schema
|
|
727
|
+
)
|
|
605
728
|
await database.execute_write(
|
|
606
729
|
"""
|
|
607
730
|
ALTER TABLE [{}] RENAME TO [{}];
|
|
@@ -610,9 +733,22 @@ async def rename_table(request, datasette, database, table, formdata):
|
|
|
610
733
|
),
|
|
611
734
|
block=True,
|
|
612
735
|
)
|
|
736
|
+
after_schema = await database.execute_fn(
|
|
737
|
+
lambda conn: sqlite_utils.Database(conn)[new_name].schema
|
|
738
|
+
)
|
|
613
739
|
datasette.add_message(
|
|
614
740
|
request, "Table renamed to '{}'".format(new_name), datasette.INFO
|
|
615
741
|
)
|
|
742
|
+
await datasette.track_event(
|
|
743
|
+
AlterTableEvent(
|
|
744
|
+
actor=request.actor,
|
|
745
|
+
database=database.name,
|
|
746
|
+
table=new_name,
|
|
747
|
+
before_schema=before_schema,
|
|
748
|
+
after_schema=after_schema,
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
|
|
616
752
|
except Exception as error:
|
|
617
753
|
datasette.add_message(
|
|
618
754
|
request, "Error renaming table: {}".format(str(error)), datasette.ERROR
|
|
@@ -22,6 +22,9 @@ select {
|
|
|
22
22
|
font-size: 1em;
|
|
23
23
|
font-family: Helvetica, sans-serif;
|
|
24
24
|
}
|
|
25
|
+
select.select-smaller {
|
|
26
|
+
width: 90%;
|
|
27
|
+
}
|
|
25
28
|
html body label {
|
|
26
29
|
font-weight: bold;
|
|
27
30
|
display: inline-block;
|
|
@@ -81,14 +84,14 @@ html body label {
|
|
|
81
84
|
<form action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/-/create" method="post">
|
|
82
85
|
<p>
|
|
83
86
|
<label for="table_name">Table name: </label>
|
|
84
|
-
<input type="text" required="1" id="table_name" name="table_name" size="20" style="width:
|
|
87
|
+
<input type="text" required="1" id="table_name" name="table_name" size="20" style="width: 25%">
|
|
85
88
|
</p>
|
|
86
89
|
<h2>Columns</h2>
|
|
87
90
|
<p style="font-size: 0.8em;">If the primary key is an integer it will automatically count up from 1</p>
|
|
88
91
|
<ul class="editable-columns">
|
|
89
92
|
<!-- primary key comes first and is not sortable -->
|
|
90
93
|
<li>
|
|
91
|
-
<input style="width:
|
|
94
|
+
<input style="width: 25%" type="text" size="10" name="primary_key_name" value="id">
|
|
92
95
|
<label>Type: <select name="primary_key_type">
|
|
93
96
|
<option value="INTEGER" selected="selected">Integer</option>
|
|
94
97
|
<option value="TEXT">Text</option>
|
|
@@ -99,7 +102,7 @@ html body label {
|
|
|
99
102
|
<ul class="sortable-columns editable-columns">
|
|
100
103
|
{% for column in columns %}
|
|
101
104
|
<li>
|
|
102
|
-
<input style="width:
|
|
105
|
+
<input style="width: 25%" type="text" size="10" name="column-name.{{ loop.index }}" value="">
|
|
103
106
|
<label>Type: <select name="column-type.{{ loop.index }}">
|
|
104
107
|
{% for type in types %}
|
|
105
108
|
<option value="{{ type.value }}">{{ type.name }}</option>
|
|
@@ -29,6 +29,9 @@ select {
|
|
|
29
29
|
font-size: 1em;
|
|
30
30
|
font-family: Helvetica, sans-serif;
|
|
31
31
|
}
|
|
32
|
+
select.select-smaller {
|
|
33
|
+
width: 90%;
|
|
34
|
+
}
|
|
32
35
|
html body label {
|
|
33
36
|
font-weight: bold;
|
|
34
37
|
display: inline-block;
|
|
@@ -85,21 +88,23 @@ html body label {
|
|
|
85
88
|
{% block content %}
|
|
86
89
|
<h1>Edit table <a href="{{ base_url }}{{ database.name|quote_plus }}/{{ table|quote_plus }}">{{ database.name }}/{{ table }}</a></h1>
|
|
87
90
|
|
|
91
|
+
{% if can_rename_table %}
|
|
88
92
|
<h2>Rename table</h2>
|
|
89
93
|
|
|
90
94
|
<form action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/{{ table|quote_plus }}" method="post">
|
|
91
95
|
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
|
92
96
|
<p><label>New name <input type="text" name="name"></label>
|
|
93
97
|
<input type="hidden" name="rename_table" value="1">
|
|
94
|
-
<input type="submit" value="Rename
|
|
98
|
+
<input type="submit" value="Rename">
|
|
95
99
|
</form>
|
|
100
|
+
{% endif %}
|
|
96
101
|
|
|
97
102
|
<form action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/{{ table|quote_plus }}" method="post">
|
|
98
103
|
<h2>Change existing columns</h2>
|
|
99
104
|
<ul class="sortable-columns">
|
|
100
105
|
{% for column in columns %}
|
|
101
106
|
<li data-original-name="{{ column.name }}">
|
|
102
|
-
<input style="width:
|
|
107
|
+
<input style="width: 25%" type="text" size="10" name="name.{{ column.name }}" value="{{ column.name }}">
|
|
103
108
|
<label>Type: <select name="type.{{ column.name }}">
|
|
104
109
|
{% for type in types %}
|
|
105
110
|
<option{% if type.value == column.type %} selected="selected"{% endif %} value="{{ type.value }}">{{ type.name }}</option>
|
|
@@ -155,10 +160,11 @@ table.foreign-key-options td {
|
|
|
155
160
|
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
|
156
161
|
<input type="hidden" name="action" value="update_foreign_keys">
|
|
157
162
|
<table class="foreign-key-options">
|
|
158
|
-
{% for column in
|
|
163
|
+
{% for column in all_columns_to_manage_foreign_keys %}
|
|
159
164
|
<tr>
|
|
160
165
|
<td><label for="fk.{{ column.name }}">{{ column.name }}</label></td>
|
|
161
|
-
<td><select id="fk.{{ column.name }}" name="fk.{{ column.name }}"
|
|
166
|
+
<td><select id="fk.{{ column.name }}" name="fk.{{ column.name }}" class="select-smaller">
|
|
167
|
+
<option value="">-- {% if not column.suggested and not column.foreign_key %}no suggestions{% else %}none{% endif %} --</option>
|
|
162
168
|
{% for option in column.html_options %}<option value="{{ option.value }}" {% if option.selected %} selected="selected"{% endif %}>{{ option.name }}</option>{% endfor %}
|
|
163
169
|
</select>
|
|
164
170
|
{% if column.suggested %}<p style="margin: 0; font-size: 0.8em">Suggested: {{ column.suggested }}</p>{% endif %}
|
|
@@ -224,13 +230,15 @@ table.foreign-key-options td {
|
|
|
224
230
|
</form>
|
|
225
231
|
{% endif %}
|
|
226
232
|
|
|
227
|
-
|
|
233
|
+
{% if can_drop_table %}
|
|
234
|
+
<h2>Drop table</h2>
|
|
228
235
|
|
|
229
|
-
<form id="
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</form>
|
|
236
|
+
<form id="drop-table-form" action="{{ base_url }}-/edit-schema/{{ database.name|quote_plus }}/{{ table|quote_plus }}" method="post">
|
|
237
|
+
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
|
238
|
+
<input type="hidden" name="drop_table" value="1">
|
|
239
|
+
<input type="submit" class="button-red" value="Drop this table">
|
|
240
|
+
</form>
|
|
241
|
+
{% endif %}
|
|
234
242
|
|
|
235
243
|
<h2>Current table schema</h2>
|
|
236
244
|
<pre>{{ schema }}</pre>
|
|
@@ -247,7 +255,7 @@ sortableColumns.on('sortable:stop', (ev) => {
|
|
|
247
255
|
}), 200);
|
|
248
256
|
});
|
|
249
257
|
|
|
250
|
-
document.getElementById('
|
|
258
|
+
document.getElementById('drop-table-form').addEventListener('submit', function(event) {
|
|
251
259
|
const userConfirmation = confirm("Are you sure you want to delete this table? This cannot be reversed.");
|
|
252
260
|
if (!userConfirmation) {
|
|
253
261
|
event.preventDefault();
|
datasette_edit_schema/utils.py
CHANGED
|
@@ -53,11 +53,13 @@ def potential_foreign_keys(conn, table_name, columns, other_table_pks):
|
|
|
53
53
|
|
|
54
54
|
def potential_primary_keys(conn, table_name, columns, max_string_len=128):
|
|
55
55
|
# First we run a query to check the max length of each column + if it has any nulls
|
|
56
|
+
if not columns:
|
|
57
|
+
return []
|
|
56
58
|
selects = []
|
|
57
59
|
for column in columns:
|
|
58
|
-
selects.append(
|
|
60
|
+
selects.append('max(length("{}")) as "maxlen.{}"'.format(column, column))
|
|
59
61
|
selects.append(
|
|
60
|
-
|
|
62
|
+
'sum(case when "{}" is null then 1 else 0 end) as "nulls.{}"'.format(
|
|
61
63
|
column, column
|
|
62
64
|
)
|
|
63
65
|
)
|
|
@@ -76,7 +78,7 @@ def potential_primary_keys(conn, table_name, columns, max_string_len=128):
|
|
|
76
78
|
# Count distinct values in each of our candidate columns
|
|
77
79
|
selects = ["count(*) as _count"]
|
|
78
80
|
for column in potential_columns:
|
|
79
|
-
selects.append(
|
|
81
|
+
selects.append('count(distinct "{}") as "distinct.{}"'.format(column, column))
|
|
80
82
|
sql = 'select {} from "{}"'.format(", ".join(selects), table_name)
|
|
81
83
|
cursor.execute(sql)
|
|
82
84
|
row = cursor.fetchone()
|
|
@@ -93,12 +95,14 @@ def examples_for_columns(conn, table_name):
|
|
|
93
95
|
columns = sqlite_utils.Database(conn)[table_name].columns_dict.keys()
|
|
94
96
|
ctes = [f'rows as (select * from "{table_name}" limit 1000)']
|
|
95
97
|
unions = []
|
|
98
|
+
params = []
|
|
96
99
|
for i, column in enumerate(columns):
|
|
97
100
|
ctes.append(
|
|
98
101
|
f'col{i} as (select distinct "{column}" from rows '
|
|
99
102
|
f'where ("{column}" is not null and "{column}" != "") limit 5)'
|
|
100
103
|
)
|
|
101
|
-
unions.append(f
|
|
104
|
+
unions.append(f'select ? as label, "{column}" as value from col{i}')
|
|
105
|
+
params.append(column)
|
|
102
106
|
ctes.append("strings as ({})".format("\nunion all\n".join(unions)))
|
|
103
107
|
ctes.append(
|
|
104
108
|
"""
|
|
@@ -120,6 +124,6 @@ def examples_for_columns(conn, table_name):
|
|
|
120
124
|
"from truncated_strings group by label"
|
|
121
125
|
)
|
|
122
126
|
output = {}
|
|
123
|
-
for column, examples in conn.execute(sql).fetchall():
|
|
127
|
+
for column, examples in conn.execute(sql, params).fetchall():
|
|
124
128
|
output[column] = list(map(str, json.loads(examples)))
|
|
125
129
|
return output
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-edit-schema
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8a1
|
|
4
4
|
Summary: Datasette plugin for modifying table schemas
|
|
5
|
-
Home-page: https://github.com/simonw/datasette-edit-schema
|
|
6
5
|
Author: Simon Willison
|
|
7
|
-
License: Apache
|
|
8
|
-
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://datasette.io/plugins/datasette-edit-schema
|
|
8
|
+
Project-URL: Changelog, https://github.com/simonw/datasette-edit-schema/releases
|
|
9
|
+
Project-URL: Issues, https://github.com/simonw/datasette-edit-schema/issues
|
|
10
|
+
Project-URL: CI, https://github.com/simonw/datasette-edit-schema/actions
|
|
11
|
+
Classifier: Framework :: Datasette
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Requires-Python: >=3.8
|
|
9
14
|
Description-Content-Type: text/markdown
|
|
10
15
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: datasette >=
|
|
16
|
+
Requires-Dist: datasette >=1.0a13
|
|
12
17
|
Requires-Dist: sqlite-utils >=3.35
|
|
13
18
|
Provides-Extra: test
|
|
14
19
|
Requires-Dist: pytest ; extra == 'test'
|
|
@@ -25,6 +30,8 @@ Requires-Dist: html5lib ; extra == 'test'
|
|
|
25
30
|
|
|
26
31
|
Datasette plugin for modifying table schemas
|
|
27
32
|
|
|
33
|
+
> :warning: The latest alpha release depends on Datasette 1.09a. Use [version 0.7.1](https://github.com/simonw/datasette-edit-schema/blob/0.7.1/README.md) with older releases of Datasette.
|
|
34
|
+
|
|
28
35
|
## Features
|
|
29
36
|
|
|
30
37
|
* Add new columns to a table
|
|
@@ -54,7 +61,9 @@ By default only [the root actor](https://datasette.readthedocs.io/en/stable/auth
|
|
|
54
61
|
|
|
55
62
|
## Permissions
|
|
56
63
|
|
|
57
|
-
The `edit-schema` permission
|
|
64
|
+
The `edit-schema` permission provides access to all functionality.
|
|
65
|
+
|
|
66
|
+
You can use permission plugins such as [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to grant additional access to the write interface.
|
|
58
67
|
|
|
59
68
|
These permission checks will call the `permission_allowed()` plugin hook with three arguments:
|
|
60
69
|
|
|
@@ -62,6 +71,38 @@ These permission checks will call the `permission_allowed()` plugin hook with th
|
|
|
62
71
|
- `actor` will be the currently authenticated actor - usually a dictionary
|
|
63
72
|
- `resource` will be the string name of the database
|
|
64
73
|
|
|
74
|
+
You can instead use more finely-grained permissions from the default Datasette permissions collection:
|
|
75
|
+
|
|
76
|
+
- `create-table` allows users to create a new table. The `resource` will be the name of the database.
|
|
77
|
+
- `drop-table` allows users to drop a table. The `resource` will be a tuple of `(database_name, table_name)`.
|
|
78
|
+
- `alter-table` allows users to alter a table. The `resource` will be a tuple of `(database_name, table_name)`.
|
|
79
|
+
|
|
80
|
+
To rename a table a user must have both `drop-table` permission for that table and `create-table` permission for that database.
|
|
81
|
+
|
|
82
|
+
For example, to configure Datasette to allow the user with ID `pelican` to create, alter and drop tables in the `marketing` database and to alter just the `notes` table in the `sales` database, you could use the following configuration:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
databases:
|
|
86
|
+
marketing:
|
|
87
|
+
permissions:
|
|
88
|
+
create-table:
|
|
89
|
+
id: pelican
|
|
90
|
+
drop-table:
|
|
91
|
+
id: pelican
|
|
92
|
+
alter-table:
|
|
93
|
+
id: pelican
|
|
94
|
+
sales:
|
|
95
|
+
tables:
|
|
96
|
+
notes:
|
|
97
|
+
permissions:
|
|
98
|
+
alter-table:
|
|
99
|
+
id: pelican
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Events
|
|
103
|
+
|
|
104
|
+
This plugin fires `create-table`, `alter-table` and `drop-table` events when tables are modified, using the [Datasette Events](https://docs.datasette.io/en/latest/events.html) system introduced in [Datasette 1.0a8](https://docs.datasette.io/en/latest/changelog.html#a8-2024-02-07).
|
|
105
|
+
|
|
65
106
|
## Screenshot
|
|
66
107
|
|
|
67
108
|

|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
datasette_edit_schema/__init__.py,sha256=CrqDxy8kEGPv2zHiQtMr5ZpUu0ufgNN3VBn0JQKKVK8,30707
|
|
2
|
+
datasette_edit_schema/utils.py,sha256=PURr7eOLOcZ7MQTCkZ_fDyeB39-MDhwIbBA-YeXSods,4388
|
|
3
|
+
datasette_edit_schema/static/draggable.1.0.0-beta.11.bundle.js,sha256=rpTEfd8N7sSY5n_mi6mwGz2f-dRXOgknFBuUXmwFnpY,202242
|
|
4
|
+
datasette_edit_schema/static/draggable.1.0.0-beta.11.bundle.min.js,sha256=JlHRb54CUGfor6ENxVjMq9rd_QRrRLoVJf-j3V_c52U,119723
|
|
5
|
+
datasette_edit_schema/templates/edit_schema_create_table.html,sha256=cUiS_LucrtvwpD_Nfb6ER4uZyhzTtPMzl3vkqhx9iGQ,3777
|
|
6
|
+
datasette_edit_schema/templates/edit_schema_database.html,sha256=0fo_n0w5JQRvc5UmlDkECpUIXxlwVI0IhTFQ55C6orY,540
|
|
7
|
+
datasette_edit_schema/templates/edit_schema_index.html,sha256=eNSOFFwv1gsOYP3fNQqcPh0gU5Ny5sDnngVZT5XiUNA,474
|
|
8
|
+
datasette_edit_schema/templates/edit_schema_table.html,sha256=dmJLMA8Revb5sdryv9NUfR4Kw6TOPQtW97oByR-pE3k,9382
|
|
9
|
+
datasette_edit_schema-0.8a1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
10
|
+
datasette_edit_schema-0.8a1.dist-info/METADATA,sha256=DsF29608AXw15hHeTlHcOKC41gL9uiMfBifXDIBU58A,5150
|
|
11
|
+
datasette_edit_schema-0.8a1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
12
|
+
datasette_edit_schema-0.8a1.dist-info/entry_points.txt,sha256=1-FeujFVTui7tFNBdz3hN5x4vRoanaAydYF5ljYsvp8,48
|
|
13
|
+
datasette_edit_schema-0.8a1.dist-info/top_level.txt,sha256=Cbx3sgneHtcDDxV5SVL8IyU8JvsSdXgpmZqw4oDuOzM,22
|
|
14
|
+
datasette_edit_schema-0.8a1.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
datasette_edit_schema/__init__.py,sha256=htKZgskL7W8fGDlMLgr3cHvgcmmnaetqyLrbQaN3Lhw,25872
|
|
2
|
-
datasette_edit_schema/utils.py,sha256=Nd45BXZweWQFACh0rX-n8nk7ieGr1JLH9UW-1DqRFuU,4313
|
|
3
|
-
datasette_edit_schema/static/draggable.1.0.0-beta.11.bundle.js,sha256=rpTEfd8N7sSY5n_mi6mwGz2f-dRXOgknFBuUXmwFnpY,202242
|
|
4
|
-
datasette_edit_schema/static/draggable.1.0.0-beta.11.bundle.min.js,sha256=JlHRb54CUGfor6ENxVjMq9rd_QRrRLoVJf-j3V_c52U,119723
|
|
5
|
-
datasette_edit_schema/templates/edit_schema_create_table.html,sha256=2ZT5kHxWTCeW5-BhNEiFjRb-_pvmlF8_R7kqJIXQZto,3735
|
|
6
|
-
datasette_edit_schema/templates/edit_schema_database.html,sha256=0fo_n0w5JQRvc5UmlDkECpUIXxlwVI0IhTFQ55C6orY,540
|
|
7
|
-
datasette_edit_schema/templates/edit_schema_index.html,sha256=eNSOFFwv1gsOYP3fNQqcPh0gU5Ny5sDnngVZT5XiUNA,474
|
|
8
|
-
datasette_edit_schema/templates/edit_schema_table.html,sha256=xpnUFklJ-Z485Efx0HZvfmQzIqciCQ3TF4q36tpgRgE,9212
|
|
9
|
-
datasette_edit_schema-0.7.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
10
|
-
datasette_edit_schema-0.7.dist-info/METADATA,sha256=qsH7EyHdiwMeGw96Y_xZ-3DprzLSrsIjNZOXZPBv2c4,3244
|
|
11
|
-
datasette_edit_schema-0.7.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
12
|
-
datasette_edit_schema-0.7.dist-info/entry_points.txt,sha256=1-FeujFVTui7tFNBdz3hN5x4vRoanaAydYF5ljYsvp8,48
|
|
13
|
-
datasette_edit_schema-0.7.dist-info/top_level.txt,sha256=Cbx3sgneHtcDDxV5SVL8IyU8JvsSdXgpmZqw4oDuOzM,22
|
|
14
|
-
datasette_edit_schema-0.7.dist-info/RECORD,,
|
|
File without changes
|
{datasette_edit_schema-0.7.dist-info → datasette_edit_schema-0.8a1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|