mirror of
https://github.com/simonw/datasette.git
synced 2026-10-21 00:36:59 +02:00
112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
import errno
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from datasette.app import Datasette
|
|
from datasette.connection_pool import ConnectionLeaseError
|
|
|
|
|
|
@pytest.fixture
|
|
def database_file(tmp_path):
|
|
path = tmp_path / "data.db"
|
|
conn = sqlite3.connect(path)
|
|
conn.executescript("create table t(id); insert into t values (1), (2)")
|
|
conn.close()
|
|
return str(path)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("kind", ["read", "write", "isolated"])
|
|
async def test_saved_methods_and_cursors_cannot_escape_callback(database_file, kind):
|
|
ds = Datasette([database_file], settings={"schema_watch_interval_ms": 0})
|
|
db = ds.get_database("data")
|
|
saved = {}
|
|
|
|
def callback(conn):
|
|
saved["commit"] = conn.commit
|
|
saved["cursor"] = conn.execute("select * from t")
|
|
saved["fetch"] = saved["cursor"].fetchone
|
|
saved["connection"] = saved["cursor"].connection
|
|
assert saved["fetch"]()[0] == 1
|
|
|
|
method = {
|
|
"read": db.execute_fn,
|
|
"write": db.execute_write_fn,
|
|
"isolated": db.execute_isolated_fn,
|
|
}[kind]
|
|
try:
|
|
await method(callback)
|
|
for call in (saved["commit"], lambda: saved["connection"].execute("select 1")):
|
|
with pytest.raises(ConnectionLeaseError):
|
|
call()
|
|
for call in (saved["fetch"], lambda: next(saved["cursor"])):
|
|
with pytest.raises((ConnectionLeaseError, sqlite3.ProgrammingError)):
|
|
call()
|
|
assert (await db.execute("select count(*) from t")).single_value() == 2
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_result_containing_cursor_is_rejected(database_file):
|
|
ds = Datasette([database_file])
|
|
db = ds.get_database("data")
|
|
try:
|
|
with pytest.raises(ConnectionLeaseError):
|
|
await db.execute_fn(lambda c: {"nested": [c.execute("select * from t")]})
|
|
result = await db.execute_write_fn(
|
|
lambda c: {"nested": [c.execute("insert into t values (3)")]}
|
|
)
|
|
assert result["nested"][0].rowcount == 1
|
|
with pytest.raises((ConnectionLeaseError, sqlite3.ProgrammingError)):
|
|
result["nested"][0].fetchone()
|
|
assert (await db.execute("select count(*) from t")).single_value() == 3
|
|
finally:
|
|
ds.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"failure",
|
|
[
|
|
sqlite3.OperationalError("unable to open database file"),
|
|
OSError(errno.EMFILE, "Too many open files"),
|
|
],
|
|
)
|
|
async def test_schema_recovers_after_resource_error_without_file_change(
|
|
database_file, monkeypatch, failure
|
|
):
|
|
ds = Datasette([database_file], settings={"schema_watch_interval_ms": 0})
|
|
try:
|
|
await ds.invoke_startup()
|
|
db = ds.get_database("data")
|
|
conn = sqlite3.connect(database_file)
|
|
conn.execute("create table added_externally(id)")
|
|
conn.close()
|
|
watcher = ds._schema_watcher
|
|
original = watcher._connect
|
|
|
|
def fail(database, prepare):
|
|
if database is db and prepare:
|
|
raise failure
|
|
return original(database, prepare)
|
|
|
|
with monkeypatch.context() as patch:
|
|
patch.setattr(watcher, "_connect", fail)
|
|
await ds.refresh_schemas()
|
|
state = db._watch_state
|
|
assert state.needs_scan
|
|
# Move past the retry backoff and fingerprint race window without
|
|
# touching the file or relying on timing-sensitive sleeps.
|
|
state.retry_at = 0
|
|
state.fp_racy = False
|
|
await watcher.sweep(background=True)
|
|
tables = await ds.get_internal_database().execute(
|
|
"select table_name from catalog_tables where database_name=?", ["data"]
|
|
)
|
|
assert {row[0] for row in tables.rows} == {"t", "added_externally"}
|
|
assert state.error is None
|
|
assert not state.needs_scan
|
|
finally:
|
|
ds.close()
|