Skip to content

Commit bf87de8

Browse files
Implement SQLALCHEMY_RECORD_QUERIES
Lifted verbatim from [Flask-SQLAlchemy](https://github.com/pallets-eco/flask-sqlalchemy/blob/3e3e92ba557649ab5251eda860a67656cc8c10af/src/flask_sqlalchemy/record_queries.py). Adding support for this will enable integration with Flask-DebugToolbar.
1 parent 0b182c5 commit bf87de8

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

src/flask_sqlalchemy_lite/_extension.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ def init_app(self, app: App) -> None:
8383
app.teardown_appcontext(_close_async_sessions)
8484
app.shell_context_processor(add_models_to_shell)
8585

86+
if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False):
87+
from . import record_queries
88+
89+
for engine in engines.values():
90+
record_queries._listen(engine)
91+
8692
def _get_state(self) -> _State:
8793
app = current_app._get_current_object() # type: ignore[attr-defined]
8894

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import inspect
5+
import typing as t
6+
from time import perf_counter
7+
8+
import sqlalchemy as sa
9+
import sqlalchemy.event as sa_event
10+
from flask import current_app
11+
from flask import g
12+
from flask import has_app_context
13+
14+
15+
def get_recorded_queries() -> list[_QueryInfo]:
16+
"""Get the list of recorded query information for the current session. Queries are
17+
recorded if the config :data:`.SQLALCHEMY_RECORD_QUERIES` is enabled.
18+
19+
Each query info object has the following attributes:
20+
21+
``statement``
22+
The string of SQL generated by SQLAlchemy with parameter placeholders.
23+
``parameters``
24+
The parameters sent with the SQL statement.
25+
``start_time`` / ``end_time``
26+
Timing info about when the query started execution and when the results where
27+
returned. Accuracy and value depends on the operating system.
28+
``duration``
29+
The time the query took in seconds.
30+
``location``
31+
A string description of where in your application code the query was executed.
32+
This may not be possible to calculate, and the format is not stable.
33+
34+
.. versionchanged:: 3.0
35+
Renamed from ``get_debug_queries``.
36+
37+
.. versionchanged:: 3.0
38+
The info object is a dataclass instead of a tuple.
39+
40+
.. versionchanged:: 3.0
41+
The info object attribute ``context`` is renamed to ``location``.
42+
43+
.. versionchanged:: 3.0
44+
Not enabled automatically in debug or testing mode.
45+
"""
46+
return g.get("_sqlalchemy_queries", []) # type: ignore[no-any-return]
47+
48+
49+
@dataclasses.dataclass
50+
class _QueryInfo:
51+
"""Information about an executed query. Returned by :func:`get_recorded_queries`.
52+
53+
.. versionchanged:: 3.0
54+
Renamed from ``_DebugQueryTuple``.
55+
56+
.. versionchanged:: 3.0
57+
Changed to a dataclass instead of a tuple.
58+
59+
.. versionchanged:: 3.0
60+
``context`` is renamed to ``location``.
61+
"""
62+
63+
statement: str | None
64+
parameters: t.Any
65+
start_time: float
66+
end_time: float
67+
location: str
68+
69+
@property
70+
def duration(self) -> float:
71+
return self.end_time - self.start_time
72+
73+
74+
def _listen(engine: sa.engine.Engine) -> None:
75+
sa_event.listen(engine, "before_cursor_execute", _record_start, named=True)
76+
sa_event.listen(engine, "after_cursor_execute", _record_end, named=True)
77+
78+
79+
def _record_start(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None:
80+
if not has_app_context():
81+
return
82+
83+
context._fsa_start_time = perf_counter() # type: ignore[attr-defined]
84+
85+
86+
def _record_end(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None:
87+
if not has_app_context():
88+
return
89+
90+
if "_sqlalchemy_queries" not in g:
91+
g._sqlalchemy_queries = []
92+
93+
import_top = current_app.import_name.partition(".")[0]
94+
import_dot = f"{import_top}."
95+
frame = inspect.currentframe()
96+
97+
while frame:
98+
name = frame.f_globals.get("__name__")
99+
100+
if name and (name == import_top or name.startswith(import_dot)):
101+
code = frame.f_code
102+
location = f"{code.co_filename}:{frame.f_lineno} ({code.co_name})"
103+
break
104+
105+
frame = frame.f_back
106+
else:
107+
location = "<unknown>"
108+
109+
g._sqlalchemy_queries.append(
110+
_QueryInfo(
111+
statement=context.statement,
112+
parameters=context.parameters,
113+
start_time=context._fsa_start_time, # type: ignore[attr-defined]
114+
end_time=perf_counter(),
115+
location=location,
116+
)
117+
)

0 commit comments

Comments
 (0)