diff --git a/datasette/app.py b/datasette/app.py index 453ce5c0..37b199a4 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -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) diff --git a/datasette/cli.py b/datasette/cli.py index 446456f4..6fbc9908 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -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 diff --git a/datasette/templates/database.html b/datasette/templates/database.html index f64d5c90..f827e584 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -19,7 +19,7 @@ {% if config.allow_sql %}

Custom SQL query

-

+

{% endif %} @@ -56,7 +56,7 @@ {% endif %} -{% if config.allow_download %} +{% if config.allow_download and database != ":memory:" %}

Download SQLite DB: {{ database }}.db {{ format_bytes(size) }}

{% endif %} diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt index caa00e33..65b9aceb 100644 --- a/docs/datasette-serve-help.txt +++ b/docs/datasette-serve-help.txt @@ -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 diff --git a/tests/fixtures.py b/tests/fixtures.py index a77a3f4a..efd85fab 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -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) diff --git a/tests/test_api.py b/tests/test_api.py index 8cd1e94e..a6ba3f37 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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