Support for :memory: databases

If you start Datasette with no files, it will connect to :memory: instead.

When starting it with files you can add --memory to also get a :memory: database.
optional-hash
Simon Willison 2019-03-14 16:42:38 -07:00
rodzic bf6b0f918d
commit 9743e1d91b
6 zmienionych plików z 90 dodań i 33 usunięć

Wyświetl plik

@ -42,7 +42,7 @@ from .version import __version__
app_root = Path(__file__).parent.parent
connections = threading.local()
MEMORY = object()
ConfigOption = collections.namedtuple(
"ConfigOption", ("name", "default", "help")
@ -123,10 +123,15 @@ class Datasette:
template_dir=None,
plugins_dir=None,
static_mounts=None,
memory=False,
config=None,
version_note=None,
):
self.files = files
if not self.files:
self.files = [MEMORY]
elif memory:
self.files = (MEMORY,) + self.files
self.cache_headers = cache_headers
self.cors = cors
self._inspect = inspect_data
@ -296,31 +301,40 @@ class Datasette:
self._inspect = {}
for filename in self.files:
path = Path(filename)
name = path.stem
if name in self._inspect:
raise Exception("Multiple files with same stem %s" % name)
try:
with sqlite3.connect(
"file:{}?immutable=1".format(path), uri=True
) as conn:
self.prepare_connection(conn)
self._inspect[name] = {
"hash": inspect_hash(path),
"file": str(path),
"size": path.stat().st_size,
"views": inspect_views(conn),
"tables": inspect_tables(conn, (self.metadata("databases") or {}).get(name, {}))
}
except sqlite3.OperationalError as e:
if (e.args[0] == 'no such module: VirtualSpatialIndex'):
raise click.UsageError(
"It looks like you're trying to load a SpatiaLite"
" database without first loading the SpatiaLite module."
"\n\nRead more: https://datasette.readthedocs.io/en/latest/spatialite.html"
)
else:
raise
if filename is MEMORY:
self._inspect[":memory:"] = {
"hash": "000",
"file": ":memory:",
"size": 0,
"views": {},
"tables": {},
}
else:
path = Path(filename)
name = path.stem
if name in self._inspect:
raise Exception("Multiple files with same stem %s" % name)
try:
with sqlite3.connect(
"file:{}?immutable=1".format(path), uri=True
) as conn:
self.prepare_connection(conn)
self._inspect[name] = {
"hash": inspect_hash(path),
"file": str(path),
"size": path.stat().st_size,
"views": inspect_views(conn),
"tables": inspect_tables(conn, (self.metadata("databases") or {}).get(name, {}))
}
except sqlite3.OperationalError as e:
if (e.args[0] == 'no such module: VirtualSpatialIndex'):
raise click.UsageError(
"It looks like you're trying to load a SpatiaLite"
" database without first loading the SpatiaLite module."
"\n\nRead more: https://datasette.readthedocs.io/en/latest/spatialite.html"
)
else:
raise
return self._inspect
def register_custom_units(self):
@ -403,11 +417,14 @@ class Datasette:
conn = getattr(connections, db_name, None)
if not conn:
info = self.inspect()[db_name]
conn = sqlite3.connect(
"file:{}?immutable=1".format(info["file"]),
uri=True,
check_same_thread=False,
)
if info["file"] == ":memory:":
conn = sqlite3.connect(":memory:")
else:
conn = sqlite3.connect(
"file:{}?immutable=1".format(info["file"]),
uri=True,
check_same_thread=False,
)
self.prepare_connection(conn)
setattr(connections, db_name, conn)

Wyświetl plik

@ -315,6 +315,9 @@ def package(
help="mountpoint:path-to-directory for serving static files",
multiple=True,
)
@click.option(
"--memory", is_flag=True, help="Make :memory: database available"
)
@click.option(
"--config",
type=Config(),
@ -340,6 +343,7 @@ def serve(
template_dir,
plugins_dir,
static,
memory,
config,
version_note,
help_config,
@ -384,6 +388,7 @@ def serve(
plugins_dir=plugins_dir,
static_mounts=static,
config=dict(config),
memory=memory,
version_note=version_note,
)
# Force initial hashing/table counting

Wyświetl plik

@ -19,7 +19,7 @@
{% if config.allow_sql %}
<form class="sql" action="/{{ database }}-{{ database_hash }}" method="get">
<h3>Custom SQL query</h3>
<p><textarea name="sql">select * from {{ tables[0].name|escape_sqlite }}</textarea></p>
<p><textarea name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></p>
<p><input type="submit" value="Run SQL"></p>
</form>
{% endif %}
@ -56,7 +56,7 @@
</ul>
{% endif %}
{% if config.allow_download %}
{% if config.allow_download and database != ":memory:" %}
<p class="download-sqlite">Download SQLite DB: <a href="/{{ database }}-{{ database_hash }}.db">{{ database }}.db</a> <em>{{ format_bytes(size) }}</em></p>
{% endif %}

Wyświetl plik

@ -17,6 +17,7 @@ Options:
--template-dir DIRECTORY Path to directory containing custom templates
--plugins-dir DIRECTORY Path to directory containing custom plugins
--static STATIC MOUNT mountpoint:path-to-directory for serving static files
--memory Make :memory: database available
--config CONFIG Set config option using configname:value
datasette.readthedocs.io/en/latest/config.html
--version-note TEXT Additional note to show on /-/versions

Wyświetl plik

@ -65,6 +65,14 @@ def app_client():
yield from make_app_client()
@pytest.fixture(scope="session")
def app_client_no_files():
ds = Datasette([])
client = TestClient(ds.app().test_client)
client.ds = ds
yield client
@pytest.fixture(scope='session')
def app_client_shorter_time_limit():
yield from make_app_client(20)

Wyświetl plik

@ -1,5 +1,6 @@
from .fixtures import ( # noqa
app_client,
app_client_no_files,
app_client_shorter_time_limit,
app_client_larger_cache_size,
app_client_returned_rows_matches_page_size,
@ -368,6 +369,31 @@ def test_database_page(app_client):
}] == data['tables']
def test_no_files_uses_memory_database(app_client_no_files):
response = app_client_no_files.get("/.json")
assert response.status == 200
assert {
":memory:": {
"hash": "000",
"hidden_table_rows_sum": 0,
"hidden_tables_count": 0,
"name": ":memory:",
"path": ":memory:-000",
"table_rows_sum": 0,
"tables_count": 0,
"tables_more": False,
"tables_truncated": [],
"views_count": 0,
}
} == response.json
# Try that SQL query
response = app_client_no_files.get(
"/:memory:-0.json?sql=select+sqlite_version()&_shape=array"
)
assert 1 == len(response.json)
assert ["sqlite_version()"] == list(response.json[0].keys())
def test_database_page_for_database_with_dot_in_name(app_client_with_dot):
response = app_client_with_dot.get("/fixtures.dot.json")
assert 200 == response.status