mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
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.
This commit is contained in:
parent
bf6b0f918d
commit
9743e1d91b
6 changed files with 90 additions and 33 deletions
|
|
@ -42,7 +42,7 @@ from .version import __version__
|
||||||
app_root = Path(__file__).parent.parent
|
app_root = Path(__file__).parent.parent
|
||||||
|
|
||||||
connections = threading.local()
|
connections = threading.local()
|
||||||
|
MEMORY = object()
|
||||||
|
|
||||||
ConfigOption = collections.namedtuple(
|
ConfigOption = collections.namedtuple(
|
||||||
"ConfigOption", ("name", "default", "help")
|
"ConfigOption", ("name", "default", "help")
|
||||||
|
|
@ -123,10 +123,15 @@ class Datasette:
|
||||||
template_dir=None,
|
template_dir=None,
|
||||||
plugins_dir=None,
|
plugins_dir=None,
|
||||||
static_mounts=None,
|
static_mounts=None,
|
||||||
|
memory=False,
|
||||||
config=None,
|
config=None,
|
||||||
version_note=None,
|
version_note=None,
|
||||||
):
|
):
|
||||||
self.files = files
|
self.files = files
|
||||||
|
if not self.files:
|
||||||
|
self.files = [MEMORY]
|
||||||
|
elif memory:
|
||||||
|
self.files = (MEMORY,) + self.files
|
||||||
self.cache_headers = cache_headers
|
self.cache_headers = cache_headers
|
||||||
self.cors = cors
|
self.cors = cors
|
||||||
self._inspect = inspect_data
|
self._inspect = inspect_data
|
||||||
|
|
@ -296,31 +301,40 @@ class Datasette:
|
||||||
|
|
||||||
self._inspect = {}
|
self._inspect = {}
|
||||||
for filename in self.files:
|
for filename in self.files:
|
||||||
path = Path(filename)
|
if filename is MEMORY:
|
||||||
name = path.stem
|
self._inspect[":memory:"] = {
|
||||||
if name in self._inspect:
|
"hash": "000",
|
||||||
raise Exception("Multiple files with same stem %s" % name)
|
"file": ":memory:",
|
||||||
try:
|
"size": 0,
|
||||||
with sqlite3.connect(
|
"views": {},
|
||||||
"file:{}?immutable=1".format(path), uri=True
|
"tables": {},
|
||||||
) as conn:
|
}
|
||||||
self.prepare_connection(conn)
|
else:
|
||||||
self._inspect[name] = {
|
path = Path(filename)
|
||||||
"hash": inspect_hash(path),
|
name = path.stem
|
||||||
"file": str(path),
|
if name in self._inspect:
|
||||||
"size": path.stat().st_size,
|
raise Exception("Multiple files with same stem %s" % name)
|
||||||
"views": inspect_views(conn),
|
try:
|
||||||
"tables": inspect_tables(conn, (self.metadata("databases") or {}).get(name, {}))
|
with sqlite3.connect(
|
||||||
}
|
"file:{}?immutable=1".format(path), uri=True
|
||||||
except sqlite3.OperationalError as e:
|
) as conn:
|
||||||
if (e.args[0] == 'no such module: VirtualSpatialIndex'):
|
self.prepare_connection(conn)
|
||||||
raise click.UsageError(
|
self._inspect[name] = {
|
||||||
"It looks like you're trying to load a SpatiaLite"
|
"hash": inspect_hash(path),
|
||||||
" database without first loading the SpatiaLite module."
|
"file": str(path),
|
||||||
"\n\nRead more: https://datasette.readthedocs.io/en/latest/spatialite.html"
|
"size": path.stat().st_size,
|
||||||
)
|
"views": inspect_views(conn),
|
||||||
else:
|
"tables": inspect_tables(conn, (self.metadata("databases") or {}).get(name, {}))
|
||||||
raise
|
}
|
||||||
|
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
|
return self._inspect
|
||||||
|
|
||||||
def register_custom_units(self):
|
def register_custom_units(self):
|
||||||
|
|
@ -403,11 +417,14 @@ class Datasette:
|
||||||
conn = getattr(connections, db_name, None)
|
conn = getattr(connections, db_name, None)
|
||||||
if not conn:
|
if not conn:
|
||||||
info = self.inspect()[db_name]
|
info = self.inspect()[db_name]
|
||||||
conn = sqlite3.connect(
|
if info["file"] == ":memory:":
|
||||||
"file:{}?immutable=1".format(info["file"]),
|
conn = sqlite3.connect(":memory:")
|
||||||
uri=True,
|
else:
|
||||||
check_same_thread=False,
|
conn = sqlite3.connect(
|
||||||
)
|
"file:{}?immutable=1".format(info["file"]),
|
||||||
|
uri=True,
|
||||||
|
check_same_thread=False,
|
||||||
|
)
|
||||||
self.prepare_connection(conn)
|
self.prepare_connection(conn)
|
||||||
setattr(connections, db_name, conn)
|
setattr(connections, db_name, conn)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,9 @@ def package(
|
||||||
help="mountpoint:path-to-directory for serving static files",
|
help="mountpoint:path-to-directory for serving static files",
|
||||||
multiple=True,
|
multiple=True,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--memory", is_flag=True, help="Make :memory: database available"
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--config",
|
"--config",
|
||||||
type=Config(),
|
type=Config(),
|
||||||
|
|
@ -340,6 +343,7 @@ def serve(
|
||||||
template_dir,
|
template_dir,
|
||||||
plugins_dir,
|
plugins_dir,
|
||||||
static,
|
static,
|
||||||
|
memory,
|
||||||
config,
|
config,
|
||||||
version_note,
|
version_note,
|
||||||
help_config,
|
help_config,
|
||||||
|
|
@ -384,6 +388,7 @@ def serve(
|
||||||
plugins_dir=plugins_dir,
|
plugins_dir=plugins_dir,
|
||||||
static_mounts=static,
|
static_mounts=static,
|
||||||
config=dict(config),
|
config=dict(config),
|
||||||
|
memory=memory,
|
||||||
version_note=version_note,
|
version_note=version_note,
|
||||||
)
|
)
|
||||||
# Force initial hashing/table counting
|
# Force initial hashing/table counting
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
{% if config.allow_sql %}
|
{% if config.allow_sql %}
|
||||||
<form class="sql" action="/{{ database }}-{{ database_hash }}" method="get">
|
<form class="sql" action="/{{ database }}-{{ database_hash }}" method="get">
|
||||||
<h3>Custom SQL query</h3>
|
<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>
|
<p><input type="submit" value="Run SQL"></p>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% 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>
|
<p class="download-sqlite">Download SQLite DB: <a href="/{{ database }}-{{ database_hash }}.db">{{ database }}.db</a> <em>{{ format_bytes(size) }}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ Options:
|
||||||
--template-dir DIRECTORY Path to directory containing custom templates
|
--template-dir DIRECTORY Path to directory containing custom templates
|
||||||
--plugins-dir DIRECTORY Path to directory containing custom plugins
|
--plugins-dir DIRECTORY Path to directory containing custom plugins
|
||||||
--static STATIC MOUNT mountpoint:path-to-directory for serving static files
|
--static STATIC MOUNT mountpoint:path-to-directory for serving static files
|
||||||
|
--memory Make :memory: database available
|
||||||
--config CONFIG Set config option using configname:value
|
--config CONFIG Set config option using configname:value
|
||||||
datasette.readthedocs.io/en/latest/config.html
|
datasette.readthedocs.io/en/latest/config.html
|
||||||
--version-note TEXT Additional note to show on /-/versions
|
--version-note TEXT Additional note to show on /-/versions
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,14 @@ def app_client():
|
||||||
yield from make_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')
|
@pytest.fixture(scope='session')
|
||||||
def app_client_shorter_time_limit():
|
def app_client_shorter_time_limit():
|
||||||
yield from make_app_client(20)
|
yield from make_app_client(20)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from .fixtures import ( # noqa
|
from .fixtures import ( # noqa
|
||||||
app_client,
|
app_client,
|
||||||
|
app_client_no_files,
|
||||||
app_client_shorter_time_limit,
|
app_client_shorter_time_limit,
|
||||||
app_client_larger_cache_size,
|
app_client_larger_cache_size,
|
||||||
app_client_returned_rows_matches_page_size,
|
app_client_returned_rows_matches_page_size,
|
||||||
|
|
@ -368,6 +369,31 @@ def test_database_page(app_client):
|
||||||
}] == data['tables']
|
}] == 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):
|
def test_database_page_for_database_with_dot_in_name(app_client_with_dot):
|
||||||
response = app_client_with_dot.get("/fixtures.dot.json")
|
response = app_client_with_dot.get("/fixtures.dot.json")
|
||||||
assert 200 == response.status
|
assert 200 == response.status
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue