-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsqlite_embedded_odbc.cpp
334 lines (288 loc) · 10.6 KB
/
sqlite_embedded_odbc.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#include <sqlite3ext.h>
SQLITE_EXTENSION_INIT1
#ifdef WIN32
#define SQLITE_EXTENSION_ENTRY_POINT __declspec(dllexport)
#else
#define SQLITE_EXTENSION_ENTRY_POINT
#endif
#include <nanodbc/nanodbc.h>
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <iostream>
#include <cstring>
#include <exception>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace std;
// This needs to be callable from C
extern "C" SQLITE_EXTENSION_ENTRY_POINT int sqlite3_openrowset_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi);
typedef void (*column_getter_function_pointer)(json& , nanodbc::result&, short);
void get_string_value(json& jv, nanodbc::result& result, short column_number){
jv = result.get<string>(column_number);
}
void get_float_value(json& jv, nanodbc::result& result, short column_number){
jv = result.get<float>(column_number);
}
void get_int_value(json& jv, nanodbc::result& result, short column_number){
jv = result.get<int>(column_number);
}
void get_date_value(json& jv, nanodbc::result& result, short column_number){
// return as ISO-8601 string
nanodbc::date d;
char buffer [50];
d = result.get<nanodbc::date>(column_number);
sprintf_s(buffer, "%d-%02d-%02d", d.year,d.month, d.day);
jv = buffer;
}
void get_timestamp_value(json& jv, nanodbc::result& result, short column_number){
// return as ISO-8601-ish string
nanodbc::timestamp ts;
char buffer [50];
ts = result.get<nanodbc::timestamp>(column_number);
sprintf_s(buffer, "%d-%02d-%02d %02d:%02d:%02d.%d",
ts.year,ts.month, ts.day,
ts.hour,ts.min, ts.sec, ts.fract);
jv = buffer;
}
void get_null_value(json& jv, nanodbc::result& result, short column_number){
jv = nullptr;
}
column_getter_function_pointer
column_to_function(nanodbc::result& result, short column_number){
auto sql_type = result.column_datatype(column_number);
column_getter_function_pointer fp;
fp = &get_null_value;
// used list of SQL types from
// https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/sql-data-types?view=sql-server-ver16
// and copied to a spreadsheet and manually grouped the types to a getter
switch (sql_type) {
case SQL_SMALLINT:
case SQL_BIT:
case SQL_INTEGER:
case SQL_TINYINT:
case SQL_BIGINT:
fp = &get_int_value;
break;
case SQL_FLOAT:
case SQL_REAL:
case SQL_NUMERIC:
case SQL_DECIMAL:
case SQL_DOUBLE:
fp = &get_float_value;
break;
case SQL_VARCHAR:
case SQL_CHAR:
case SQL_LONGVARCHAR:
case SQL_WCHAR:
case SQL_WVARCHAR:
case SQL_WLONGVARCHAR:
case SQL_GUID:
fp = &get_string_value;
break;
case SQL_BINARY:
case SQL_VARBINARY:
case SQL_LONGVARBINARY:
cout << "do not know how to deal with binary type " << sql_type;
fp = &get_null_value;
break;
case SQL_TYPE_DATE:
fp = &get_date_value;
break;
case SQL_TYPE_TIMESTAMP:
fp = &get_timestamp_value;
break;
default:
cout << "do not recognise type " << sql_type << " for column " << column_number << "\n";
fp = &get_null_value;
}
return fp;
}
void result_to_clob(string &clob, nanodbc::result& result){
result.next();
clob = result.get<string>(0);
// make sure entire result-set is consumed.
while (result.next()) {
;
}
}
void result_to_json(nlohmann::ordered_json& retval, nanodbc::result& result){
int n = result.columns();
std::vector<string> column_names(n);
std::vector<column_getter_function_pointer> function_pointers(n);
// set up column names and function pointers from the result
// metadata. Hope that most ODBC drivers will set the types of each column
// before the results are iterated over.
for(int i=0; i< result.columns(); i++) {
column_names[i]=result.column_name(i);
function_pointers[i] = column_to_function(result, i);
}
while (result.next())
{
// very nice to have the keys in the select order
nlohmann::ordered_json j;
for(int i=0; i< result.columns(); i++) {
json jv;
// Note that it is much easier to check for null in a type independent
// way (and set jv to nullptr, which will be serialized as null) than
// to do it within a column_getter_function
if (result.is_null(i)) {
jv=nullptr;
} else {
(*function_pointers[i])(jv,result,i);
}
j[column_names[i]]=jv;
}
retval.push_back(j);
}
}
static void openrowset_clob_func(
sqlite3_context *context,
int argc,
sqlite3_value **argv)
{
// TODO: replace with an error
// TODO: support bind array and/or single bind params
assert(argc >= 2);
// do some more soundess checking
if (sqlite3_value_type(argv[0]) == SQLITE_NULL)
return;
std::string odbc_connection_string, query_string, clob;
// hope that overloaded assignment operator will do the right thing.
// I don't know if there is a cleaner way to do this.
odbc_connection_string = (reinterpret_cast<const char *>(sqlite3_value_text(argv[0])));
query_string = (reinterpret_cast<const char *>(sqlite3_value_text(argv[1])));
// TODO: figure out when to return an error vs sqlite3_result_null
try
{
nanodbc::connection conn(odbc_connection_string);
nanodbc::result result;
if (argc > 2) {
nanodbc::statement statement(conn);
nanodbc::prepare(statement,query_string);
std::vector<string> bind_values(argc-2);
for (int i=2; i<argc; i++) {
int bv_ind = i-2;
bind_values[bv_ind]=(reinterpret_cast<const char *>(sqlite3_value_text(argv[i])));
statement.bind(bv_ind, bind_values[bv_ind].c_str());
}
result = nanodbc::execute(statement);
} else {
result = nanodbc::execute(conn, query_string);
}
result_to_clob(clob, result);
}
catch (nanodbc::database_error e) {
std::string message = e.what();
sqlite3_result_error(context, message.data(), (int)message.length());
return;
}
catch (std::runtime_error e) {
std::string message = e.what();
sqlite3_result_error(context, message.data(), (int)message.length());
return;
}
// TODO: perhaps use the sqlite3_result_blob interface?
// For the kind of data volumes envisioned, it does not seem necessary
// and would likely be a complication. It seems unlikely that we will ever have
// any kind of data that is sufficiently large to require streaming. Using incremental
// reads from the underlying ODBC API (if indeed such APIs event exist. I simply don't know)
// seems like it would be difficult and error prone. This approach should be 'good enough'
// for the moment.
sqlite3_result_text(context, clob.data(), (int)clob.length(), SQLITE_TRANSIENT);
// not sure if we have to do anything with freeing 'expanded'
// I think it will be taken care of by the runtime simply by going out of scope
// and that nothing has to be done to it explicitly.
return;
}
static void openrowset_json_func(
sqlite3_context *context,
int argc,
sqlite3_value **argv)
{
// TODO: replace with an error
// TODO: support bind array and/or single bind params
assert(argc >= 2);
// do some more soundess checking
if (sqlite3_value_type(argv[0]) == SQLITE_NULL)
return;
// makes the order of the keys match up with their position in the select list.
nlohmann::ordered_json retval;
std::string odbc_connection_string, query_string, expanded;
// hope that overloaded assignment operator will do the right thing.
// I don't know if there is a cleaner way to do this.
odbc_connection_string = (reinterpret_cast<const char *>(sqlite3_value_text(argv[0])));
query_string = (reinterpret_cast<const char *>(sqlite3_value_text(argv[1])));
// TODO: figure out when to return an error vs sqlite3_result_null
// TODO: prepared statements, placeholders and bind values: want to make the code-generation safer.
try
{
nanodbc::connection conn(odbc_connection_string);
nanodbc::result result;
if (argc > 2) {
nanodbc::statement statement(conn);
nanodbc::prepare(statement,query_string);
std::vector<string> bind_values(argc-2);
for (int i=2; i<argc; i++) {
int bv_ind = i-2;
bind_values[bv_ind]=(reinterpret_cast<const char *>(sqlite3_value_text(argv[i])));
statement.bind(bv_ind, bind_values[bv_ind].c_str());
}
result = nanodbc::execute(statement);
} else {
result = nanodbc::execute(conn, query_string);
}
result_to_json(retval, result);
expanded = retval.dump(); // serialize the entire thing to a string
// given that this is meant for metadata queries, the volume of data is likely to be
// quite low and this many-functions-calls-per-value naive approach may be OK.
}
catch (nanodbc::database_error e) {
std::string message = e.what();
sqlite3_result_error(context, message.data(), (int)message.length());
return;
}
catch (json::exception e) {
std::string message = e.what();
sqlite3_result_error(context, message.data(), (int)message.length());
return;
}
catch (std::runtime_error e) {
std::string message = e.what();
sqlite3_result_error(context, message.data(), (int)message.length());
return;
}
// TODO: deal with encodings, preferred encodings etc.
sqlite3_result_text(context, expanded.data(), (int)expanded.length(), SQLITE_TRANSIENT);
// not sure if we have to do anything with freeing 'expanded'
// I think it will be taken care of by the runtime simply by going out of scope
// and that nothing has to be done to it explicitly.
return;
}
// we need
// -DSQLITE_API=__declspec(dllexport)
// for Windows.
// Note that .load in the shell can take the initialization function name as an
// argument so that we don't have to rely on naming conventions. Whatever function is used,
// it needs to have C style symbol which is globally visible. Verify with DUMPBIN /EXPORTS (Windows)
// or nm (Linux) once the dll has been built.
int sqlite3_openrowset_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi)
{
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg; /* Unused parameter */
rc = sqlite3_create_function(db, "openrowset_json", -1,
SQLITE_UTF8 | SQLITE_DETERMINISTIC,
0, openrowset_json_func, 0, 0);
rc = sqlite3_create_function(db, "openrowset_clob", -1,
SQLITE_UTF8 | SQLITE_DETERMINISTIC,
0, openrowset_clob_func, 0, 0);
return rc;
}