Skip to content

Commit c6f3f5e

Browse files
committed
Optimize parameter binding
Closes mkleehammer#214
1 parent cfe0575 commit c6f3f5e

8 files changed

Lines changed: 563 additions & 22 deletions

File tree

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ def get_compiler_settings():
127127
# about this *a lot*.
128128
settings['extra_compile_args'].extend([
129129
'-Wno-write-strings',
130-
'-Wno-deprecated-declarations'
130+
'-Wno-deprecated-declarations',
131+
'-std=c++17',
131132
])
132133

133134
# Homebrew installs odbc_config

src/connection.cpp

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,54 @@
1818
#include "cnxninfo.h"
1919

2020

21+
// Insert or update an entry. If the cache is full, the least recently used item is evicted.
22+
void BindInfoCache::put(const std::string& key, const BindInfoSet& value) {
23+
auto it = map_.find(key);
24+
if (it != map_.end()) {
25+
// Key exists: update value and move to front.
26+
it->second->second = value;
27+
list_.splice(list_.begin(), list_, it->second);
28+
return;
29+
}
30+
// Key new: if full, evict the last (least recently used) item.
31+
if (map_.size() >= capacity_) {
32+
const auto& lastKey = list_.back().first;
33+
map_.erase(lastKey);
34+
list_.pop_back();
35+
}
36+
// Insert new item at the front.
37+
list_.emplace_front(key, value);
38+
map_[key] = list_.begin();
39+
}
40+
41+
// Retrieve a value by key. Returns std::nullopt if not found.
42+
std::optional<BindInfoSet> BindInfoCache::get(const std::string& key) {
43+
auto it = map_.find(key);
44+
if (it == map_.end())
45+
return std::nullopt;
46+
47+
// Move the accessed node to the front (unless it's already there).
48+
if (it->second != list_.begin())
49+
list_.splice(list_.begin(), list_, it->second);
50+
return it->second->second;
51+
}
52+
53+
// Adjust the capacity of the cache, discarding any entries we can no longer accommodate.
54+
void BindInfoCache::resize(size_t new_capacity) {
55+
if (new_capacity == capacity_) return;
56+
57+
if (new_capacity < capacity_) {
58+
// Shrink: remove least recent entries from the back
59+
while (map_.size() > new_capacity) {
60+
const auto& last_key = list_.back().first;
61+
map_.erase(last_key);
62+
list_.pop_back();
63+
}
64+
}
65+
// For both grow and shrink we need to update the private member variable.
66+
capacity_ = new_capacity;
67+
}
68+
2169
static char connection_doc[] =
2270
"Connection objects manage connections to the database.\n"
2371
"\n"
@@ -249,6 +297,9 @@ PyObject* Connection_New(PyObject* pConnectString, bool fAutoCommit, long timeou
249297
cnxn->searchescape = 0;
250298
cnxn->maxwrite = 0;
251299
cnxn->timeout = 0;
300+
cnxn->bindinfo_cache = NULL;
301+
cnxn->none_binding = SQL_UNKNOWN_TYPE;
302+
cnxn->var_binding_length = 0;
252303
cnxn->map_sqltype_to_converter = 0;
253304

254305
cnxn->attrs_before = attrs_before_o.Detach();
@@ -426,6 +477,9 @@ static int Connection_clear(PyObject* self)
426477
Py_XDECREF(cnxn->map_sqltype_to_converter);
427478
cnxn->map_sqltype_to_converter = 0;
428479

480+
delete cnxn->bindinfo_cache;
481+
cnxn->bindinfo_cache = 0;
482+
429483
return 0;
430484
}
431485

@@ -991,6 +1045,90 @@ static int Connection_settimeout(PyObject* self, PyObject* value, void* closure)
9911045
return 0;
9921046
}
9931047

