Display column type in column action menu, closes #993

Also added new documented db.table_column_details() introspection method.
This commit is contained in:
Simon Willison 2020-10-05 17:32:10 -07:00
commit 5a184a5d21
8 changed files with 103 additions and 28 deletions

View file

@ -16,6 +16,7 @@ from .utils import (
sqlite_timelimit,
sqlite3,
table_columns,
table_column_details,
)
from .inspect import inspect_hash
@ -231,6 +232,9 @@ class Database:
async def table_columns(self, 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):
return await self.execute_fn(lambda conn: detect_primary_keys(conn, table))

View file

@ -396,7 +396,6 @@ svg.dropdown-menu-icon {
opacity: 0.8;
}
.dropdown-menu {
display: inline-flex;
border: 1px solid #ccc;
border-radius: 4px;
line-height: 1.4;
@ -410,6 +409,13 @@ svg.dropdown-menu-icon {
margin: 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 {
border-bottom: 1px solid #ccc;
}

View file

@ -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-not-blank" href="#">Show not-blank rows</a></li>
</ul>
<p class="dropdown-column-type"></p>
</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">
@ -115,10 +116,20 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
} else {
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.top = (menuTop + 6) + 'px';
menu.style.left = menuLeft + 'px';
menu.style.display = 'inline-flex';
menu.style.display = 'block';
}
var svg = document.createElement('div');
svg.innerHTML = DROPDOWN_ICON_SVG;

View file

@ -3,7 +3,7 @@
<thead>
<tr>
{% 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 %}
{{ column.name }}
{% else %}

View file

@ -1,6 +1,6 @@
import asyncio
from contextlib import contextmanager
from collections import OrderedDict
from collections import OrderedDict, namedtuple
import base64
import click
import hashlib
@ -54,6 +54,11 @@ RUN apt-get update && \
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):
if callable(value):
@ -525,8 +530,12 @@ def detect_json1(conn=None):
def table_columns(conn, table):
return [column.name for column in table_column_details(conn, table)]
def table_column_details(conn, table):
return [
r[1]
Column(*r)
for r in conn.execute(
"PRAGMA table_info({});".format(escape_sqlite(table))
).fetchall()

View file

@ -90,19 +90,31 @@ class RowTableShared(DataView):
"Returns columns, rows for specified table - including fancy foreign key treatment"
db = self.ds.databases[database]
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)
pks = await db.primary_keys(table)
pks_for_display = pks
if not pks_for_display:
pks_for_display = ["rowid"]
columns = [
{
"name": r[0],
"sortable": r[0] in sortable_columns,
"is_pk": r[0] in pks_for_display,
}
for r in description
]
columns = []
for r in description:
if r[0] == "rowid" and "rowid" not in column_details:
type_ = "integer"
notnull = 0
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 = {
fk["column"]: fk["other_table"]
for fk in await db.foreign_keys_for_table(table)
@ -217,12 +229,25 @@ class RowTableShared(DataView):
# Add the link column header.
# If it's a simple primary key, we have to remove and re-add that column name at
# the beginning of the header row.
first_column = None
if len(pks) == 1:
columns = [col for col in columns if col["name"] != pks[0]]
columns = [
{"name": pks[0] if len(pks) == 1 else "Link", "sortable": len(pks) == 1}
] + columns
first_column = {
"name": pks[0],
"sortable": len(pks) == 1,
"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
@ -291,7 +316,8 @@ class TableView(RowTableShared):
)
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)

View file

@ -500,6 +500,9 @@ The ``Database`` class also provides properties and methods for introspecting th
``await db.table_columns(table)`` - list of strings
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
Names of the columns that are part of the primary key for this table.

View file

@ -342,80 +342,96 @@ def test_sort_links(app_client):
}
for th in ths
]
assert [
assert attrs_and_link_attrs == [
{
"attrs": {
"class": ["col-Link"],
"data-column": "Link",
"data-is-pk": "0",
"scope": "col",
"data-column": "Link",
"data-column-type": "",
"data-column-not-null": "0",
"data-is-pk": "0",
},
"a_href": None,
},
{
"attrs": {
"class": ["col-pk1"],
"data-column": "pk1",
"data-is-pk": "1",
"scope": "col",
"data-column": "pk1",
"data-column-type": "varchar(30)",
"data-column-not-null": "0",
"data-is-pk": "1",
},
"a_href": None,
},
{
"attrs": {
"class": ["col-pk2"],
"data-column": "pk2",
"data-is-pk": "1",
"scope": "col",
"data-column": "pk2",
"data-column-type": "varchar(30)",
"data-column-not-null": "0",
"data-is-pk": "1",
},
"a_href": None,
},
{
"attrs": {
"class": ["col-content"],
"data-column": "content",
"data-is-pk": "0",
"scope": "col",
"data-column": "content",
"data-column-type": "text",
"data-column-not-null": "0",
"data-is-pk": "0",
},
"a_href": None,
},
{
"attrs": {
"class": ["col-sortable"],
"data-column": "sortable",
"data-is-pk": "0",
"scope": "col",
"data-column": "sortable",
"data-column-type": "integer",
"data-column-not-null": "0",
"data-is-pk": "0",
},
"a_href": "sortable?_sort_desc=sortable",
},
{
"attrs": {
"class": ["col-sortable_with_nulls"],
"data-column": "sortable_with_nulls",
"data-is-pk": "0",
"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",
},
{
"attrs": {
"class": ["col-sortable_with_nulls_2"],
"data-column": "sortable_with_nulls_2",
"data-is-pk": "0",
"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",
},
{
"attrs": {
"class": ["col-text"],
"data-column": "text",
"data-is-pk": "0",
"scope": "col",
"data-column": "text",
"data-column-type": "text",
"data-column-not-null": "0",
"data-is-pk": "0",
},
"a_href": "sortable?_sort=text",
},
] == attrs_and_link_attrs
]
def test_facet_display(app_client):