diff --git a/datasette/database.py b/datasette/database.py index 7ba1456b..a9f39253 100644 --- a/datasette/database.py +++ b/datasette/database.py @@ -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)) diff --git a/datasette/static/app.css b/datasette/static/app.css index 4b43a9f0..da8ed2ab 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -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; } diff --git a/datasette/static/table.js b/datasette/static/table.js index 2c9057fc..7e839b9c 100644 --- a/datasette/static/table.js +++ b/datasette/static/table.js @@ -6,6 +6,7 @@ var DROPDOWN_HTML = ``; var DROPDOWN_ICON_SVG = ` @@ -115,10 +116,20 @@ var DROPDOWN_ICON_SVG = `
{% for column in display_columns %} - + {% if not column.sortable %} {{ column.name }} {% else %} diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 0c310f6a..7b8918a5 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -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() diff --git a/datasette/views/table.py b/datasette/views/table.py index 245bac9c..1bdb911e 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -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) diff --git a/docs/internals.rst b/docs/internals.rst index ff7e883c..bffda3f7 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -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. diff --git a/tests/test_html.py b/tests/test_html.py index 81d128c9..f3924082 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -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):