Support for generated columns

* Support for generated columns, closes #1116
* Show SQLite version in pytest report header
* Use table_info() if SQLite < 3.26.0
* Cache sqlite_version() rather than re-calculate every time
* Adjust test_database_page for SQLite 3.26.0 or higher
This commit is contained in:
Simon Willison 2020-11-30 13:29:57 -08:00 committed by GitHub
commit 461670a0b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 135 additions and 26 deletions

View file

@ -19,15 +19,9 @@ import urllib
import numbers import numbers
import yaml import yaml
from .shutil_backport import copytree from .shutil_backport import copytree
from .sqlite import sqlite3, sqlite_version
from ..plugins import pm from ..plugins import pm
try:
import pysqlite3 as sqlite3
except ImportError:
import sqlite3
if hasattr(sqlite3, "enable_callback_tracebacks"):
sqlite3.enable_callback_tracebacks(True)
# From https://www.sqlite.org/lang_keywords.html # From https://www.sqlite.org/lang_keywords.html
reserved_words = set( reserved_words = set(
@ -64,7 +58,7 @@ HASH_LENGTH = 7
# Can replace this with Column from sqlite_utils when I add that dependency # Can replace this with Column from sqlite_utils when I add that dependency
Column = namedtuple( Column = namedtuple(
"Column", ("cid", "name", "type", "notnull", "default_value", "is_pk") "Column", ("cid", "name", "type", "notnull", "default_value", "is_pk", "hidden")
) )
@ -458,13 +452,10 @@ def temporary_docker_directory(
def detect_primary_keys(conn, table): def detect_primary_keys(conn, table):
" Figure out primary keys for a table. " " Figure out primary keys for a table. "
table_info_rows = [ columns = table_column_details(conn, table)
row pks = [column for column in columns if column.is_pk]
for row in conn.execute(f'PRAGMA table_info("{table}")').fetchall() pks.sort(key=lambda column: column.is_pk)
if row[-1] return [column.name for column in pks]
]
table_info_rows.sort(key=lambda row: row[-1])
return [str(r[1]) for r in table_info_rows]
def get_outbound_foreign_keys(conn, table): def get_outbound_foreign_keys(conn, table):
@ -570,9 +561,21 @@ def table_columns(conn, table):
def table_column_details(conn, table): def table_column_details(conn, table):
if sqlite_version() >= (3, 26, 0):
# table_xinfo was added in 3.26.0
return [ return [
Column(*r) Column(*r)
for r in conn.execute(f"PRAGMA table_info({escape_sqlite(table)});").fetchall() for r in conn.execute(
f"PRAGMA table_xinfo({escape_sqlite(table)});"
).fetchall()
]
else:
# Treat hidden as 0 for all columns
return [
Column(*(list(r) + [0]))
for r in conn.execute(
f"PRAGMA table_info({escape_sqlite(table)});"
).fetchall()
] ]

28
datasette/utils/sqlite.py Normal file
View file

@ -0,0 +1,28 @@
try:
import pysqlite3 as sqlite3
except ImportError:
import sqlite3
if hasattr(sqlite3, "enable_callback_tracebacks"):
sqlite3.enable_callback_tracebacks(True)
_cached_sqlite_version = None
def sqlite_version():
global _cached_sqlite_version
if _cached_sqlite_version is None:
_cached_sqlite_version = _sqlite_version()
return _cached_sqlite_version
def _sqlite_version():
return tuple(
map(
int,
sqlite3.connect(":memory:")
.execute("select sqlite_version()")
.fetchone()[0]
.split("."),
)
)

View file

@ -3,6 +3,11 @@ import pathlib
import pytest import pytest
import re import re
try:
import pysqlite3 as sqlite3
except ImportError:
import sqlite3
UNDOCUMENTED_PERMISSIONS = { UNDOCUMENTED_PERMISSIONS = {
"this_is_allowed", "this_is_allowed",
"this_is_denied", "this_is_denied",
@ -12,6 +17,12 @@ UNDOCUMENTED_PERMISSIONS = {
} }
def pytest_report_header(config):
return "SQLite: {}".format(
sqlite3.connect(":memory:").execute("select sqlite_version()").fetchone()[0]
)
def pytest_configure(config): def pytest_configure(config):
import sys import sys

View file

@ -1,5 +1,5 @@
from datasette.app import Datasette from datasette.app import Datasette
from datasette.utils import sqlite3 from datasette.utils.sqlite import sqlite3
from datasette.utils.testing import TestClient from datasette.utils.testing import TestClient
import click import click
import contextlib import contextlib

View file

@ -1,5 +1,7 @@
from datasette.app import Datasette
from datasette.plugins import DEFAULT_PLUGINS from datasette.plugins import DEFAULT_PLUGINS
from datasette.utils import detect_json1 from datasette.utils import detect_json1
from datasette.utils.sqlite import sqlite3, sqlite_version
from datasette.version import __version__ from datasette.version import __version__
from .fixtures import ( # noqa from .fixtures import ( # noqa
app_client, app_client,
@ -514,7 +516,20 @@ def test_database_page(app_client):
}, },
{ {
"name": "searchable_fts", "name": "searchable_fts",
"columns": ["text1", "text2", "name with . and spaces"], "columns": [
"text1",
"text2",
"name with . and spaces",
]
+ (
[
"searchable_fts",
"docid",
"__langid",
]
if sqlite_version() >= (3, 26, 0)
else []
),
"primary_keys": [], "primary_keys": [],
"count": 2, "count": 2,
"hidden": True, "hidden": True,
@ -1913,3 +1928,37 @@ def test_paginate_using_link_header(app_client, qs):
else: else:
path = None path = None
assert num_pages == 21 assert num_pages == 21
@pytest.mark.skipif(
sqlite_version() < (3, 31, 0),
reason="generated columns were added in SQLite 3.31.0",
)
@pytest.mark.asyncio
async def test_generated_columns_are_visible_in_datasette(tmp_path_factory):
db_directory = tmp_path_factory.mktemp("dbs")
db_path = db_directory / "test.db"
conn = sqlite3.connect(str(db_path))
conn.executescript(
"""
CREATE TABLE deeds (
body TEXT,
id INT GENERATED ALWAYS AS (json_extract(body, '$.id')) STORED,
consideration INT GENERATED ALWAYS AS (json_extract(body, '$.consideration')) STORED
);
INSERT INTO deeds (body) VALUES ('{
"id": 1,
"consideration": "This is the consideration"
}');
"""
)
datasette = Datasette([db_path])
response = await datasette.client.get("/test/deeds.json?_shape=array")
assert response.json() == [
{
"rowid": 1,
"body": '{\n "id": 1,\n "consideration": "This is the consideration"\n }',
"id": 1,
"consideration": "This is the consideration",
}
]

View file

@ -1,9 +1,9 @@
import json import json
import pytest import pytest
import sqlite3
from datasette.app import Datasette from datasette.app import Datasette
from datasette.cli import cli from datasette.cli import cli
from datasette.utils.sqlite import sqlite3
from .fixtures import TestClient as _TestClient from .fixtures import TestClient as _TestClient
from click.testing import CliRunner from click.testing import CliRunner

View file

@ -2,7 +2,8 @@
Tests for the datasette.database.Database class Tests for the datasette.database.Database class
""" """
from datasette.database import Database, Results, MultipleValues from datasette.database import Database, Results, MultipleValues
from datasette.utils import sqlite3, Column from datasette.utils.sqlite import sqlite3
from datasette.utils import Column
from .fixtures import app_client from .fixtures import app_client
import pytest import pytest
import time import time
@ -120,6 +121,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=1, is_pk=1,
hidden=0,
), ),
Column( Column(
cid=1, cid=1,
@ -128,6 +130,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=2, cid=2,
@ -136,6 +139,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=3, cid=3,
@ -144,6 +148,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=4, cid=4,
@ -152,6 +157,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=5, cid=5,
@ -160,6 +166,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=6, cid=6,
@ -168,6 +175,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=7, cid=7,
@ -176,6 +184,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=8, cid=8,
@ -184,6 +193,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=9, cid=9,
@ -192,6 +202,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
], ],
), ),
@ -205,6 +216,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=1, is_pk=1,
hidden=0,
), ),
Column( Column(
cid=1, cid=1,
@ -213,6 +225,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=2, is_pk=2,
hidden=0,
), ),
Column( Column(
cid=2, cid=2,
@ -221,6 +234,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=3, cid=3,
@ -229,6 +243,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=4, cid=4,
@ -237,6 +252,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=5, cid=5,
@ -245,6 +261,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
Column( Column(
cid=6, cid=6,
@ -253,6 +270,7 @@ async def test_table_columns(db, table, expected):
notnull=0, notnull=0,
default_value=None, default_value=None,
is_pk=0, is_pk=0,
hidden=0,
), ),
], ],
), ),

View file

@ -9,14 +9,14 @@ from .fixtures import (
from datasette.app import Datasette from datasette.app import Datasette
from datasette import cli from datasette import cli
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
from datasette.utils import sqlite3, CustomRow from datasette.utils.sqlite import sqlite3
from datasette.utils import CustomRow
from jinja2.environment import Template from jinja2.environment import Template
import base64 import base64
import json import json
import os import os
import pathlib import pathlib
import re import re
import sqlite3
import textwrap import textwrap
import pytest import pytest
import urllib import urllib

View file

@ -4,11 +4,11 @@ Tests for various datasette helper functions.
from datasette.app import Datasette from datasette.app import Datasette
from datasette import utils from datasette import utils
from datasette.utils.asgi import Request from datasette.utils.asgi import Request
from datasette.utils.sqlite import sqlite3
import json import json
import os import os
import pathlib import pathlib
import pytest import pytest
import sqlite3
import tempfile import tempfile
from unittest.mock import patch from unittest.mock import patch