fix(performance): lazily load expensive modules within aurweb.db

Closes #374

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2022-08-12 19:58:55 -07:00
parent 0e82916b0a
commit 913ce8a4f0
No known key found for this signature in database
GPG key ID: F7E46DED420788F3

View file

@ -1,34 +1,15 @@
import functools # Supported database drivers.
import hashlib
import math
import os
import re
from typing import Iterable, NewType
import sqlalchemy
from sqlalchemy import create_engine, event
from sqlalchemy.engine.base import Engine
from sqlalchemy.engine.url import URL
from sqlalchemy.orm import Query, Session, SessionTransaction, scoped_session, sessionmaker
import aurweb.config
import aurweb.util
DRIVERS = { DRIVERS = {
"mysql": "mysql+mysqldb" "mysql": "mysql+mysqldb"
} }
# Some types we don't get access to in this module.
Base = NewType("Base", "aurweb.models.declarative_base.Base")
def make_random_value(table: str, column: str, length: int): def make_random_value(table: str, column: str, length: int):
""" Generate a unique, random value for a string column in a table. """ Generate a unique, random value for a string column in a table.
:return: A unique string that is not in the database :return: A unique string that is not in the database
""" """
import aurweb.util
string = aurweb.util.make_random_string(length) string = aurweb.util.make_random_string(length)
while query(table).filter(column == string).first(): while query(table).filter(column == string).first():
string = aurweb.util.make_random_string(length) string = aurweb.util.make_random_string(length)
@ -52,6 +33,10 @@ def test_name() -> str:
:return: Unhashed database name :return: Unhashed database name
""" """
import os
import aurweb.config
db = os.environ.get("PYTEST_CURRENT_TEST", db = os.environ.get("PYTEST_CURRENT_TEST",
aurweb.config.get("database", "name")) aurweb.config.get("database", "name"))
return db.split(":")[0] return db.split(":")[0]
@ -70,7 +55,10 @@ def name() -> str:
dbname = test_name() dbname = test_name()
if not dbname.startswith("test/"): if not dbname.startswith("test/"):
return dbname return dbname
import hashlib
sha1 = hashlib.sha1(dbname.encode()).hexdigest() sha1 = hashlib.sha1(dbname.encode()).hexdigest()
return "db" + sha1 return "db" + sha1
@ -78,12 +66,13 @@ def name() -> str:
_sessions = dict() _sessions = dict()
def get_session(engine: Engine = None) -> Session: def get_session(engine=None):
""" Return aurweb.db's global session. """ """ Return aurweb.db's global session. """
dbname = name() dbname = name()
global _sessions global _sessions
if dbname not in _sessions: if dbname not in _sessions:
from sqlalchemy.orm import scoped_session, sessionmaker
if not engine: # pragma: no cover if not engine: # pragma: no cover
engine = get_engine() engine = get_engine()
@ -106,13 +95,17 @@ def pop_session(dbname: str) -> None:
_sessions.pop(dbname) _sessions.pop(dbname)
def refresh(model: Base) -> Base: def refresh(model):
""" Refresh the session's knowledge of `model`. """ """
Refresh the session's knowledge of `model`.
:returns: Passed in `model`
"""
get_session().refresh(model) get_session().refresh(model)
return model return model
def query(Model: Base, *args, **kwargs) -> Query: def query(Model, *args, **kwargs):
""" """
Perform an ORM query against the database session. Perform an ORM query against the database session.
@ -124,7 +117,7 @@ def query(Model: Base, *args, **kwargs) -> Query:
return get_session().query(Model).filter(*args, **kwargs) return get_session().query(Model).filter(*args, **kwargs)
def create(Model: Base, *args, **kwargs) -> Base: def create(Model, *args, **kwargs):
""" """
Create a record and add() it to the database session. Create a record and add() it to the database session.
@ -135,7 +128,7 @@ def create(Model: Base, *args, **kwargs) -> Base:
return add(instance) return add(instance)
def delete(model: Base) -> None: def delete(model) -> None:
""" """
Delete a set of records found by Query.filter(*args, **kwargs). Delete a set of records found by Query.filter(*args, **kwargs).
@ -144,8 +137,9 @@ def delete(model: Base) -> None:
get_session().delete(model) get_session().delete(model)
def delete_all(iterable: Iterable) -> None: def delete_all(iterable) -> None:
""" Delete each instance found in `iterable`. """ """ Delete each instance found in `iterable`. """
import aurweb.util
session_ = get_session() session_ = get_session()
aurweb.util.apply_all(iterable, session_.delete) aurweb.util.apply_all(iterable, session_.delete)
@ -155,23 +149,29 @@ def rollback() -> None:
get_session().rollback() get_session().rollback()
def add(model: Base) -> Base: def add(model):
""" Add `model` to the database session. """ """ Add `model` to the database session. """
get_session().add(model) get_session().add(model)
return model return model
def begin() -> SessionTransaction: def begin():
""" Begin an SQLAlchemy SessionTransaction. """ """ Begin an SQLAlchemy SessionTransaction. """
return get_session().begin() return get_session().begin()
def get_sqlalchemy_url() -> URL: def get_sqlalchemy_url():
""" """
Build an SQLAlchemy URL for use with create_engine. Build an SQLAlchemy URL for use with create_engine.
:return: sqlalchemy.engine.url.URL :return: sqlalchemy.engine.url.URL
""" """
import sqlalchemy
from sqlalchemy.engine.url import URL
import aurweb.config
constructor = URL constructor = URL
parts = sqlalchemy.__version__.split('.') parts = sqlalchemy.__version__.split('.')
@ -209,13 +209,17 @@ def get_sqlalchemy_url() -> URL:
def sqlite_regexp(regex, item) -> bool: # pragma: no cover def sqlite_regexp(regex, item) -> bool: # pragma: no cover
""" Method which mimics SQL's REGEXP for SQLite. """ """ Method which mimics SQL's REGEXP for SQLite. """
import re
return bool(re.search(regex, str(item))) return bool(re.search(regex, str(item)))
def setup_sqlite(engine: Engine) -> None: # pragma: no cover def setup_sqlite(engine) -> None: # pragma: no cover
""" Perform setup for an SQLite engine. """ """ Perform setup for an SQLite engine. """
from sqlalchemy import event
@event.listens_for(engine, "connect") @event.listens_for(engine, "connect")
def do_begin(conn, record): def do_begin(conn, record):
import functools
create_deterministic_function = functools.partial( create_deterministic_function = functools.partial(
conn.create_function, conn.create_function,
deterministic=True deterministic=True
@ -227,7 +231,7 @@ def setup_sqlite(engine: Engine) -> None: # pragma: no cover
_engines = dict() _engines = dict()
def get_engine(dbname: str = None, echo: bool = False) -> Engine: def get_engine(dbname: str = None, echo: bool = False):
""" """
Return the SQLAlchemy engine for `dbname`. Return the SQLAlchemy engine for `dbname`.
@ -238,6 +242,8 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine:
:param echo: Flag passed through to sqlalchemy.create_engine :param echo: Flag passed through to sqlalchemy.create_engine
:return: SQLAlchemy Engine instance :return: SQLAlchemy Engine instance
""" """
import aurweb.config
if not dbname: if not dbname:
dbname = name() dbname = name()
@ -254,6 +260,7 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine:
"echo": echo, "echo": echo,
"connect_args": connect_args "connect_args": connect_args
} }
from sqlalchemy import create_engine
_engines[dbname] = create_engine(get_sqlalchemy_url(), **kwargs) _engines[dbname] = create_engine(get_sqlalchemy_url(), **kwargs)
if is_sqlite: # pragma: no cover if is_sqlite: # pragma: no cover
@ -301,7 +308,10 @@ class ConnectionExecutor:
_conn = None _conn = None
_paramstyle = None _paramstyle = None
def __init__(self, conn, backend=aurweb.config.get("database", "backend")): def __init__(self, conn, backend=None):
import aurweb.config
backend = backend or aurweb.config.get("database", "backend")
self._conn = conn self._conn = conn
if backend == "mysql": if backend == "mysql":
self._paramstyle = "format" self._paramstyle = "format"
@ -339,6 +349,7 @@ class Connection:
_conn = None _conn = None
def __init__(self): def __init__(self):
import aurweb.config
aur_db_backend = aurweb.config.get('database', 'backend') aur_db_backend = aurweb.config.get('database', 'backend')
if aur_db_backend == 'mysql': if aur_db_backend == 'mysql':
@ -357,7 +368,9 @@ class Connection:
elif aur_db_backend == 'sqlite': # pragma: no cover elif aur_db_backend == 'sqlite': # pragma: no cover
# TODO: SQLite support has been removed in FastAPI. It remains # TODO: SQLite support has been removed in FastAPI. It remains
# here to fund its support for PHP until it is removed. # here to fund its support for PHP until it is removed.
import math
import sqlite3 import sqlite3
aur_db_name = aurweb.config.get('database', 'name') aur_db_name = aurweb.config.get('database', 'name')
self._conn = sqlite3.connect(aur_db_name) self._conn = sqlite3.connect(aur_db_name)
self._conn.create_function("POWER", 2, math.pow) self._conn.create_function("POWER", 2, math.pow)