mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Display column type in column action menu, closes #993
Also added new documented db.table_column_details() introspection method.
This commit is contained in:
parent
e807c4eac0
commit
5a184a5d21
8 changed files with 103 additions and 28 deletions
|
|
@ -16,6 +16,7 @@ from .utils import (
|
||||||
sqlite_timelimit,
|
sqlite_timelimit,
|
||||||
sqlite3,
|
sqlite3,
|
||||||
table_columns,
|
table_columns,
|
||||||
|
table_column_details,
|
||||||
)
|
)
|
||||||
from .inspect import inspect_hash
|
from .inspect import inspect_hash
|
||||||
|
|
||||||
|
|
@ -231,6 +232,9 @@ class Database:
|
||||||
async def table_columns(self, table):
|
async def table_columns(self, table):
|
||||||
return await self.execute_fn(lambda conn: table_columns(conn, table))
|
return await self.execute_fn(lambda conn: table_columns(conn, table))
|
||||||
|
|
||||||
|
async def table_column_details(self, table):
|
||||||
|
return await self.execute_fn(lambda conn: table_column_details(conn, table))
|
||||||
|
|
||||||
async def primary_keys(self, table):
|
async def primary_keys(self, table):
|
||||||
return await self.execute_fn(lambda conn: detect_primary_keys(conn, table))
|
return await self.execute_fn(lambda conn: detect_primary_keys(conn, table))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,6 @@ svg.dropdown-menu-icon {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
display: inline-flex;
|
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
|
@ -410,6 +409,13 @@ svg.dropdown-menu-icon {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.dropdown-menu .dropdown-column-type {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding: 4px 8px 4px 8px;
|
||||||
|
}
|
||||||
.dropdown-menu li {
|
.dropdown-menu li {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ var DROPDOWN_HTML = `<div class="dropdown-menu">
|
||||||
<li><a class="dropdown-facet" href="#">Facet by this</a></li>
|
<li><a class="dropdown-facet" href="#">Facet by this</a></li>
|
||||||
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
|
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="dropdown-column-type"></p>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|
@ -115,10 +116,20 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
|
||||||
} else {
|
} else {
|
||||||
notBlank.style.display = 'none';
|
notBlank.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
var columnTypeP = menu.querySelector('.dropdown-column-type');
|
||||||
|
var columnType = th.dataset.columnType;
|
||||||
|
var notNull = th.dataset.columnNotNull == 1 ? ' NOT NULL' : '';
|
||||||
|
|
||||||
|
if (columnType) {
|
||||||
|
columnTypeP.style.display = 'block';
|
||||||
|
columnTypeP.innerText = `Type: ${columnType.toUpperCase()}${notNull}`;
|
||||||
|
} else {
|
||||||
|
columnTypeP.style.display = 'none';
|
||||||
|
}
|
||||||
menu.style.position = 'absolute';
|
menu.style.position = 'absolute';
|
||||||
menu.style.top = (menuTop + 6) + 'px';
|
menu.style.top = (menuTop + 6) + 'px';
|
||||||
menu.style.left = menuLeft + 'px';
|
menu.style.left = menuLeft + 'px';
|
||||||
menu.style.display = 'inline-flex';
|
menu.style.display = 'block';
|
||||||
}
|
}
|
||||||
var svg = document.createElement('div');
|
var svg = document.createElement('div');
|
||||||
svg.innerHTML = DROPDOWN_ICON_SVG;
|
svg.innerHTML = DROPDOWN_ICON_SVG;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in display_columns %}
|
{% for column in display_columns %}
|
||||||
<th class="col-{{ column.name|to_css_class }}" scope="col" data-column="{{ column.name }}" data-is-pk="{% if column.is_pk %}1{% else %}0{% endif %}">
|
<th class="col-{{ column.name|to_css_class }}" scope="col" data-column="{{ column.name }}" data-column-type="{{ column.type }}" data-column-not-null="{{ column.notnull }}" data-is-pk="{% if column.is_pk %}1{% else %}0{% endif %}">
|
||||||
{% if not column.sortable %}
|
{% if not column.sortable %}
|
||||||
{{ column.name }}
|
{{ column.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, namedtuple
|
||||||
import base64
|
import base64
|
||||||
import click
|
import click
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -54,6 +54,11 @@ RUN apt-get update && \
|
||||||
ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so
|
ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Can replace this with Column from sqlite_utils when I add that dependency
|
||||||
|
Column = namedtuple(
|
||||||
|
"Column", ("cid", "name", "type", "notnull", "default_value", "is_pk")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def await_me_maybe(value):
|
async def await_me_maybe(value):
|
||||||
if callable(value):
|
if callable(value):
|
||||||
|
|
@ -525,8 +530,12 @@ def detect_json1(conn=None):
|
||||||
|
|
||||||
|
|
||||||
def table_columns(conn, table):
|
def table_columns(conn, table):
|
||||||
|
return [column.name for column in table_column_details(conn, table)]
|
||||||
|
|
||||||
|
|
||||||
|
def table_column_details(conn, table):
|
||||||
return [
|
return [
|
||||||
r[1]
|
Column(*r)
|
||||||
for r in conn.execute(
|
for r in conn.execute(
|
||||||
"PRAGMA table_info({});".format(escape_sqlite(table))
|
"PRAGMA table_info({});".format(escape_sqlite(table))
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
|
||||||
|
|
@ -90,19 +90,31 @@ class RowTableShared(DataView):
|
||||||
"Returns columns, rows for specified table - including fancy foreign key treatment"
|
"Returns columns, rows for specified table - including fancy foreign key treatment"
|
||||||
db = self.ds.databases[database]
|
db = self.ds.databases[database]
|
||||||
table_metadata = self.ds.table_metadata(database, table)
|
table_metadata = self.ds.table_metadata(database, table)
|
||||||
|
column_details = {col.name: col for col in await db.table_column_details(table)}
|
||||||
sortable_columns = await self.sortable_columns_for_table(database, table, True)
|
sortable_columns = await self.sortable_columns_for_table(database, table, True)
|
||||||
pks = await db.primary_keys(table)
|
pks = await db.primary_keys(table)
|
||||||
pks_for_display = pks
|
pks_for_display = pks
|
||||||
if not pks_for_display:
|
if not pks_for_display:
|
||||||
pks_for_display = ["rowid"]
|
pks_for_display = ["rowid"]
|
||||||
columns = [
|
|
||||||
{
|
columns = []
|
||||||
"name": r[0],
|
for r in description:
|
||||||
"sortable": r[0] in sortable_columns,
|
if r[0] == "rowid" and "rowid" not in column_details:
|
||||||
"is_pk": r[0] in pks_for_display,
|
type_ = "integer"
|
||||||
}
|
notnull = 0
|
||||||
for r in description
|
else:
|
||||||
]
|
type_ = column_details[r[0]].type
|
||||||
|
notnull = column_details[r[0]].notnull
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"name": r[0],
|
||||||
|
"sortable": r[0] in sortable_columns,
|
||||||
|
"is_pk": r[0] in pks_for_display,
|
||||||
|
"type": type_,
|
||||||
|
"notnull": notnull,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
column_to_foreign_key_table = {
|
column_to_foreign_key_table = {
|
||||||
fk["column"]: fk["other_table"]
|
fk["column"]: fk["other_table"]
|
||||||
for fk in await db.foreign_keys_for_table(table)
|
for fk in await db.foreign_keys_for_table(table)
|
||||||
|
|
@ -217,12 +229,25 @@ class RowTableShared(DataView):
|
||||||
# Add the link column header.
|
# Add the link column header.
|
||||||
# If it's a simple primary key, we have to remove and re-add that column name at
|
# If it's a simple primary key, we have to remove and re-add that column name at
|
||||||
# the beginning of the header row.
|
# the beginning of the header row.
|
||||||
|
first_column = None
|
||||||
if len(pks) == 1:
|
if len(pks) == 1:
|
||||||
columns = [col for col in columns if col["name"] != pks[0]]
|
columns = [col for col in columns if col["name"] != pks[0]]
|
||||||
|
first_column = {
|
||||||
columns = [
|
"name": pks[0],
|
||||||
{"name": pks[0] if len(pks) == 1 else "Link", "sortable": len(pks) == 1}
|
"sortable": len(pks) == 1,
|
||||||
] + columns
|
"is_pk": True,
|
||||||
|
"type": column_details[pks[0]].type,
|
||||||
|
"notnull": column_details[pks[0]].notnull,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
first_column = {
|
||||||
|
"name": "Link",
|
||||||
|
"sortable": False,
|
||||||
|
"is_pk": False,
|
||||||
|
"type": "",
|
||||||
|
"notnull": 0,
|
||||||
|
}
|
||||||
|
columns = [first_column] + columns
|
||||||
return columns, cell_rows
|
return columns, cell_rows
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -291,7 +316,8 @@ class TableView(RowTableShared):
|
||||||
)
|
)
|
||||||
|
|
||||||
pks = await db.primary_keys(table)
|
pks = await db.primary_keys(table)
|
||||||
table_columns = await db.table_columns(table)
|
table_column_details = await db.table_column_details(table)
|
||||||
|
table_columns = [column.name for column in table_column_details]
|
||||||
|
|
||||||
select_columns = ", ".join(escape_sqlite(t) for t in table_columns)
|
select_columns = ", ".join(escape_sqlite(t) for t in table_columns)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -500,6 +500,9 @@ The ``Database`` class also provides properties and methods for introspecting th
|
||||||
``await db.table_columns(table)`` - list of strings
|
``await db.table_columns(table)`` - list of strings
|
||||||
Names of columns in a specific table.
|
Names of columns in a specific table.
|
||||||
|
|
||||||
|
``await db.table_column_details(table)`` - list of named tuples
|
||||||
|
Full details of the columns in a specific table. Each column is represented by a ``Column`` named tuple with fields ``cid`` (integer representing the column position), ``name`` (string), ``type`` (string, e.g. ``REAL`` or ``VARCHAR(30)``), ``notnull`` (integer 1 or 0), ``default_value`` (string or None), ``is_pk`` (integer 1 or 0).
|
||||||
|
|
||||||
``await db.primary_keys(table)`` - list of strings
|
``await db.primary_keys(table)`` - list of strings
|
||||||
Names of the columns that are part of the primary key for this table.
|
Names of the columns that are part of the primary key for this table.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -342,80 +342,96 @@ def test_sort_links(app_client):
|
||||||
}
|
}
|
||||||
for th in ths
|
for th in ths
|
||||||
]
|
]
|
||||||
assert [
|
assert attrs_and_link_attrs == [
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-Link"],
|
"class": ["col-Link"],
|
||||||
"data-column": "Link",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "Link",
|
||||||
|
"data-column-type": "",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": None,
|
"a_href": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-pk1"],
|
"class": ["col-pk1"],
|
||||||
"data-column": "pk1",
|
|
||||||
"data-is-pk": "1",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "pk1",
|
||||||
|
"data-column-type": "varchar(30)",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "1",
|
||||||
},
|
},
|
||||||
"a_href": None,
|
"a_href": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-pk2"],
|
"class": ["col-pk2"],
|
||||||
"data-column": "pk2",
|
|
||||||
"data-is-pk": "1",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "pk2",
|
||||||
|
"data-column-type": "varchar(30)",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "1",
|
||||||
},
|
},
|
||||||
"a_href": None,
|
"a_href": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-content"],
|
"class": ["col-content"],
|
||||||
"data-column": "content",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "content",
|
||||||
|
"data-column-type": "text",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": None,
|
"a_href": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-sortable"],
|
"class": ["col-sortable"],
|
||||||
"data-column": "sortable",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "sortable",
|
||||||
|
"data-column-type": "integer",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": "sortable?_sort_desc=sortable",
|
"a_href": "sortable?_sort_desc=sortable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-sortable_with_nulls"],
|
"class": ["col-sortable_with_nulls"],
|
||||||
"data-column": "sortable_with_nulls",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "sortable_with_nulls",
|
||||||
|
"data-column-type": "real",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": "sortable?_sort=sortable_with_nulls",
|
"a_href": "sortable?_sort=sortable_with_nulls",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-sortable_with_nulls_2"],
|
"class": ["col-sortable_with_nulls_2"],
|
||||||
"data-column": "sortable_with_nulls_2",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "sortable_with_nulls_2",
|
||||||
|
"data-column-type": "real",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": "sortable?_sort=sortable_with_nulls_2",
|
"a_href": "sortable?_sort=sortable_with_nulls_2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["col-text"],
|
"class": ["col-text"],
|
||||||
"data-column": "text",
|
|
||||||
"data-is-pk": "0",
|
|
||||||
"scope": "col",
|
"scope": "col",
|
||||||
|
"data-column": "text",
|
||||||
|
"data-column-type": "text",
|
||||||
|
"data-column-not-null": "0",
|
||||||
|
"data-is-pk": "0",
|
||||||
},
|
},
|
||||||
"a_href": "sortable?_sort=text",
|
"a_href": "sortable?_sort=text",
|
||||||
},
|
},
|
||||||
] == attrs_and_link_attrs
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_facet_display(app_client):
|
def test_facet_display(app_client):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue