1 | #include "catch.hpp" |
2 | #include "duckdb.h" |
3 | #include "test_helpers.hpp" |
4 | #include "duckdb/common/exception.hpp" |
5 | |
6 | using namespace duckdb; |
7 | using namespace std; |
8 | |
9 | class CAPIResult { |
10 | public: |
11 | ~CAPIResult() { |
12 | duckdb_destroy_result(&result); |
13 | } |
14 | void Query(duckdb_connection connection, string query) { |
15 | success = (duckdb_query(connection, query.c_str(), &result) == DuckDBSuccess); |
16 | } |
17 | |
18 | idx_t column_count() { |
19 | return result.column_count; |
20 | } |
21 | |
22 | idx_t row_count() { |
23 | return result.row_count; |
24 | } |
25 | |
26 | template <class T> T Fetch(idx_t col, idx_t row) { |
27 | throw NotImplementedException("Unimplemented type for fetch" ); |
28 | } |
29 | |
30 | bool IsNull(idx_t col, idx_t row) { |
31 | return result.columns[col].nullmask[row]; |
32 | } |
33 | |
34 | public: |
35 | bool success = false; |
36 | |
37 | private: |
38 | duckdb_result result; |
39 | }; |
40 | |
41 | static bool NO_FAIL(CAPIResult &result) { |
42 | return result.success; |
43 | } |
44 | |
45 | static bool NO_FAIL(unique_ptr<CAPIResult> result) { |
46 | return NO_FAIL(*result); |
47 | } |
48 | |
49 | template <> bool CAPIResult::Fetch(idx_t col, idx_t row) { |
50 | return duckdb_value_boolean(&result, col, row); |
51 | } |
52 | |
53 | template <> int8_t CAPIResult::Fetch(idx_t col, idx_t row) { |
54 | return duckdb_value_int8(&result, col, row); |
55 | } |
56 | |
57 | template <> int16_t CAPIResult::Fetch(idx_t col, idx_t row) { |
58 | return duckdb_value_int16(&result, col, row); |
59 | } |
60 | |
61 | template <> int32_t CAPIResult::Fetch(idx_t col, idx_t row) { |
62 | return duckdb_value_int32(&result, col, row); |
63 | } |
64 | |
65 | template <> int64_t CAPIResult::Fetch(idx_t col, idx_t row) { |
66 | return duckdb_value_int64(&result, col, row); |
67 | } |
68 | |
69 | template <> float CAPIResult::Fetch(idx_t col, idx_t row) { |
70 | return duckdb_value_float(&result, col, row); |
71 | } |
72 | |
73 | template <> double CAPIResult::Fetch(idx_t col, idx_t row) { |
74 | return duckdb_value_double(&result, col, row); |
75 | } |
76 | |
77 | template <> duckdb_date CAPIResult::Fetch(idx_t col, idx_t row) { |
78 | auto data = (duckdb_date *)result.columns[col].data; |
79 | return data[row]; |
80 | } |
81 | |
82 | template <> duckdb_timestamp CAPIResult::Fetch(idx_t col, idx_t row) { |
83 | auto data = (duckdb_timestamp *)result.columns[col].data; |
84 | return data[row]; |
85 | } |
86 | |
87 | template <> string CAPIResult::Fetch(idx_t col, idx_t row) { |
88 | auto value = duckdb_value_varchar(&result, col, row); |
89 | string strval = string(value); |
90 | free((void *)value); |
91 | return strval; |
92 | } |
93 | |
94 | class CAPITester { |
95 | public: |
96 | CAPITester() : database(nullptr), connection(nullptr) { |
97 | } |
98 | ~CAPITester() { |
99 | Cleanup(); |
100 | } |
101 | |
102 | void Cleanup() { |
103 | if (connection) { |
104 | duckdb_disconnect(&connection); |
105 | connection = nullptr; |
106 | } |
107 | if (database) { |
108 | duckdb_close(&database); |
109 | database = nullptr; |
110 | } |
111 | } |
112 | |
113 | bool OpenDatabase(const char *path) { |
114 | Cleanup(); |
115 | if (duckdb_open(path, &database) != DuckDBSuccess) { |
116 | return false; |
117 | } |
118 | if (duckdb_connect(database, &connection) != DuckDBSuccess) { |
119 | return false; |
120 | } |
121 | return true; |
122 | } |
123 | |
124 | unique_ptr<CAPIResult> Query(string query) { |
125 | auto result = make_unique<CAPIResult>(); |
126 | result->Query(connection, query); |
127 | return result; |
128 | } |
129 | |
130 | duckdb_database database = nullptr; |
131 | duckdb_connection connection = nullptr; |
132 | }; |
133 | |
134 | TEST_CASE("Basic test of C API" , "[capi]" ) { |
135 | CAPITester tester; |
136 | unique_ptr<CAPIResult> result; |
137 | |
138 | // open the database in in-memory mode |
139 | REQUIRE(tester.OpenDatabase(nullptr)); |
140 | |
141 | // select scalar value |
142 | result = tester.Query("SELECT CAST(42 AS BIGINT)" ); |
143 | REQUIRE_NO_FAIL(*result); |
144 | REQUIRE(result->column_count() == 1); |
145 | REQUIRE(result->row_count() == 1); |
146 | REQUIRE(result->Fetch<int64_t>(0, 0) == 42); |
147 | REQUIRE(!result->IsNull(0, 0)); |
148 | |
149 | // select scalar NULL |
150 | result = tester.Query("SELECT NULL" ); |
151 | REQUIRE_NO_FAIL(*result); |
152 | REQUIRE(result->column_count() == 1); |
153 | REQUIRE(result->row_count() == 1); |
154 | REQUIRE(result->Fetch<int64_t>(0, 0) == 0); |
155 | REQUIRE(result->IsNull(0, 0)); |
156 | |
157 | // select scalar string |
158 | result = tester.Query("SELECT 'hello'" ); |
159 | REQUIRE_NO_FAIL(*result); |
160 | REQUIRE(result->column_count() == 1); |
161 | REQUIRE(result->row_count() == 1); |
162 | REQUIRE(result->Fetch<string>(0, 0) == "hello" ); |
163 | REQUIRE(!result->IsNull(0, 0)); |
164 | |
165 | // multiple insertions |
166 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE test (a INTEGER, b INTEGER);" )); |
167 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO test VALUES (11, 22)" )); |
168 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO test VALUES (NULL, 21)" )); |
169 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO test VALUES (13, 22)" )); |
170 | |
171 | // NULL selection |
172 | result = tester.Query("SELECT a, b FROM test ORDER BY a" ); |
173 | REQUIRE_NO_FAIL(*result); |
174 | // NULL, 11, 13 |
175 | REQUIRE(result->IsNull(0, 0)); |
176 | REQUIRE(result->Fetch<int32_t>(0, 1) == 11); |
177 | REQUIRE(result->Fetch<int32_t>(0, 2) == 13); |
178 | // 21, 22, 22 |
179 | REQUIRE(result->Fetch<int32_t>(1, 0) == 21); |
180 | REQUIRE(result->Fetch<int32_t>(1, 1) == 22); |
181 | REQUIRE(result->Fetch<int32_t>(1, 2) == 22); |
182 | } |
183 | |
184 | TEST_CASE("Test different types of C API" , "[capi]" ) { |
185 | CAPITester tester; |
186 | unique_ptr<CAPIResult> result; |
187 | |
188 | // open the database in in-memory mode |
189 | REQUIRE(tester.OpenDatabase(nullptr)); |
190 | |
191 | // integer columns |
192 | vector<string> types = {"TINYINT" , "SMALLINT" , "INTEGER" , "BIGINT" }; |
193 | for (auto &type : types) { |
194 | // create the table and insert values |
195 | REQUIRE_NO_FAIL(tester.Query("BEGIN TRANSACTION" )); |
196 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE integers(i " + type + ")" )); |
197 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO integers VALUES (1), (NULL)" )); |
198 | |
199 | result = tester.Query("SELECT * FROM integers ORDER BY i" ); |
200 | REQUIRE_NO_FAIL(*result); |
201 | REQUIRE(result->IsNull(0, 0)); |
202 | REQUIRE(result->Fetch<int8_t>(0, 0) == 0); |
203 | REQUIRE(result->Fetch<int16_t>(0, 0) == 0); |
204 | REQUIRE(result->Fetch<int32_t>(0, 0) == 0); |
205 | REQUIRE(result->Fetch<int64_t>(0, 0) == 0); |
206 | REQUIRE(ApproxEqual(result->Fetch<float>(0, 0), 0.0f)); |
207 | REQUIRE(ApproxEqual(result->Fetch<double>(0, 0), 0.0)); |
208 | |
209 | REQUIRE(!result->IsNull(0, 1)); |
210 | REQUIRE(result->Fetch<int8_t>(0, 1) == 1); |
211 | REQUIRE(result->Fetch<int16_t>(0, 1) == 1); |
212 | REQUIRE(result->Fetch<int32_t>(0, 1) == 1); |
213 | REQUIRE(result->Fetch<int64_t>(0, 1) == 1); |
214 | REQUIRE(ApproxEqual(result->Fetch<float>(0, 1), 1.0f)); |
215 | REQUIRE(ApproxEqual(result->Fetch<double>(0, 1), 1.0)); |
216 | REQUIRE(result->Fetch<string>(0, 1) == "1" ); |
217 | |
218 | REQUIRE_NO_FAIL(tester.Query("ROLLBACK" )); |
219 | } |
220 | // real/double columns |
221 | types = {"REAL" , "DOUBLE" }; |
222 | for (auto &type : types) { |
223 | // create the table and insert values |
224 | REQUIRE_NO_FAIL(tester.Query("BEGIN TRANSACTION" )); |
225 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE doubles(i " + type + ")" )); |
226 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO doubles VALUES (1), (NULL)" )); |
227 | |
228 | result = tester.Query("SELECT * FROM doubles ORDER BY i" ); |
229 | REQUIRE_NO_FAIL(*result); |
230 | REQUIRE(result->IsNull(0, 0)); |
231 | REQUIRE(result->Fetch<int8_t>(0, 0) == 0); |
232 | REQUIRE(result->Fetch<int16_t>(0, 0) == 0); |
233 | REQUIRE(result->Fetch<int32_t>(0, 0) == 0); |
234 | REQUIRE(result->Fetch<int64_t>(0, 0) == 0); |
235 | REQUIRE(ApproxEqual(result->Fetch<float>(0, 0), 0.0f)); |
236 | REQUIRE(ApproxEqual(result->Fetch<double>(0, 0), 0.0)); |
237 | |
238 | REQUIRE(!result->IsNull(0, 1)); |
239 | REQUIRE(result->Fetch<int8_t>(0, 1) == 1); |
240 | REQUIRE(result->Fetch<int16_t>(0, 1) == 1); |
241 | REQUIRE(result->Fetch<int32_t>(0, 1) == 1); |
242 | REQUIRE(result->Fetch<int64_t>(0, 1) == 1); |
243 | REQUIRE(ApproxEqual(result->Fetch<float>(0, 1), 1.0f)); |
244 | REQUIRE(ApproxEqual(result->Fetch<double>(0, 1), 1.0)); |
245 | |
246 | REQUIRE_NO_FAIL(tester.Query("ROLLBACK" )); |
247 | } |
248 | // date columns |
249 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE dates(d DATE)" )); |
250 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO dates VALUES ('1992-09-20'), (NULL), ('1000000-09-20')" )); |
251 | |
252 | result = tester.Query("SELECT * FROM dates ORDER BY d" ); |
253 | REQUIRE_NO_FAIL(*result); |
254 | REQUIRE(result->IsNull(0, 0)); |
255 | duckdb_date date = result->Fetch<duckdb_date>(0, 1); |
256 | REQUIRE(date.year == 1992); |
257 | REQUIRE(date.month == 9); |
258 | REQUIRE(date.day == 20); |
259 | REQUIRE(result->Fetch<string>(0, 1) == Value::DATE(1992, 9, 20).ToString(SQLType::DATE)); |
260 | date = result->Fetch<duckdb_date>(0, 2); |
261 | REQUIRE(date.year == 1000000); |
262 | REQUIRE(date.month == 9); |
263 | REQUIRE(date.day == 20); |
264 | REQUIRE(result->Fetch<string>(0, 2) == Value::DATE(1000000, 9, 20).ToString(SQLType::DATE)); |
265 | |
266 | // timestamp columns |
267 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE timestamps(t TIMESTAMP)" )); |
268 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO timestamps VALUES ('1992-09-20 12:01:30'), (NULL)" )); |
269 | |
270 | result = tester.Query("SELECT * FROM timestamps ORDER BY t" ); |
271 | REQUIRE_NO_FAIL(*result); |
272 | REQUIRE(result->IsNull(0, 0)); |
273 | duckdb_timestamp stamp = result->Fetch<duckdb_timestamp>(0, 1); |
274 | REQUIRE(stamp.date.year == 1992); |
275 | REQUIRE(stamp.date.month == 9); |
276 | REQUIRE(stamp.date.day == 20); |
277 | REQUIRE(stamp.time.hour == 12); |
278 | REQUIRE(stamp.time.min == 1); |
279 | REQUIRE(stamp.time.sec == 30); |
280 | REQUIRE(stamp.time.msec == 0); |
281 | REQUIRE(result->Fetch<string>(0, 1) == Value::TIMESTAMP(1992, 9, 20, 12, 1, 30, 0).ToString(SQLType::TIMESTAMP)); |
282 | |
283 | // boolean columns |
284 | REQUIRE_NO_FAIL(tester.Query("CREATE TABLE booleans(b BOOLEAN)" )); |
285 | REQUIRE_NO_FAIL(tester.Query("INSERT INTO booleans VALUES (42 > 60), (42 > 20), (42 > NULL)" )); |
286 | |
287 | result = tester.Query("SELECT * FROM booleans ORDER BY b" ); |
288 | REQUIRE_NO_FAIL(*result); |
289 | REQUIRE(result->IsNull(0, 0)); |
290 | REQUIRE(!result->Fetch<bool>(0, 0)); |
291 | REQUIRE(!result->Fetch<bool>(0, 1)); |
292 | REQUIRE(result->Fetch<bool>(0, 2)); |
293 | REQUIRE(result->Fetch<string>(0, 2) == Value::BOOLEAN(true).ToString()); |
294 | } |
295 | |
296 | TEST_CASE("Test errors in C API" , "[capi]" ) { |
297 | CAPITester tester; |
298 | unique_ptr<CAPIResult> result; |
299 | |
300 | // cannot open database in random directory |
301 | REQUIRE(!tester.OpenDatabase("/bla/this/directory/should/not/exist/hopefully/awerar333" )); |
302 | REQUIRE(tester.OpenDatabase(nullptr)); |
303 | |
304 | // syntax error in query |
305 | REQUIRE_FAIL(tester.Query("SELEC * FROM TABLE" )); |
306 | // bind error |
307 | REQUIRE_FAIL(tester.Query("SELECT * FROM TABLE" )); |
308 | |
309 | duckdb_result res; |
310 | duckdb_prepared_statement stmt = nullptr; |
311 | // fail prepare API calls |
312 | REQUIRE(duckdb_prepare(NULL, "SELECT 42" , &stmt) == DuckDBError); |
313 | REQUIRE(duckdb_prepare(tester.connection, NULL, &stmt) == DuckDBError); |
314 | REQUIRE(duckdb_bind_boolean(NULL, 0, true) == DuckDBError); |
315 | REQUIRE(duckdb_execute_prepared(NULL, &res) == DuckDBError); |
316 | duckdb_destroy_prepare(NULL); |
317 | } |
318 | |
319 | TEST_CASE("Test prepared statements in C API" , "[capi]" ) { |
320 | CAPITester tester; |
321 | unique_ptr<CAPIResult> result; |
322 | duckdb_result res; |
323 | duckdb_prepared_statement stmt = nullptr; |
324 | duckdb_state status; |
325 | |
326 | // open the database in in-memory mode |
327 | REQUIRE(tester.OpenDatabase(nullptr)); |
328 | |
329 | status = duckdb_prepare(tester.connection, "SELECT CAST($1 AS BIGINT)" , &stmt); |
330 | REQUIRE(status == DuckDBSuccess); |
331 | REQUIRE(stmt != nullptr); |
332 | |
333 | status = duckdb_bind_boolean(stmt, 1, 1); |
334 | REQUIRE(status == DuckDBSuccess); |
335 | status = duckdb_bind_boolean(stmt, 2, 1); |
336 | REQUIRE(status == DuckDBError); |
337 | |
338 | status = duckdb_execute_prepared(stmt, &res); |
339 | REQUIRE(status == DuckDBSuccess); |
340 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 1); |
341 | duckdb_destroy_result(&res); |
342 | |
343 | duckdb_bind_int8(stmt, 1, 8); |
344 | status = duckdb_execute_prepared(stmt, &res); |
345 | REQUIRE(status == DuckDBSuccess); |
346 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 8); |
347 | duckdb_destroy_result(&res); |
348 | |
349 | duckdb_bind_int16(stmt, 1, 16); |
350 | status = duckdb_execute_prepared(stmt, &res); |
351 | REQUIRE(status == DuckDBSuccess); |
352 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 16); |
353 | duckdb_destroy_result(&res); |
354 | |
355 | duckdb_bind_int32(stmt, 1, 32); |
356 | status = duckdb_execute_prepared(stmt, &res); |
357 | REQUIRE(status == DuckDBSuccess); |
358 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 32); |
359 | duckdb_destroy_result(&res); |
360 | |
361 | duckdb_bind_int64(stmt, 1, 64); |
362 | status = duckdb_execute_prepared(stmt, &res); |
363 | REQUIRE(status == DuckDBSuccess); |
364 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 64); |
365 | duckdb_destroy_result(&res); |
366 | |
367 | duckdb_bind_float(stmt, 1, 42.0); |
368 | status = duckdb_execute_prepared(stmt, &res); |
369 | REQUIRE(status == DuckDBSuccess); |
370 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 42); |
371 | duckdb_destroy_result(&res); |
372 | |
373 | duckdb_bind_double(stmt, 1, 43.0); |
374 | status = duckdb_execute_prepared(stmt, &res); |
375 | REQUIRE(status == DuckDBSuccess); |
376 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 43); |
377 | duckdb_destroy_result(&res); |
378 | |
379 | duckdb_bind_varchar(stmt, 1, "44" ); |
380 | status = duckdb_execute_prepared(stmt, &res); |
381 | REQUIRE(status == DuckDBSuccess); |
382 | REQUIRE(duckdb_value_int64(&res, 0, 0) == 44); |
383 | duckdb_destroy_result(&res); |
384 | |
385 | duckdb_bind_null(stmt, 1); |
386 | status = duckdb_execute_prepared(stmt, &res); |
387 | REQUIRE(status == DuckDBSuccess); |
388 | REQUIRE(res.columns[0].nullmask[0] == true); |
389 | duckdb_destroy_result(&res); |
390 | |
391 | duckdb_destroy_prepare(&stmt); |
392 | // again to make sure it does not crash |
393 | duckdb_destroy_result(&res); |
394 | duckdb_destroy_prepare(&stmt); |
395 | |
396 | status = duckdb_query(tester.connection, "CREATE TABLE a (i INTEGER)" , NULL); |
397 | REQUIRE(status == DuckDBSuccess); |
398 | |
399 | status = duckdb_prepare(tester.connection, "INSERT INTO a VALUES (?)" , &stmt); |
400 | REQUIRE(status == DuckDBSuccess); |
401 | REQUIRE(stmt != nullptr); |
402 | idx_t nparams; |
403 | REQUIRE(duckdb_nparams(stmt, &nparams) == DuckDBSuccess); |
404 | REQUIRE(nparams == 1); |
405 | |
406 | for (int32_t i = 1; i <= 1000; i++) { |
407 | duckdb_bind_int32(stmt, 1, i); |
408 | status = duckdb_execute_prepared(stmt, nullptr); |
409 | REQUIRE(status == DuckDBSuccess); |
410 | } |
411 | duckdb_destroy_prepare(&stmt); |
412 | |
413 | status = duckdb_prepare(tester.connection, "SELECT SUM(i)*$1-$2 FROM a" , &stmt); |
414 | REQUIRE(status == DuckDBSuccess); |
415 | REQUIRE(stmt != nullptr); |
416 | duckdb_bind_int32(stmt, 1, 2); |
417 | duckdb_bind_int32(stmt, 2, 1000); |
418 | |
419 | status = duckdb_execute_prepared(stmt, &res); |
420 | REQUIRE(status == DuckDBSuccess); |
421 | REQUIRE(duckdb_value_int32(&res, 0, 0) == 1000000); |
422 | duckdb_destroy_result(&res); |
423 | duckdb_destroy_prepare(&stmt); |
424 | |
425 | // not-so-happy path |
426 | status = duckdb_prepare(tester.connection, "SELECT XXXXX" , &stmt); |
427 | REQUIRE(status == DuckDBError); |
428 | duckdb_destroy_prepare(&stmt); |
429 | |
430 | status = duckdb_prepare(tester.connection, "SELECT CAST($1 AS INTEGER)" , &stmt); |
431 | REQUIRE(status == DuckDBSuccess); |
432 | REQUIRE(stmt != nullptr); |
433 | |
434 | status = duckdb_execute_prepared(stmt, &res); |
435 | REQUIRE(status == DuckDBError); |
436 | duckdb_destroy_result(&res); |
437 | duckdb_destroy_prepare(&stmt); |
438 | } |
439 | |