Skip to content

Commit b5613cc

Browse files
committed
fix python prepare params implicit cause RSS raising
Signed-off-by: ericyuanhui <285521263@qq.com>
1 parent 2a28669 commit b5613cc

2 files changed

Lines changed: 171 additions & 1 deletion

File tree

src_py/connection.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, database: Database, num_threads: int = 0):
5252
self._query_timeout_ms = 0
5353
self._query_results: WeakSet[QueryResult] = WeakSet()
5454
self._capi_scan_tables: set[str] = set()
55+
self._pybind_implicit_prepared_cache: dict[str, Any] = {}
5556
self.database._register_connection(self)
5657
self.init_connection()
5758

@@ -116,6 +117,7 @@ def close(self) -> None:
116117
for query_result in list(self._query_results):
117118
query_result.close()
118119
self._query_results.clear()
120+
self._pybind_implicit_prepared_cache.clear()
119121

120122
if self._connection is not None and not self.database.is_closed:
121123
self._connection.close()
@@ -460,9 +462,23 @@ def _execute_with_pybind(
460462
return py_connection.query(query)
461463

462464
query, parameters = self._normalize_parameters_for_pybind(query, parameters)
463-
prepared = py_connection.prepare(query, parameters)
465+
prepared = self._get_or_prepare_pybind_statement(
466+
py_connection, query, parameters
467+
)
464468
return py_connection.execute(prepared, parameters)
465469

470+
def _get_or_prepare_pybind_statement(
471+
self,
472+
py_connection: Any,
473+
query: str,
474+
parameters: dict[str, Any],
475+
) -> Any:
476+
prepared = self._pybind_implicit_prepared_cache.get(query)
477+
if prepared is None:
478+
prepared = py_connection.prepare(query, parameters)
479+
self._pybind_implicit_prepared_cache[query] = prepared
480+
return prepared
481+
466482
def _maybe_raise_scan_unsupported_object(self, query: str) -> None:
467483
match = re.search(
468484
r"\bLOAD\s+FROM\s+([A-Za-z_][A-Za-z0-9_]*)\b", query, re.IGNORECASE
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from __future__ import annotations
2+
3+
from types import SimpleNamespace
4+
5+
import pytest
6+
7+
import ladybug as lb
8+
import ladybug.connection as lb_connection
9+
10+
11+
class _FakeResult:
12+
def isSuccess(self) -> bool:
13+
return True
14+
15+
def hasNextQueryResult(self) -> bool:
16+
return False
17+
18+
19+
class _FakePreparedStatement:
20+
def __init__(self, query: str, parameters: dict[str, object]):
21+
self.query = query
22+
self.parameters = dict(parameters)
23+
24+
25+
class _FakePybindConnection:
26+
def __init__(self) -> None:
27+
self.prepare_calls: list[tuple[str, dict[str, object]]] = []
28+
self.execute_calls: list[
29+
tuple[_FakePreparedStatement, dict[str, object]]
30+
] = []
31+
self.query_calls: list[str] = []
32+
self.closed = False
33+
34+
def prepare(
35+
self, query: str, parameters: dict[str, object]
36+
) -> _FakePreparedStatement:
37+
self.prepare_calls.append((query, dict(parameters)))
38+
return _FakePreparedStatement(query, parameters)
39+
40+
def execute(
41+
self, prepared: _FakePreparedStatement, parameters: dict[str, object]
42+
) -> _FakeResult:
43+
self.execute_calls.append((prepared, dict(parameters)))
44+
return _FakeResult()
45+
46+
def query(self, query: str) -> _FakeResult:
47+
self.query_calls.append(query)
48+
return _FakeResult()
49+
50+
def close(self) -> None:
51+
self.closed = True
52+
53+
54+
class _FakeBackendConnection:
55+
def __init__(self) -> None:
56+
self.closed = False
57+
58+
def close(self) -> None:
59+
self.closed = True
60+
61+
62+
class _FakeDatabase:
63+
def __init__(self) -> None:
64+
self._use_pybind_backend = True
65+
self._database = object()
66+
self.is_closed = False
67+
self.registered_connections: list[lb.Connection] = []
68+
69+
def _register_connection(self, connection: lb.Connection) -> None:
70+
self.registered_connections.append(connection)
71+
72+
def _unregister_connection(self, connection: lb.Connection) -> None:
73+
self.registered_connections.remove(connection)
74+
75+
def init_database(self) -> None:
76+
return None
77+
78+
79+
@pytest.fixture
80+
def fake_pybind_connection(monkeypatch: pytest.MonkeyPatch) -> _FakePybindConnection:
81+
fake_pybind = _FakePybindConnection()
82+
83+
monkeypatch.setattr(
84+
lb_connection, "get_pybind_module", lambda: SimpleNamespace()
85+
)
86+
monkeypatch.setattr(
87+
lb.Connection,
88+
"init_connection",
89+
lambda self: setattr(self, "_connection", _FakeBackendConnection()),
90+
)
91+
monkeypatch.setattr(
92+
lb.Connection, "_get_pybind_connection", lambda self: fake_pybind
93+
)
94+
return fake_pybind
95+
96+
97+
def test_pybind_implicit_prepare_reuses_same_query(
98+
fake_pybind_connection: _FakePybindConnection,
99+
) -> None:
100+
conn = lb.Connection(_FakeDatabase())
101+
102+
conn.execute("RETURN $value", {"value": 1})
103+
conn.execute("RETURN $value", {"value": 2})
104+
105+
assert fake_pybind_connection.query_calls == []
106+
assert len(fake_pybind_connection.prepare_calls) == 1
107+
assert [call[0].query for call in fake_pybind_connection.execute_calls] == [
108+
"RETURN $value",
109+
"RETURN $value",
110+
]
111+
assert [call[1] for call in fake_pybind_connection.execute_calls] == [
112+
{"value": 1},
113+
{"value": 2},
114+
]
115+
116+
117+
def test_pybind_implicit_prepare_does_not_share_different_queries(
118+
fake_pybind_connection: _FakePybindConnection,
119+
) -> None:
120+
conn = lb.Connection(_FakeDatabase())
121+
122+
conn.execute("RETURN $value", {"value": 1})
123+
conn.execute("RETURN $other", {"other": 1})
124+
125+
assert [call[0] for call in fake_pybind_connection.prepare_calls] == [
126+
"RETURN $value",
127+
"RETURN $other",
128+
]
129+
130+
131+
def test_pybind_no_parameter_query_skips_prepare_cache(
132+
fake_pybind_connection: _FakePybindConnection,
133+
) -> None:
134+
conn = lb.Connection(_FakeDatabase())
135+
136+
conn.execute("RETURN 1")
137+
138+
assert fake_pybind_connection.prepare_calls == []
139+
assert fake_pybind_connection.query_calls == ["RETURN 1"]
140+
141+
142+
def test_pybind_close_clears_implicit_prepare_cache(
143+
fake_pybind_connection: _FakePybindConnection,
144+
) -> None:
145+
conn = lb.Connection(_FakeDatabase())
146+
147+
conn.execute("RETURN $value", {"value": 1})
148+
149+
assert set(conn._pybind_implicit_prepared_cache) == {"RETURN $value"}
150+
151+
conn.close()
152+
153+
assert conn._pybind_implicit_prepared_cache == {}
154+
assert fake_pybind_connection.closed is True

0 commit comments

Comments
 (0)