1048+
static PyObject* Connection_getbindinfocachesize(PyObject* self, void* closure)
1049+
{
1050+
UNUSED(closure);
1051+
Connection* cnxn = Connection_Validate(self);
1052+
if (!cnxn)
1053+
return 0;
1054+
if (!cnxn->bindinfo_cache)
1055+
return PyLong_FromLong(0);
1056+
return PyLong_FromSize_t(cnxn->bindinfo_cache->capacity());
1057+
}
1058+
1059+
static int Connection_setbindinfocachesize(PyObject* self, PyObject* value, void* closure)
1060+
{
1061+
UNUSED(closure);
1062+
Connection* cnxn = Connection_Validate(self);
1063+
if (!cnxn)
1064+
return -1;
1065+
size_t cache_size = PyLong_AsSize_t(value);
1066+
if (cache_size == (size_t)-1 && PyErr_Occurred())
1067+
return -1;
1068+
if (cache_size > 0) {
1069+
if (!cnxn->bindinfo_cache) {
1070+
cnxn->bindinfo_cache = new BindInfoCache(cache_size);
1071+
} else {
1072+
cnxn->bindinfo_cache->resize(cache_size);
1073+
}
1074+
} else {
1075+
delete cnxn->bindinfo_cache;
1076+
cnxn->bindinfo_cache = NULL;
1077+
}
1078+
return 0;
1079+
}
1080+
1081+
static PyObject* Connection_getvarbindinglength(PyObject* self, void* closure)
1082+
{
1083+
UNUSED(closure);
1084+
Connection* cnxn = Connection_Validate(self);
1085+
if (!cnxn)
1086+
return 0;
1087+
if (cnxn->var_binding_length)
1088+
return PyLong_FromSize_t(cnxn->var_binding_length);
1089+
Py_INCREF(Py_None);
1090+
return Py_None;
1091+
}
1092+
1093+
static int Connection_setvarbindinglength(PyObject* self, PyObject* value, void* closure)
1094+
{
1095+
UNUSED(closure);
1096+
Connection* cnxn = Connection_Validate(self);
1097+
if (!cnxn)
1098+
return -1;
1099+
if (value == Py_None)
1100+
cnxn->var_binding_length = 0;
1101+
else {
1102+
size_t length = PyLong_AsSize_t(value);
1103+
if (length == (size_t)-1 && PyErr_Occurred())
1104+
return -1;
1105+
cnxn->var_binding_length = length;
1106+
}
1107+
return 0;
1108+
}
1109+
1110+
static PyObject* Connection_getnonebinding(PyObject* self, void* closure)
1111+
{
1112+
UNUSED(closure);
1113+
Connection* cnxn = Connection_Validate(self);
1114+
if (!cnxn)
1115+
return 0;
1116+
return PyLong_FromLong((long)cnxn->none_binding);
1117+
}
1118+
1119+
static int Connection_setnonebinding(PyObject* self, PyObject* value, void* closure)
1120+
{
1121+
UNUSED(closure);
1122+
Connection* cnxn = Connection_Validate(self);
1123+
if (!cnxn)
1124+
return -1;
1125+
long binding = PyLong_AsLong(value);
1126+
if (binding == -1 && PyErr_Occurred())
1127+
return -1;
1128+
cnxn->none_binding = binding;
1129+
return 0;
1130+
}
1131+
9941132
static bool _remove_converter(PyObject* self, SQLSMALLINT sqltype)
9951133
{
9961134
Connection* cnxn = (Connection*)self;
@@ -1394,6 +1532,21 @@ static PyGetSetDef Connection_getseters[] = {
13941532
{ "timeout", Connection_gettimeout, Connection_settimeout,
13951533
"The timeout in seconds, zero means no timeout.", 0 },
13961534
{ "maxwrite", Connection_getmaxwrite, Connection_setmaxwrite, "The maximum bytes to write before using SQLPutData.", 0 },
1535+
{ "bindinfo_cache_size", Connection_getbindinfocachesize, Connection_setbindinfocachesize,
1536+
"The number of prepared queries for which the connection should\n"
1537+
"remember bind information (zero disables cache).", 0 },
1538+
{ "var_binding_length", Connection_getvarbindinglength, Connection_setvarbindinglength,
1539+
"If set to an integer greater than zero, bind information is determined\n"
1540+
"based on the values rather than by preparing the statements, and column\n"
1541+
"lengths for variable-length value types are rounded up to multiples of\n"
1542+
"the value of this property. Set to None (the default) or 0 to disable\n"
1543+
"this override.", 0 },
1544+
{ "none_binding", Connection_getnonebinding, Connection_setnonebinding,
1545+
"By default, any query for which at least one parameter value is None will\n"
1546+
"result in calls to SQLPrepare() and SQLDescribeParam() (providing the\n"
1547+
"driver supports both calls), ignoring the var_binding_length property.\n"
1548+
"Set this property to pyodbc.SQLVARCHAR to avoid those calls when the\n"
1549+
"var_binding_length has been set to an integer greater than zero.", 0 },
13971550
{ 0 }
13981551
};
13991552

src/connection.h

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,55 @@
1212
#ifndef CONNECTION_H
1313
#define CONNECTION_H
1414

15+
#include <string>
16+
#include <list>
17+
#include <unordered_map>
18+
#include <vector>
19+
#include <optional>
20+
21+
// -------------------------------------------------------------------
22+
// BindInfo - plain data structure
23+
// Used for caching information needed for SQLBindParameter.
24+
// -------------------------------------------------------------------
25+
struct BindInfo
26+
{
27+
SQLULEN column_size;
28+
SQLSMALLINT parameter_type;
29+
SQLSMALLINT decimal_digits;
30+
};
31+
32+
// -------------------------------------------------------------------
33+
// BindInfoCache - cache with fixed capacity
34+
// -------------------------------------------------------------------
35+
typedef std::vector<BindInfo> BindInfoSet;
36+
class BindInfoCache {
37+
public:
38+
explicit BindInfoCache(size_t capacity) : capacity_(capacity) {}
39+
40+
// Insert or update an entry. Moves it to front (most recent).
41+
void put(const std::string& key, const BindInfoSet& value);
42+
43+
// Retrieve by key. If found, moves to front and returns value.
44+
std::optional<BindInfoSet> get(const std::string& key);
45+
46+
// Check existence without affecting LRU order.
47+
bool contains(const std::string& key) const { return map_.find(key) != map_.end(); }
48+
49+
// Capacity (maximum number of entries).
50+
size_t capacity() const { return capacity_; }
51+
52+
// Number of entries currently in the cache.
53+
size_t size() const { return map_.size(); }
54+
55+
// Shrink or expand capacity, preserving what we can.
56+
void resize(size_t new_capacity);
57+
58+
private:
59+
size_t capacity_;
60+
std::list<std::pair<std::string, BindInfoSet>> list_; // front = most recent
61+
std::unordered_map<std::string, decltype(list_)::iterator> map_;
62+
};
63+
1564
struct Cursor;
1665

1766
extern PyTypeObject ConnectionType;
@@ -45,6 +94,15 @@ struct Connection
4594
// The connection timeout in seconds.
4695
long timeout;
4796

97+
// Save binding information that might help performance.
98+
BindInfoCache* bindinfo_cache;
99+
100+
// Round column lengths up by multiples of this number when binding for variable-length types.
101+
size_t var_binding_length;
102+
103+
// Set to anything other than pyodbc.SQL_UNKNOWN_TYPE to avoid calls to SQLPrepare with NULL parameters.
104+
SQLSMALLINT none_binding;
105+
48106
// Pointer connection attributes may require that the pointed-to object be kept
49107
// valid until some unspecified time in the future, so keep them here for now.
50108
PyObject* attrs_before;

src/cursor.cpp

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -716,25 +716,22 @@ static PyObject* execute(Cursor* cur, PyObject* pSql, PyObject* params, bool ski
716716

717717
if (cParams > 0)
718718
{
719-
// There are parameters, so we'll need to prepare the SQL statement and bind the parameters. (We need to
720-
// prepare the statement because we can't bind a NULL (None) object without knowing the target datatype. There
721-
// is no one data type that always maps to the others (no, not even varchar)).
722-
723719
if (!PrepareAndBind(cur, pSql, params, skip_first))
724720
return 0;
725-
721+
}
722+
else
723+
{
724+
Py_XDECREF(cur->pPreparedSQL);
725+
cur->pPreparedSQL = 0;
726+
}
727+
if (cur->pPreparedSQL) {
726728
szLastFunction = "SQLExecute";
727729
Py_BEGIN_ALLOW_THREADS
728730
ret = SQLExecute(cur->hstmt);
729731
Py_END_ALLOW_THREADS
730732
}
731733
else
732734
{
733-
// REVIEW: Why don't we always prepare? It is highly unlikely that a user would need to execute the same SQL
734-
// repeatedly if it did not have parameters, so we are not losing performance, but it would simplify the code.
735-
736-
Py_XDECREF(cur->pPreparedSQL);
737-
cur->pPreparedSQL = 0;
738735

739736
szLastFunction = "SQLExecDirect";
740737

@@ -1134,7 +1131,7 @@ static PyObject* Cursor_setinputsizes(PyObject* self, PyObject* sizes)
11341131
PyErr_SetString(ProgrammingError, "Invalid cursor object.");
11351132
return 0;
11361133
}
1137-
1134+
11381135
Cursor *cur = (Cursor*)self;
11391136
if (Py_None == sizes)
11401137
{

0 commit comments

Comments
 (0)