Skip to content

Commit f7da02e

Browse files
Fix database settings (#1231)
* Correct indentation * Move role insertion out of scope of table creation * Fix check for in-memory db * Remove seemingly useless db commit * Skip invalid metrics for StaticPool * Cache in-memory databases * Pre-commit fixes * Add test for in-memory authn * Pre-commit fixes * Remove unused global variables * Add option to not used cached databases * Revert "Add option to not used cached databases" This reverts commit 9339ab4. * In tests with multiple databases, use file to avoid colliding in-memory. * Use distinct named memory databases instead of on-disk. * Improve checking of in-memory SQLite database. * Use random named memory name to avoid inter-module collisions * Recognize mode=memory query parameter * Remove database settings * Fix is_memory_sqlite conditional on non-existent property * Stop caching in-memory databases * Auto-format * More formatting --------- Co-authored-by: Dan Allan <[email protected]>
1 parent 98466e2 commit f7da02e

File tree

10 files changed

+120
-28
lines changed

10 files changed

+120
-28
lines changed

example_configs/toy_authentication.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ authentication:
1212
tiled_admins:
1313
- provider: toy
1414
id: admin
15-
database:
16-
uri: "sqlite:///file:authn_mem?mode=memory&cache=shared&uri=true"
17-
init_if_not_exists: true
1815
access_control:
1916
access_policy: "tiled.access_control.access_policies:TagBasedAccessPolicy"
2017
args:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# config.yml
2+
trees:
3+
- path: /
4+
tree: tiled.examples.generated_minimal:tree
5+
uvicorn:
6+
host: 0.0.0.0
7+
port: 8000
8+
authentication:
9+
providers:
10+
- provider: test
11+
authenticator: tiled.authenticators:DictionaryAuthenticator
12+
args:
13+
users_to_passwords:
14+
alice: PASSWORD
15+
secret_keys:
16+
- SECRET
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Union
2+
3+
import pytest
4+
from sqlalchemy.engine import URL, make_url
5+
6+
from tiled.server.connection_pool import is_memory_sqlite
7+
8+
9+
@pytest.mark.parametrize(
10+
("uri", "expected"),
11+
[
12+
("sqlite://", True), # accepts str
13+
(make_url("sqlite://"), True), # accepts URL
14+
("sqlite:///:memory:", True),
15+
("sqlite:///file::memory:?cache=shared", True),
16+
("sqlite:///file:name:?cache=shared&mode=memory", True),
17+
("sqlite:////tmp/example.db", False),
18+
],
19+
)
20+
def test_is_memory_sqlite(uri: Union[str, URL], expected: bool):
21+
actual = is_memory_sqlite(uri)
22+
assert actual is expected
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pathlib import Path
2+
3+
import yaml
4+
5+
from tiled._tests.utils import enter_username_password
6+
from tiled.client import Context, from_context
7+
from tiled.server.app import build_app_from_config
8+
9+
here = Path(__file__).parent.absolute()
10+
11+
12+
def test_good_path():
13+
"""Test authn database defaults to in-memory catalog"""
14+
with open(here / "test_configs" / "config_in_memory_authn.yml") as config_file:
15+
config = yaml.load(config_file, Loader=yaml.BaseLoader)
16+
17+
app = build_app_from_config(config)
18+
context = Context.from_app(app)
19+
20+
with enter_username_password("alice", "PASSWORD"):
21+
client = from_context(context, remember_me=False)
22+
23+
client.logout()
24+
context.close()
25+
26+
assert True

tiled/_tests/test_validation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
This tests tiled's validation registry
33
"""
4-
54
import numpy as np
65
import pandas as pd
76
import pytest

tiled/authn_database/core.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,13 @@ async def create_default_roles(db):
7676

7777

7878
async def initialize_database(engine: AsyncEngine) -> None:
79-
async with engine.connect() as conn:
79+
async with engine.begin() as conn:
8080
# Create all tables.
8181
await conn.run_sync(Base.metadata.create_all)
82-
await conn.commit()
8382

84-
# Initialize Roles table.
85-
async with AsyncSession(engine) as db:
86-
await create_default_roles(db)
83+
# Initialize Roles table.
84+
async with AsyncSession(engine) as db:
85+
await create_default_roles(db)
8786

8887

8988
async def purge_expired(db: AsyncSession, cls) -> int:

tiled/catalog/adapter.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@
6868
ZARR_MIMETYPE,
6969
)
7070
from ..query_registration import QueryTranslationRegistry
71-
from ..server.connection_pool import close_database_connection_pool, get_database_engine
71+
from ..server.connection_pool import (
72+
close_database_connection_pool,
73+
get_database_engine,
74+
is_memory_sqlite,
75+
)
7276
from ..server.core import NoEntry
7377
from ..server.schemas import Asset, DataSource, Management, Revision
7478
from ..server.settings import DatabaseSettings
@@ -229,10 +233,7 @@ async def execute(self, statement, explain=None):
229233
return result
230234

231235
async def startup(self):
232-
if (self.engine.dialect.name == "sqlite") and (
233-
self.engine.url.database == ":memory:"
234-
or self.engine.url.query.get("mode") == "memory"
235-
):
236+
if is_memory_sqlite(self.engine.url):
236237
# Special-case for in-memory SQLite: Because it is transient we can
237238
# skip over anything related to migrations.
238239
await initialize_database(self.engine)

tiled/server/app.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -468,13 +468,14 @@ def override_get_settings():
468468
settings.database_settings.max_overflow = database.max_overflow
469469
if database.init_if_not_exists is not None:
470470
settings.database_init_if_not_exists = database.init_if_not_exists
471-
if authenticators:
472-
# If we support authentication providers, we need a database, so if one is
473-
# not set, use a SQLite database in memory. Horizontally scaled deployments
474-
# must specify a persistent database.
475-
settings.database_settings.uri = (
476-
settings.database_settings.uri or "sqlite://"
477-
)
471+
if authenticators:
472+
# If we support authentication providers, we need a database, so if one is
473+
# not set, use a SQLite database in memory. Horizontally scaled deployments
474+
# must specify a persistent database.
475+
settings.database_settings.uri = (
476+
settings.database_settings.uri
477+
or "sqlite:///file:authdb?mode=memory&cache=shared&uri=true"
478+
)
478479
if (
479480
authenticators
480481
and len(authenticators) == 1

tiled/server/connection_pool.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from fastapi import Depends
77
from sqlalchemy import event
8-
from sqlalchemy.engine import make_url
8+
from sqlalchemy.engine import URL, make_url
99
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
1010
from sqlalchemy.pool import AsyncAdaptedQueuePool
1111

@@ -55,18 +55,14 @@ async def __aexit__(self, *excinfo):
5555

5656

5757
def open_database_connection_pool(database_settings: DatabaseSettings) -> AsyncEngine:
58-
if make_url(database_settings.uri).database == ":memory:":
59-
# For SQLite databases that exist only in process memory,
60-
# pooling is not applicable. Just return an engine and don't cache it.
58+
if is_memory_sqlite(database_settings.uri):
6159
engine = create_async_engine(
6260
ensure_specified_sql_driver(database_settings.uri),
6361
echo=DEFAULT_ECHO,
6462
json_serializer=json_serializer,
6563
)
6664

6765
else:
68-
# For file-backed SQLite databases, and for PostgreSQL databases,
69-
# connection pooling offers a significant performance boost.
7066
engine = create_async_engine(
7167
ensure_specified_sql_driver(database_settings.uri),
7268
echo=DEFAULT_ECHO,
@@ -137,3 +133,34 @@ def _set_sqlite_pragma(conn, record):
137133
cursor = conn.cursor()
138134
cursor.execute("PRAGMA foreign_keys=ON")
139135
cursor.close()
136+
137+
138+
def is_memory_sqlite(url: Union[URL, str]) -> bool:
139+
"""
140+
Check if a SQLAlchemy URL is a memory-backed SQLite database.
141+
142+
Handles various memory database URL formats:
143+
- sqlite:///:memory:
144+
- sqlite:///file::memory:?cache=shared
145+
- sqlite://
146+
- etc.
147+
"""
148+
url = make_url(url)
149+
# Check if it's SQLite at all
150+
if url.get_dialect().name != "sqlite":
151+
return False
152+
153+
# Check if database is None or empty (default memory DB)
154+
if not url.database:
155+
return True
156+
157+
# Check for explicit :memory: string (case-insensitive)
158+
database = str(url.database).lower()
159+
if ":memory:" in database:
160+
return True
161+
162+
# Check for mode=memory query parameter
163+
if (mode := url.query.get("mode")) and mode.lower() == "memory":
164+
return True
165+
166+
return False

tiled/server/metrics.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from prometheus_client import Counter, Gauge, Histogram
88
from sqlalchemy import event
9-
from sqlalchemy.pool import QueuePool
9+
from sqlalchemy.pool import QueuePool, StaticPool
1010

1111
REQUEST_DURATION = Histogram(
1212
"tiled_request_duration_seconds",
@@ -220,6 +220,10 @@ def on_checkout(dbapi_connection, connection_record, connection_proxy):
220220
DB_POOL_CHECKEDOUT.labels(name).inc()
221221
DB_POOL_CHECKOUTS_TOTAL.labels(name).inc()
222222

223+
# Skip for single-connection database:
224+
if isinstance(pool, StaticPool):
225+
return
226+
223227
# First overflow: we just used the very first overflow slot
224228
if pool.overflow() == 1:
225229
DB_POOL_FIRST_OVERFLOW_TOTAL.labels(name).inc()

0 commit comments

Comments
 (0)