diff --git a/.github/workflows/ci-run-test-extensions.yml b/.github/workflows/ci-run-test-extensions.yml new file mode 100644 index 00000000..c01bd667 --- /dev/null +++ b/.github/workflows/ci-run-test-extensions.yml @@ -0,0 +1,89 @@ +# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +name: ci-run-test-extensions + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + test-on-ubuntu-2204: + name: test-on-ubuntu-22.04 + runs-on: ubuntu-latest + container: + image: docker.io/ubuntu:22.04 + env: + TZ: Asia/Shanghai + DEBIAN_FRONTEND: noninteractive + volumes: + - ${{ github.workspace }}:${{ github.workspace }} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Intall Deps + run: | + export DEBIAN_FRONTEND=noninteractive + echo "Asia/Shanghai" > /etc/timezone + apt-get update + apt-get install -y gcc make cmake autoconf sudo lsb-release curl ca-certificates tzdata + dpkg-reconfigure --frontend noninteractive tzdata + + - name: Install database + run: | + install -d /usr/share/postgresql-common/pgdg + curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc + sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + apt-get update + apt-get install -y postgresql-15 libpq-dev + + - name: Start database + run: | + sudo -u postgres echo "123456" > /tmp/pg.pwfile + sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/data -U postgres -A md5 --pwfile=/tmp/pg.pwfile + sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/data -l /tmp/data/logfile start + PGPASSWORD=123456 psql -h 127.0.0.1 -U postgres -c "SELECT 1" + + - name: Configure And make + working-directory: ${{ github.workspace }} + run: | + # autoconf + ./configure --with-pgsql \ + --with-pgsql-incdir=/usr/include/postgresql \ + --with-pgsql-libdir=/usr/lib/x86_64-linux-gnu + make + + - name: Test + working-directory: ${{ github.workspace }} + run: | + cd tests/ + make test + make test-ext + make clean + cd ../ + make clean + + - name: Cmake And make + working-directory: ${{ github.workspace }} + run: | + mkdir build/ && cd build/ + cmake .. \ + -DWITH_PGSQL=ON \ + -DWITH_PGSQL_INCLUDE_DIR=/usr/include/postgresql \ + -DWITH_PGSQL_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu + make && make install + + - name: Cmake Test + working-directory: ${{ github.workspace }} + run: | + cd build/ + make test diff --git a/CMakeLists.txt b/CMakeLists.txt index 63ce5540..103840bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,25 @@ IF (NOT CMAKE_USE_PTHREADS_INIT) MESSAGE(FATAL_ERROR "Couldn't find pthreads.") ENDIF() +IF(NOT "${WITH_PGSQL}" STREQUAL "") + message(STATUS "with_pgsql: ${WITH_PGSQL}") + # check "libpq-fe.h" + if(EXISTS "${WITH_PGSQL_INCLUDE_DIR}/libpq-fe.h") + message(STATUS "found 'libpq-fe.h' header.") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_PGSQL") + else() + message(FATAL_ERROR "Cannot find 'libpq-fe.h' header. Use '-DWITH_PGSQL_INCLUDE_DIR=DIR' to specify the directory where 'libpq-fe.h' is located.") + endif() + + # check "libpq.so" + if(EXISTS "${WITH_PGSQL_LIBRARY_DIR}/libpq.so") + message(STATUS "found 'libpq.so' library.") + SET(QLIBC_LINK_LIBS "pq") + else() + message(FATAL_ERROR "Cannot find 'libpq.so' library. Use '-DWITH_PGSQL_LIBRARY_DIR=DIR' to specify the directory where 'libpq.so' is located.") + endif() +ENDIF() + SET(SRC_SUBPATHS containers/*.c utilities/*.c @@ -80,20 +99,26 @@ FILE(GLOB_RECURSE SRC_LIB_EXT SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${qlibc_SOURCE_DIR}/lib) SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${qlibc_SOURCE_DIR}/lib) +LIST(APPEND QLIBC_INCLUDE_DIRECTORIES ${qlibc_SOURCE_DIR}/src/internal) +LIST(APPEND QLIBC_INCLUDE_DIRECTORIES ${WITH_PGSQL_INCLUDE_DIR}) +LIST(APPEND QLIBC_LINK_DIRECTORIES ${WITH_PGSQL_LIBRARY_DIR}) + +INCLUDE_DIRECTORIES(${QLIBC_INCLUDE_DIRECTORIES}) +LINK_DIRECTORIES(${QLIBC_LINK_DIRECTORIES}) + ADD_LIBRARY(qlibc-static STATIC ${SRC_LIB}) ADD_LIBRARY(qlibc SHARED ${SRC_LIB}) ADD_LIBRARY(qlibcext-static STATIC ${SRC_LIB_EXT}) ADD_LIBRARY(qlibcext SHARED ${SRC_LIB_EXT}) -INCLUDE_DIRECTORIES(${qlibc_SOURCE_DIR}/src/internal) TARGET_INCLUDE_DIRECTORIES(qlibc-static PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_INCLUDE_DIRECTORIES(qlibc PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_INCLUDE_DIRECTORIES(qlibcext-static PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_LINK_LIBRARIES(qlibc-static PRIVATE ${CMAKE_THREAD_LIBS_INIT}) TARGET_LINK_LIBRARIES(qlibc PRIVATE ${CMAKE_THREAD_LIBS_INIT}) -TARGET_LINK_LIBRARIES(qlibcext-static PRIVATE ${CMAKE_THREAD_LIBS_INIT}) -TARGET_LINK_LIBRARIES(qlibcext PUBLIC qlibc) +TARGET_LINK_LIBRARIES(qlibcext-static PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${QLIBC_LINK_LIBS}) +TARGET_LINK_LIBRARIES(qlibcext PUBLIC qlibc ${QLIBC_LINK_LIBS}) SET(QLIBC_HEADER "${qlibc_SOURCE_DIR}/include/qlibc") INSTALL(DIRECTORY ${QLIBC_HEADER} DESTINATION include) diff --git a/Makefile.in b/Makefile.in index 6372eeb1..ae431b4e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -34,6 +34,9 @@ all: test: all make -C tests test +test-ext: all + make -C tests test-ext + install: make -C src install @@ -44,7 +47,6 @@ uninstall: clean: make -C src clean - distclean: clean @for DIR in src tests examples; do \ echo "===> $${DIR}"; \ diff --git a/README.md b/README.md index 86df44d3..dfcf1421 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please refer the LICENSE document included in the package for more details. * INI-style Configuration File Parser. * HTTP client. * Rotating File Logger. - * Database(MySQL) interface. + * Database(MySQL/PostgreSQL) interface. * [Token-Bucket](https://en.wikipedia.org/wiki/Token_bucket) ## qLibc Tables at a Glance diff --git a/configure b/configure index 64ab9529..49a1e077 100755 --- a/configure +++ b/configure @@ -732,6 +732,9 @@ enable_ext_qhttpclient enable_ext_qdatabase with_openssl with_mysql +with_pgsql +with_pgsql_incdir +with_pgsql_libdir ' ac_precious_vars='build_alias host_alias @@ -1372,6 +1375,11 @@ Optional Packages: extension API. When it's enabled, user applications need to link mysql client library. (ex: -lmysqlclient) + --with-pgsql This will enable PostgreSQL database support in + qdatabase extension API. When it's enabled, user + applications need to link libpq library. (ex: -lpq) + --with-pgsql-incdir=DIR site header files for PostgreSQL in DIR. + --with-pgsql-libdir=DIR site library files for PostgreSQL in DIR Some influential environment variables: CC C compiler command @@ -4843,6 +4851,101 @@ See \`config.log' for more details" "$LINENO" 5; } fi fi + +# Check whether --with-pgsql was given. +if test "${with_pgsql+set}" = set; then : + withval=$with_pgsql; +else + withval=no +fi + +if test "$withval" = yes; then + # check libpq-fe.h + as_ac_File=`$as_echo "ac_cv_file_$with_pgsql_incdir/libpq-fe.h" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $with_pgsql_incdir/libpq-fe.h" >&5 +$as_echo_n "checking for $with_pgsql_incdir/libpq-fe.h... " >&6; } +if eval \${$as_ac_File+:} false; then : + $as_echo_n "(cached) " >&6 +else + test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r "$with_pgsql_incdir/libpq-fe.h"; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi +fi +eval ac_res=\$$as_ac_File + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes"; then : + withval=yes +else + withval=no +fi + + if test "$withval" = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: find '$with_pgsql_incdir/libpq-fe.h'" >&5 +$as_echo "$as_me: find '$with_pgsql_incdir/libpq-fe.h'" >&6;} + CPPFLAGS="$CPPFLAGS -DENABLE_PGSQL -I$with_pgsql_incdir" + else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "Cannot find 'libpq-fe.h' header. Use --with-pgsql-incdir=DIR to specify the directory where 'libpq-fe.h' is located. +See \`config.log' for more details" "$LINENO" 5; } + fi + # check libpq.so + as_ac_File=`$as_echo "ac_cv_file_$with_pgsql_libdir/libpq.so" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $with_pgsql_libdir/libpq.so" >&5 +$as_echo_n "checking for $with_pgsql_libdir/libpq.so... " >&6; } +if eval \${$as_ac_File+:} false; then : + $as_echo_n "(cached) " >&6 +else + test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r "$with_pgsql_libdir/libpq.so"; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi +fi +eval ac_res=\$$as_ac_File + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes"; then : + withval=yes +else + withval=no +fi + + if test "$withval" = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: find '$with_pgsql_libdir/libpq.so'" >&5 +$as_echo "$as_me: find '$with_pgsql_libdir/libpq.so'" >&6;} + CPPFLAGS="$CPPFLAGS -Wl,-rpath,$with_pgsql_libdir -L$with_pgsql_libdir -lpq" + else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "Cannot find 'libpq.so' library. Use --with-pgsql-libdir=DIR to specify the directory where 'libpq.so' is located. +See \`config.log' for more details" "$LINENO" 5; } + fi +fi + +# Check whether --with-pgsql_incdir was given. +if test "${with_pgsql_incdir+set}" = set; then : + withval=$with_pgsql_incdir; +else + withval=no +fi + + +# Check whether --with-pgsql_libdir was given. +if test "${with_pgsql_libdir+set}" = set; then : + withval=$with_pgsql_libdir; +else + withval=no +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: CFLAGS $CFLAGS" >&5 $as_echo "$as_me: CFLAGS $CFLAGS" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: CPPFLAGS $CPPFLAGS" >&5 diff --git a/configure.ac b/configure.ac index 0630fa55..bceef450 100644 --- a/configure.ac +++ b/configure.ac @@ -198,6 +198,28 @@ if test "$withval" = yes; then fi fi +AC_ARG_WITH([pgsql],[AS_HELP_STRING([--with-pgsql], [This will enable PostgreSQL database support in qdatabase extension API. When it's enabled, user applications need to link libpq library. (ex: -lpq)])],[],[withval=no]) +if test "$withval" = yes; then + # check libpq-fe.h + AC_CHECK_FILE([$with_pgsql_incdir/libpq-fe.h],[withval=yes],[withval=no]) + if test "$withval" = yes; then + AC_MSG_NOTICE([find '$with_pgsql_incdir/libpq-fe.h']) + CPPFLAGS="$CPPFLAGS -DENABLE_PGSQL -I$with_pgsql_incdir" + else + AC_MSG_FAILURE([Cannot find 'libpq-fe.h' header. Use --with-pgsql-incdir=DIR to specify the directory where 'libpq-fe.h' is located.]) + fi + # check libpq.so + AC_CHECK_FILE([$with_pgsql_libdir/libpq.so],[withval=yes],[withval=no]) + if test "$withval" = yes; then + AC_MSG_NOTICE([find '$with_pgsql_libdir/libpq.so']) + CPPFLAGS="$CPPFLAGS -Wl,-rpath,$with_pgsql_libdir -L$with_pgsql_libdir -lpq" + else + AC_MSG_FAILURE([Cannot find 'libpq.so' library. Use --with-pgsql-libdir=DIR to specify the directory where 'libpq.so' is located.]) + fi +fi +AC_ARG_WITH([pgsql_incdir],[AS_HELP_STRING([--with-pgsql-incdir=DIR], [site header files for PostgreSQL in DIR.])],[],[withval=no]) +AC_ARG_WITH([pgsql_libdir],[AS_HELP_STRING([--with-pgsql-libdir=DIR], [site library files for PostgreSQL in DIR])],[],[withval=no]) + AC_MSG_NOTICE([CFLAGS $CFLAGS]) AC_MSG_NOTICE([CPPFLAGS $CPPFLAGS]) #AC_MSG_NOTICE([LIBS $LIBS]) diff --git a/include/qlibc/extensions/qdatabase.h b/include/qlibc/extensions/qdatabase.h index 1101731d..3da36d06 100644 --- a/include/qlibc/extensions/qdatabase.h +++ b/include/qlibc/extensions/qdatabase.h @@ -51,6 +51,10 @@ extern "C" { #define Q_ENABLE_MYSQL (1) #endif /* _mysql_h */ +#ifdef LIBPQ_FE_H +#define Q_ENABLE_PGSQL (1) +#endif /* LIBPQ_FE_H */ + /* types */ typedef struct qdbresult_s qdbresult_t; typedef struct qdb_s qdb_t; @@ -65,11 +69,11 @@ extern qdb_t *qdb(const char *dbtype, */ struct qdbresult_s { /* encapsulated member functions */ - const char *(*getstr) (qdbresult_t *result, const char *field); + const char *(*get_str) (qdbresult_t *result, const char *field); const char *(*get_str_at) (qdbresult_t *result, int idx); - int (*getint) (qdbresult_t *result, const char *field); + int (*get_int) (qdbresult_t *result, const char *field); int (*get_int_at) (qdbresult_t *result, int idx); - bool (*getnext) (qdbresult_t *result); + bool (*get_next) (qdbresult_t *result); int (*get_cols) (qdbresult_t *result); int (*get_rows) (qdbresult_t *result); @@ -86,6 +90,11 @@ struct qdbresult_s { int cols; int cursor; #endif + +#ifdef Q_ENABLE_PGSQL + /* private variables for pgsql database - do not access directly */ + void *pgsql; +#endif }; /* qdb object structure */ @@ -131,6 +140,11 @@ struct qdb_s { /* private variables for mysql database - do not access directly */ MYSQL *mysql; #endif + +#ifdef Q_ENABLE_PGSQL + /* private variables for pgsql database - do not access directly */ + void *pgsql; +#endif }; #ifdef __cplusplus diff --git a/src/extensions/qdatabase.c b/src/extensions/qdatabase.c index ad238307..29895baf 100644 --- a/src/extensions/qdatabase.c +++ b/src/extensions/qdatabase.c @@ -77,7 +77,7 @@ #ifndef DISABLE_QDATABASE -#if defined(ENABLE_MYSQL) || defined( _DOXYGEN_SKIP) +#if defined(ENABLE_MYSQL) || defined(ENABLE_PGSQL) ||defined( _DOXYGEN_SKIP) #ifdef ENABLE_MYSQL #include "mysql.h" @@ -86,7 +86,11 @@ #define Q_MYSQL_OPT_CONNECT_TIMEOUT (10) #define Q_MYSQL_OPT_READ_TIMEOUT (30) #define Q_MYSQL_OPT_WRITE_TIMEOUT (30) -#endif +#endif /* ENABLE_MYSQL */ + +#ifdef ENABLE_PGSQL +#include "qlibpq.h" +#endif /* ENABLE_PGSQL */ #include #include @@ -120,11 +124,11 @@ static const char *get_error(qdb_t *db, unsigned int *errorno); static void free_(qdb_t *db); // qdbresult_t object -static const char *_resultGetStr(qdbresult_t *result, const char *field); -static const char *_resultGetStrAt(qdbresult_t *result, int idx); -static int _resultGetInt(qdbresult_t *result, const char *field); -static int _resultGetIntAt(qdbresult_t *result, int idx); -static bool _resultGetNext(qdbresult_t *result); +static const char *result_get_str(qdbresult_t *result, const char *field); +static const char *result_get_str_at(qdbresult_t *result, int idx); +static int result_get_int(qdbresult_t *result, const char *field); +static int result_get_int_at(qdbresult_t *result, int idx); +static bool result_get_next(qdbresult_t *result); static int result_get_cols(qdbresult_t *result); static int result_get_rows(qdbresult_t *result); @@ -132,39 +136,52 @@ static int result_get_row(qdbresult_t *result); static void result_free(qdbresult_t *result); -#endif +#endif /* _DOXYGEN_SKIP */ /** * Initialize internal connector structure * - * @param dbtype database server type. currently "MYSQL" is only supported + * @param dbtype database server type. currently "MYSQL" and "PGSQL" are supported * @param addr ip or fqdn address. * @param port port number + * @param database database name. * @param username database username * @param password database password - * @param database database server type. currently "MYSQL" is only supported * @param autocommit sets autocommit mode on if autocommit is true, off if * autocommit is false + * (can only be set to true, when dbtype is "PGSQL") * * @return a pointer of qdb_t object in case of successful, * otherwise returns NULL. * * @code - * qdb_t *db = qdb("MYSQL", - * "dbhost.qdecoder.org", 3306, "test", "secret", - * "sampledb", true); + * qdb_t *db = qdb("MYSQL", "dbhost.qdecoder.org", 3306, + * "sampledb", "test", "secret", true); + * if (db == NULL) { + * printf("ERROR: Not supported database type.\n"); + * return -1; + * } + * @endcode + * + * @code + * qdb_t *db = qdb("PGSQL", "127.0.0.1", 5432, + * "sampledb", "test", "secret", true); * if (db == NULL) { * printf("ERROR: Not supported database type.\n"); * return -1; * } * @endcode + */ -qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, - const char *password, const char *database, bool autocommit) +qdb_t *qdb(const char *dbtype, + const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit) { // check db type #ifdef Q_ENABLE_MYSQL if (strcmp(dbtype, "MYSQL")) return NULL; +#elif defined(Q_ENABLE_PGSQL) + if (strcmp(dbtype, "PGSQL")) return NULL; #else return NULL; #endif @@ -176,6 +193,12 @@ qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, return NULL; } +#ifdef Q_ENABLE_PGSQL + if (autocommit == false) { + return NULL; + } +#endif + // initialize qdb_t *db; if ((db = (qdb_t *)malloc(sizeof(qdb_t))) == NULL) return NULL; @@ -195,6 +218,8 @@ qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, // set db handler #ifdef Q_ENABLE_MYSQL db->mysql = NULL; +#elif defined(Q_ENABLE_PGSQL) + db->pgsql = NULL; #endif // assign methods @@ -304,6 +329,61 @@ static bool open_(qdb_t *db) db->connected = true; Q_MUTEX_LEAVE(db->qmutex); return true; +#elif defined(Q_ENABLE_PGSQL) + Q_MUTEX_ENTER(db->qmutex); + + bool ret = true; + PGconn* pgconn = NULL; + + // initialize handler + if (db->pgsql != NULL) close_(db); + + if ((db->pgsql = pgsql_init()) == NULL) { + ret = false; + goto lable_quit; + } + + // set options + /* do nothing */ + + /* int to char* */ + char pgport[10]; + snprintf(pgport, sizeof(pgport), "%d", db->info.port); + + // try to connect + pgconn = PQsetdbLogin(db->info.addr, pgport, + NULL, NULL, + db->info.database, db->info.username, db->info.password); + if (pgconn == NULL) { + close_(db); // free pgsql handler + ret = false; + goto lable_quit; + } + + if (PQstatus(pgconn) != CONNECTION_OK) { + const char* emsg = PQerrorMessage(pgconn); + pgsql_set_emsg(db->pgsql, emsg); + // fprintf(stderr, "%s", emsg); + ret = false; + goto lable_quit; + } + + qtype_cast(pgsql_t*, db->pgsql)->pgconn = pgconn; + + // set auto-commit + /* do nothing */ + + // set flag + db->connected = true; + +lable_quit: + + if (ret == false) { + PQfinish(pgconn); + } + + Q_MUTEX_LEAVE(db->qmutex); + return ret; #else return false; #endif @@ -334,6 +414,17 @@ static bool close_(qdb_t *db) } db->connected = false; + Q_MUTEX_LEAVE(db->qmutex); + return true; +#elif defined(Q_ENABLE_PGSQL) + Q_MUTEX_ENTER(db->qmutex); + + if (db->pgsql != NULL) { + pgsql_close(db->pgsql); + db->pgsql = NULL; + } + db->connected = false; + Q_MUTEX_LEAVE(db->qmutex); return true; #else @@ -365,6 +456,20 @@ static int execute_update(qdb_t *db, const char *query) if ((affected = mysql_affected_rows(db->mysql)) < 0) affected = -1; } + Q_MUTEX_LEAVE(db->qmutex); + return affected; +#elif defined(Q_ENABLE_PGSQL) + Q_MUTEX_ENTER(db->qmutex); + + int affected = -1; + + // query + DEBUG("%s", query); + if (pgsql_query(db->pgsql, query, PQ_WRITE) == 0) { + /* get affected rows */ + if ((affected = pgsql_affected_rows(db->pgsql)) < 0) affected = -1; + } + Q_MUTEX_LEAVE(db->qmutex); return affected; #else @@ -431,11 +536,41 @@ static qdbresult_t *execute_query(qdb_t *db, const char *query) result->cursor = 0; /* assign methods */ - result->get_str = _resultGetStr; - result->get_str_at = _resultGetStrAt; - result->get_int = _resultGetInt; - result->get_int_at = _resultGetIntAt; - result->get_next = _resultGetNext; + result->get_str = result_get_str; + result->get_str_at = result_get_str_at; + result->get_int = result_get_int; + result->get_int_at = result_get_int_at; + result->get_next = result_get_next; + + result->get_cols = result_get_cols; + result->get_rows = result_get_rows; + result->get_row = result_get_row; + + result->free = result_free; + + return result; +#elif defined(Q_ENABLE_PGSQL) + // query + DEBUG("%s", query); + if (pgsql_query(db->pgsql, query, PQ_READ)) return NULL; + + // store + qdbresult_t *result = (qdbresult_t *)malloc(sizeof(qdbresult_t)); + if (result == NULL) return NULL; + result->pgsql = db->pgsql; + + /* get meta data */ + // result->row = NULL; + qtype_cast(pgsql_t*, result->pgsql)->rows = pgsql_num_rows(result->pgsql); + qtype_cast(pgsql_t*, result->pgsql)->cols = pgsql_num_fields(result->pgsql); + qtype_cast(pgsql_t*, result->pgsql)->cursor = 0; + + /* assign methods */ + result->get_str = result_get_str; + result->get_str_at = result_get_str_at; + result->get_int = result_get_int; + result->get_int_at = result_get_int_at; + result->get_next = result_get_next; result->get_cols = result_get_cols; result->get_rows = result_get_rows; @@ -500,6 +635,18 @@ static bool begin_tran(qdb_t *db) } result->free(result); return true; +#elif defined(Q_ENABLE_PGSQL) + Q_MUTEX_ENTER(db->qmutex); + if (qtype_cast(qmutex_t*, db->qmutex)->count != 1) { + Q_MUTEX_LEAVE(db->qmutex); + return false; + } + + if (pgsql_query(db->pgsql, "START TRANSACTION", PQ_WRITE) < 0) { + Q_MUTEX_LEAVE(db->qmutex); + return false; + } + return true; #else return false; #endif @@ -526,6 +673,16 @@ static bool commit(qdb_t *db) Q_MUTEX_LEAVE(db->qmutex); } return ret; +#elif defined(Q_ENABLE_PGSQL) + bool ret = false; + if (pgsql_query(db->pgsql, "COMMIT", PQ_WRITE) == 0) { + ret = true; + } + + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { + Q_MUTEX_LEAVE(db->qmutex); + } + return ret; #else return false; #endif @@ -552,6 +709,16 @@ static bool rollback(qdb_t *db) Q_MUTEX_LEAVE(db->qmutex); } return ret; +#elif defined(Q_ENABLE_PGSQL) + bool ret = false; + if (pgsql_query(db->pgsql, "ROLLBACK", PQ_WRITE) == 0) { + ret = true; + } + + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { + Q_MUTEX_LEAVE(db->qmutex); + } + return ret; #else return 0; #endif @@ -577,8 +744,17 @@ static bool rollback(qdb_t *db) static bool set_fetchtype(qdb_t *db, bool fromdb) { if (db == NULL) return false; + +#ifdef Q_ENABLE_MYSQL db->info.fetchtype = fromdb; return true; +#elif defined(Q_ENABLE_PGSQL) + if (db->pgsql == NULL) return false; + pgsql_set_emsg(db->pgsql, "unsupported operation"); + return false; +#else + return false; +#endif } /** @@ -623,6 +799,17 @@ static bool ping(qdb_t *db) } } + return false; +#elif defined(Q_ENABLE_PGSQL) + if (db->connected == true && pgsql_query(db->pgsql, NULL, PQ_PING) == 0) { + return true; + } else { // ping test failed + if (open_(db) == true) { // try re-connect + DEBUG("Connection recovered."); + return true; + } + } + return false; #else return false; @@ -650,6 +837,14 @@ static const char *get_error(qdb_t *db, unsigned int *errorno) eno = mysql_errno(db->mysql); if (eno == 0) emsg = "(no error)"; else emsg = mysql_error(db->mysql); +#elif defined(Q_ENABLE_PGSQL) + emsg = qtype_cast(pgsql_t*, db->pgsql)->emsg; + if (emsg == NULL) { + eno = 0; + emsg = "(no error)"; + } else { + eno = 1; + } #else emsg = "(not implemented)"; #endif @@ -695,7 +890,7 @@ static void free_(qdb_t *db) * @note * Do not free returned string. */ -static const char *_resultGetStr(qdbresult_t *result, const char *field) +static const char *result_get_str(qdbresult_t *result, const char *field) { #ifdef Q_ENABLE_MYSQL if (result == NULL || result->rs == NULL || result->cols <= 0) return NULL; @@ -710,6 +905,34 @@ static const char *_resultGetStr(qdbresult_t *result, const char *field) } return NULL; +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->cols <= 0) { + return NULL; + } + + const char *val = NULL; + int rows = qtype_cast(pgsql_t*, result->pgsql)->rows; + int cols = qtype_cast(pgsql_t*, result->pgsql)->cols; + int cur = qtype_cast(pgsql_t*, result->pgsql)->cursor; + + /* get row num */ + int row = -1; + for (int i = 0; i < rows; i++) { + val = PQfname(qtype_cast(pgsql_t*, result->pgsql)->pgresult, i); + if (!strcasecmp(val, field)) { + row = i; + break; + } + } + + if (row == -1) { + return NULL; + } + + val = PQgetvalue(qtype_cast(pgsql_t*, result->pgsql)->pgresult, cur, row); + return val; #else return NULL; #endif @@ -723,7 +946,7 @@ static const char *_resultGetStr(qdbresult_t *result, const char *field) * * @return a string pointer if successful, otherwise returns NULL. */ -static const char *_resultGetStrAt(qdbresult_t *result, int idx) +static const char *result_get_str_at(qdbresult_t *result, int idx) { #ifdef Q_ENABLE_MYSQL if (result == NULL @@ -734,6 +957,19 @@ static const char *_resultGetStrAt(qdbresult_t *result, int idx) return NULL; } return result->row[idx-1]; +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->rows == 0 + || qtype_cast(pgsql_t*, result->pgsql)->cols == 0 + || idx <= 0 + || idx > qtype_cast(pgsql_t*, result->pgsql)->cols) { + return NULL; + } + + int cur = qtype_cast(pgsql_t*, result->pgsql)->cursor; + const char *val = PQgetvalue(qtype_cast(pgsql_t*, result->pgsql)->pgresult, cur, idx - 1); + return val; #else return NULL; #endif @@ -747,7 +983,7 @@ static const char *_resultGetStrAt(qdbresult_t *result, int idx) * * @return a integer converted value */ -static int _resultGetInt(qdbresult_t *result, const char *field) +static int result_get_int(qdbresult_t *result, const char *field) { const char *val = result->get_str(result, field); if (val == NULL) return 0; @@ -762,7 +998,7 @@ static int _resultGetInt(qdbresult_t *result, const char *field) * * @return a integer converted value */ -static int _resultGetIntAt(qdbresult_t *result, int idx) +static int result_get_int_at(qdbresult_t *result, int idx) { const char *val = result->get_str_at(result, idx); if (val == NULL) return 0; @@ -776,7 +1012,7 @@ static int _resultGetIntAt(qdbresult_t *result, int idx) * * @return true if successful, false if no more rows are left */ -static bool _resultGetNext(qdbresult_t *result) +static bool result_get_next(qdbresult_t *result) { #ifdef Q_ENABLE_MYSQL if (result == NULL || result->rs == NULL) return false; @@ -784,6 +1020,20 @@ static bool _resultGetNext(qdbresult_t *result) if ((result->row = mysql_fetch_row(result->rs)) == NULL) return false; result->cursor++; + return true; +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->pgresult == NULL) { + return false; + } + + int cursor = qtype_cast(pgsql_t*, result->pgsql)->cursor; + if (++cursor == qtype_cast(pgsql_t*, result->pgsql)->rows) { + return false; + } + + qtype_cast(pgsql_t*, result->pgsql)->cursor = cursor; return true; #else return false; @@ -802,6 +1052,9 @@ static int result_get_cols(qdbresult_t *result) #ifdef Q_ENABLE_MYSQL if (result == NULL || result->rs == NULL) return 0; return result->cols; +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->cols; #else return 0; #endif @@ -819,6 +1072,9 @@ static int result_get_rows(qdbresult_t *result) #ifdef Q_ENABLE_MYSQL if (result == NULL || result->rs == NULL) return 0; return mysql_num_rows(result->rs); +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->rows; #else return 0; #endif @@ -839,6 +1095,9 @@ static int result_get_row(qdbresult_t *result) #ifdef Q_ENABLE_MYSQL if (result == NULL || result->rs == NULL) return 0; return result->cursor; +#elif defined(Q_ENABLE_PGSQL) + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->cursor; #else return 0; #endif @@ -862,6 +1121,12 @@ static void result_free(qdbresult_t *result) } free(result); return; +#elif defined(Q_ENABLE_PGSQL) + if (result) { + result->pgsql = NULL; + free(result); + } + return; #else return; #endif diff --git a/src/internal/qlibpq.h b/src/internal/qlibpq.h new file mode 100644 index 00000000..60353cf6 --- /dev/null +++ b/src/internal/qlibpq.h @@ -0,0 +1,183 @@ +/****************************************************************************** + * qLibc + * + * Copyright (c) 2024 SunBeau. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +/* Wrap the low-level functions of the libpq library */ + +#ifndef QLIBPQ_H +#define QLIBPQ_H + +#include "libpq-fe.h" +#include +#include + +#define qtype_cast(_type, _src) ((_type)_src) + +typedef enum { + PQ_UNKNOWN, + PQ_WRITE, + PQ_READ, + PQ_PING, +} pgquery_t; + +typedef struct { + char *emsg; + PGconn *pgconn; + + PGresult *pgresult; + int rows; + int cols; + int cursor; +} pgsql_t; + +static inline pgsql_t* pgsql_init(void) +{ + pgsql_t* pgsql = (pgsql_t *)calloc(1, sizeof(pgsql_t)); + return pgsql; +} + +static inline const char* pgsql_set_emsg(pgsql_t* pgsql, const char* emsg) +{ + if (pgsql == NULL || emsg == NULL) { + return NULL; + } + + if (pgsql->emsg) { + free(pgsql->emsg); + } + + pgsql->emsg = strdup(emsg); + return pgsql->emsg; +} + +static inline void pgsql_close(pgsql_t* pgsql) +{ + if (pgsql == NULL) { + return; + } + + if (pgsql->emsg) { + free(pgsql->emsg); + pgsql->emsg = NULL; + } + + if (pgsql->pgresult) { + PQclear(pgsql->pgresult); + pgsql->pgresult = NULL; + } + + if (pgsql->pgconn) { + PQfinish(pgsql->pgconn); + pgsql->pgconn = NULL; + } + + free(pgsql); +} + +static inline void pgsql_query_result(pgsql_t* pgsql) +{ + if (pgsql->pgresult) { + PQclear(pgsql->pgresult); + pgsql->pgresult = NULL; + } +} + +static inline int pgsql_query(pgsql_t* pgsql, const char *query, pgquery_t type) +{ + if (pgsql == NULL || pgsql->pgconn == NULL) { + return -1; /* error */ + } + + /* free previous query results */ + pgsql_query_result(pgsql); + + if (type == PQ_PING) { + query = ""; + } + + PGresult *result = PQexec(pgsql->pgconn, query); + ExecStatusType status = PQresultStatus(result); + + switch (type) { + case PQ_PING: { + if (status != PGRES_EMPTY_QUERY) { + return -1; /* error */ + } + break; + } + + case PQ_READ: { + if (status != PGRES_TUPLES_OK) { + return -1; /* error */ + } + break; + } + + default: { + if (status != PGRES_COMMAND_OK) { + return -1; /* error */ + } + break; + } + } + + pgsql->pgresult = result; + + return 0; /* ok */ +} + +static inline int pgsql_affected_rows(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return atoi(PQcmdTuples(pgsql->pgresult)); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_rows(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return PQntuples(pgsql->pgresult); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_fields(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return PQnfields(pgsql->pgresult); + } + + /* error */ + return -1; +} + +#endif /* QLIBPQ_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d7721b58..459c24df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,10 @@ SET(test_list test_qhash ) +IF(NOT "${WITH_PGSQL}" STREQUAL "") + LIST(APPEND test_list test_qdatabase_pgsql) +ENDIF() + SET(test_file_list test_qhash_data_1.bin test_qhash_data_2.bin diff --git a/tests/Makefile.in b/tests/Makefile.in index f5fd7ece..3cd2730b 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -52,13 +52,18 @@ TARGETS = \ test_qstack \ test_qhash +TARGETS_EXT = \ + test_qdatabase_pgsql + LIBQLIBC = ${QLIBC_LIBDIR}/libqlibc.a ${DEPLIBS} LIBQLIBCEXT = ${QLIBC_LIBDIR}/libqlibcext.a ## Main all: ${TARGETS} +ext: ${TARGETS_EXT} + +run: test test-ext -run: test test: all @./launcher.sh ${TARGETS} @@ -92,10 +97,20 @@ test_qvector: test_qvector.o test_qhash: test_qhash.o ${CC} ${CFLAGS} ${CPPFLAGS} -g -o $@ test_qhash.o ${LIBQLIBC} +## Test extensions +test-ext: ext + @./launcher.sh ${TARGETS_EXT} + +test_qdatabase_pgsql: test_qdatabase_pgsql.o + ${CC} -o $@ test_qdatabase_pgsql.o ${LIBQLIBCEXT} ${LIBQLIBC} ${CFLAGS} ${CPPFLAGS} + ## Clear Module clean: ${RM} -f *.o ${TARGETS} +clean-ext: + ${RM} -f *.o ${TARGETS_EXT} + ## Compile Module .c.o: ${CC} ${CFLAGS} ${CPPFLAGS} -c -o $@ $< diff --git a/tests/test_qdatabase_pgsql.c b/tests/test_qdatabase_pgsql.c new file mode 100644 index 00000000..96605e9f --- /dev/null +++ b/tests/test_qdatabase_pgsql.c @@ -0,0 +1,303 @@ +/****************************************************************************** + * qLibc + * + * Copyright (c) 2024 SunBeau. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ +/* This code is written and updated by following people and released under + * the same license as above qLibc license. + * Copyright (c) 2015 Zhenjiang Xie - https://github.com/Charles0429 + *****************************************************************************/ + +#include "libpq-fe.h" + +#include "qunit.h" +#include "qlibc.h" +#include "qlibcext.h" + +const char *default_dbtype = "PGSQL"; +const char *default_addr = "127.0.0.1"; +const int default_port = 5432; +const char *default_database = "postgres"; +const char *default_username = "postgres"; +const char *default_password = "123456"; +const bool default_autocommit = true; + +QUNIT_START("Test qdatabase_pgsql.c"); + +qdb_t* test_connect_database(const char *database) { + if (database == NULL) { + database = default_database; + } + + qdb_t *db = qdb( + default_dbtype, + default_addr, + default_port, + database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(db); + ASSERT_FALSE(db->connected); + ASSERT_FALSE(db->get_conn_status(db)); + + ASSERT_TRUE(db->open(db)); + ASSERT_TRUE(db->connected); + ASSERT_TRUE(db->get_conn_status(db)); + + return db; +} + +qdb_t* test_connect_testdb(qdb_t* database) { + qdb_t *testdb = NULL; + char query[128]; + const char *test_database = "qdb_testdb"; + + /* test execute_update() */ + snprintf(query, sizeof(query), "DROP DATABASE IF EXISTS %s;", test_database); + ASSERT_EQUAL_INT(database->execute_update(database, query), 0); + + /* create new database */ + snprintf(query, sizeof(query), "CREATE DATABASE %s;", test_database); + ASSERT_EQUAL_INT(database->execute_update(database, query), 0); + + /* connect "qdb_testdb" database */ + testdb = qdb( + default_dbtype, + default_addr, + default_port, + test_database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(testdb); + ASSERT_FALSE(testdb->connected); + + ASSERT_TRUE(testdb->open(testdb)); + ASSERT_TRUE(testdb->connected); + + return testdb; +} + +/* + CREATE TABLE animals ( + animal_id INT PRIMARY KEY, + animal_name VARCHAR(255), + species VARCHAR(255), + gender VARCHAR(255), + age INT, + weight INT, + description VARCHAR(255) + ); + + INSERT INTO animals (animal_id, animal_name, species, gender, age, weight, description) + VALUES + (1, 'Dog', 'Canis lupus familiaris', 'Male', 2, 15, 'A friendly dog named Max'), + (2, 'Cat', 'Felis catus', 'Female', 5, 8, 'A lazy cat named Lucy'), + (3, 'Rabbit', 'Oryctolagus cuniculus', 'Male', 1, 2, 'A cute little rabbit named Bugs'), + (4, 'Hamster', 'Mesocricetus auratus', 'Female', 2, 50, 'A hyperactive hamster named Sparky'), + (5, 'Fish', 'Poecilia reticulata', 'Female', 1, 0.1, 'A peaceful betta fish named Blue'); +*/ +void test_create_table_animals(qdb_t* database) { + int rows; + + rows = database->execute_update(database, + "CREATE TABLE animals (" + "animal_id INT PRIMARY KEY," + "animal_name VARCHAR(255)," + "species VARCHAR(255)," + "gender VARCHAR(255)," + "age INT," + "weight INT," + "description VARCHAR(255)" + ");" + ); + ASSERT_EQUAL_INT(rows, 0); + + rows = database->execute_updatef(database, + "INSERT INTO animals VALUES" + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s);", + "1", "'Dog'", "'Canis lupus familiaris'", "'Male'", "2", "15", "'A friendly dog named Max'", + "2", "'Cat'", "'Felis catus'", "'Female'", "5", "8", "'A lazy cat named Lucy'", + "3", "'Rabbit'", "'Oryctolagus cuniculus'", "'Male'", "1", "2", "'A cute little rabbit named Bugs'", + "4", "'Hamster'", "'Mesocricetus auratus'", "'Female'", "2", "50", "'A hyperactive hamster named Sparky'", + "5", "'Fish'", "'Poecilia reticulata'", "'Female'", "1", "0.1", "'A peaceful betta fish named Blue'"); + ASSERT_EQUAL_INT(rows, 5); +} + +TEST("Test1: qdb()/open()/close()/free()") { + qdb_t *db = NULL; + + /* test qdb() */ + db = qdb( + default_dbtype, + default_addr, + default_port, + default_database, + default_username, + default_password, + default_autocommit + ); + + ASSERT_NOT_NULL(db); + ASSERT_FALSE(db->connected); + ASSERT_TRUE(db->open(db)); + ASSERT_TRUE(db->connected); + ASSERT_TRUE(db->close(db)); + ASSERT_FALSE(db->connected); + db->free(db); +} + +TEST("Test2: execute_updatef()") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + + /* test: execute_updatef */ + test_create_table_animals(test_db); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test3: execute_queryf()") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + + /* test: execute_queryf */ + qdbresult_t *qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 5); + ASSERT_EQUAL_INT(qrst->get_cols(qrst), 7); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 0); + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Dog"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Canis lupus familiaris"); + ASSERT_TRUE(qrst->get_next(qrst)); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 1); + ASSERT_TRUE(qrst->get_next(qrst)); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 2); + + ASSERT_EQUAL_INT(qrst->get_int(qrst, "animal_id"), 3); + ASSERT_EQUAL_INT(qrst->get_int_at(qrst, 5), 1); + qrst->free(qrst); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test4: transaction") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + int rows; + qdbresult_t *qrst; + + /* test: rollback */ + ASSERT_TRUE(test_db->begin_tran(test_db)); + rows = test_db->execute_update(test_db, "DELETE FROM animals WHERE animal_id > 3;"); + ASSERT_EQUAL_INT(rows, 2); + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 3); + qrst->free(qrst); + ASSERT_TRUE(test_db->rollback(test_db)); /* rollback */ + + /* test: commit */ + ASSERT_TRUE(test_db->begin_tran(test_db)); + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 5); + rows = test_db->execute_update(test_db, "DELETE FROM animals WHERE animal_id > 3;"); + ASSERT_EQUAL_INT(rows, 2); + ASSERT_TRUE(test_db->commit(test_db)); /* commit */ + + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 3); + qrst->free(qrst); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test5: autocommit") { + qdb_t *db = qdb( + default_dbtype, + default_addr, + default_port, + default_database, + default_username, + default_password, + false + ); + + /* Autocommit set to false is not supported in pgsql */ + ASSERT_NULL(db); +} + +TEST("Test6: other") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + + /* test:get_error */ + ASSERT_EQUAL_STR(test_db->get_error(test_db, NULL), "(no error)"); + ASSERT_FALSE(test_db->set_fetchtype(test_db, true)); + ASSERT_EQUAL_STR(test_db->get_error(test_db, NULL), "unsupported operation"); + + /* test: ping */ + ASSERT_TRUE(test_db->ping(test_db)); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +QUNIT_END(); \ No newline at end of file