Skip to content

Commit

Permalink
Database(memory_name=) for shared in-memory databases, closes #1151
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Dec 18, 2020
1 parent 6119bd7 commit 5e9895c
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 5 deletions.
24 changes: 22 additions & 2 deletions datasette/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@


class Database:
def __init__(self, ds, path=None, is_mutable=False, is_memory=False):
def __init__(
self, ds, path=None, is_mutable=False, is_memory=False, memory_name=None
):
self.ds = ds
self.path = path
self.is_mutable = is_mutable
self.is_memory = is_memory
self.memory_name = memory_name
if memory_name is not None:
self.path = memory_name
self.is_memory = True
self.is_mutable = True
self.hash = None
self.cached_size = None
self.cached_table_counts = None
Expand All @@ -46,6 +53,16 @@ def __init__(self, ds, path=None, is_mutable=False, is_memory=False):
}

def connect(self, write=False):
if self.memory_name:
uri = "file:{}?mode=memory&cache=shared".format(self.memory_name)
conn = sqlite3.connect(
uri,
uri=True,
check_same_thread=False,
)
if not write:
conn.execute("PRAGMA query_only=1")
return conn
if self.is_memory:
return sqlite3.connect(":memory:")
# mode=ro or immutable=1?
Expand Down Expand Up @@ -215,7 +232,10 @@ def mtime_ns(self):
@property
def name(self):
if self.is_memory:
return ":memory:"
if self.memory_name:
return ":memory:{}".format(self.memory_name)
else:
return ":memory:"
else:
return Path(self.path).stem

Expand Down
37 changes: 34 additions & 3 deletions docs/internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,16 @@ The ``db`` parameter should be an instance of the ``datasette.database.Database`
This will add a mutable database from the provided file path.

The ``Database()`` constructor takes four arguments: the first is the ``datasette`` instance you are attaching to, the second is a ``path=``, then ``is_mutable`` and ``is_memory`` are both optional arguments.
To create a shared in-memory database named ``statistics``, use the following:

Use ``is_mutable`` if it is possible that updates will be made to that database - otherwise Datasette will open it in immutable mode and any changes could cause undesired behavior.
.. code-block:: python
from datasette.database import Database
Use ``is_memory`` if the connection is to an in-memory SQLite database.
datasette.add_database("statistics", Database(
datasette,
memory_name="statistics"
))
.. _datasette_remove_database:

Expand Down Expand Up @@ -480,6 +485,32 @@ Database class

Instances of the ``Database`` class can be used to execute queries against attached SQLite databases, and to run introspection against their schemas.

.. _database_constructor:

Database(ds, path=None, is_mutable=False, is_memory=False, memory_name=None)
----------------------------------------------------------------------------

The ``Database()`` constructor can be used by plugins, in conjunction with :ref:`datasette_add_database`, to create and register new databases.

The arguments are as follows:

``ds`` - :ref:`internals_datasette` (required)
The Datasette instance you are attaching this database to.

``path`` - string
Path to a SQLite database file on disk.

``is_mutable`` - boolean
Set this to ``True`` if it is possible that updates will be made to that database - otherwise Datasette will open it in immutable mode and any changes could cause undesired behavior.

``is_memory`` - boolean
Use this to create non-shared memory connections.

``memory_name`` - string or ``None``
Use this to create a named in-memory database. Unlike regular memory databases these can be accessed by multiple threads and will persist an changes made to them for the lifetime of the Datasette server process.

The first argument is the ``datasette`` instance you are attaching to, the second is a ``path=``, then ``is_mutable`` and ``is_memory`` are both optional arguments.

.. _database_execute:

await db.execute(sql, ...)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_internals_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,33 @@ def test_mtime_ns_is_none_for_memory(app_client):
def test_is_mutable(app_client):
assert Database(app_client.ds, is_memory=True, is_mutable=True).is_mutable is True
assert Database(app_client.ds, is_memory=True, is_mutable=False).is_mutable is False


@pytest.mark.asyncio
async def test_database_memory_name(app_client):
ds = app_client.ds
foo1 = Database(ds, memory_name="foo")
foo2 = Database(ds, memory_name="foo")
bar1 = Database(ds, memory_name="bar")
bar2 = Database(ds, memory_name="bar")
for db in (foo1, foo2, bar1, bar2):
table_names = await db.table_names()
assert table_names == []
# Now create a table in foo
await foo1.execute_write("create table foo (t text)", block=True)
assert await foo1.table_names() == ["foo"]
assert await foo2.table_names() == ["foo"]
assert await bar1.table_names() == []
assert await bar2.table_names() == []


@pytest.mark.asyncio
async def test_in_memory_databases_forbid_writes(app_client):
ds = app_client.ds
db = Database(ds, memory_name="test")
with pytest.raises(sqlite3.OperationalError):
await db.execute("create table foo (t text)")
assert await db.table_names() == []
# Using db.execute_write() should work:
await db.execute_write("create table foo (t text)", block=True)
assert await db.table_names() == ["foo"]

0 comments on commit 5e9895c

Please sign in to comment.