diff --git a/DBI.lua b/DBI.lua index 797cdef..8c4dba5 100644 --- a/DBI.lua +++ b/DBI.lua @@ -9,7 +9,8 @@ local name_to_module = { SQLite3 = 'dbd.sqlite3', DB2 = 'dbd.db2', Oracle = 'dbd.oracle', - ODBC = 'dbd.odbc' + ODBC = 'dbd.odbc', + DuckDB = 'dbd.duckdb' } local string = require('string') diff --git a/Makefile b/Makefile index c010fc3..e0066f6 100644 --- a/Makefile +++ b/Makefile @@ -6,15 +6,16 @@ MKDIR ?= mkdir -p INSTALL ?= install INSTALL_PROGRAM ?= $(INSTALL) INSTALL_DATA ?= $(INSTALL) -m 644 -LUA_V ?= 5.1 +LUA_V ?= 5.4 LUA_LDIR ?= /usr/share/lua/$(LUA_V) LUA_CDIR ?= /usr/lib/lua/$(LUA_V) -COMMON_CFLAGS ?= -g -pedantic -Wall -O2 -shared -fPIC -DPIC -std=c99 +COMMON_CFLAGS ?= -g -pedantic -Wall -O2 -shared -fPIC -DPIC -std=c99 LUA_INC ?= -I/usr/include/lua$(LUA_V) MYSQL_INC ?= -I/usr/include/mysql PSQL_INC ?= -I/usr/include/postgresql SQLITE3_INC ?= -I/usr/include +DUCKDB_INC ?= -I/usr/include DB2_INC ?= -I/opt/ibm/db2exc/V9.5/include ORACLE_INC ?= -I/usr/lib/oracle/xe/app/oracle/product/10.2.0/client/rdbms/public CF = $(LUA_INC) $(COMMON_CFLAGS) $(CFLAGS) -I. @@ -23,6 +24,7 @@ COMMON_LDFLAGS ?= -shared MYSQL_LDFLAGS ?= -lmysqlclient PSQL_LDFLAGS ?= -lpq SQLITE3_LDFLAGS ?= -lsqlite3 +DUCKDB_LDFLAGS ?= -lduckdb DB2_LDFLAGS ?= -L/opt/ibm/db2exc/V9.5/lib64 -L/opt/ibm/db2exc/V9.5/lib32 -ldb2 ORACLE_LDFLAGS ?= -L/usr/lib/oracle/xe/app/oracle/product/10.2.0/client/lib/ -locixe LF = $(COMMON_LDFLAGS) $(LDFLAGS) @@ -30,6 +32,7 @@ LF = $(COMMON_LDFLAGS) $(LDFLAGS) MYSQL_FLAGS = $(CF) $(LF) $(MYSQL_INC) $(MYSQL_LDFLAGS) PSQL_FLAGS = $(CF) $(LF) $(PSQL_INC) $(PSQL_LDFLAGS) SQLITE3_FLAGS = $(CF) $(LF) $(SQLITE3_INC) $(SQLITE3_LDFLAGS) +DUCKDB_FLAGS = $(CF) $(LF) $(DUCKDB_INC) $(DUCKDB_LDFLAGS) DB2_FLAGS = $(CF) $(LF) $(DB2_INC) $(DB2_LDFLAGS) ORACLE_FLAGS = $(CF) $(LF) $(ORACLE_INC) $(ORACLE_LDFLAGS) -DORA_ENABLE_PING -DORA_ENABLE_TAF @@ -39,6 +42,7 @@ BUILDDIR = build DBDMYSQL = dbd/mysql.so DBDPSQL = dbd/postgresql.so DBDSQLITE3 = dbd/sqlite3.so +DBDDUCKDB = dbd/duckdb.so DBDDB2 = dbd/db2.so DBDORACLE = dbd/oracle.so @@ -46,12 +50,13 @@ OBJS = build/dbd_common.o MYSQL_OBJS = $(OBJS) build/dbd_mysql_main.o build/dbd_mysql_connection.o build/dbd_mysql_statement.o PSQL_OBJS = $(OBJS) build/dbd_postgresql_main.o build/dbd_postgresql_connection.o build/dbd_postgresql_statement.o SQLITE3_OBJS = $(OBJS) build/dbd_sqlite3_main.o build/dbd_sqlite3_connection.o build/dbd_sqlite3_statement.o +DUCKDB_OBJS = $(OBJS) build/dbd_duckdb_main.o build/dbd_duckdb_connection.o build/dbd_duckdb_statement.o DB2_OBJS = $(OBJS) build/dbd_db2_main.o build/dbd_db2_connection.o build/dbd_db2_statement.o ORACLE_OBJS = $(OBJS) build/dbd_oracle_main.o build/dbd_oracle_connection.o build/dbd_oracle_statement.o -free: mysql psql sqlite3 +free: mysql psql sqlite3 duckdb -all: mysql psql sqlite3 db2 oracle +all: mysql psql sqlite3 duckdb db2 oracle mysql: $(BUILDDIR) $(MYSQL_OBJS) $(CC) $(MYSQL_OBJS) -o $(DBDMYSQL) $(MYSQL_FLAGS) @@ -62,6 +67,9 @@ psql: $(BUILDDIR) $(PSQL_OBJS) sqlite3: $(BUILDDIR) $(SQLITE3_OBJS) $(CC) $(SQLITE3_OBJS) -o $(DBDSQLITE3) $(SQLITE3_FLAGS) +duckdb: $(BUILDDIR) $(DUCKDB_OBJS) + $(CC) $(DUCKDB_OBJS) -o $(DBDDUCKDB) $(DUCKDB_FLAGS) + db2: $(BUILDDIR) $(DB2_OBJS) $(CC) $(DB2_OBJS) -o $(DBDDB2) $(DB2_FLAGS) @@ -69,7 +77,7 @@ oracle: $(BUILDDIR) $(ORACLE_OBJS) $(CC) $(ORACLE_OBJS) -o $(DBDORACLE) $(ORACLE_FLAGS) clean: - $(RM) $(MYSQL_OBJS) $(PSQL_OBJS) $(SQLITE3_OBJS) $(DB2_OBJS) $(ORACLE_OBJS) $(DBDMYSQL) $(DBDPSQL) $(DBDSQLITE3) $(DBDDB2) $(DBDORACLE) + $(RM) $(MYSQL_OBJS) $(PSQL_OBJS) $(SQLITE3_OBJS) $(DB2_OBJS) $(ORACLE_OBJS) $(DBDMYSQL) $(DBDPSQL) $(DBDSQLITE3) $(DBDDB2) $(DBDORACLE) $(DUCKDB_OBJS) build/dbd_common.o: dbd/common.c dbd/common.h $(CC) -c -o $@ $< $(CF) @@ -95,6 +103,13 @@ build/dbd_sqlite3_main.o: dbd/sqlite3/main.c dbd/sqlite3/dbd_sqlite3.h dbd/commo build/dbd_sqlite3_statement.o: dbd/sqlite3/statement.c dbd/sqlite3/dbd_sqlite3.h dbd/common.h $(CC) -c -o $@ $< $(SQLITE3_FLAGS) +build/dbd_duckdb_connection.o: dbd/duckdb/connection.c dbd/duckdb/dbd_duckdb.h dbd/common.h + $(CC) -c -o $@ $< $(DUCKDB_FLAGS) +build/dbd_duckdb_main.o: dbd/duckdb/main.c dbd/duckdb/dbd_duckdb.h dbd/common.h + $(CC) -c -o $@ $< $(DUCKDB_FLAGS) +build/dbd_duckdb_statement.o: dbd/duckdb/statement.c dbd/duckdb/dbd_duckdb.h dbd/common.h + $(CC) -c -o $@ $< $(DUCKDB_FLAGS) + build/dbd_db2_connection.o: dbd/db2/connection.c dbd/db2/dbd_db2.h dbd/common.h $(CC) -c -o $@ $< $(DB2_FLAGS) build/dbd_db2_main.o: dbd/db2/main.c dbd/db2/dbd_db2.h dbd/common.h @@ -124,12 +139,15 @@ install_psql: psql install_lua install_sqlite3: sqlite3 install_lua $(INSTALL_PROGRAM) -D $(DBDSQLITE3) $(DESTDIR)$(LUA_CDIR)/$(DBDSQLITE3) +install_duckdb: duckdb install_lua + $(INSTALL_PROGRAM) -D $(DBDDUCKDB) $(DESTDIR)$(LUA_CDIR)/$DBDDUCKDB) + install_db2: db2 install_lua $(INSTALL_PROGRAM) -D $(DBDDB2) $(DESTDIR)$(LUA_CDIR)/$(DBDDB2) install_oracle: oracle install_lua $(INSTALL_PROGRAM) -D $(DBDORACLE) $(DESTDIR)$(LUA_CDIR)/$(DBDORACLE) -install_free: install_lua install_mysql install_psql install_sqlite3 +install_free: install_lua install_mysql install_psql install_sqlite3 install_duckdb -install_all: install_lua install_mysql install_psql install_sqlite3 install_db2 install_oracle +install_all: install_lua install_mysql install_psql install_sqlite3 install_duckdb install_db2 install_oracle diff --git a/dbd/duckdb/connection.c b/dbd/duckdb/connection.c new file mode 100644 index 0000000..d6ab666 --- /dev/null +++ b/dbd/duckdb/connection.c @@ -0,0 +1,220 @@ +#include "dbd_duckdb.h" + + +int dbd_duckdb_statement_create(lua_State *L, connection_t *conn, const char *sql_query); + + +/* + * connection,err = DBD.DuckDB.New(dbfile) + */ +static int connection_new(lua_State *L) { + int n = lua_gettop(L); + + char *errmessage; + const char *db = NULL; + connection_t *conn = NULL; + + switch(n) { + default: + /* + * db is the only parameter for now + */ + db = luaL_checkstring(L, 1); + } + + conn = (connection_t *)lua_newuserdata(L, sizeof(connection_t)); + conn->autocommit = 1; + conn->in_transaction = 0; + + if (duckdb_open_ext( db, &(conn->db), NULL, &errmessage ) == DuckDBError) { + lua_pushnil(L); + lua_pushfstring(L, DBI_ERR_CONNECTION_FAILED, errmessage); + + duckdb_free(errmessage); + return 2; + } + + if (duckdb_connect( conn->db, &(conn->conn) ) == DuckDBError) { + duckdb_close( &(conn->db) ); + + lua_pushnil(L); + lua_pushfstring(L, DBI_ERR_CONNECTION_FAILED); + return 2; + } + + luaL_getmetatable(L, DBD_DUCKDB_CONNECTION); + lua_setmetatable(L, -2); + + return 1; +} + + +static int connection_prepare(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + if (conn->conn) { + return dbd_duckdb_statement_create(L, conn, luaL_checkstring(L, 2)); + } + + lua_pushnil(L); + lua_pushstring(L, DBI_ERR_DB_UNAVAILABLE); + return 2; +} + + +static int connection_commit(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + if (duckdb_query(conn->conn, "COMMIT;", NULL) == DuckDBError) { + lua_pushboolean(L, 0); + return 0; + } + + conn->in_transaction = 0; + lua_pushboolean(L, 1); + return 1; +} + + +static int connection_rollback(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + if (duckdb_query(conn->conn, "ROLLBACK;", NULL) == DuckDBError) { + lua_pushboolean(L, 0); + return 0; + } + + conn->in_transaction = 0; + lua_pushboolean(L, 1); + return 1; +} + + +static int connection_autocommit(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + bool mode = lua_toboolean(L, 1); + + // no change + if (conn->autocommit == mode) { + lua_pushboolean(L, 1); + return 1; + } + + // enable autocommit while in a transaction means commit it + if (conn->in_transaction) { + if (mode) { + if (duckdb_query(conn->conn, "COMMIT;", NULL) == DuckDBError) { + lua_pushboolean(L, 0); + return 0; + } + + conn->in_transaction = 0; + } + } + + conn->autocommit = mode; + lua_pushboolean(L, 1); + return 1; +} + +static int connection_ping(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + int ok = 1; + + if (!conn->db) { + ok = 0; + } else if (!conn->conn) { + ok = 0; + } + + lua_pushboolean(L, ok); + return 1; +} + + +static int connection_close(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + duckdb_disconnect(&(conn->conn)); + conn->conn = NULL; + + duckdb_close(&(conn->db)); + conn->db = NULL; + + lua_pushboolean(L, 1); + return 1; +} + + +/* + * last_id = connection:last_id() + */ +static int connection_lastid(lua_State *L) { + luaL_error(L, DBI_ERR_NOT_IMPLEMENTED, DBD_DUCKDB_CONNECTION, "last_id"); + return 0; +} + + +/* + * num_rows = statement:rowcount() + */ +static int connection_quote(lua_State *L) { + luaL_error(L, DBI_ERR_NOT_IMPLEMENTED, DBD_DUCKDB_CONNECTION, "quote"); + return 0; +} + + +/* + * __gc + */ +static int connection_gc(lua_State *L) { + /* always close the connection */ + connection_close(L); + + return 0; +} + + +/* + * __tostring + */ +static int connection_tostring(lua_State *L) { + connection_t *conn = (connection_t *)luaL_checkudata(L, 1, DBD_DUCKDB_CONNECTION); + + lua_pushfstring(L, "%s: %p", DBD_DUCKDB_CONNECTION, conn); + + return 1; +} + + +int dbd_duckdb_connection(lua_State *L) { + /* + * instance methods + */ + static const luaL_Reg connection_methods[] = { + {"autocommit", connection_autocommit}, + {"close", connection_close}, + {"commit", connection_commit}, + {"ping", connection_ping}, + {"prepare", connection_prepare}, + {"quote", connection_quote}, + {"rollback", connection_rollback}, + {"last_id", connection_lastid}, + {NULL, NULL} + }; + + /* + * class methods + */ + static const luaL_Reg connection_class_methods[] = { + {"New", connection_new}, + {NULL, NULL} + }; + + dbd_register(L, DBD_DUCKDB_CONNECTION, + connection_methods, connection_class_methods, + connection_gc, connection_tostring); + + return 1; +} diff --git a/dbd/duckdb/dbd_duckdb.h b/dbd/duckdb/dbd_duckdb.h new file mode 100644 index 0000000..3c49990 --- /dev/null +++ b/dbd/duckdb/dbd_duckdb.h @@ -0,0 +1,99 @@ +#include +#include + +#define DBD_DUCKDB_CONNECTION "DBD.DuckDB.Connection" +#define DBD_DUCKDB_STATEMENT "DBD.DuckDB.Statement" + +/* + * connection object + */ +typedef struct _connection { + duckdb_database db; + duckdb_connection conn; + bool autocommit; + bool in_transaction; +} connection_t; + +/* + * statement object + */ +typedef struct _statement { + connection_t *conn; + duckdb_prepared_statement stmt; + duckdb_result result; + + duckdb_data_chunk cur_chunk; + int cur_row; + //duckdb_vector *cur_cols; + + bool is_result; + +} statement_t; + + +/* + * Modified versions of what's in common.h, because DuckDB does things + * differently. + */ +#define LDB_PUSH_ATTRIB_INT(v) \ + lua_pushinteger(L, v); \ + lua_rawset(L, -3); + +#define LDB_PUSH_ATTRIB_FLOAT(v) \ + lua_pushnumber(L, v); \ + lua_rawset(L, -3); + +#define LDB_PUSH_ATTRIB_STRING_BY_LENGTH(v, len) \ + lua_pushlstring(L, v, len); \ + lua_rawset(L, -3); + +#define LDB_PUSH_ATTRIB_STRING(v) \ + if (duckdb_string_is_inlined(v)) { \ + lua_pushstring(L, v.value.inlined.inlined); \ + lua_rawset(L, -3); \ + } else { \ + lua_pushlstring(L, v.value.pointer.ptr, v.value.pointer.length); \ + lua_rawset(L, -3); \ + } + +#define LDB_PUSH_ATTRIB_BOOL(v) \ + lua_pushboolean(L, v); \ + lua_rawset(L, -3); + +#define LDB_PUSH_ATTRIB_NIL() \ + lua_pushnil(L); \ + lua_rawset(L, -3); + + + +#if LUA_VERSION_NUM > 502 +#define LDB_PUSH_ARRAY_INT(n, v) \ + lua_pushinteger(L, v); \ + lua_rawseti(L, -2, n); +#else +#define LDB_PUSH_ARRAY_INT(n, v) \ + lua_pushnumber(L, v); \ + lua_rawseti(L, -2, n); +#endif + +#define LDB_PUSH_ARRAY_FLOAT(n, v) \ + lua_pushnumber(L, v); \ + lua_rawseti(L, -2, n); + +// DuckDB stores strings in two different ways, compensate for that here +#define LDB_PUSH_ARRAY_STRING(n, v) \ + if (duckdb_string_is_inlined(v)) { \ + lua_pushstring(L, v.value.inlined.inlined); \ + lua_rawseti(L, -2, n); \ + } else { \ + lua_pushlstring(L, v.value.pointer.ptr, v.value.pointer.length); \ + lua_rawseti(L, -2, n); \ + } + +#define LDB_PUSH_ARRAY_BOOL(n, v) \ + lua_pushboolean(L, v); \ + lua_rawseti(L, -2, n); + +#define LDB_PUSH_ARRAY_NIL(n) \ + lua_pushnil(L); \ + lua_rawseti(L, -2, n); diff --git a/dbd/duckdb/main.c b/dbd/duckdb/main.c new file mode 100644 index 0000000..415e8cd --- /dev/null +++ b/dbd/duckdb/main.c @@ -0,0 +1,15 @@ +#include "dbd_duckdb.h" + +int dbd_duckdb_connection(lua_State *L); +int dbd_duckdb_statement(lua_State *L); + +/* + * library entry point + */ +LUA_EXPORT int luaopen_dbd_duckdb(lua_State *L) { + dbd_duckdb_statement(L); + dbd_duckdb_connection(L); + + return 1; +} + diff --git a/dbd/duckdb/statement.c b/dbd/duckdb/statement.c new file mode 100644 index 0000000..b050947 --- /dev/null +++ b/dbd/duckdb/statement.c @@ -0,0 +1,462 @@ +#include "dbd_duckdb.h" +#include + + +int dbd_duckdb_statement_create(lua_State *L, connection_t *conn, const char *sql_query) { + statement_t *statement = NULL; + + statement = (statement_t *)lua_newuserdata(L, sizeof(statement_t)); + statement->conn = conn; + statement->stmt = NULL; + statement->is_result = 0; + statement->cur_chunk = NULL; + statement->cur_row = 0; + + if (duckdb_prepare(conn->conn, sql_query, &(statement->stmt) ) != DuckDBSuccess) { + lua_pushnil(L); + lua_pushfstring(L, DBI_ERR_PREP_STATEMENT, duckdb_prepare_error( statement->stmt )); + + duckdb_destroy_prepare( &(statement->stmt) ); + statement->stmt = NULL; + return 2; + } + + luaL_getmetatable(L, DBD_DUCKDB_STATEMENT); + lua_setmetatable(L, -2); + return 1; +} + + +/* + * DuckDB API - the not-deprecated parts, anyway - are weird so this'll + * be a fun one to implement. + */ +int statement_fetch_impl(lua_State *L, statement_t *statement, int named_columns) { + //statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + idx_t cols, i; + + + if (!statement->stmt) { + luaL_error(L, DBI_ERR_INVALID_STATEMENT); + return 0; + } + + if (!statement->is_result) { + luaL_error(L, DBI_ERR_FETCH_NO_EXECUTE); + return 0; + } + + if (!statement->cur_chunk) { + statement->cur_chunk = duckdb_fetch_chunk( statement->result ); + statement->cur_row = 0; + + // all data has been fetched. + if (!statement->cur_chunk) { + duckdb_destroy_result( &(statement->result) ); + statement->is_result = 0; + lua_pushnil(L); + return 1; + } + } + + lua_newtable(L); + cols = duckdb_column_count(&(statement->result)); + + for (i = 0; i < cols; ++i) { + duckdb_vector col1 = duckdb_data_chunk_get_vector(statement->cur_chunk, i); + + if (named_columns) { + lua_pushstring(L, duckdb_column_name(&(statement->result), i)); + } + + uint64_t *col1_validity = duckdb_vector_get_validity(col1); + if (duckdb_validity_row_is_valid(col1_validity, statement->cur_row)) { + switch ( duckdb_column_type( &(statement->result), i ) ) { + + case DUCKDB_TYPE_TINYINT: + case DUCKDB_TYPE_UTINYINT: { + int8_t *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_INT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_INT(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_SMALLINT: + case DUCKDB_TYPE_USMALLINT: { + int16_t *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_INT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_INT(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_INTEGER: + case DUCKDB_TYPE_UINTEGER: { + int32_t *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_INT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_INT(i + 1, res[statement->cur_row]); + } + } + break; + + + case DUCKDB_TYPE_BIGINT: + case DUCKDB_TYPE_UBIGINT: { + int64_t *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_INT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_INT(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_FLOAT: { + float *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_FLOAT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_FLOAT(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_DOUBLE: { + double *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_FLOAT( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_FLOAT(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_BOOLEAN: { + bool *res = duckdb_vector_get_data(col1); + if (named_columns) { + LDB_PUSH_ATTRIB_BOOL( res[statement->cur_row] ); + } else { + LDB_PUSH_ARRAY_BOOL(i + 1, res[statement->cur_row]); + } + } + break; + + case DUCKDB_TYPE_BLOB: + case DUCKDB_TYPE_VARCHAR: { + duckdb_string_t *vector_data = (duckdb_string_t *) duckdb_vector_get_data(col1); + duckdb_string_t str = vector_data[ statement->cur_row ]; + if (named_columns) { + LDB_PUSH_ATTRIB_STRING( str ); + } else { + LDB_PUSH_ARRAY_STRING(i + 1, str); + } + } + break; + + default: + luaL_error(L, DBI_ERR_EXECUTE_FAILED, "unknown datatype to bind"); + break; + } + } else { + // NULL value + if (named_columns) { + LDB_PUSH_ATTRIB_NIL(); + } else { + LDB_PUSH_ARRAY_NIL( i + 1 ); + } + } + } + + ++(statement->cur_row); + + if (statement->cur_row >= duckdb_data_chunk_get_size( statement->cur_chunk )) { + duckdb_destroy_data_chunk(&(statement->cur_chunk)); + statement->cur_chunk = NULL; + } + + return 1; +} + + +static int next_iterator(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, lua_upvalueindex(1), DBD_DUCKDB_STATEMENT); + int named_columns = lua_toboolean(L, lua_upvalueindex(2)); + + return statement_fetch_impl(L, statement, named_columns); +} + +/* + * table = statement:fetch(named_indexes) + */ +static int statement_fetch(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + int named_columns = lua_toboolean(L, 2); + + return statement_fetch_impl(L, statement, named_columns); +} + +/* + * iterfunc = statement:rows(named_indexes) + */ +static int statement_rows(lua_State *L) { + if (lua_gettop(L) == 1) { + lua_pushvalue(L, 1); + lua_pushboolean(L, 0); + } else { + lua_pushvalue(L, 1); + lua_pushboolean(L, lua_toboolean(L, 2)); + } + + lua_pushcclosure(L, next_iterator, 2); + return 1; +} + + +int statement_execute(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + int n = lua_gettop(L); + int expected_params, p; + int num_bind_params = n - 1; + int errflag = 0; + const char *errstr = NULL; + + + /* + * Sanity checks + */ + if (!statement->conn->conn) { + luaL_error(L, DBI_ERR_DB_UNAVAILABLE); + } + + + /* + * Setup phase - Check arguments, clear handle + */ + expected_params = duckdb_nparams( statement->stmt ); + if (expected_params != num_bind_params) { + lua_pushboolean(L, 0); + lua_pushfstring(L, DBI_ERR_PARAM_MISCOUNT, expected_params, num_bind_params); + return 2; + } + + if (statement->is_result) { + duckdb_destroy_result( &(statement->result) ); + statement->is_result = 0; + } + + + /* + * Bind Values + */ + for (p = 2; p <= n; p++) { + int i = p - 1; + int type = lua_type(L, p); + char err[64]; + + + switch(type) { + case LUA_TNIL: + errflag = duckdb_bind_null( statement->stmt, i ) != DuckDBSuccess; + break; + + case LUA_TNUMBER: +#if LUA_VERSION_NUM > 502 + if (lua_isinteger(L, p)) { + errflag = duckdb_bind_int64(statement->stmt, i, lua_tointeger(L, p)) != DuckDBSuccess; + break; + } +#endif + errflag = duckdb_bind_double(statement->stmt, i, lua_tonumber(L, p)) != DuckDBSuccess; + break; + + case LUA_TSTRING: + errflag = duckdb_bind_varchar(statement->stmt, i, lua_tostring(L, p)) != DuckDBSuccess; + break; + + case LUA_TBOOLEAN: + errflag = duckdb_bind_boolean(statement->stmt, i, lua_toboolean(L, p)) != DuckDBSuccess; + break; + + default: + /* + * Unknown/unsupported value type + */ + errflag = 1; + snprintf(err, sizeof(err)-1, DBI_ERR_BINDING_TYPE_ERR, lua_typename(L, type)); + errstr = err; + } + + if (errflag) { + break; + } + } + + + /* + * Something went wrong, reset and return error + */ + if (errflag) { + if (duckdb_clear_bindings( statement->stmt ) != DuckDBSuccess) { + lua_pushnil(L); + lua_pushstring(L, DBI_ERR_STATEMENT_BROKEN); + return 2; + } + + lua_pushnil(L); + if (errstr) { + lua_pushstring(L, errstr); + } else { + lua_pushfstring(L, DBI_ERR_BINDING_PARAMS, duckdb_prepare_error( statement->stmt )); + } + + return 2; + } + + + + /* + * Actual execute + */ + if (duckdb_execute_prepared(statement->stmt, &(statement->result)) != DuckDBSuccess) { + // error case + lua_pushnil(L); + lua_pushfstring(L, DBI_ERR_EXECUTE_FAILED, duckdb_result_error( &(statement->result) )); + duckdb_destroy_result(&(statement->result)); + return 2; + } + + // success case + statement->is_result = 1; + lua_pushboolean(L, 1); + return 1; +} + + + +/* + * success = statement:close() + */ +static int statement_close(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + int ok = 0; + + if (statement->is_result) { + duckdb_destroy_result( &(statement->result) ); + statement->is_result = 0; + } + + if (statement->stmt) { + duckdb_destroy_prepare( &(statement->stmt) ); + statement->stmt = NULL; + ok = 1; + } + + lua_pushboolean(L, ok); + return 1; +} + + +/* + * column_names = statement:columns() + */ +static int statement_columns(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + + int i; + int num_columns; + int d = 1; + + if (!statement->stmt) { + luaL_error(L, DBI_ERR_INVALID_STATEMENT); + return 0; + } + + if (!statement->is_result) { + luaL_error(L, DBI_ERR_FETCH_NO_EXECUTE); + return 0; + } + + num_columns = duckdb_column_count(&(statement->result)); + lua_newtable(L); + for (i = 0; i < num_columns; i++) { + const char *name = duckdb_column_name(&(statement->result), i); + LUA_PUSH_ARRAY_STRING(d, name); + } + + return 1; +} + + +/* + * rows_changed = statement:affected() + */ +static int statement_affected(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + lua_pushinteger( L, duckdb_rows_changed( &(statement->result) ) ); + return 1; +} + +/* + * num_rows = statement:rowcount() + */ +static int statement_rowcount(lua_State *L) { + /* + * This functionality exists in DuckDB's API but is marked deprecated + * so not implementing it. + */ + luaL_error(L, DBI_ERR_NOT_IMPLEMENTED, DBD_DUCKDB_STATEMENT, "rowcount"); + return 0; +} + + +/* + * __gc + */ +static int statement_gc(lua_State *L) { + /* always free the handle */ + statement_close(L); + + return 0; +} + +/* + * __tostring + */ +static int statement_tostring(lua_State *L) { + statement_t *statement = (statement_t *)luaL_checkudata(L, 1, DBD_DUCKDB_STATEMENT); + + lua_pushfstring(L, "%s: %p", DBD_DUCKDB_STATEMENT, statement); + + return 1; +} + + +int dbd_duckdb_statement(lua_State *L) { + static const luaL_Reg statement_methods[] = { + {"affected", statement_affected}, + {"close", statement_close}, + {"columns", statement_columns}, + {"execute", statement_execute}, + {"fetch", statement_fetch}, + {"rows", statement_rows}, + {"rowcount", statement_rowcount}, + {NULL, NULL} + }; + + static const luaL_Reg statement_class_methods[] = { + {NULL, NULL} + }; + + dbd_register(L, DBD_DUCKDB_STATEMENT, + statement_methods, statement_class_methods, + statement_gc, statement_tostring); + + return 1; +} diff --git a/tests/run_tests.lua b/tests/run_tests.lua index e5ee4f4..995acad 100755 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -403,7 +403,9 @@ local function test_insert_returning() assert.is_nil(err) assert.is_true(success) - assert.is_equal(1, sth:rowcount()) + if config.has_rowcount then + assert.is_equal(1, sth:rowcount()) + end -- @@ -443,7 +445,9 @@ local function test_insert_null_returning() assert.is_nil(err) assert.is_true(success) - assert.is_equal(1, sth:rowcount()) + if config.has_rowcount then + assert.is_equal(1, sth:rowcount()) + end -- -- Grab it back, make sure it's all good @@ -582,14 +586,15 @@ local function test_db_close_doesnt_segfault() sth:close() end + local function test_postgres_statement_leak() for i = 1, 10 do - local sth = dbh:prepare("SELECT 1"); - sth:execute(); + local sth = dbh:prepare("SELECT 1") + sth:execute() for r in sth:rows() do assert(r[1] == 1, "result should be 1") end - sth:close(); + sth:close() end for i = 1, 5 do @@ -606,6 +611,16 @@ local function test_postgres_statement_leak() end +local function test_must_execute_before_fetch() + + sth = dbh:prepare("select 1;") + assert.has_error(function() + sth:fetch() + end) + +end + + describe("PostgreSQL #psql", function() db_type = "PostgreSQL" config = dofile("configs/" .. db_type .. ".lua") @@ -625,6 +640,7 @@ describe("PostgreSQL #psql", function() it( "Tests affected rows", test_update ) it( "Tests for prepared statement leak", test_postgres_statement_leak ) it( "Tests closing dbh doesn't segfault", test_db_close_doesnt_segfault ) + it( "Tests must execute before fetch", test_must_execute_before_fetch ) teardown(teardown_tests) end) @@ -645,6 +661,7 @@ describe("SQLite3 #sqlite3", function() it( "Tests no rowcount", test_no_rowcount ) it( "Tests affected rows", test_update ) it( "Tests closing dbh doesn't segfault", test_db_close_doesnt_segfault ) + it( "Tests must execute before fetch", test_must_execute_before_fetch ) teardown(teardown_tests) end) @@ -666,5 +683,29 @@ describe("MySQL #mysql", function() it( "Tests statement reuse", test_insert_multi ) it( "Tests affected rows", test_update ) it( "Tests closing dbh doesn't segfault", test_db_close_doesnt_segfault ) + it( "Tests must execute before fetch", test_must_execute_before_fetch ) teardown(teardown_tests) end) + +describe("DuckDB #duckdb", function() + db_type = "DuckDB" + config = dofile("configs/" .. db_type .. ".lua") + -- luacheck: ignore DBI dbh + local DBI, dbh + + setup(setup_tests) + it( "Tests syntax error", syntax_error ) + it( "Tests value encoding", test_encoding ) + it( "Tests simple selects", test_select ) + it( "Tests selects with limit", test_select_limit ) + it( "Tests multi-row selects", test_select_multi ) + it( "Tests inserts", test_insert_returning ) + it( "Tests inserts of NULL", test_insert_null_returning ) + it( "Tests statement reuse", test_insert_multi ) + it( "Tests affected rows", test_update ) + it( "Tests no insert_id", test_no_insert_id ) + it( "Tests closing dbh doesn't segfault", test_db_close_doesnt_segfault ) + it( "Tests must execute before fetch", test_must_execute_before_fetch ) + teardown(teardown_tests) +end) +