Skip to content

Commit e7a04de

Browse files
committed
perf: pre-allocate PyString column keys, cache json.dumps
- rows_to_dicts: allocate column name PyStrings once, reuse for every row dict (eliminates N*M PyString allocations) - json.dumps: cache via GILOnceCell instead of module lookup per call
1 parent f23facc commit e7a04de

File tree

2 files changed

+22
-5
lines changed

2 files changed

+22
-5
lines changed

src/database.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use pyo3::prelude::*;
16-
use pyo3::types::{PyDict, PyList};
16+
use pyo3::types::{PyDict, PyList, PyString};
1717
use std::sync::Arc;
1818

1919
use stoolap::api::Database as ApiDatabase;
@@ -223,12 +223,15 @@ pub fn to_named_params(named: &[(String, stoolap::core::Value)]) -> stoolap::api
223223
/// Convert Rows iterator to a list of Python dicts.
224224
pub fn rows_to_dicts(py: Python<'_>, rows: stoolap::api::Rows) -> PyResult<PyObject> {
225225
let columns: Vec<String> = rows.columns().to_vec();
226+
// Pre-allocate column names as PyString once, reuse for every row
227+
let py_col_names: Vec<Bound<'_, PyString>> =
228+
columns.iter().map(|c| PyString::new(py, c)).collect();
226229
let result = PyList::empty(py);
227230

228231
for row_result in rows {
229232
let row = row_result.map_err(to_py)?;
230233
let dict = PyDict::new(py);
231-
for (i, col) in columns.iter().enumerate() {
234+
for (i, col) in py_col_names.iter().enumerate() {
232235
let val = match row.get_value(i) {
233236
Some(v) => value_to_py(py, v),
234237
None => py.None(),
@@ -248,7 +251,10 @@ pub fn first_row_to_dict(py: Python<'_>, mut rows: stoolap::api::Rows) -> PyResu
248251
if let Some(row_result) = rows.next() {
249252
let row = row_result.map_err(to_py)?;
250253
let dict = PyDict::new(py);
251-
for (i, col) in columns.iter().enumerate() {
254+
// Single row — pre-allocate column names as PyString
255+
let py_col_names: Vec<Bound<'_, PyString>> =
256+
columns.iter().map(|c| PyString::new(py, c)).collect();
257+
for (i, col) in py_col_names.iter().enumerate() {
252258
let val = match row.get_value(i) {
253259
Some(v) => value_to_py(py, v),
254260
None => py.None(),

src/value.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use pyo3::exceptions::PyTypeError;
1616
use pyo3::prelude::*;
17+
use pyo3::sync::GILOnceCell;
1718
use pyo3::types::{
1819
PyBool, PyDateAccess, PyDateTime, PyDict, PyFloat, PyInt, PyList, PyString, PyTimeAccess,
1920
PyTuple, timezone_utc,
@@ -23,6 +24,16 @@ use chrono::{Datelike, Timelike};
2324
use stoolap::api::ParamVec;
2425
use stoolap::core::Value;
2526

27+
/// Cached `json.dumps` callable — avoids module lookup per JSON parameter.
28+
static JSON_DUMPS: GILOnceCell<PyObject> = GILOnceCell::new();
29+
30+
fn get_json_dumps<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
31+
let obj = JSON_DUMPS.get_or_try_init(py, || {
32+
py.import("json")?.getattr("dumps").map(|f| f.unbind())
33+
})?;
34+
Ok(obj.bind(py).clone())
35+
}
36+
2637
/// Parsed bind parameters from Python.
2738
pub enum BindParams {
2839
Positional(ParamVec),
@@ -63,8 +74,8 @@ pub fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
6374

6475
// dict/list -> JSON string
6576
if obj.downcast::<PyDict>().is_ok() || obj.downcast::<PyList>().is_ok() {
66-
let json_mod = obj.py().import("json")?;
67-
let json_str: String = json_mod.call_method1("dumps", (obj,))?.extract()?;
77+
let dumps = get_json_dumps(obj.py())?;
78+
let json_str: String = dumps.call1((obj,))?.extract()?;
6879
return Ok(Value::json(&json_str));
6980
}
7081

0 commit comments

Comments
 (0)