summaryrefslogtreecommitdiffstats
path: root/src/lib-sql
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-sql')
-rw-r--r--src/lib-sql/Makefile.am144
-rw-r--r--src/lib-sql/Makefile.in1159
-rw-r--r--src/lib-sql/driver-cassandra.c2588
-rw-r--r--src/lib-sql/driver-mysql.c844
-rw-r--r--src/lib-sql/driver-pgsql.c1344
-rw-r--r--src/lib-sql/driver-sqlite.c555
-rw-r--r--src/lib-sql/driver-sqlpool.c934
-rw-r--r--src/lib-sql/driver-test.c514
-rw-r--r--src/lib-sql/driver-test.h28
-rw-r--r--src/lib-sql/sql-api-private.h255
-rw-r--r--src/lib-sql/sql-api.c846
-rw-r--r--src/lib-sql/sql-api.h251
-rw-r--r--src/lib-sql/sql-db-cache.c156
-rw-r--r--src/lib-sql/sql-db-cache.h13
14 files changed, 9631 insertions, 0 deletions
diff --git a/src/lib-sql/Makefile.am b/src/lib-sql/Makefile.am
new file mode 100644
index 0000000..89c3d2d
--- /dev/null
+++ b/src/lib-sql/Makefile.am
@@ -0,0 +1,144 @@
+noinst_LTLIBRARIES = libsql.la libdriver_test.la
+
+SQL_DRIVER_PLUGINS =
+
+# automake seems to force making this unconditional..
+NOPLUGIN_LDFLAGS =
+
+if SQL_PLUGINS
+if BUILD_MYSQL
+MYSQL_LIB = libdriver_mysql.la
+SQL_DRIVER_PLUGINS += mysql
+endif
+if BUILD_PGSQL
+PGSQL_LIB = libdriver_pgsql.la
+SQL_DRIVER_PLUGINS += pgsql
+endif
+if BUILD_SQLITE
+SQLITE_LIB = libdriver_sqlite.la
+SQL_DRIVER_PLUGINS += sqlite
+endif
+if BUILD_CASSANDRA
+CASSANDRA_LIB = libdriver_cassandra.la
+SQL_DRIVER_PLUGINS += cassandra
+endif
+
+sql_module_LTLIBRARIES = \
+ $(MYSQL_LIB) \
+ $(PGSQL_LIB) \
+ $(SQLITE_LIB) \
+ $(CASSANDRA_LIB)
+
+sql_moduledir = $(moduledir)
+endif
+
+sql_drivers = @sql_drivers@
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+dist_sources = \
+ sql-api.c \
+ sql-db-cache.c
+
+if ! SQL_PLUGINS
+driver_sources = \
+ driver-mysql.c \
+ driver-pgsql.c \
+ driver-sqlite.c \
+ driver-cassandra.c
+endif
+
+libsql_la_SOURCES = \
+ $(dist_sources) \
+ $(driver_sources) \
+ driver-sqlpool.c
+libsql_la_LIBADD = $(SQL_LIBS)
+
+nodist_libsql_la_SOURCES = sql-drivers-register.c
+
+deplibs = \
+ ../lib-dovecot/libdovecot.la
+
+if SQL_PLUGINS
+libdriver_mysql_la_LDFLAGS = -module -avoid-version
+libdriver_mysql_la_LIBADD = $(MYSQL_LIBS)
+libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS)
+libdriver_mysql_la_SOURCES = driver-mysql.c
+
+libdriver_pgsql_la_LDFLAGS = -module -avoid-version
+libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS)
+libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS)
+libdriver_pgsql_la_SOURCES = driver-pgsql.c
+
+libdriver_sqlite_la_LDFLAGS = -module -avoid-version
+libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS)
+libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS)
+libdriver_sqlite_la_SOURCES = driver-sqlite.c
+
+libdriver_cassandra_la_LDFLAGS = -module -avoid-version
+libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS)
+libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS)
+libdriver_cassandra_la_SOURCES = driver-cassandra.c
+else
+endif
+
+libdriver_test_la_LDFLAGS = -avoid-version
+libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+libdriver_test_la_SOURCES = driver-test.c
+
+noinst_HEADERS = driver-test.h
+
+pkglib_LTLIBRARIES = libdovecot-sql.la
+libdovecot_sql_la_SOURCES =
+libdovecot_sql_la_LIBADD = libsql.la $(deplibs)
+libdovecot_sql_la_DEPENDENCIES = libsql.la
+libdovecot_sql_la_LDFLAGS = -export-dynamic
+
+headers = \
+ sql-api.h \
+ sql-api-private.h \
+ sql-db-cache.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+sql-drivers-register.c: Makefile
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "sql-api.h"' >>$@
+if ! SQL_PLUGINS
+ for i in $(sql_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \
+ fi; \
+ done
+endif
+ echo 'void sql_drivers_register_all(void) {' >>$@
+if ! SQL_PLUGINS
+ for i in $(sql_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \
+ fi; \
+ done
+endif
+ echo '}' >>$@
+
+if SQL_PLUGINS
+install-exec-local:
+ for d in auth dict; do \
+ $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \
+ for driver in $(SQL_DRIVER_PLUGINS); do \
+ rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \
+ $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \
+ done; \
+ done
+endif
+
+
+distclean-generic:
+ rm -f Makefile sql-drivers-register.c
diff --git a/src/lib-sql/Makefile.in b/src/lib-sql/Makefile.in
new file mode 100644
index 0000000..e7d8964
--- /dev/null
+++ b/src/lib-sql/Makefile.in
@@ -0,0 +1,1159 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_1 = mysql
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_2 = pgsql
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am__append_3 = sqlite
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am__append_4 = cassandra
+subdir = src/lib-sql
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES) \
+ $(sql_module_LTLIBRARIES)
+am_libdovecot_sql_la_OBJECTS =
+libdovecot_sql_la_OBJECTS = $(am_libdovecot_sql_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_sql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_sql_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am__DEPENDENCIES_1 =
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_cassandra_la_SOURCES_DIST = driver-cassandra.c
+@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_cassandra_la-driver-cassandra.lo
+libdriver_cassandra_la_OBJECTS = $(am_libdriver_cassandra_la_OBJECTS)
+libdriver_cassandra_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_cassandra_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_rpath = \
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ -rpath \
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_mysql_la_SOURCES_DIST = driver-mysql.c
+@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_mysql_la-driver-mysql.lo
+libdriver_mysql_la_OBJECTS = $(am_libdriver_mysql_la_OBJECTS)
+libdriver_mysql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_mysql_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_rpath = \
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_pgsql_la_SOURCES_DIST = driver-pgsql.c
+@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_pgsql_la-driver-pgsql.lo
+libdriver_pgsql_la_OBJECTS = $(am_libdriver_pgsql_la_OBJECTS)
+libdriver_pgsql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_pgsql_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_rpath = \
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_sqlite_la_SOURCES_DIST = driver-sqlite.c
+@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_sqlite_la-driver-sqlite.lo
+libdriver_sqlite_la_OBJECTS = $(am_libdriver_sqlite_la_OBJECTS)
+libdriver_sqlite_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_sqlite_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_rpath = \
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+libdriver_test_la_LIBADD =
+am_libdriver_test_la_OBJECTS = libdriver_test_la-driver-test.lo
+libdriver_test_la_OBJECTS = $(am_libdriver_test_la_OBJECTS)
+libdriver_test_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_test_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+libsql_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am__libsql_la_SOURCES_DIST = sql-api.c sql-db-cache.c driver-mysql.c \
+ driver-pgsql.c driver-sqlite.c driver-cassandra.c \
+ driver-sqlpool.c
+am__objects_1 = sql-api.lo sql-db-cache.lo
+@SQL_PLUGINS_FALSE@am__objects_2 = driver-mysql.lo driver-pgsql.lo \
+@SQL_PLUGINS_FALSE@ driver-sqlite.lo driver-cassandra.lo
+am_libsql_la_OBJECTS = $(am__objects_1) $(am__objects_2) \
+ driver-sqlpool.lo
+nodist_libsql_la_OBJECTS = sql-drivers-register.lo
+libsql_la_OBJECTS = $(am_libsql_la_OBJECTS) \
+ $(nodist_libsql_la_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/driver-cassandra.Plo \
+ ./$(DEPDIR)/driver-mysql.Plo ./$(DEPDIR)/driver-pgsql.Plo \
+ ./$(DEPDIR)/driver-sqlite.Plo ./$(DEPDIR)/driver-sqlpool.Plo \
+ ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo \
+ ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo \
+ ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo \
+ ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo \
+ ./$(DEPDIR)/libdriver_test_la-driver-test.Plo \
+ ./$(DEPDIR)/sql-api.Plo ./$(DEPDIR)/sql-db-cache.Plo \
+ ./$(DEPDIR)/sql-drivers-register.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_sql_la_SOURCES) \
+ $(libdriver_cassandra_la_SOURCES) \
+ $(libdriver_mysql_la_SOURCES) $(libdriver_pgsql_la_SOURCES) \
+ $(libdriver_sqlite_la_SOURCES) $(libdriver_test_la_SOURCES) \
+ $(libsql_la_SOURCES) $(nodist_libsql_la_SOURCES)
+DIST_SOURCES = $(libdovecot_sql_la_SOURCES) \
+ $(am__libdriver_cassandra_la_SOURCES_DIST) \
+ $(am__libdriver_mysql_la_SOURCES_DIST) \
+ $(am__libdriver_pgsql_la_SOURCES_DIST) \
+ $(am__libdriver_sqlite_la_SOURCES_DIST) \
+ $(libdriver_test_la_SOURCES) $(am__libsql_la_SOURCES_DIST)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+
+# automake seems to force making this unconditional..
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libsql.la libdriver_test.la
+SQL_DRIVER_PLUGINS = $(am__append_1) $(am__append_2) $(am__append_3) \
+ $(am__append_4)
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@MYSQL_LIB = libdriver_mysql.la
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@PGSQL_LIB = libdriver_pgsql.la
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@SQLITE_LIB = libdriver_sqlite.la
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@CASSANDRA_LIB = libdriver_cassandra.la
+@SQL_PLUGINS_TRUE@sql_module_LTLIBRARIES = \
+@SQL_PLUGINS_TRUE@ $(MYSQL_LIB) \
+@SQL_PLUGINS_TRUE@ $(PGSQL_LIB) \
+@SQL_PLUGINS_TRUE@ $(SQLITE_LIB) \
+@SQL_PLUGINS_TRUE@ $(CASSANDRA_LIB)
+
+@SQL_PLUGINS_TRUE@sql_moduledir = $(moduledir)
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+dist_sources = \
+ sql-api.c \
+ sql-db-cache.c
+
+@SQL_PLUGINS_FALSE@driver_sources = \
+@SQL_PLUGINS_FALSE@ driver-mysql.c \
+@SQL_PLUGINS_FALSE@ driver-pgsql.c \
+@SQL_PLUGINS_FALSE@ driver-sqlite.c \
+@SQL_PLUGINS_FALSE@ driver-cassandra.c
+
+libsql_la_SOURCES = \
+ $(dist_sources) \
+ $(driver_sources) \
+ driver-sqlpool.c
+
+libsql_la_LIBADD = $(SQL_LIBS)
+nodist_libsql_la_SOURCES = sql-drivers-register.c
+deplibs = \
+ ../lib-dovecot/libdovecot.la
+
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_LIBADD = $(MYSQL_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_SOURCES = driver-mysql.c
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_SOURCES = driver-pgsql.c
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_SOURCES = driver-sqlite.c
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_SOURCES = driver-cassandra.c
+libdriver_test_la_LDFLAGS = -avoid-version
+libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+
+libdriver_test_la_SOURCES = driver-test.c
+noinst_HEADERS = driver-test.h
+pkglib_LTLIBRARIES = libdovecot-sql.la
+libdovecot_sql_la_SOURCES =
+libdovecot_sql_la_LIBADD = libsql.la $(deplibs)
+libdovecot_sql_la_DEPENDENCIES = libsql.la
+libdovecot_sql_la_LDFLAGS = -export-dynamic
+headers = \
+ sql-api.h \
+ sql-api-private.h \
+ sql-db-cache.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-sql/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-sql/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-sql_moduleLTLIBRARIES: $(sql_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(sql_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(sql_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(sql_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(sql_moduledir)"; \
+ }
+
+uninstall-sql_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(sql_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(sql_moduledir)/$$f"; \
+ done
+
+clean-sql_moduleLTLIBRARIES:
+ -test -z "$(sql_module_LTLIBRARIES)" || rm -f $(sql_module_LTLIBRARIES)
+ @list='$(sql_module_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-sql.la: $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_DEPENDENCIES) $(EXTRA_libdovecot_sql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_sql_la_LINK) -rpath $(pkglibdir) $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_LIBADD) $(LIBS)
+
+libdriver_cassandra.la: $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_DEPENDENCIES) $(EXTRA_libdriver_cassandra_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_cassandra_la_LINK) $(am_libdriver_cassandra_la_rpath) $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_LIBADD) $(LIBS)
+
+libdriver_mysql.la: $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_DEPENDENCIES) $(EXTRA_libdriver_mysql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_mysql_la_LINK) $(am_libdriver_mysql_la_rpath) $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_LIBADD) $(LIBS)
+
+libdriver_pgsql.la: $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_DEPENDENCIES) $(EXTRA_libdriver_pgsql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_pgsql_la_LINK) $(am_libdriver_pgsql_la_rpath) $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_LIBADD) $(LIBS)
+
+libdriver_sqlite.la: $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_DEPENDENCIES) $(EXTRA_libdriver_sqlite_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_sqlite_la_LINK) $(am_libdriver_sqlite_la_rpath) $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_LIBADD) $(LIBS)
+
+libdriver_test.la: $(libdriver_test_la_OBJECTS) $(libdriver_test_la_DEPENDENCIES) $(EXTRA_libdriver_test_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_test_la_LINK) $(libdriver_test_la_OBJECTS) $(libdriver_test_la_LIBADD) $(LIBS)
+
+libsql.la: $(libsql_la_OBJECTS) $(libsql_la_DEPENDENCIES) $(EXTRA_libsql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libsql_la_OBJECTS) $(libsql_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-cassandra.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-mysql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-pgsql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlite.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlpool.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_test_la-driver-test.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-api.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-db-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-drivers-register.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdriver_cassandra_la-driver-cassandra.lo: driver-cassandra.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_cassandra_la-driver-cassandra.lo -MD -MP -MF $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-cassandra.c' object='libdriver_cassandra_la-driver-cassandra.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c
+
+libdriver_mysql_la-driver-mysql.lo: driver-mysql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_mysql_la-driver-mysql.lo -MD -MP -MF $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo $(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-mysql.c' object='libdriver_mysql_la-driver-mysql.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c
+
+libdriver_pgsql_la-driver-pgsql.lo: driver-pgsql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_pgsql_la-driver-pgsql.lo -MD -MP -MF $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-pgsql.c' object='libdriver_pgsql_la-driver-pgsql.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c
+
+libdriver_sqlite_la-driver-sqlite.lo: driver-sqlite.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_sqlite_la-driver-sqlite.lo -MD -MP -MF $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-sqlite.c' object='libdriver_sqlite_la-driver-sqlite.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c
+
+libdriver_test_la-driver-test.lo: driver-test.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_test_la-driver-test.lo -MD -MP -MF $(DEPDIR)/libdriver_test_la-driver-test.Tpo -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_test_la-driver-test.Tpo $(DEPDIR)/libdriver_test_la-driver-test.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-test.c' object='libdriver_test_la-driver-test.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+@SQL_PLUGINS_FALSE@install-exec-local:
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlpool.Plo
+ -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo
+ -rm -f ./$(DEPDIR)/sql-api.Plo
+ -rm -f ./$(DEPDIR)/sql-db-cache.Plo
+ -rm -f ./$(DEPDIR)/sql-drivers-register.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS \
+ install-sql_moduleLTLIBRARIES
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-exec-local install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlpool.Plo
+ -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo
+ -rm -f ./$(DEPDIR)/sql-api.Plo
+ -rm -f ./$(DEPDIR)/sql-db-cache.Plo
+ -rm -f ./$(DEPDIR)/sql-drivers-register.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \
+ uninstall-sql_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-exec-local install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-sql_moduleLTLIBRARIES install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \
+ uninstall-sql_moduleLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+sql-drivers-register.c: Makefile
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "sql-api.h"' >>$@
+@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \
+@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \
+@SQL_PLUGINS_FALSE@ echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \
+@SQL_PLUGINS_FALSE@ fi; \
+@SQL_PLUGINS_FALSE@ done
+ echo 'void sql_drivers_register_all(void) {' >>$@
+@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \
+@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \
+@SQL_PLUGINS_FALSE@ echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \
+@SQL_PLUGINS_FALSE@ fi; \
+@SQL_PLUGINS_FALSE@ done
+ echo '}' >>$@
+
+@SQL_PLUGINS_TRUE@install-exec-local:
+@SQL_PLUGINS_TRUE@ for d in auth dict; do \
+@SQL_PLUGINS_TRUE@ $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \
+@SQL_PLUGINS_TRUE@ for driver in $(SQL_DRIVER_PLUGINS); do \
+@SQL_PLUGINS_TRUE@ rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \
+@SQL_PLUGINS_TRUE@ $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \
+@SQL_PLUGINS_TRUE@ done; \
+@SQL_PLUGINS_TRUE@ done
+
+distclean-generic:
+ rm -f Makefile sql-drivers-register.c
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-sql/driver-cassandra.c b/src/lib-sql/driver-cassandra.c
new file mode 100644
index 0000000..2b86a12
--- /dev/null
+++ b/src/lib-sql/driver-cassandra.c
@@ -0,0 +1,2588 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "array.h"
+#include "hostpid.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "ioloop.h"
+#include "net.h"
+#include "write-full.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "safe-memset.h"
+#include "settings-parser.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_CASSANDRA
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <cassandra.h>
+#include <pthread.h>
+
+#define IS_CONNECTED(db) \
+ ((db)->api.state != SQL_DB_STATE_DISCONNECTED && \
+ (db)->api.state != SQL_DB_STATE_CONNECTING)
+
+#define CASSANDRA_FALLBACK_WARN_INTERVAL_SECS 60
+#define CASSANDRA_FALLBACK_FIRST_RETRY_MSECS 50
+#define CASSANDRA_FALLBACK_MAX_RETRY_MSECS (1000*60)
+
+#define CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS (5*1000)
+
+typedef void driver_cassandra_callback_t(CassFuture *future, void *context);
+
+enum cassandra_counter_type {
+ CASSANDRA_COUNTER_TYPE_QUERY_SENT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER,
+ CASSANDRA_COUNTER_TYPE_QUERY_SLOW,
+
+ CASSANDRA_COUNTER_COUNT
+};
+static const char *counter_names[CASSANDRA_COUNTER_COUNT] = {
+ "sent",
+ "recv_ok",
+ "recv_err_no_hosts",
+ "recv_err_queue_full",
+ "recv_err_client_timeout",
+ "recv_err_server_timeout",
+ "recv_err_server_unavailable",
+ "recv_err_other",
+ "slow",
+};
+
+enum cassandra_query_type {
+ CASSANDRA_QUERY_TYPE_READ,
+ CASSANDRA_QUERY_TYPE_READ_MORE,
+ CASSANDRA_QUERY_TYPE_WRITE,
+ CASSANDRA_QUERY_TYPE_DELETE,
+
+ CASSANDRA_QUERY_TYPE_COUNT
+};
+
+static const char *cassandra_query_type_names[CASSANDRA_QUERY_TYPE_COUNT] = {
+ "read", "read-more", "write", "delete"
+};
+
+struct cassandra_callback {
+ unsigned int id;
+ struct timeout *to;
+ CassFuture *future;
+ struct cassandra_db *db;
+ driver_cassandra_callback_t *callback;
+ void *context;
+};
+
+struct cassandra_db {
+ struct sql_db api;
+
+ char *hosts, *keyspace, *user, *password;
+ CassConsistency read_consistency, write_consistency, delete_consistency;
+ CassConsistency read_fallback_consistency, write_fallback_consistency;
+ CassConsistency delete_fallback_consistency;
+ CassLogLevel log_level;
+ bool debug_queries;
+ bool latency_aware_routing;
+ bool init_ssl;
+ unsigned int protocol_version;
+ unsigned int num_threads;
+ unsigned int connect_timeout_msecs, request_timeout_msecs;
+ unsigned int warn_timeout_msecs;
+ unsigned int heartbeat_interval_secs, idle_timeout_secs;
+ unsigned int execution_retry_interval_msecs, execution_retry_times;
+ unsigned int page_size;
+ in_port_t port;
+
+ CassCluster *cluster;
+ CassSession *session;
+ CassTimestampGen *timestamp_gen;
+ CassSsl *ssl;
+
+ int fd_pipe[2];
+ struct io *io_pipe;
+ ARRAY(struct cassandra_sql_prepared_statement *) pending_prepares;
+ ARRAY(struct cassandra_callback *) callbacks;
+ ARRAY(struct cassandra_result *) results;
+ unsigned int callback_ids;
+
+ char *metrics_path;
+ char *ssl_ca_file;
+ char *ssl_cert_file;
+ char *ssl_private_key_file;
+ char *ssl_private_key_password;
+ CassSslVerifyFlags ssl_verify_flags;
+
+ struct timeout *to_metrics;
+ uint64_t counters[CASSANDRA_COUNTER_COUNT];
+
+ struct timeval primary_query_last_sent[CASSANDRA_QUERY_TYPE_COUNT];
+ time_t last_fallback_warning[CASSANDRA_QUERY_TYPE_COUNT];
+ unsigned int fallback_failures[CASSANDRA_QUERY_TYPE_COUNT];
+
+ /* for synchronous queries: */
+ struct ioloop *ioloop, *orig_ioloop;
+ struct sql_result *sync_result;
+
+ char *error;
+};
+
+struct cassandra_result {
+ struct sql_result api;
+ CassStatement *statement;
+ const CassResult *result;
+ CassIterator *iterator;
+ char *log_query;
+ char *error;
+ CassConsistency consistency, fallback_consistency;
+ enum cassandra_query_type query_type;
+ struct timeval page0_start_time, start_time, finish_time;
+ unsigned int row_count, total_row_count, page_num;
+ cass_int64_t timestamp;
+
+ pool_t row_pool;
+ ARRAY_TYPE(const_string) fields;
+ ARRAY(size_t) field_sizes;
+
+ sql_query_callback_t *callback;
+ void *context;
+
+ bool is_prepared:1;
+ bool query_sent:1;
+ bool finished:1;
+ bool paging_continues:1;
+};
+
+struct cassandra_transaction_context {
+ struct sql_transaction_context ctx;
+ int refcount;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ struct cassandra_sql_statement *stmt;
+ char *query;
+ char *log_query;
+ cass_int64_t query_timestamp;
+ char *error;
+
+ bool begin_succeeded:1;
+ bool begin_failed:1;
+ bool failed:1;
+};
+
+struct cassandra_sql_arg {
+ unsigned int column_idx;
+
+ char *value_str;
+ const unsigned char *value_binary;
+ size_t value_binary_size;
+ int64_t value_int64;
+};
+
+struct cassandra_sql_statement {
+ struct sql_statement stmt;
+
+ struct cassandra_sql_prepared_statement *prep;
+ CassStatement *cass_stmt;
+
+ ARRAY(struct cassandra_sql_arg) pending_args;
+ cass_int64_t timestamp;
+
+ struct cassandra_result *result;
+};
+
+struct cassandra_sql_prepared_statement {
+ struct sql_prepared_statement prep_stmt;
+
+ /* NULL, until the prepare is asynchronously finished */
+ const CassPrepared *prepared;
+ /* statements waiting for prepare to finish */
+ ARRAY(struct cassandra_sql_statement *) pending_statements;
+ /* an error here will cause the prepare to be retried on the next
+ execution attempt. */
+ char *error;
+
+ bool pending;
+};
+
+extern const struct sql_db driver_cassandra_db;
+extern const struct sql_result driver_cassandra_result;
+
+static struct {
+ CassConsistency consistency;
+ const char *name;
+} cass_consistency_names[] = {
+ { CASS_CONSISTENCY_ANY, "any" },
+ { CASS_CONSISTENCY_ONE, "one" },
+ { CASS_CONSISTENCY_TWO, "two" },
+ { CASS_CONSISTENCY_THREE, "three" },
+ { CASS_CONSISTENCY_QUORUM, "quorum" },
+ { CASS_CONSISTENCY_ALL, "all" },
+ { CASS_CONSISTENCY_LOCAL_QUORUM, "local-quorum" },
+ { CASS_CONSISTENCY_EACH_QUORUM, "each-quorum" },
+ { CASS_CONSISTENCY_SERIAL, "serial" },
+ { CASS_CONSISTENCY_LOCAL_SERIAL, "local-serial" },
+ { CASS_CONSISTENCY_LOCAL_ONE, "local-one" }
+};
+
+static struct {
+ CassLogLevel log_level;
+ const char *name;
+} cass_log_level_names[] = {
+ { CASS_LOG_CRITICAL, "critical" },
+ { CASS_LOG_ERROR, "error" },
+ { CASS_LOG_WARN, "warn" },
+ { CASS_LOG_INFO, "info" },
+ { CASS_LOG_DEBUG, "debug" },
+ { CASS_LOG_TRACE, "trace" }
+};
+
+static struct event_category event_category_cassandra = {
+ .parent = &event_category_sql,
+ .name = "cassandra"
+};
+
+static pthread_t main_thread_id;
+static bool main_thread_id_set;
+
+static void driver_cassandra_prepare_pending(struct cassandra_db *db);
+static void
+prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt);
+static void driver_cassandra_result_send_query(struct cassandra_result *result);
+static void driver_cassandra_send_queries(struct cassandra_db *db);
+static void result_finish(struct cassandra_result *result);
+
+static void log_one_line(const CassLogMessage *message,
+ enum log_type log_type, const char *log_level_str,
+ const char *text, size_t text_len)
+{
+ /* NOTE: We may not be in the main thread. We can't use the
+ standard Dovecot functions that may use data stack. That's why
+ we can't use i_log_type() in here, but have to re-implement the
+ internal logging protocol. Otherwise preserve Cassandra's own
+ logging format. */
+ fprintf(stderr, "\001%c%s %u.%03u %s(%s:%d:%s): %.*s\n",
+ log_type+1, my_pid,
+ (unsigned int)(message->time_ms / 1000),
+ (unsigned int)(message->time_ms % 1000),
+ log_level_str,
+ message->file, message->line, message->function,
+ (int)text_len, text);
+}
+
+static void
+driver_cassandra_log_handler(const CassLogMessage* message,
+ void *data ATTR_UNUSED)
+{
+ enum log_type log_type = LOG_TYPE_ERROR;
+ const char *log_level_str = "";
+
+ switch (message->severity) {
+ case CASS_LOG_DISABLED:
+ case CASS_LOG_LAST_ENTRY:
+ i_unreached();
+ case CASS_LOG_CRITICAL:
+ log_type = LOG_TYPE_PANIC;
+ break;
+ case CASS_LOG_ERROR:
+ log_type = LOG_TYPE_ERROR;
+ break;
+ case CASS_LOG_WARN:
+ log_type = LOG_TYPE_WARNING;
+ break;
+ case CASS_LOG_INFO:
+ log_type = LOG_TYPE_INFO;
+ break;
+ case CASS_LOG_TRACE:
+ log_level_str = "[TRACE] ";
+ /* fall through */
+ case CASS_LOG_DEBUG:
+ log_type = LOG_TYPE_DEBUG;
+ break;
+ }
+
+ /* Log message may contain LFs, so log each line separately. */
+ const char *p, *line = message->message;
+ while ((p = strchr(line, '\n')) != NULL) {
+ log_one_line(message, log_type, log_level_str, line, p - line);
+ line = p+1;
+ }
+ log_one_line(message, log_type, log_level_str, line, strlen(line));
+}
+
+static void driver_cassandra_init_log(void)
+{
+ failure_callback_t *fatal_callback, *error_callback;
+ failure_callback_t *info_callback, *debug_callback;
+
+ i_get_failure_handlers(&fatal_callback, &error_callback,
+ &info_callback, &debug_callback);
+ if (i_failure_handler_is_internal(debug_callback)) {
+ /* Using internal logging protocol. Use it ourself to set log
+ levels correctly. */
+ cass_log_set_callback(driver_cassandra_log_handler, NULL);
+ }
+}
+
+static int consistency_parse(const char *str, CassConsistency *consistency_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(cass_consistency_names); i++) {
+ if (strcmp(cass_consistency_names[i].name, str) == 0) {
+ *consistency_r = cass_consistency_names[i].consistency;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static int log_level_parse(const char *str, CassLogLevel *log_level_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(cass_log_level_names); i++) {
+ if (strcmp(cass_log_level_names[i].name, str) == 0) {
+ *log_level_r = cass_log_level_names[i].log_level;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static void driver_cassandra_set_state(struct cassandra_db *db,
+ enum sql_db_state state)
+{
+ /* switch back to original ioloop in case the caller wants to
+ add/remove timeouts */
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->orig_ioloop);
+ sql_db_set_state(&db->api, state);
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->ioloop);
+}
+
+static void driver_cassandra_close(struct cassandra_db *db, const char *error)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt;
+ struct cassandra_result *const *resultp;
+
+ io_remove(&db->io_pipe);
+ if (db->fd_pipe[0] != -1) {
+ i_close_fd(&db->fd_pipe[0]);
+ i_close_fd(&db->fd_pipe[1]);
+ }
+ driver_cassandra_set_state(db, SQL_DB_STATE_DISCONNECTED);
+
+ array_foreach_elem(&db->pending_prepares, prep_stmt) {
+ prep_stmt->pending = FALSE;
+ prep_stmt->error = i_strdup(error);
+ prepare_finish_pending_statements(prep_stmt);
+ }
+ array_clear(&db->pending_prepares);
+
+ while (array_count(&db->results) > 0) {
+ resultp = array_front(&db->results);
+ if ((*resultp)->error == NULL)
+ (*resultp)->error = i_strdup(error);
+ result_finish(*resultp);
+ }
+
+ if (db->ioloop != NULL) {
+ /* running a sync query, stop it */
+ io_loop_stop(db->ioloop);
+ }
+}
+
+static void driver_cassandra_log_error(struct cassandra_db *db,
+ CassFuture *future, const char *str)
+{
+ const char *message;
+ size_t size;
+
+ cass_future_error_message(future, &message, &size);
+ e_error(db->api.event, "%s: %.*s", str, (int)size, message);
+}
+
+static struct cassandra_callback *
+cassandra_callback_detach(struct cassandra_db *db, unsigned int id)
+{
+ struct cassandra_callback *cb, *const *cbp;
+
+ /* usually there are only a few callbacks, so don't bother with using
+ a hash table */
+ array_foreach(&db->callbacks, cbp) {
+ cb = *cbp;
+ if (cb->id == id) {
+ array_delete(&db->callbacks,
+ array_foreach_idx(&db->callbacks, cbp), 1);
+ return cb;
+ }
+ }
+ return NULL;
+}
+
+static void cassandra_callback_run(struct cassandra_callback *cb)
+{
+ timeout_remove(&cb->to);
+ cb->callback(cb->future, cb->context);
+ cass_future_free(cb->future);
+ i_free(cb);
+}
+
+static void driver_cassandra_future_callback(CassFuture *future ATTR_UNUSED,
+ void *context)
+{
+ struct cassandra_callback *cb = context;
+
+ if (pthread_equal(pthread_self(), main_thread_id) != 0) {
+ /* called immediately from the main thread. */
+ cassandra_callback_detach(cb->db, cb->id);
+ cb->to = timeout_add_short(0, cassandra_callback_run, cb);
+ return;
+ }
+
+ /* this isn't the main thread - communicate with main thread by
+ writing the callback id to the pipe. note that we must not use
+ almost any dovecot functions here because most of them are using
+ data-stack, which isn't thread-safe. especially don't use
+ i_error() here. */
+ if (write_full(cb->db->fd_pipe[1], &cb->id, sizeof(cb->id)) < 0) {
+ const char *str = t_strdup_printf(
+ "cassandra: write(pipe) failed: %s\n",
+ strerror(errno));
+ (void)write_full(STDERR_FILENO, str, strlen(str));
+ }
+}
+
+static void driver_cassandra_input_id(struct cassandra_db *db, unsigned int id)
+{
+ struct cassandra_callback *cb;
+
+ cb = cassandra_callback_detach(db, id);
+ if (cb == NULL)
+ i_panic("cassandra: Received unknown ID %u", id);
+ cassandra_callback_run(cb);
+}
+
+static void driver_cassandra_input(struct cassandra_db *db)
+{
+ unsigned int ids[1024];
+ ssize_t ret;
+
+ ret = read(db->fd_pipe[0], ids, sizeof(ids));
+ if (ret < 0)
+ e_error(db->api.event, "read(pipe) failed: %m");
+ else if (ret == 0)
+ e_error(db->api.event, "read(pipe) failed: EOF");
+ else if (ret % sizeof(ids[0]) != 0)
+ e_error(db->api.event, "read(pipe) returned wrong amount of data");
+ else {
+ /* success */
+ unsigned int i, count = ret / sizeof(ids[0]);
+
+ for (i = 0; i < count &&
+ db->api.state != SQL_DB_STATE_DISCONNECTED; i++)
+ driver_cassandra_input_id(db, ids[i]);
+ return;
+ }
+ driver_cassandra_close(db, "IPC pipe closed");
+}
+
+static void
+driver_cassandra_set_callback(CassFuture *future, struct cassandra_db *db,
+ driver_cassandra_callback_t *callback,
+ void *context)
+{
+ struct cassandra_callback *cb;
+
+ i_assert(callback != NULL);
+
+ cb = i_new(struct cassandra_callback, 1);
+ cb->future = future;
+ cb->callback = callback;
+ cb->context = context;
+ cb->db = db;
+
+ array_push_back(&db->callbacks, &cb);
+ cb->id = ++db->callback_ids;
+ if (cb->id == 0)
+ cb->id = ++db->callback_ids;
+
+ /* NOTE: The callback may be called immediately by this same thread.
+ This is checked within the callback. It may also be called at any
+ time after this call by another thread. So we must not access "cb"
+ again after this call. */
+ cass_future_set_callback(future, driver_cassandra_future_callback, cb);
+}
+
+static void connect_callback(CassFuture *future, void *context)
+{
+ struct cassandra_db *db = context;
+
+ if (cass_future_error_code(future) != CASS_OK) {
+ driver_cassandra_log_error(db, future,
+ "Couldn't connect to Cassandra");
+ driver_cassandra_close(db, "Couldn't connect to Cassandra");
+ return;
+ }
+ driver_cassandra_set_state(db, SQL_DB_STATE_IDLE);
+ if (db->ioloop != NULL) {
+ /* driver_cassandra_sync_init() waiting for connection to
+ finish */
+ io_loop_stop(db->ioloop);
+ }
+ driver_cassandra_prepare_pending(db);
+ driver_cassandra_send_queries(db);
+}
+
+static int driver_cassandra_connect(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ CassFuture *future;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ if (pipe(db->fd_pipe) < 0) {
+ e_error(_db->event, "pipe() failed: %m");
+ return -1;
+ }
+ db->io_pipe = io_add(db->fd_pipe[0], IO_READ,
+ driver_cassandra_input, db);
+ driver_cassandra_set_state(db, SQL_DB_STATE_CONNECTING);
+
+ future = cass_session_connect_keyspace(db->session, db->cluster,
+ db->keyspace);
+ driver_cassandra_set_callback(future, db, connect_callback, db);
+ return 0;
+}
+
+static void driver_cassandra_disconnect(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ driver_cassandra_close(db, "Disconnected");
+}
+
+static const char *
+driver_cassandra_escape_string(struct sql_db *db ATTR_UNUSED,
+ const char *string)
+{
+ string_t *escaped;
+ unsigned int i;
+
+ if (strchr(string, '\'') == NULL)
+ return string;
+ escaped = t_str_new(strlen(string)+10);
+ for (i = 0; string[i] != '\0'; i++) {
+ if (string[i] == '\'')
+ str_append_c(escaped, '\'');
+ str_append_c(escaped, string[i]);
+ }
+ return str_c(escaped);
+}
+
+static int driver_cassandra_parse_connect_string(struct cassandra_db *db,
+ const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *key, *value, *error;
+ string_t *hosts = t_str_new(64);
+ bool read_fallback_set = FALSE, write_fallback_set = FALSE;
+ bool delete_fallback_set = FALSE;
+
+ db->log_level = CASS_LOG_WARN;
+ db->read_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->write_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->delete_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->connect_timeout_msecs = SQL_CONNECT_TIMEOUT_SECS*1000;
+ db->request_timeout_msecs = SQL_QUERY_TIMEOUT_SECS*1000;
+ db->warn_timeout_msecs = CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS;
+
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ *error_r = t_strdup_printf(
+ "Missing value in connect string: %s", *args);
+ return -1;
+ }
+ key = t_strdup_until(*args, value++);
+
+ if (str_begins(key, "ssl_"))
+ db->init_ssl = TRUE;
+
+ if (strcmp(key, "host") == 0) {
+ if (str_len(hosts) > 0)
+ str_append_c(hosts, ',');
+ str_append(hosts, value);
+ } else if (strcmp(key, "port") == 0) {
+ if (net_str2port(value, &db->port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid port: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "dbname") == 0 ||
+ strcmp(key, "keyspace") == 0) {
+ i_free(db->keyspace);
+ db->keyspace = i_strdup(value);
+ } else if (strcmp(key, "user") == 0) {
+ i_free(db->user);
+ db->user = i_strdup(value);
+ } else if (strcmp(key, "password") == 0) {
+ i_free(db->password);
+ db->password = i_strdup(value);
+ } else if (strcmp(key, "read_consistency") == 0) {
+ if (consistency_parse(value, &db->read_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown read_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "read_fallback_consistency") == 0) {
+ if (consistency_parse(value, &db->read_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown read_fallback_consistency: %s", value);
+ return -1;
+ }
+ read_fallback_set = TRUE;
+ } else if (strcmp(key, "write_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->write_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown write_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "write_fallback_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->write_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown write_fallback_consistency: %s",
+ value);
+ return -1;
+ }
+ write_fallback_set = TRUE;
+ } else if (strcmp(key, "delete_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->delete_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown delete_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "delete_fallback_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->delete_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown delete_fallback_consistency: %s",
+ value);
+ return -1;
+ }
+ delete_fallback_set = TRUE;
+ } else if (strcmp(key, "log_level") == 0) {
+ if (log_level_parse(value, &db->log_level) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown log_level: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "debug_queries") == 0) {
+ db->debug_queries = TRUE;
+ } else if (strcmp(key, "latency_aware_routing") == 0) {
+ db->latency_aware_routing = TRUE;
+ } else if (strcmp(key, "version") == 0) {
+ if (str_to_uint(value, &db->protocol_version) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid version: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "num_threads") == 0) {
+ if (str_to_uint(value, &db->num_threads) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid num_threads: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "heartbeat_interval") == 0) {
+ if (settings_get_time(value, &db->heartbeat_interval_secs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid heartbeat_interval '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "idle_timeout") == 0) {
+ if (settings_get_time(value, &db->idle_timeout_secs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid idle_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "connect_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->connect_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid connect_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "request_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->request_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid request_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "warn_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->warn_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid warn_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "metrics") == 0) {
+ i_free(db->metrics_path);
+ db->metrics_path = i_strdup(value);
+ } else if (strcmp(key, "execution_retry_interval") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->execution_retry_interval_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid execution_retry_interval '%s': %s",
+ value, error);
+ return -1;
+ }
+#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ *error_r = t_strdup_printf(
+ "This cassandra version does not support execution_retry_interval");
+ return -1;
+#endif
+ } else if (strcmp(key, "execution_retry_times") == 0) {
+ if (str_to_uint(value, &db->execution_retry_times) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid execution_retry_times %s",
+ value);
+ return -1;
+ }
+#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ *error_r = t_strdup_printf(
+ "This cassandra version does not support execution_retry_times");
+ return -1;
+#endif
+ } else if (strcmp(key, "page_size") == 0) {
+ if (str_to_uint(value, &db->page_size) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid page_size: %s",
+ value);
+ return -1;
+ }
+ } else if (strcmp(key, "ssl_ca") == 0) {
+ db->ssl_ca_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_cert_file") == 0) {
+ db->ssl_cert_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_private_key_file") == 0) {
+ db->ssl_private_key_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_private_key_password") == 0) {
+ db->ssl_private_key_password = i_strdup(value);
+ } else if (strcmp(key, "ssl_verify") == 0) {
+ if (strcmp(value, "none") == 0) {
+ db->ssl_verify_flags = CASS_SSL_VERIFY_NONE;
+ } else if (strcmp(value, "cert") == 0) {
+ db->ssl_verify_flags = CASS_SSL_VERIFY_PEER_CERT;
+ } else if (strcmp(value, "cert-ip") == 0) {
+ db->ssl_verify_flags =
+ CASS_SSL_VERIFY_PEER_CERT |
+ CASS_SSL_VERIFY_PEER_IDENTITY;
+#if HAVE_DECL_CASS_SSL_VERIFY_PEER_IDENTITY_DNS == 1
+ } else if (strcmp(value, "cert-dns") == 0) {
+ db->ssl_verify_flags =
+ CASS_SSL_VERIFY_PEER_CERT |
+ CASS_SSL_VERIFY_PEER_IDENTITY_DNS;
+#endif
+ } else {
+ *error_r = t_strdup_printf(
+ "Unsupported ssl_verify flags: '%s'",
+ value);
+ return -1;
+ }
+ } else {
+ *error_r = t_strdup_printf(
+ "Unknown connect string: %s", key);
+ return -1;
+ }
+ }
+
+ if (!read_fallback_set)
+ db->read_fallback_consistency = db->read_consistency;
+ if (!write_fallback_set)
+ db->write_fallback_consistency = db->write_consistency;
+ if (!delete_fallback_set)
+ db->delete_fallback_consistency = db->delete_consistency;
+
+ if (str_len(hosts) == 0) {
+ *error_r = t_strdup_printf("No hosts given in connect string");
+ return -1;
+ }
+ if (db->keyspace == NULL) {
+ *error_r = t_strdup_printf("No dbname given in connect string");
+ return -1;
+ }
+
+ if ((db->ssl_cert_file != NULL && db->ssl_private_key_file == NULL) ||
+ (db->ssl_cert_file == NULL && db->ssl_private_key_file != NULL)) {
+ *error_r = "ssl_cert_file and ssl_private_key_file need to be both set";
+ return -1;
+ }
+
+ db->hosts = i_strdup(str_c(hosts));
+ return 0;
+}
+
+static void
+driver_cassandra_get_metrics_json(struct cassandra_db *db, string_t *dest)
+{
+#define ADD_UINT64(_struct, _field) \
+ str_printfa(dest, "\""#_field"\": %llu,", \
+ (unsigned long long)metrics._struct._field);
+#define ADD_DOUBLE(_struct, _field) \
+ str_printfa(dest, "\""#_field"\": %02lf,", metrics._struct._field);
+ CassMetrics metrics;
+
+ cass_session_get_metrics(db->session, &metrics);
+ str_append(dest, "{ \"requests\": {");
+ ADD_UINT64(requests, min);
+ ADD_UINT64(requests, max);
+ ADD_UINT64(requests, mean);
+ ADD_UINT64(requests, stddev);
+ ADD_UINT64(requests, median);
+ ADD_UINT64(requests, percentile_75th);
+ ADD_UINT64(requests, percentile_95th);
+ ADD_UINT64(requests, percentile_98th);
+ ADD_UINT64(requests, percentile_99th);
+ ADD_UINT64(requests, percentile_999th);
+ ADD_DOUBLE(requests, mean_rate);
+ ADD_DOUBLE(requests, one_minute_rate);
+ ADD_DOUBLE(requests, five_minute_rate);
+ ADD_DOUBLE(requests, fifteen_minute_rate);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"stats\": {");
+ ADD_UINT64(stats, total_connections);
+ ADD_UINT64(stats, available_connections);
+ ADD_UINT64(stats, exceeded_pending_requests_water_mark);
+ ADD_UINT64(stats, exceeded_write_bytes_water_mark);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"errors\": {");
+ ADD_UINT64(errors, connection_timeouts);
+ ADD_UINT64(errors, pending_request_timeouts);
+ ADD_UINT64(errors, request_timeouts);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"queries\": {");
+ for (unsigned int i = 0; i < CASSANDRA_COUNTER_COUNT; i++) {
+ str_printfa(dest, "\"%s\": %"PRIu64",", counter_names[i],
+ db->counters[i]);
+ }
+ str_truncate(dest, str_len(dest)-1);
+ str_append(dest, "}}");
+}
+
+static void driver_cassandra_metrics_write(struct cassandra_db *db)
+{
+ struct var_expand_table tab[] = {
+ { '\0', NULL, NULL }
+ };
+ string_t *path = t_str_new(64);
+ string_t *data;
+ const char *error;
+ int fd;
+
+ if (var_expand(path, db->metrics_path, tab, &error) <= 0) {
+ e_error(db->api.event, "Failed to expand metrics_path=%s: %s",
+ db->metrics_path, error);
+ return;
+ }
+
+ fd = open(str_c(path), O_WRONLY | O_CREAT | O_TRUNC | O_NONBLOCK, 0600);
+ if (fd == -1) {
+ e_error(db->api.event, "creat(%s) failed: %m", str_c(path));
+ return;
+ }
+ data = t_str_new(1024);
+ driver_cassandra_get_metrics_json(db, data);
+ if (write_full(fd, str_data(data), str_len(data)) < 0)
+ e_error(db->api.event, "write(%s) failed: %m", str_c(path));
+ i_close_fd(&fd);
+}
+
+static void driver_cassandra_free(struct cassandra_db **_db)
+{
+ struct cassandra_db *db = *_db;
+ *_db = NULL;
+
+ event_unref(&db->api.event);
+ i_free(db->metrics_path);
+ i_free(db->hosts);
+ i_free(db->error);
+ i_free(db->keyspace);
+ i_free(db->user);
+ i_free(db->password);
+ i_free(db->ssl_ca_file);
+ i_free(db->ssl_cert_file);
+ i_free(db->ssl_private_key_file);
+ i_free_and_null(db->ssl_private_key_password);
+ array_free(&db->api.module_contexts);
+ if (db->ssl != NULL)
+ cass_ssl_free(db->ssl);
+ i_free(db);
+}
+
+static int driver_cassandra_init_ssl(struct cassandra_db *db, const char **error_r)
+{
+ buffer_t *buf = t_buffer_create(512);
+ CassError c_err;
+
+ db->ssl = cass_ssl_new();
+ i_assert(db->ssl != NULL);
+
+ if (db->ssl_ca_file != NULL) {
+ if (buffer_append_full_file(buf, db->ssl_ca_file, SIZE_MAX,
+ error_r) < 0)
+ return -1;
+ if ((c_err = cass_ssl_add_trusted_cert(db->ssl, str_c(buf))) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+ }
+
+ if (db->ssl_private_key_file != NULL && db->ssl_cert_file != NULL) {
+ buffer_set_used_size(buf, 0);
+ if (buffer_append_full_file(buf, db->ssl_private_key_file,
+ SIZE_MAX, error_r) < 0)
+ return -1;
+ c_err = cass_ssl_set_private_key(db->ssl, str_c(buf),
+ db->ssl_private_key_password);
+ safe_memset(buffer_get_modifiable_data(buf, NULL), 0, buf->used);
+ if (c_err != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+
+ buffer_set_used_size(buf, 0);
+ if (buffer_append_full_file(buf, db->ssl_cert_file, SIZE_MAX, error_r) < 0)
+ return -1;
+ if ((c_err = cass_ssl_set_cert(db->ssl, str_c(buf))) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+ }
+
+ cass_ssl_set_verify_flags(db->ssl, db->ssl_verify_flags);
+
+ return 0;
+}
+
+static int driver_cassandra_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r,
+ const char **error_r)
+{
+ struct cassandra_db *db;
+ int ret;
+
+ db = i_new(struct cassandra_db, 1);
+ db->api = driver_cassandra_db;
+ db->fd_pipe[0] = db->fd_pipe[1] = -1;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_cassandra);
+ event_set_append_log_prefix(db->api.event, "cassandra: ");
+
+ T_BEGIN {
+ ret = driver_cassandra_parse_connect_string(db,
+ set->connect_string, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+
+ if (ret < 0) {
+ driver_cassandra_free(&db);
+ return -1;
+ }
+
+ if (db->init_ssl && driver_cassandra_init_ssl(db, error_r) < 0) {
+ driver_cassandra_free(&db);
+ return -1;
+ }
+
+ driver_cassandra_init_log();
+ cass_log_set_level(db->log_level);
+ if (db->log_level >= CASS_LOG_DEBUG)
+ event_set_forced_debug(db->api.event, TRUE);
+
+ if (db->protocol_version > 0 && db->protocol_version < 4) {
+ /* binding with column indexes requires v4 */
+ db->api.v.prepared_statement_init = NULL;
+ db->api.v.prepared_statement_deinit = NULL;
+ db->api.v.statement_init_prepared = NULL;
+ }
+
+ db->timestamp_gen = cass_timestamp_gen_monotonic_new();
+ db->cluster = cass_cluster_new();
+
+#ifdef HAVE_CASS_CLUSTER_SET_USE_HOSTNAME_RESOLUTION
+ if ((db->ssl_verify_flags & CASS_SSL_VERIFY_PEER_IDENTITY_DNS) != 0) {
+ CassError c_err;
+ if ((c_err = cass_cluster_set_use_hostname_resolution(
+ db->cluster, cass_true)) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ driver_cassandra_free(&db);
+ return -1;
+ }
+ }
+#endif
+ cass_cluster_set_ssl(db->cluster, db->ssl);
+ cass_cluster_set_timestamp_gen(db->cluster, db->timestamp_gen);
+ cass_cluster_set_connect_timeout(db->cluster, db->connect_timeout_msecs);
+ cass_cluster_set_request_timeout(db->cluster, db->request_timeout_msecs);
+ cass_cluster_set_contact_points(db->cluster, db->hosts);
+ if (db->user != NULL && db->password != NULL)
+ cass_cluster_set_credentials(db->cluster, db->user, db->password);
+ if (db->port != 0)
+ cass_cluster_set_port(db->cluster, db->port);
+ if (db->protocol_version != 0)
+ cass_cluster_set_protocol_version(db->cluster, db->protocol_version);
+ if (db->num_threads != 0)
+ cass_cluster_set_num_threads_io(db->cluster, db->num_threads);
+ if (db->latency_aware_routing)
+ cass_cluster_set_latency_aware_routing(db->cluster, cass_true);
+ if (db->heartbeat_interval_secs != 0)
+ cass_cluster_set_connection_heartbeat_interval(db->cluster,
+ db->heartbeat_interval_secs);
+ if (db->idle_timeout_secs != 0)
+ cass_cluster_set_connection_idle_timeout(db->cluster,
+ db->idle_timeout_secs);
+#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ if (db->execution_retry_times > 0 && db->execution_retry_interval_msecs > 0)
+ cass_cluster_set_constant_speculative_execution_policy(
+ db->cluster, db->execution_retry_interval_msecs,
+ db->execution_retry_times);
+#endif
+ if (db->ssl != NULL) {
+ e_debug(db->api.event, "Enabling TLS for cluster");
+ cass_cluster_set_ssl(db->cluster, db->ssl);
+ }
+ db->session = cass_session_new();
+ if (db->metrics_path != NULL)
+ db->to_metrics = timeout_add(1000, driver_cassandra_metrics_write,
+ db);
+ i_array_init(&db->results, 16);
+ i_array_init(&db->callbacks, 16);
+ i_array_init(&db->pending_prepares, 16);
+ if (!main_thread_id_set) {
+ main_thread_id = pthread_self();
+ main_thread_id_set = TRUE;
+ }
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_cassandra_deinit_v(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ driver_cassandra_close(db, "Deinitialized");
+
+ i_assert(array_count(&db->callbacks) == 0);
+ array_free(&db->callbacks);
+ i_assert(array_count(&db->results) == 0);
+ array_free(&db->results);
+ i_assert(array_count(&db->pending_prepares) == 0);
+ array_free(&db->pending_prepares);
+
+ cass_session_free(db->session);
+ cass_cluster_free(db->cluster);
+ cass_timestamp_gen_free(db->timestamp_gen);
+ timeout_remove(&db->to_metrics);
+ sql_connection_log_finished(_db);
+ driver_cassandra_free(&db);
+}
+
+static void driver_cassandra_result_unlink(struct cassandra_db *db,
+ struct cassandra_result *result)
+{
+ struct cassandra_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&db->results, &count);
+ for (i = 0; i < count; i++) {
+ if (results[i] == result) {
+ array_delete(&db->results, i, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static void driver_cassandra_log_result(struct cassandra_result *result,
+ bool all_pages, long long reply_usecs)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ struct timeval now;
+ unsigned int row_count;
+
+ i_gettimeofday(&now);
+
+ string_t *str = t_str_new(128);
+ str_printfa(str, "Finished %squery '%s' (",
+ result->is_prepared ? "prepared " : "", result->log_query);
+ if (result->timestamp != 0)
+ str_printfa(str, "timestamp=%"PRId64", ", result->timestamp);
+ if (all_pages) {
+ str_printfa(str, "%u pages in total, ", result->page_num);
+ row_count = result->total_row_count;
+ } else {
+ if (result->page_num > 0 || result->paging_continues)
+ str_printfa(str, "page %u, ", result->page_num);
+ row_count = result->row_count;
+ }
+ str_printfa(str, "%u rows, %lld+%lld us): %s", row_count, reply_usecs,
+ timeval_diff_usecs(&now, &result->finish_time),
+ result->error != NULL ? result->error : "success");
+
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->api.event,
+ result->log_query, result->error == NULL,
+ NULL);
+ if (result->error != NULL)
+ e->add_str("error", result->error);
+
+ struct event *event = e->event();
+ if (db->debug_queries)
+ event_set_forced_debug(event, TRUE);
+ if (reply_usecs/1000 >= db->warn_timeout_msecs) {
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SLOW]++;
+ e_warning(event, "%s", str_c(str));
+ } else {
+ e_debug(event, "%s", str_c(str));
+ }
+}
+
+static void driver_cassandra_result_free(struct sql_result *_result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_result->db;
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ long long reply_usecs;
+
+ i_assert(!result->api.callback);
+ i_assert(result->callback == NULL);
+
+ if (_result == db->sync_result)
+ db->sync_result = NULL;
+
+ reply_usecs = timeval_diff_usecs(&result->finish_time,
+ &result->start_time);
+ driver_cassandra_log_result(result, FALSE, reply_usecs);
+
+ if (result->page_num > 0 && !result->paging_continues) {
+ /* Multi-page query finishes now. Log a debug/warning summary
+ message about it separate from the per-page messages. */
+ reply_usecs = timeval_diff_usecs(&result->finish_time,
+ &result->page0_start_time);
+ driver_cassandra_log_result(result, TRUE, reply_usecs);
+ }
+
+ if (result->result != NULL)
+ cass_result_free(result->result);
+ if (result->iterator != NULL)
+ cass_iterator_free(result->iterator);
+ if (result->statement != NULL)
+ cass_statement_free(result->statement);
+ pool_unref(&result->row_pool);
+ event_unref(&result->api.event);
+ i_free(result->log_query);
+ i_free(result->error);
+ i_free(result);
+}
+
+static void result_finish(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ bool free_result = TRUE;
+
+ result->finished = TRUE;
+ result->finish_time = ioloop_timeval;
+ driver_cassandra_result_unlink(db, result);
+
+ i_assert((result->error != NULL) == (result->iterator == NULL));
+
+ result->api.callback = TRUE;
+ T_BEGIN {
+ result->callback(&result->api, result->context);
+ } T_END;
+ result->api.callback = FALSE;
+
+ free_result = db->sync_result != &result->api;
+ if (db->ioloop != NULL)
+ io_loop_stop(db->ioloop);
+
+ i_assert(!free_result || result->api.refcount > 0);
+ result->callback = NULL;
+ if (free_result)
+ sql_result_unref(&result->api);
+}
+
+static void query_resend_with_fallback(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ time_t last_warning =
+ ioloop_time - db->last_fallback_warning[result->query_type];
+
+ if (last_warning >= CASSANDRA_FALLBACK_WARN_INTERVAL_SECS) {
+ e_warning(db->api.event,
+ "%s - retrying future %s queries with consistency %s (instead of %s)",
+ result->error, cassandra_query_type_names[result->query_type],
+ cass_consistency_string(result->fallback_consistency),
+ cass_consistency_string(result->consistency));
+ db->last_fallback_warning[result->query_type] = ioloop_time;
+ }
+ i_free_and_null(result->error);
+ db->fallback_failures[result->query_type]++;
+
+ result->consistency = result->fallback_consistency;
+ driver_cassandra_result_send_query(result);
+}
+
+static void counters_inc_error(struct cassandra_db *db, CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS]++;
+ break;
+ case CASS_ERROR_LIB_REQUEST_QUEUE_FULL:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL]++;
+ break;
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT]++;
+ break;
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT]++;
+ break;
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE]++;
+ break;
+ default:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER]++;
+ break;
+ }
+}
+
+static bool query_error_want_fallback(CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_LIB_WRITE_ERROR:
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ /* Communication problems on client side. Maybe it will work
+ with fallback consistency? */
+ return TRUE;
+ case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE:
+ /* The client library couldn't connect to enough Cassandra
+ nodes. The error message text is the same as for
+ CASS_ERROR_SERVER_UNAVAILABLE. */
+ return TRUE;
+ case CASS_ERROR_SERVER_SERVER_ERROR:
+ case CASS_ERROR_SERVER_OVERLOADED:
+ case CASS_ERROR_SERVER_IS_BOOTSTRAPPING:
+ case CASS_ERROR_SERVER_READ_TIMEOUT:
+ case CASS_ERROR_SERVER_READ_FAILURE:
+ case CASS_ERROR_SERVER_WRITE_FAILURE:
+ /* Servers are having trouble. Maybe with fallback consistency
+ we can reach non-troubled servers? */
+ return TRUE;
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ /* Cassandra server knows that there aren't enough nodes
+ available. "All hosts in current policy attempted and were
+ either unavailable or failed". */
+ return TRUE;
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ /* Cassandra server couldn't reach all the needed nodes.
+ This may be because it hasn't yet detected that the servers
+ are down, or because the servers are just too busy. We'll
+ try the fallback consistency to avoid unnecessary temporary
+ errors. */
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static enum sql_result_error_type
+driver_cassandra_error_is_uncertain(CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_SERVER_WRITE_FAILURE:
+ /* This happens when some of the replicas that were contacted
+ * by the coordinator replied with an error. */
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ /* A Cassandra timeout during a write query. */
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ /* The coordinator knows there are not enough replicas alive
+ * to perform a query with the requested consistency level. */
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ /* A request sent from the driver has timed out. */
+ case CASS_ERROR_LIB_WRITE_ERROR:
+ /* A write error occured. */
+ return SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN;
+ default:
+ return SQL_RESULT_ERROR_TYPE_UNKNOWN;
+ }
+}
+
+static void query_callback(CassFuture *future, void *context)
+{
+ struct cassandra_result *result = context;
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ CassError error = cass_future_error_code(future);
+
+ if (error != CASS_OK) {
+ const char *errmsg;
+ size_t errsize;
+ int msecs;
+
+ cass_future_error_message(future, &errmsg, &errsize);
+ i_free(result->error);
+
+ msecs = timeval_diff_msecs(&ioloop_timeval, &result->start_time);
+ counters_inc_error(db, error);
+ /* Timeouts bring uncertainty whether the query succeeded or
+ not. Also _SERVER_UNAVAILABLE could have actually written
+ enough copies of the data for the query to succeed. */
+ result->api.error_type = driver_cassandra_error_is_uncertain(error);
+ result->error = i_strdup_printf(
+ "Query '%s' failed: %.*s (in %u.%03u secs%s)",
+ result->log_query, (int)errsize, errmsg, msecs/1000, msecs%1000,
+ result->page_num == 0 ?
+ "" :
+ t_strdup_printf(", page %u", result->page_num));
+
+ if (query_error_want_fallback(error) &&
+ result->fallback_consistency != result->consistency) {
+ /* retry with fallback consistency */
+ query_resend_with_fallback(result);
+ return;
+ }
+ result_finish(result);
+ return;
+ }
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK]++;
+
+ if (result->fallback_consistency != result->consistency) {
+ /* non-fallback query finished successfully. if there had been
+ any fallbacks, reset them. */
+ db->fallback_failures[result->query_type] = 0;
+ }
+
+ result->result = cass_future_get_result(future);
+ result->iterator = cass_iterator_from_result(result->result);
+ result_finish(result);
+}
+
+static void driver_cassandra_init_statement(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+
+ cass_statement_set_consistency(result->statement, result->consistency);
+
+#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ cass_statement_set_is_idempotent(result->statement, cass_true);
+#endif
+ if (db->page_size > 0)
+ cass_statement_set_paging_size(result->statement, db->page_size);
+}
+
+static void driver_cassandra_result_send_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ CassFuture *future;
+
+ i_assert(result->statement != NULL);
+
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SENT]++;
+ if (result->query_type != CASSANDRA_QUERY_TYPE_READ_MORE)
+ driver_cassandra_init_statement(result);
+
+ future = cass_session_execute(db->session, result->statement);
+ driver_cassandra_set_callback(future, db, query_callback, result);
+}
+
+static bool
+driver_cassandra_want_fallback_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ unsigned int failure_count = db->fallback_failures[result->query_type];
+ unsigned int i, msecs = CASSANDRA_FALLBACK_FIRST_RETRY_MSECS;
+ struct timeval tv;
+
+ if (failure_count == 0)
+ return FALSE;
+ /* double the retries every time. */
+ for (i = 1; i < failure_count; i++) {
+ msecs *= 2;
+ if (msecs >= CASSANDRA_FALLBACK_MAX_RETRY_MSECS) {
+ msecs = CASSANDRA_FALLBACK_MAX_RETRY_MSECS;
+ break;
+ }
+ }
+ /* If last primary query sent timestamp + msecs is older than current
+ time, we need to retry the primary query. Note that this practically
+ prevents multiple primary queries from being attempted
+ simultaneously, because the caller updates primary_query_last_sent
+ immediately when returning.
+
+ The only time when multiple primary queries can be running in
+ parallel is when the earlier query is being slow and hasn't finished
+ early enough. This could even be a wanted feature, since while the
+ first query might have to wait for a timeout, Cassandra could have
+ been fixed in the meantime and the second query finishes
+ successfully. */
+ tv = db->primary_query_last_sent[result->query_type];
+ timeval_add_msecs(&tv, msecs);
+ return timeval_cmp(&ioloop_timeval, &tv) < 0;
+}
+
+static int driver_cassandra_send_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ int ret;
+
+ if (!SQL_DB_IS_READY(&db->api)) {
+ if ((ret = sql_connect(&db->api)) <= 0) {
+ if (ret < 0)
+ driver_cassandra_close(db,
+ "Couldn't connect to Cassandra");
+ return ret;
+ }
+ }
+
+ if (result->page0_start_time.tv_sec == 0)
+ result->page0_start_time = ioloop_timeval;
+ result->start_time = ioloop_timeval;
+ result->row_pool = pool_alloconly_create("cassandra result", 512);
+ switch (result->query_type) {
+ case CASSANDRA_QUERY_TYPE_READ:
+ result->consistency = db->read_consistency;
+ result->fallback_consistency = db->read_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_READ_MORE:
+ /* consistency is already set and we don't want to fallback
+ at this point anymore. */
+ result->fallback_consistency = result->consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_WRITE:
+ result->consistency = db->write_consistency;
+ result->fallback_consistency = db->write_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_DELETE:
+ result->consistency = db->delete_consistency;
+ result->fallback_consistency = db->delete_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_COUNT:
+ i_unreached();
+ }
+
+ if (driver_cassandra_want_fallback_query(result))
+ result->consistency = result->fallback_consistency;
+ else
+ db->primary_query_last_sent[result->query_type] = ioloop_timeval;
+
+ driver_cassandra_result_send_query(result);
+ result->query_sent = TRUE;
+ return 1;
+}
+
+static void driver_cassandra_send_queries(struct cassandra_db *db)
+{
+ struct cassandra_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&db->results, &count);
+ for (i = 0; i < count; i++) {
+ if (!results[i]->query_sent && results[i]->statement != NULL) {
+ if (driver_cassandra_send_query(results[i]) <= 0)
+ break;
+ }
+ }
+}
+
+static void exec_callback(struct sql_result *_result ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static struct cassandra_result *
+driver_cassandra_query_init(struct cassandra_db *db, const char *log_query,
+ enum cassandra_query_type query_type,
+ bool is_prepared,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_result *result;
+
+ result = i_new(struct cassandra_result, 1);
+ result->api = driver_cassandra_result;
+ result->api.db = &db->api;
+ result->api.refcount = 1;
+ result->callback = callback;
+ result->context = context;
+ result->query_type = query_type;
+ result->log_query = i_strdup(log_query);
+ result->is_prepared = is_prepared;
+ result->api.event = event_create(db->api.event);
+ array_push_back(&db->results, &result);
+ return result;
+}
+
+static void
+driver_cassandra_query_full(struct sql_db *_db, const char *query,
+ enum cassandra_query_type query_type,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ struct cassandra_result *result;
+
+ result = driver_cassandra_query_init(db, query, query_type, FALSE,
+ callback, context);
+ result->statement = cass_statement_new(query, 0);
+ (void)driver_cassandra_send_query(result);
+}
+
+static void driver_cassandra_exec(struct sql_db *db, const char *query)
+{
+ driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_WRITE,
+ exec_callback, NULL);
+}
+
+static void driver_cassandra_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_READ,
+ callback, context);
+}
+
+static void cassandra_query_s_callback(struct sql_result *result, void *context)
+{
+ struct cassandra_db *db = context;
+
+ db->sync_result = result;
+}
+
+static void driver_cassandra_sync_init(struct cassandra_db *db)
+{
+ if (sql_connect(&db->api) < 0)
+ return;
+ db->orig_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ if (IS_CONNECTED(db))
+ return;
+ i_assert(db->api.state == SQL_DB_STATE_CONNECTING);
+
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ /* wait for connecting to finish */
+ io_loop_run(db->ioloop);
+}
+
+static void driver_cassandra_sync_deinit(struct cassandra_db *db)
+{
+ if (db->orig_ioloop == NULL)
+ return;
+ if (db->io_pipe != NULL) {
+ io_loop_set_current(db->orig_ioloop);
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_set_current(db->ioloop);
+ }
+ io_loop_destroy(&db->ioloop);
+}
+
+static struct sql_result *
+driver_cassandra_sync_query(struct cassandra_db *db, const char *query,
+ enum cassandra_query_type query_type)
+{
+ struct sql_result *result;
+
+ i_assert(db->sync_result == NULL);
+
+ switch (db->api.state) {
+ case SQL_DB_STATE_CONNECTING:
+ case SQL_DB_STATE_BUSY:
+ i_unreached();
+ case SQL_DB_STATE_DISCONNECTED:
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ case SQL_DB_STATE_IDLE:
+ break;
+ }
+
+ driver_cassandra_query_full(&db->api, query, query_type,
+ cassandra_query_s_callback, db);
+ if (db->sync_result == NULL) {
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_run(db->ioloop);
+ }
+
+ result = db->sync_result;
+ if (result == &sql_not_connected_result) {
+ /* we don't end up in cassandra's free function, so sync_result
+ won't be set to NULL if we don't do it here. */
+ db->sync_result = NULL;
+ } else if (result == NULL) {
+ result = &sql_not_connected_result;
+ result->refcount++;
+ }
+ return result;
+}
+
+static struct sql_result *
+driver_cassandra_query_s(struct sql_db *_db, const char *query)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ struct sql_result *result;
+
+ driver_cassandra_sync_init(db);
+ result = driver_cassandra_sync_query(db, query,
+ CASSANDRA_QUERY_TYPE_READ);
+ driver_cassandra_sync_deinit(db);
+ return result;
+}
+
+static int
+driver_cassandra_get_value(struct cassandra_result *result,
+ const CassValue *value, const char **str_r,
+ size_t *len_r)
+{
+ const unsigned char *output;
+ void *output_dup;
+ size_t output_size;
+ CassError rc;
+ const char *type;
+
+ if (cass_value_is_null(value) != 0) {
+ *str_r = NULL;
+ *len_r = 0;
+ return 0;
+ }
+
+ switch (cass_data_type_type(cass_value_data_type(value))) {
+ case CASS_VALUE_TYPE_INT: {
+ cass_int32_t num;
+
+ rc = cass_value_get_int32(value, &num);
+ if (rc == CASS_OK) {
+ const char *str = t_strdup_printf("%d", num);
+ output_size = strlen(str);
+ output = (const void *)str;
+ }
+ type = "int32";
+ break;
+ }
+ case CASS_VALUE_TYPE_TIMESTAMP:
+ case CASS_VALUE_TYPE_BIGINT: {
+ cass_int64_t num;
+
+ rc = cass_value_get_int64(value, &num);
+ if (rc == CASS_OK) {
+ const char *str = t_strdup_printf("%lld", (long long)num);
+ output_size = strlen(str);
+ output = (const void *)str;
+ }
+ type = "int64";
+ break;
+ }
+ default:
+ rc = cass_value_get_bytes(value, &output, &output_size);
+ type = "bytes";
+ break;
+ }
+ if (rc != CASS_OK) {
+ i_free(result->error);
+ result->error = i_strdup_printf("Couldn't get value as %s: %s",
+ type, cass_error_desc(rc));
+ return -1;
+ }
+ output_dup = p_malloc(result->row_pool, output_size + 1);
+ memcpy(output_dup, output, output_size);
+ *str_r = output_dup;
+ *len_r = output_size;
+ return 0;
+}
+
+static int driver_cassandra_result_next_page(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+
+ if (db->page_size == 0) {
+ /* no paging */
+ return 0;
+ }
+ if (cass_result_has_more_pages(result->result) == cass_false)
+ return 0;
+
+ /* callers that don't support sql_query_more() will still get a useful
+ error message. */
+ i_free(result->error);
+ result->error = i_strdup(
+ "Paged query has more results, but not supported by the caller");
+ return SQL_RESULT_NEXT_MORE;
+}
+
+static int driver_cassandra_result_next_row(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ const CassRow *row;
+ const CassValue *value;
+ const char *str;
+ size_t size;
+ unsigned int i;
+ int ret = 1;
+
+ if (result->iterator == NULL)
+ return -1;
+
+ if (cass_iterator_next(result->iterator) == 0)
+ return driver_cassandra_result_next_page(result);
+ result->row_count++;
+ result->total_row_count++;
+
+ p_clear(result->row_pool);
+ p_array_init(&result->fields, result->row_pool, 8);
+ p_array_init(&result->field_sizes, result->row_pool, 8);
+
+ row = cass_iterator_get_row(result->iterator);
+ for (i = 0; (value = cass_row_get_column(row, i)) != NULL; i++) {
+ if (driver_cassandra_get_value(result, value, &str, &size) < 0) {
+ ret = -1;
+ break;
+ }
+ array_push_back(&result->fields, &str);
+ array_push_back(&result->field_sizes, &size);
+ }
+ return ret;
+}
+
+static void
+driver_cassandra_result_more(struct sql_result **_result, bool async,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_db *db = (struct cassandra_db *)(*_result)->db;
+ struct cassandra_result *new_result;
+ struct cassandra_result *old_result =
+ (struct cassandra_result *)*_result;
+
+ /* Initialize the next page as a new sql_result */
+ new_result = driver_cassandra_query_init(db, old_result->log_query,
+ CASSANDRA_QUERY_TYPE_READ_MORE,
+ old_result->is_prepared,
+ callback, context);
+
+ /* Preserve the statement and update its paging state */
+ new_result->statement = old_result->statement;
+ old_result->statement = NULL;
+ cass_statement_set_paging_state(new_result->statement,
+ old_result->result);
+ old_result->paging_continues = TRUE;
+ /* The caller did support paging. Clear out the "...not supported by
+ the caller" error text, so it won't be in the debug log output. */
+ i_free_and_null(old_result->error);
+
+ new_result->timestamp = old_result->timestamp;
+ new_result->consistency = old_result->consistency;
+ new_result->page_num = old_result->page_num + 1;
+ new_result->page0_start_time = old_result->page0_start_time;
+ new_result->total_row_count = old_result->total_row_count;
+
+ sql_result_unref(*_result);
+ *_result = NULL;
+
+ if (async)
+ (void)driver_cassandra_send_query(new_result);
+ else {
+ i_assert(db->api.state == SQL_DB_STATE_IDLE);
+ driver_cassandra_sync_init(db);
+ (void)driver_cassandra_send_query(new_result);
+ if (new_result->result == NULL) {
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_run(db->ioloop);
+ }
+ driver_cassandra_sync_deinit(db);
+
+ callback(&new_result->api, context);
+ }
+}
+
+static unsigned int
+driver_cassandra_result_get_fields_count(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_count(&result->fields);
+}
+
+static const char *
+driver_cassandra_result_get_field_name(struct sql_result *_result ATTR_UNUSED,
+ unsigned int idx ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static int
+driver_cassandra_result_find_field(struct sql_result *_result ATTR_UNUSED,
+ const char *field_name ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static const char *
+driver_cassandra_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_idx_elem(&result->fields, idx);
+}
+
+static const unsigned char *
+driver_cassandra_result_get_field_value_binary(struct sql_result *_result ATTR_UNUSED,
+ unsigned int idx ATTR_UNUSED,
+ size_t *size_r ATTR_UNUSED)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ const char *str;
+ const size_t *sizep;
+
+ str = array_idx_elem(&result->fields, idx);
+ sizep = array_idx(&result->field_sizes, idx);
+ *size_r = *sizep;
+ return (const void *)str;
+}
+
+static const char *
+driver_cassandra_result_find_field_value(struct sql_result *result ATTR_UNUSED,
+ const char *field_name ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static const char *const *
+driver_cassandra_result_get_values(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_front(&result->fields);
+}
+
+static const char *driver_cassandra_result_get_error(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ if (result->error != NULL)
+ return result->error;
+ return "FIXME";
+}
+
+static struct sql_transaction_context *
+driver_cassandra_transaction_begin(struct sql_db *db)
+{
+ struct cassandra_transaction_context *ctx;
+
+ ctx = i_new(struct cassandra_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->ctx.event = event_create(db->event);
+ ctx->refcount = 1;
+ return &ctx->ctx;
+}
+
+static void
+driver_cassandra_transaction_unref(struct cassandra_transaction_context **_ctx)
+{
+ struct cassandra_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+
+ event_unref(&ctx->ctx.event);
+ i_free(ctx->log_query);
+ i_free(ctx->query);
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static void
+transaction_set_failed(struct cassandra_transaction_context *ctx,
+ const char *error)
+{
+ if (ctx->failed) {
+ i_assert(ctx->error != NULL);
+ } else {
+ i_assert(ctx->error == NULL);
+ ctx->failed = TRUE;
+ ctx->error = i_strdup(error);
+ }
+}
+
+static void
+transaction_commit_callback(struct sql_result *result, void *context)
+{
+ struct cassandra_transaction_context *ctx = context;
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ if (sql_result_next_row(result) < 0) {
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed");
+ } else {
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->event(),
+ "Transaction committed");
+ }
+ ctx->callback(&commit_result, ctx->context);
+ driver_cassandra_transaction_unref(&ctx);
+}
+
+static void
+driver_cassandra_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+ struct cassandra_db *db = (struct cassandra_db *)_ctx->db;
+ enum cassandra_query_type query_type;
+ struct sql_commit_result result;
+
+ i_zero(&result);
+ ctx->callback = callback;
+ ctx->context = context;
+
+ if (ctx->failed || (ctx->query == NULL && ctx->stmt == NULL)) {
+ if (ctx->failed)
+ result.error = ctx->error;
+
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+ callback(&result, context);
+ driver_cassandra_transaction_unref(&ctx);
+ return;
+ }
+
+ /* just a single query, send it */
+ const char *query = ctx->query != NULL ?
+ ctx->query : sql_statement_get_query(&ctx->stmt->stmt);
+ if (strncasecmp(query, "DELETE ", 7) == 0)
+ query_type = CASSANDRA_QUERY_TYPE_DELETE;
+ else
+ query_type = CASSANDRA_QUERY_TYPE_WRITE;
+
+ if (ctx->query != NULL) {
+ struct cassandra_result *cass_result;
+
+ cass_result = driver_cassandra_query_init(db, ctx->log_query,
+ query_type, FALSE, transaction_commit_callback, ctx);
+ cass_result->statement = cass_statement_new(query, 0);
+ if (ctx->query_timestamp != 0) {
+ cass_result->timestamp = ctx->query_timestamp;
+ cass_statement_set_timestamp(cass_result->statement,
+ ctx->query_timestamp);
+ }
+ (void)driver_cassandra_send_query(cass_result);
+ } else {
+ ctx->stmt->result =
+ driver_cassandra_query_init(db,
+ sql_statement_get_log_query(&ctx->stmt->stmt),
+ query_type, TRUE, transaction_commit_callback,
+ ctx);
+ if (ctx->stmt->cass_stmt == NULL) {
+ /* wait for prepare to finish */
+ } else {
+ ctx->stmt->result->statement = ctx->stmt->cass_stmt;
+ ctx->stmt->result->timestamp = ctx->stmt->timestamp;
+ (void)driver_cassandra_send_query(ctx->stmt->result);
+ pool_unref(&ctx->stmt->stmt.pool);
+ }
+ }
+}
+
+static void
+driver_cassandra_try_commit_s(struct cassandra_transaction_context *ctx)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ struct cassandra_db *db = (struct cassandra_db *)_ctx->db;
+ struct sql_result *result = NULL;
+ enum cassandra_query_type query_type;
+
+ /* just a single query, send it */
+ if (strncasecmp(ctx->query, "DELETE ", 7) == 0)
+ query_type = CASSANDRA_QUERY_TYPE_DELETE;
+ else
+ query_type = CASSANDRA_QUERY_TYPE_WRITE;
+ driver_cassandra_sync_init(db);
+ result = driver_cassandra_sync_query(db, ctx->query, query_type);
+ driver_cassandra_sync_deinit(db);
+
+ if (sql_result_next_row(result) < 0)
+ transaction_set_failed(ctx, sql_result_get_error(result));
+ sql_result_unref(result);
+}
+
+static int
+driver_cassandra_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ if (ctx->stmt != NULL) {
+ /* nothing should be using this - don't bother implementing */
+ i_panic("cassandra: sql_transaction_commit_s() not supported for prepared statements");
+ }
+
+ if (ctx->query != NULL && !ctx->failed)
+ driver_cassandra_try_commit_s(ctx);
+ *error_r = t_strdup(ctx->error);
+
+ i_assert(ctx->refcount == 1);
+ i_assert((*error_r != NULL) == ctx->failed);
+ driver_cassandra_transaction_unref(&ctx);
+ return *error_r == NULL ? 0 : -1;
+}
+
+static void
+driver_cassandra_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ i_assert(ctx->refcount == 1);
+ driver_cassandra_transaction_unref(&ctx);
+}
+
+static void
+driver_cassandra_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ i_assert(affected_rows == NULL);
+
+ if (ctx->query != NULL || ctx->stmt != NULL) {
+ transaction_set_failed(ctx, "Multiple changes in transaction not supported");
+ return;
+ }
+ ctx->query = i_strdup(query);
+ /* When log_query is set here it can contain expanded values even
+ if stmt->no_log_expanded_values is set. */
+ ctx->log_query = i_strdup(query);
+}
+
+static const char *
+driver_cassandra_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "0x");
+ binary_to_hex_append(str, data, size);
+ return str_c(str);
+}
+
+static CassError
+driver_cassandra_bind_int(struct cassandra_sql_statement *stmt,
+ unsigned int column_idx, int64_t value)
+{
+ const CassDataType *data_type;
+ CassValueType value_type;
+
+ i_assert(stmt->prep != NULL);
+
+ /* statements require exactly correct value type */
+ data_type = cass_prepared_parameter_data_type(stmt->prep->prepared,
+ column_idx);
+ value_type = cass_data_type_type(data_type);
+
+ switch (value_type) {
+ case CASS_VALUE_TYPE_INT:
+ if (value < INT32_MIN || value > INT32_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int32(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_TIMESTAMP:
+ case CASS_VALUE_TYPE_BIGINT:
+ return cass_statement_bind_int64(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_SMALL_INT:
+ if (value < INT16_MIN || value > INT16_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int16(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_TINY_INT:
+ if (value < INT8_MIN || value > INT8_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int8(stmt->cass_stmt, column_idx,
+ value);
+ default:
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ }
+}
+
+static void prepare_finish_arg(struct cassandra_sql_statement *stmt,
+ const struct cassandra_sql_arg *arg)
+{
+ CassError rc;
+
+ if (arg->value_str != NULL) {
+ rc = cass_statement_bind_string(stmt->cass_stmt, arg->column_idx,
+ arg->value_str);
+ } else if (arg->value_binary != NULL) {
+ rc = cass_statement_bind_bytes(stmt->cass_stmt, arg->column_idx,
+ arg->value_binary,
+ arg->value_binary_size);
+ } else {
+ rc = driver_cassandra_bind_int(stmt, arg->column_idx,
+ arg->value_int64);
+ }
+ if (rc != CASS_OK) {
+ e_error(stmt->stmt.db->event,
+ "Statement '%s': Failed to bind column %u: %s",
+ stmt->stmt.query_template, arg->column_idx,
+ cass_error_desc(rc));
+ }
+}
+
+static void prepare_finish_statement(struct cassandra_sql_statement *stmt)
+{
+ const struct cassandra_sql_arg *arg;
+
+ if (stmt->prep->prepared == NULL) {
+ i_assert(stmt->prep->error != NULL);
+
+ if (stmt->result != NULL) {
+ stmt->result->error = i_strdup(stmt->prep->error);
+ result_finish(stmt->result);
+ }
+ pool_unref(&stmt->stmt.pool);
+ return;
+ }
+ stmt->cass_stmt = cass_prepared_bind(stmt->prep->prepared);
+
+ if (stmt->timestamp != 0)
+ cass_statement_set_timestamp(stmt->cass_stmt, stmt->timestamp);
+
+ if (array_is_created(&stmt->pending_args)) {
+ array_foreach(&stmt->pending_args, arg)
+ prepare_finish_arg(stmt, arg);
+ }
+ if (stmt->result != NULL) {
+ stmt->result->statement = stmt->cass_stmt;
+ stmt->result->timestamp = stmt->timestamp;
+ (void)driver_cassandra_send_query(stmt->result);
+ pool_unref(&stmt->stmt.pool);
+ }
+}
+
+static void
+prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt)
+{
+ struct cassandra_sql_statement *stmt;
+
+ array_foreach_elem(&prep_stmt->pending_statements, stmt)
+ prepare_finish_statement(stmt);
+ array_clear(&prep_stmt->pending_statements);
+}
+
+static void prepare_callback(CassFuture *future, void *context)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt = context;
+ CassError error = cass_future_error_code(future);
+
+ if (error != CASS_OK) {
+ const char *errmsg;
+ size_t errsize;
+
+ cass_future_error_message(future, &errmsg, &errsize);
+ i_free(prep_stmt->error);
+ prep_stmt->error = i_strndup(errmsg, errsize);
+ } else {
+ prep_stmt->prepared = cass_future_get_prepared(future);
+ }
+
+ prepare_finish_pending_statements(prep_stmt);
+}
+
+static void prepare_start(struct cassandra_sql_prepared_statement *prep_stmt)
+{
+ struct cassandra_db *db = (struct cassandra_db *)prep_stmt->prep_stmt.db;
+ CassFuture *future;
+
+ if (!SQL_DB_IS_READY(&db->api)) {
+ if (!prep_stmt->pending) {
+ prep_stmt->pending = TRUE;
+ array_push_back(&db->pending_prepares, &prep_stmt);
+
+ if (sql_connect(&db->api) < 0)
+ i_unreached();
+ }
+ return;
+ }
+
+ /* clear the current error in case we're retrying */
+ i_free_and_null(prep_stmt->error);
+
+ future = cass_session_prepare(db->session,
+ prep_stmt->prep_stmt.query_template);
+ driver_cassandra_set_callback(future, db, prepare_callback, prep_stmt);
+}
+
+static void driver_cassandra_prepare_pending(struct cassandra_db *db)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt;
+
+ i_assert(SQL_DB_IS_READY(&db->api));
+
+ array_foreach_elem(&db->pending_prepares, prep_stmt) {
+ prep_stmt->pending = FALSE;
+ prepare_start(prep_stmt);
+ }
+ array_clear(&db->pending_prepares);
+}
+
+static struct sql_prepared_statement *
+driver_cassandra_prepared_statement_init(struct sql_db *db,
+ const char *query_template)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ i_new(struct cassandra_sql_prepared_statement, 1);
+ prep_stmt->prep_stmt.db = db;
+ prep_stmt->prep_stmt.refcount = 1;
+ prep_stmt->prep_stmt.query_template = i_strdup(query_template);
+ i_array_init(&prep_stmt->pending_statements, 4);
+ prepare_start(prep_stmt);
+ return &prep_stmt->prep_stmt;
+}
+
+static void
+driver_cassandra_prepared_statement_deinit(struct sql_prepared_statement *_prep_stmt)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ (struct cassandra_sql_prepared_statement *)_prep_stmt;
+
+ i_assert(array_count(&prep_stmt->pending_statements) == 0);
+ if (prep_stmt->prepared != NULL)
+ cass_prepared_free(prep_stmt->prepared);
+ array_free(&prep_stmt->pending_statements);
+ i_free(prep_stmt->error);
+ i_free(prep_stmt->prep_stmt.query_template);
+ i_free(prep_stmt);
+}
+
+static struct sql_statement *
+driver_cassandra_statement_init(struct sql_db *db ATTR_UNUSED,
+ const char *query_template ATTR_UNUSED)
+{
+ pool_t pool = pool_alloconly_create("cassandra sql statement", 1024);
+ struct cassandra_sql_statement *stmt =
+ p_new(pool, struct cassandra_sql_statement, 1);
+ stmt->stmt.pool = pool;
+ return &stmt->stmt;
+}
+
+static struct sql_statement *
+driver_cassandra_statement_init_prepared(struct sql_prepared_statement *_prep_stmt)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ (struct cassandra_sql_prepared_statement *)_prep_stmt;
+ pool_t pool = pool_alloconly_create("cassandra prepared sql statement", 1024);
+ struct cassandra_sql_statement *stmt =
+ p_new(pool, struct cassandra_sql_statement, 1);
+
+ stmt->stmt.pool = pool;
+ stmt->stmt.query_template =
+ p_strdup(stmt->stmt.pool, prep_stmt->prep_stmt.query_template);
+ stmt->prep = prep_stmt;
+
+ if (prep_stmt->prepared != NULL) {
+ /* statement is already prepared. we can use it immediately. */
+ stmt->cass_stmt = cass_prepared_bind(prep_stmt->prepared);
+ } else {
+ if (prep_stmt->error != NULL)
+ prepare_start(prep_stmt);
+ /* need to wait until prepare is finished */
+ array_push_back(&prep_stmt->pending_statements, &stmt);
+ }
+ return &stmt->stmt;
+}
+
+static void
+driver_cassandra_statement_abort(struct sql_statement *_stmt)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL)
+ cass_statement_free(stmt->cass_stmt);
+}
+
+static void
+driver_cassandra_statement_set_timestamp(struct sql_statement *_stmt,
+ const struct timespec *ts)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ cass_int64_t ts_usecs =
+ (cass_int64_t)ts->tv_sec * 1000000ULL +
+ ts->tv_nsec / 1000;
+
+ i_assert(stmt->result == NULL);
+
+ if (stmt->cass_stmt != NULL)
+ cass_statement_set_timestamp(stmt->cass_stmt, ts_usecs);
+ stmt->timestamp = ts_usecs;
+}
+
+static struct cassandra_sql_arg *
+driver_cassandra_add_pending_arg(struct cassandra_sql_statement *stmt,
+ unsigned int column_idx)
+{
+ struct cassandra_sql_arg *arg;
+
+ if (!array_is_created(&stmt->pending_args))
+ p_array_init(&stmt->pending_args, stmt->stmt.pool, 8);
+ arg = array_append_space(&stmt->pending_args);
+ arg->column_idx = column_idx;
+ return arg;
+}
+
+static void
+driver_cassandra_statement_bind_str(struct sql_statement *_stmt,
+ unsigned int column_idx,
+ const char *value)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ if (stmt->cass_stmt != NULL)
+ cass_statement_bind_string(stmt->cass_stmt, column_idx, value);
+ else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_str = p_strdup(_stmt->pool, value);
+ }
+}
+
+static void
+driver_cassandra_statement_bind_binary(struct sql_statement *_stmt,
+ unsigned int column_idx,
+ const void *value, size_t value_size)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL) {
+ cass_statement_bind_bytes(stmt->cass_stmt, column_idx,
+ value, value_size);
+ } else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_binary = value_size == 0 ? &uchar_nul :
+ p_memdup(_stmt->pool, value, value_size);
+ arg->value_binary_size = value_size;
+ }
+}
+
+static void
+driver_cassandra_statement_bind_int64(struct sql_statement *_stmt,
+ unsigned int column_idx, int64_t value)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL)
+ driver_cassandra_bind_int(stmt, column_idx, value);
+ else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_int64 = value;
+ }
+}
+
+static void
+driver_cassandra_statement_query(struct sql_statement *_stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ struct cassandra_db *db = (struct cassandra_db *)_stmt->db;
+ const char *query = sql_statement_get_query(_stmt);
+ bool is_prepared = stmt->cass_stmt != NULL || stmt->prep != NULL;
+
+ stmt->result = driver_cassandra_query_init(db,
+ sql_statement_get_log_query(_stmt),
+ CASSANDRA_QUERY_TYPE_READ,
+ is_prepared,
+ callback, context);
+ if (stmt->cass_stmt != NULL) {
+ stmt->result->statement = stmt->cass_stmt;
+ stmt->result->timestamp = stmt->timestamp;
+ } else if (stmt->prep != NULL) {
+ /* wait for prepare to finish */
+ return;
+ } else {
+ stmt->result->statement = cass_statement_new(query, 0);
+ stmt->result->timestamp = stmt->timestamp;
+ if (stmt->timestamp != 0) {
+ cass_statement_set_timestamp(stmt->result->statement,
+ stmt->timestamp);
+ }
+ }
+ (void)driver_cassandra_send_query(stmt->result);
+ pool_unref(&_stmt->pool);
+}
+
+static struct sql_result *
+driver_cassandra_statement_query_s(struct sql_statement *_stmt ATTR_UNUSED)
+{
+ i_panic("cassandra: sql_statement_query_s() not supported");
+}
+
+static void
+driver_cassandra_update_stmt(struct sql_transaction_context *_ctx,
+ struct sql_statement *_stmt,
+ unsigned int *affected_rows)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ i_assert(affected_rows == NULL);
+
+ if (ctx->query != NULL || ctx->stmt != NULL) {
+ transaction_set_failed(ctx,
+ "Multiple changes in transaction not supported");
+ return;
+ }
+ if (stmt->prep != NULL)
+ ctx->stmt = stmt;
+ else {
+ ctx->query = i_strdup(sql_statement_get_query(_stmt));
+ ctx->log_query = i_strdup(sql_statement_get_log_query(_stmt));
+ ctx->query_timestamp = stmt->timestamp;
+ pool_unref(&_stmt->pool);
+ }
+}
+
+static bool driver_cassandra_have_work(struct cassandra_db *db)
+{
+ return array_not_empty(&db->pending_prepares) ||
+ array_not_empty(&db->callbacks) ||
+ array_not_empty(&db->results);
+}
+
+static void driver_cassandra_wait(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ if (!driver_cassandra_have_work(db))
+ return;
+
+ struct ioloop *prev_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ while (driver_cassandra_have_work(db))
+ io_loop_run(db->ioloop);
+
+ io_loop_set_current(prev_ioloop);
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_set_current(db->ioloop);
+ io_loop_destroy(&db->ioloop);
+}
+
+const struct sql_db driver_cassandra_db = {
+ .name = "cassandra",
+ .flags = SQL_DB_FLAG_PREP_STATEMENTS,
+
+ .v = {
+ .init_full = driver_cassandra_init_full_v,
+ .deinit = driver_cassandra_deinit_v,
+ .connect = driver_cassandra_connect,
+ .disconnect = driver_cassandra_disconnect,
+ .escape_string = driver_cassandra_escape_string,
+ .exec = driver_cassandra_exec,
+ .query = driver_cassandra_query,
+ .query_s = driver_cassandra_query_s,
+ .wait = driver_cassandra_wait,
+
+ .transaction_begin = driver_cassandra_transaction_begin,
+ .transaction_commit = driver_cassandra_transaction_commit,
+ .transaction_commit_s = driver_cassandra_transaction_commit_s,
+ .transaction_rollback = driver_cassandra_transaction_rollback,
+
+ .update = driver_cassandra_update,
+
+ .escape_blob = driver_cassandra_escape_blob,
+
+ .prepared_statement_init = driver_cassandra_prepared_statement_init,
+ .prepared_statement_deinit = driver_cassandra_prepared_statement_deinit,
+ .statement_init = driver_cassandra_statement_init,
+ .statement_init_prepared = driver_cassandra_statement_init_prepared,
+ .statement_abort = driver_cassandra_statement_abort,
+ .statement_set_timestamp = driver_cassandra_statement_set_timestamp,
+ .statement_bind_str = driver_cassandra_statement_bind_str,
+ .statement_bind_binary = driver_cassandra_statement_bind_binary,
+ .statement_bind_int64 = driver_cassandra_statement_bind_int64,
+ .statement_query = driver_cassandra_statement_query,
+ .statement_query_s = driver_cassandra_statement_query_s,
+ .update_stmt = driver_cassandra_update_stmt,
+ }
+};
+
+const struct sql_result driver_cassandra_result = {
+ .v = {
+ driver_cassandra_result_free,
+ driver_cassandra_result_next_row,
+ driver_cassandra_result_get_fields_count,
+ driver_cassandra_result_get_field_name,
+ driver_cassandra_result_find_field,
+ driver_cassandra_result_get_field_value,
+ driver_cassandra_result_get_field_value_binary,
+ driver_cassandra_result_find_field_value,
+ driver_cassandra_result_get_values,
+ driver_cassandra_result_get_error,
+ driver_cassandra_result_more,
+ }
+};
+
+const char *driver_cassandra_version = DOVECOT_ABI_VERSION;
+
+void driver_cassandra_init(void);
+void driver_cassandra_deinit(void);
+
+void driver_cassandra_init(void)
+{
+ sql_driver_register(&driver_cassandra_db);
+}
+
+void driver_cassandra_deinit(void)
+{
+ sql_driver_unregister(&driver_cassandra_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-mysql.c b/src/lib-sql/driver-mysql.c
new file mode 100644
index 0000000..2693a07
--- /dev/null
+++ b/src/lib-sql/driver-mysql.c
@@ -0,0 +1,844 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "net.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_MYSQL
+#include <unistd.h>
+#include <time.h>
+#ifdef HAVE_ATTR_NULL
+/* ugly way to tell clang that mysql.h is a system header and we don't want
+ to enable nonnull attributes for it by default.. */
+# 4 "driver-mysql.c" 3
+#endif
+#include <mysql.h>
+#ifdef HAVE_ATTR_NULL
+# 4 "driver-mysql.c" 3
+# line 20
+#endif
+#include <errmsg.h>
+
+#define MYSQL_DEFAULT_READ_TIMEOUT_SECS 30
+#define MYSQL_DEFAULT_WRITE_TIMEOUT_SECS 30
+
+struct mysql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const char *user, *password, *dbname, *host, *unix_socket;
+ const char *ssl_cert, *ssl_key, *ssl_ca, *ssl_ca_path, *ssl_cipher;
+ int ssl_verify_server_cert;
+ const char *option_file, *option_group;
+ in_port_t port;
+ unsigned int client_flags;
+ unsigned int connect_timeout, read_timeout, write_timeout;
+ time_t last_success;
+
+ MYSQL *mysql;
+ unsigned int next_query_connection;
+
+ bool ssl_set:1;
+};
+
+struct mysql_result {
+ struct sql_result api;
+
+ MYSQL_RES *result;
+ MYSQL_ROW row;
+
+ MYSQL_FIELD *fields;
+ unsigned int fields_count;
+
+ my_ulonglong affected_rows;
+};
+
+struct mysql_transaction_context {
+ struct sql_transaction_context ctx;
+
+ pool_t query_pool;
+ const char *error;
+
+ bool failed:1;
+ bool committed:1;
+ bool commit_started:1;
+};
+
+extern const struct sql_db driver_mysql_db;
+extern const struct sql_result driver_mysql_result;
+extern const struct sql_result driver_mysql_error_result;
+
+static struct event_category event_category_mysql = {
+ .parent = &event_category_sql,
+ .name = "mysql"
+};
+
+static int driver_mysql_connect(struct sql_db *_db)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+ const char *unix_socket, *host;
+ unsigned long client_flags = db->client_flags;
+ unsigned int secs_used;
+ time_t start_time;
+ bool failed;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ sql_db_set_state(&db->api, SQL_DB_STATE_CONNECTING);
+
+ if (db->host == NULL) {
+ /* assume option_file overrides the host, or if not we'll just
+ connect to localhost */
+ unix_socket = NULL;
+ host = NULL;
+ } else if (*db->host == '/') {
+ unix_socket = db->host;
+ host = NULL;
+ } else {
+ unix_socket = NULL;
+ host = db->host;
+ }
+
+ if (db->option_file != NULL) {
+ mysql_options(db->mysql, MYSQL_READ_DEFAULT_FILE,
+ db->option_file);
+ }
+
+ if (db->host != NULL)
+ event_set_append_log_prefix(_db->event, t_strdup_printf("mysql(%s): ", db->host));
+
+ e_debug(_db->event, "Connecting");
+
+ mysql_options(db->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &db->connect_timeout);
+ mysql_options(db->mysql, MYSQL_OPT_READ_TIMEOUT, &db->read_timeout);
+ mysql_options(db->mysql, MYSQL_OPT_WRITE_TIMEOUT, &db->write_timeout);
+ mysql_options(db->mysql, MYSQL_READ_DEFAULT_GROUP,
+ db->option_group != NULL ? db->option_group : "client");
+
+ if (!db->ssl_set && (db->ssl_ca != NULL || db->ssl_ca_path != NULL)) {
+#ifdef HAVE_MYSQL_SSL
+ mysql_ssl_set(db->mysql, db->ssl_key, db->ssl_cert,
+ db->ssl_ca, db->ssl_ca_path
+#ifdef HAVE_MYSQL_SSL_CIPHER
+ , db->ssl_cipher
+#endif
+ );
+#ifdef HAVE_MYSQL_SSL_VERIFY_SERVER_CERT
+ mysql_options(db->mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT,
+ (void *)&db->ssl_verify_server_cert);
+#endif
+ db->ssl_set = TRUE;
+#else
+ i_fatal("mysql: SSL support not compiled in "
+ "(remove ssl_ca and ssl_ca_path settings)");
+#endif
+ }
+
+#ifdef CLIENT_MULTI_RESULTS
+ client_flags |= CLIENT_MULTI_RESULTS;
+#endif
+ /* CLIENT_MULTI_RESULTS allows the use of stored procedures */
+ start_time = time(NULL);
+ failed = mysql_real_connect(db->mysql, host, db->user, db->password,
+ db->dbname, db->port, unix_socket,
+ client_flags) == NULL;
+ secs_used = time(NULL) - start_time;
+ if (failed) {
+ /* connecting could have taken a while. make sure that any
+ timeouts that get added soon will get a refreshed
+ timestamp. */
+ io_loop_time_refresh();
+
+ if (db->api.connect_delay < secs_used)
+ db->api.connect_delay = secs_used;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+ e_error(_db->event, "Connect failed to database (%s): %s - "
+ "waiting for %u seconds before retry",
+ db->dbname, mysql_error(db->mysql), db->api.connect_delay);
+ sql_disconnect(&db->api);
+ return -1;
+ } else {
+ db->last_success = ioloop_time;
+ sql_db_set_state(&db->api, SQL_DB_STATE_IDLE);
+ return 1;
+ }
+}
+
+static void driver_mysql_disconnect(struct sql_db *_db)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+ if (db->mysql != NULL)
+ mysql_close(db->mysql);
+}
+
+static int driver_mysql_parse_connect_string(struct mysql_db *db,
+ const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *name, *value;
+ const char **field;
+
+ db->ssl_cipher = "HIGH";
+ db->ssl_verify_server_cert = 1;
+ db->connect_timeout = SQL_CONNECT_TIMEOUT_SECS;
+ db->read_timeout = MYSQL_DEFAULT_READ_TIMEOUT_SECS;
+ db->write_timeout = MYSQL_DEFAULT_WRITE_TIMEOUT_SECS;
+
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ *error_r = t_strdup_printf("Missing value in connect string: %s",
+ *args);
+ return -1;
+ }
+ name = t_strdup_until(*args, value);
+ value++;
+
+ field = NULL;
+ if (strcmp(name, "host") == 0 ||
+ strcmp(name, "hostaddr") == 0)
+ field = &db->host;
+ else if (strcmp(name, "user") == 0)
+ field = &db->user;
+ else if (strcmp(name, "password") == 0)
+ field = &db->password;
+ else if (strcmp(name, "dbname") == 0)
+ field = &db->dbname;
+ else if (strcmp(name, "port") == 0) {
+ if (net_str2port(value, &db->port) < 0) {
+ *error_r = t_strdup_printf("Invalid port number: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "client_flags") == 0) {
+ if (str_to_uint(value, &db->client_flags) < 0) {
+ *error_r = t_strdup_printf("Invalid client flags: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "connect_timeout") == 0) {
+ if (str_to_uint(value, &db->connect_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "read_timeout") == 0) {
+ if (str_to_uint(value, &db->read_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "write_timeout") == 0) {
+ if (str_to_uint(value, &db->write_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "ssl_cert") == 0)
+ field = &db->ssl_cert;
+ else if (strcmp(name, "ssl_key") == 0)
+ field = &db->ssl_key;
+ else if (strcmp(name, "ssl_ca") == 0)
+ field = &db->ssl_ca;
+ else if (strcmp(name, "ssl_ca_path") == 0)
+ field = &db->ssl_ca_path;
+ else if (strcmp(name, "ssl_cipher") == 0)
+ field = &db->ssl_cipher;
+ else if (strcmp(name, "ssl_verify_server_cert") == 0) {
+ if (strcmp(value, "yes") == 0)
+ db->ssl_verify_server_cert = 1;
+ else if (strcmp(value, "no") == 0)
+ db->ssl_verify_server_cert = 0;
+ else {
+ *error_r = t_strdup_printf("Invalid boolean: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "option_file") == 0)
+ field = &db->option_file;
+ else if (strcmp(name, "option_group") == 0)
+ field = &db->option_group;
+ else {
+ *error_r = t_strdup_printf("Unknown connect string: %s", name);
+ return -1;
+ }
+ if (field != NULL)
+ *field = p_strdup(db->pool, value);
+ }
+
+ if (db->host == NULL && db->option_file == NULL) {
+ *error_r = "No hosts given in connect string";
+ return -1;
+ }
+ if (db->mysql == NULL) {
+ db->mysql = p_new(db->pool, MYSQL, 1);
+ MYSQL *ptr = mysql_init(db->mysql);
+ if (ptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "mysql_init() failed");
+ }
+ return 0;
+}
+
+static int driver_mysql_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct mysql_db *db;
+ const char *error = NULL;
+ pool_t pool;
+ int ret;
+
+ pool = pool_alloconly_create("mysql driver", 1024);
+ db = p_new(pool, struct mysql_db, 1);
+ db->pool = pool;
+ db->api = driver_mysql_db;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_mysql);
+ event_set_append_log_prefix(db->api.event, "mysql: ");
+ T_BEGIN {
+ ret = driver_mysql_parse_connect_string(db, set->connect_string, &error);
+ error = p_strdup(db->pool, error);
+ } T_END;
+
+ if (ret < 0) {
+ *error_r = t_strdup(error);
+ pool_unref(&db->pool);
+ return ret;
+ }
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_mysql_deinit_v(struct sql_db *_db)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+
+ _db->no_reconnect = TRUE;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+
+ driver_mysql_disconnect(_db);
+
+ sql_connection_log_finished(_db);
+ event_unref(&_db->event);
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static int driver_mysql_do_query(struct mysql_db *db, const char *query,
+ struct event *event)
+{
+ int ret, diff;
+ struct event_passthrough *e;
+
+ ret = mysql_query(db->mysql, query);
+ io_loop_time_refresh();
+ e = sql_query_finished_event(&db->api, event, query, ret == 0, &diff);
+
+ if (ret != 0) {
+ e->add_int("error_code", mysql_errno(db->mysql));
+ e->add_str("error", mysql_error(db->mysql));
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", query,
+ diff, mysql_error(db->mysql));
+ } else
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT, query, diff);
+
+ if (ret == 0)
+ return 0;
+
+ /* failed */
+ switch (mysql_errno(db->mysql)) {
+ case CR_SERVER_GONE_ERROR:
+ case CR_SERVER_LOST:
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+ break;
+ default:
+ break;
+ }
+ return -1;
+}
+
+static const char *
+driver_mysql_escape_string(struct sql_db *_db, const char *string)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+ size_t len = strlen(string);
+ char *to;
+
+ if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+ /* try connecting */
+ (void)sql_connect(&db->api);
+ }
+
+ if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+ /* FIXME: we don't have a valid connection, so fallback
+ to using default escaping. the next query will most
+ likely fail anyway so it shouldn't matter that much
+ what we return here.. Anyway, this API needs
+ changing so that the escaping function could already
+ fail the query reliably. */
+ to = t_buffer_get(len * 2 + 1);
+ len = mysql_escape_string(to, string, len);
+ t_buffer_alloc(len + 1);
+ return to;
+ }
+
+ to = t_buffer_get(len * 2 + 1);
+ len = mysql_real_escape_string(db->mysql, to, string, len);
+ t_buffer_alloc(len + 1);
+ return to;
+}
+
+static void driver_mysql_exec(struct sql_db *_db, const char *query)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+ struct event *event = event_create(_db->event);
+
+ (void)driver_mysql_do_query(db, query, event);
+
+ event_unref(&event);
+}
+
+static void driver_mysql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result;
+
+ result = sql_query_s(db, query);
+ result->callback = TRUE;
+ callback(result, context);
+ result->callback = FALSE;
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_mysql_query_s(struct sql_db *_db, const char *query)
+{
+ struct mysql_db *db = container_of(_db, struct mysql_db, api);
+ struct mysql_result *result;
+ struct event *event;
+ int ret;
+
+ result = i_new(struct mysql_result, 1);
+ result->api = driver_mysql_result;
+ event = event_create(_db->event);
+
+ if (driver_mysql_do_query(db, query, event) < 0)
+ result->api = driver_mysql_error_result;
+ else {
+ /* query ok */
+ result->affected_rows = mysql_affected_rows(db->mysql);
+ result->result = mysql_store_result(db->mysql);
+#ifdef CLIENT_MULTI_RESULTS
+ /* Because we've enabled CLIENT_MULTI_RESULTS, we need to read
+ (ignore) extra results - there should not be any.
+ ret is: -1 = done, >0 = error, 0 = more results. */
+ while ((ret = mysql_next_result(db->mysql)) == 0) ;
+#else
+ ret = -1;
+#endif
+
+ if (ret < 0 &&
+ (result->result != NULL || mysql_errno(db->mysql) == 0)) {
+ /* ok */
+ } else {
+ /* failed */
+ if (result->result != NULL)
+ mysql_free_result(result->result);
+ result->api = driver_mysql_error_result;
+ }
+ }
+
+ result->api.db = _db;
+ result->api.refcount = 1;
+ result->api.event = event;
+ return &result->api;
+}
+
+static void driver_mysql_result_free(struct sql_result *_result)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ i_assert(_result != &sql_not_connected_result);
+ if (_result->callback)
+ return;
+
+ if (result->result != NULL)
+ mysql_free_result(result->result);
+ event_unref(&_result->event);
+ i_free(result);
+}
+
+static int driver_mysql_result_next_row(struct sql_result *_result)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+ struct mysql_db *db = container_of(_result->db, struct mysql_db, api);
+ int ret;
+
+ if (result->result == NULL) {
+ /* no results */
+ return 0;
+ }
+
+ result->row = mysql_fetch_row(result->result);
+ if (result->row != NULL)
+ ret = 1;
+ else {
+ if (mysql_errno(db->mysql) != 0)
+ return -1;
+ ret = 0;
+ }
+ db->last_success = ioloop_time;
+ return ret;
+}
+
+static void driver_mysql_result_fetch_fields(struct mysql_result *result)
+{
+ if (result->fields != NULL)
+ return;
+
+ result->fields_count = mysql_num_fields(result->result);
+ result->fields = mysql_fetch_fields(result->result);
+}
+
+static unsigned int
+driver_mysql_result_get_fields_count(struct sql_result *_result)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ driver_mysql_result_fetch_fields(result);
+ return result->fields_count;
+}
+
+static const char *
+driver_mysql_result_get_field_name(struct sql_result *_result, unsigned int idx)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ driver_mysql_result_fetch_fields(result);
+ i_assert(idx < result->fields_count);
+ return result->fields[idx].name;
+}
+
+static int driver_mysql_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+ unsigned int i;
+
+ driver_mysql_result_fetch_fields(result);
+ for (i = 0; i < result->fields_count; i++) {
+ if (strcmp(result->fields[i].name, field_name) == 0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_mysql_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ return (const char *)result->row[idx];
+}
+
+static const unsigned char *
+driver_mysql_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+ unsigned long *lengths;
+
+ lengths = mysql_fetch_lengths(result->result);
+
+ *size_r = lengths[idx];
+ return (const void *)result->row[idx];
+}
+
+static const char *
+driver_mysql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_mysql_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_mysql_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_mysql_result_get_values(struct sql_result *_result)
+{
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ return (const char *const *)result->row;
+}
+
+static const char *driver_mysql_result_get_error(struct sql_result *_result)
+{
+ struct mysql_db *db = container_of(_result->db, struct mysql_db, api);
+ const char *errstr;
+ unsigned int idle_time;
+ int err;
+
+ err = mysql_errno(db->mysql);
+ errstr = mysql_error(db->mysql);
+ if ((err == CR_SERVER_GONE_ERROR || err == CR_SERVER_LOST) &&
+ db->last_success != 0) {
+ idle_time = ioloop_time - db->last_success;
+ errstr = t_strdup_printf("%s (idled for %u secs)",
+ errstr, idle_time);
+ }
+ return errstr;
+}
+
+static struct sql_transaction_context *
+driver_mysql_transaction_begin(struct sql_db *db)
+{
+ struct mysql_transaction_context *ctx;
+
+ ctx = i_new(struct mysql_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->query_pool = pool_alloconly_create("mysql transaction", 1024);
+ ctx->ctx.event = event_create(db->event);
+ return &ctx->ctx;
+}
+
+static void
+driver_mysql_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_commit_result result;
+ const char *error;
+
+ i_zero(&result);
+ if (sql_transaction_commit_s(&ctx, &error) < 0)
+ result.error = error;
+ callback(&result, context);
+}
+
+static int ATTR_NULL(3)
+transaction_send_query(struct mysql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows_r)
+{
+ struct sql_result *_result;
+ int ret = 0;
+
+ if (ctx->failed)
+ return -1;
+
+ _result = sql_query_s(ctx->ctx.db, query);
+ if (sql_result_next_row(_result) < 0) {
+ ctx->error = sql_result_get_error(_result);
+ ctx->failed = TRUE;
+ ret = -1;
+ } else if (affected_rows_r != NULL) {
+ struct mysql_result *result =
+ container_of(_result, struct mysql_result, api);
+
+ i_assert(result->affected_rows != (my_ulonglong)-1);
+ *affected_rows_r = result->affected_rows;
+ }
+ sql_result_unref(_result);
+ return ret;
+}
+
+static int driver_mysql_try_commit_s(struct mysql_transaction_context *ctx)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ bool multi = _ctx->head != NULL && _ctx->head->next != NULL;
+
+ /* wrap in BEGIN/COMMIT only if transaction has mutiple statements. */
+ if (multi && transaction_send_query(ctx, "BEGIN", NULL) < 0) {
+ if (_ctx->db->state != SQL_DB_STATE_DISCONNECTED)
+ return -1;
+ /* we got disconnected, retry */
+ return 0;
+ } else if (multi) {
+ ctx->commit_started = TRUE;
+ }
+
+ while (_ctx->head != NULL) {
+ if (transaction_send_query(ctx, _ctx->head->query,
+ _ctx->head->affected_rows) < 0)
+ return -1;
+ _ctx->head = _ctx->head->next;
+ }
+ if (multi && transaction_send_query(ctx, "COMMIT", NULL) < 0)
+ return -1;
+ return 1;
+}
+
+static int
+driver_mysql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct mysql_transaction_context *ctx =
+ container_of(_ctx, struct mysql_transaction_context, ctx);
+ struct mysql_db *db = container_of(_ctx->db, struct mysql_db, api);
+ int ret = 1;
+
+ *error_r = NULL;
+
+ if (_ctx->head != NULL) {
+ ret = driver_mysql_try_commit_s(ctx);
+ *error_r = t_strdup(ctx->error);
+ if (ret == 0) {
+ e_info(db->api.event, "Disconnected from database, "
+ "retrying commit");
+ if (sql_connect(_ctx->db) >= 0) {
+ ctx->failed = FALSE;
+ ret = driver_mysql_try_commit_s(ctx);
+ }
+ }
+ }
+
+ if (ret > 0)
+ ctx->committed = TRUE;
+
+ sql_transaction_rollback(&_ctx);
+ return ret <= 0 ? -1 : 0;
+}
+
+static void
+driver_mysql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct mysql_transaction_context *ctx =
+ container_of(_ctx, struct mysql_transaction_context, ctx);
+
+ if (ctx->failed) {
+ bool rolledback = FALSE;
+ const char *orig_error = t_strdup(ctx->error);
+ if (ctx->commit_started) {
+ /* reset failed flag so ROLLBACK is actually sent.
+ otherwise, transaction_send_query() will return
+ without trying to send the query. */
+ ctx->failed = FALSE;
+ if (transaction_send_query(ctx, "ROLLBACK", NULL) < 0)
+ e_debug(event_create_passthrough(_ctx->event)->
+ add_str("error", ctx->error)->event(),
+ "Rollback failed: %s", ctx->error);
+ else
+ rolledback = TRUE;
+ }
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", orig_error)->event(),
+ "Transaction failed: %s%s", orig_error,
+ rolledback ? " - Rolled back" : "");
+ } else if (ctx->committed)
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ else
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+
+ event_unref(&ctx->ctx.event);
+ pool_unref(&ctx->query_pool);
+ i_free(ctx);
+}
+
+static void
+driver_mysql_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct mysql_transaction_context *ctx =
+ container_of(_ctx, struct mysql_transaction_context, ctx);
+
+ sql_transaction_add_query(&ctx->ctx, ctx->query_pool,
+ query, affected_rows);
+}
+
+static const char *
+driver_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "X'");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+const struct sql_db driver_mysql_db = {
+ .name = "mysql",
+ .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_POOLED |
+ SQL_DB_FLAG_ON_DUPLICATE_KEY,
+
+ .v = {
+ .init_full = driver_mysql_init_full_v,
+ .deinit = driver_mysql_deinit_v,
+ .connect = driver_mysql_connect,
+ .disconnect = driver_mysql_disconnect,
+ .escape_string = driver_mysql_escape_string,
+ .exec = driver_mysql_exec,
+ .query = driver_mysql_query,
+ .query_s = driver_mysql_query_s,
+
+ .transaction_begin = driver_mysql_transaction_begin,
+ .transaction_commit = driver_mysql_transaction_commit,
+ .transaction_commit_s = driver_mysql_transaction_commit_s,
+ .transaction_rollback = driver_mysql_transaction_rollback,
+
+ .update = driver_mysql_update,
+
+ .escape_blob = driver_mysql_escape_blob,
+ }
+};
+
+const struct sql_result driver_mysql_result = {
+ .v = {
+ .free = driver_mysql_result_free,
+ .next_row = driver_mysql_result_next_row,
+ .get_fields_count = driver_mysql_result_get_fields_count,
+ .get_field_name = driver_mysql_result_get_field_name,
+ .find_field = driver_mysql_result_find_field,
+ .get_field_value = driver_mysql_result_get_field_value,
+ .get_field_value_binary = driver_mysql_result_get_field_value_binary,
+ .find_field_value = driver_mysql_result_find_field_value,
+ .get_values = driver_mysql_result_get_values,
+ .get_error = driver_mysql_result_get_error,
+ }
+};
+
+static int
+driver_mysql_result_error_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+const struct sql_result driver_mysql_error_result = {
+ .v = {
+ .free = driver_mysql_result_free,
+ .next_row = driver_mysql_result_error_next_row,
+ .get_error = driver_mysql_result_get_error,
+ },
+ .failed_try_retry = TRUE
+};
+
+const char *driver_mysql_version = DOVECOT_ABI_VERSION;
+
+void driver_mysql_init(void);
+void driver_mysql_deinit(void);
+
+void driver_mysql_init(void)
+{
+ sql_driver_register(&driver_mysql_db);
+}
+
+void driver_mysql_deinit(void)
+{
+ sql_driver_unregister(&driver_mysql_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-pgsql.c b/src/lib-sql/driver-pgsql.c
new file mode 100644
index 0000000..63188c0
--- /dev/null
+++ b/src/lib-sql/driver-pgsql.c
@@ -0,0 +1,1344 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+#include "llist.h"
+
+#ifdef BUILD_PGSQL
+#include <libpq-fe.h>
+
+#define PGSQL_DNS_WARN_MSECS 500
+
+struct pgsql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ char *connect_string;
+ char *host;
+ PGconn *pg;
+
+ struct io *io;
+ struct timeout *to_connect;
+ enum io_condition io_dir;
+
+ struct pgsql_result *pending_results;
+ struct pgsql_result *cur_result;
+ struct ioloop *ioloop, *orig_ioloop;
+ struct sql_result *sync_result;
+
+ bool (*next_callback)(void *);
+ void *next_context;
+
+ char *error;
+ const char *connect_state;
+
+ bool fatal_error:1;
+};
+
+struct pgsql_binary_value {
+ unsigned char *value;
+ size_t size;
+};
+
+struct pgsql_result {
+ struct sql_result api;
+
+ struct pgsql_result *prev, *next;
+
+ PGresult *pgres;
+ struct timeout *to;
+
+ unsigned int rownum, rows;
+ unsigned int fields_count;
+ const char **fields;
+ const char **values;
+ char *query;
+
+ ARRAY(struct pgsql_binary_value) binary_values;
+
+ sql_query_callback_t *callback;
+ void *context;
+
+ bool timeout:1;
+};
+
+struct pgsql_transaction_context {
+ struct sql_transaction_context ctx;
+ int refcount;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ pool_t query_pool;
+ const char *error;
+
+ bool failed:1;
+};
+
+extern const struct sql_db driver_pgsql_db;
+extern const struct sql_result driver_pgsql_result;
+
+static void result_finish(struct pgsql_result *result);
+static void
+transaction_update_callback(struct sql_result *result,
+ struct sql_transaction_query *query);
+
+static struct event_category event_category_pgsql = {
+ .parent = &event_category_sql,
+ .name = "pgsql"
+};
+
+static void driver_pgsql_set_state(struct pgsql_db *db, enum sql_db_state state)
+{
+ i_assert(state == SQL_DB_STATE_BUSY || db->cur_result == NULL);
+
+ /* switch back to original ioloop in case the caller wants to
+ add/remove timeouts */
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->orig_ioloop);
+ sql_db_set_state(&db->api, state);
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->ioloop);
+}
+
+static bool driver_pgsql_next_callback(struct pgsql_db *db)
+{
+ bool (*next_callback)(void *) = db->next_callback;
+ void *next_context = db->next_context;
+
+ if (next_callback == NULL)
+ return FALSE;
+
+ db->next_callback = NULL;
+ db->next_context = NULL;
+ return next_callback(next_context);
+}
+
+static void driver_pgsql_stop_io(struct pgsql_db *db)
+{
+ if (db->io != NULL) {
+ io_remove(&db->io);
+ db->io_dir = 0;
+ }
+}
+
+static void driver_pgsql_close(struct pgsql_db *db)
+{
+ db->io_dir = 0;
+ db->fatal_error = FALSE;
+
+ driver_pgsql_stop_io(db);
+
+ PQfinish(db->pg);
+ db->pg = NULL;
+
+ timeout_remove(&db->to_connect);
+
+ driver_pgsql_set_state(db, SQL_DB_STATE_DISCONNECTED);
+
+ if (db->ioloop != NULL) {
+ /* running a sync query, stop it */
+ io_loop_stop(db->ioloop);
+ }
+ driver_pgsql_next_callback(db);
+}
+
+static const char *last_error(struct pgsql_db *db)
+{
+ const char *msg;
+ size_t len;
+
+ msg = PQerrorMessage(db->pg);
+ if (msg == NULL)
+ return "(no error set)";
+
+ /* Error message should contain trailing \n, we don't want it */
+ len = strlen(msg);
+ return len == 0 || msg[len-1] != '\n' ? msg :
+ t_strndup(msg, len-1);
+}
+
+static void connect_callback(struct pgsql_db *db)
+{
+ enum io_condition io_dir = 0;
+ int ret;
+
+ driver_pgsql_stop_io(db);
+
+ while ((ret = PQconnectPoll(db->pg)) == PGRES_POLLING_ACTIVE)
+ ;
+
+ switch (ret) {
+ case PGRES_POLLING_READING:
+ db->connect_state = "wait for input";
+ io_dir = IO_READ;
+ break;
+ case PGRES_POLLING_WRITING:
+ db->connect_state = "wait for output";
+ io_dir = IO_WRITE;
+ break;
+ case PGRES_POLLING_OK:
+ break;
+ case PGRES_POLLING_FAILED:
+ e_error(db->api.event, "Connect failed to database %s: %s (state: %s)",
+ PQdb(db->pg), last_error(db), db->connect_state);
+ driver_pgsql_close(db);
+ return;
+ }
+
+ if (io_dir != 0) {
+ db->io = io_add(PQsocket(db->pg), io_dir, connect_callback, db);
+ db->io_dir = io_dir;
+ }
+
+ if (io_dir == 0) {
+ db->connect_state = "connected";
+ timeout_remove(&db->to_connect);
+ if (PQserverVersion(db->pg) >= 90500) {
+ /* v9.5+ */
+ db->api.flags |= SQL_DB_FLAG_ON_CONFLICT_DO;
+ }
+ driver_pgsql_set_state(db, SQL_DB_STATE_IDLE);
+ if (db->ioloop != NULL) {
+ /* driver_pgsql_sync_init() waiting for connection to
+ finish */
+ io_loop_stop(db->ioloop);
+ }
+ }
+}
+
+static void driver_pgsql_connect_timeout(struct pgsql_db *db)
+{
+ unsigned int secs = ioloop_time - db->api.last_connect_try;
+
+ e_error(db->api.event, "Connect failed: Timeout after %u seconds (state: %s)",
+ secs, db->connect_state);
+ driver_pgsql_close(db);
+}
+
+static int driver_pgsql_connect(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ struct timeval tv_start;
+ int msecs;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ io_loop_time_refresh();
+ tv_start = ioloop_timeval;
+
+ db->pg = PQconnectStart(db->connect_string);
+ if (db->pg == NULL) {
+ i_fatal("pgsql: PQconnectStart() failed (out of memory)");
+ }
+
+ if (PQstatus(db->pg) == CONNECTION_BAD) {
+ e_error(_db->event, "Connect failed to database %s: %s",
+ PQdb(db->pg), last_error(db));
+ driver_pgsql_close(db);
+ return -1;
+ }
+ /* PQconnectStart() blocks on host name resolving. Log a warning if
+ it takes too long. Also don't include time spent on that in the
+ connect timeout (by refreshing ioloop time). */
+ io_loop_time_refresh();
+ msecs = timeval_diff_msecs(&ioloop_timeval, &tv_start);
+ if (msecs > PGSQL_DNS_WARN_MSECS) {
+ e_warning(_db->event, "DNS lookup took %d.%03d s",
+ msecs/1000, msecs % 1000);
+ }
+
+ /* nonblocking connecting begins. */
+ if (PQsetnonblocking(db->pg, 1) < 0)
+ e_error(_db->event, "PQsetnonblocking() failed");
+ i_assert(db->to_connect == NULL);
+ db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000,
+ driver_pgsql_connect_timeout, db);
+ db->connect_state = "connecting";
+ db->io = io_add(PQsocket(db->pg), IO_WRITE, connect_callback, db);
+ db->io_dir = IO_WRITE;
+ driver_pgsql_set_state(db, SQL_DB_STATE_CONNECTING);
+ return 0;
+}
+
+static void driver_pgsql_disconnect(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ if (db->cur_result != NULL && db->cur_result->to != NULL) {
+ driver_pgsql_stop_io(db);
+ result_finish(db->cur_result);
+ }
+
+ _db->no_reconnect = TRUE;
+ driver_pgsql_close(db);
+ _db->no_reconnect = FALSE;
+}
+
+static void driver_pgsql_free(struct pgsql_db **_db)
+{
+ struct pgsql_db *db = *_db;
+ *_db = NULL;
+
+ event_unref(&db->api.event);
+ i_free(db->connect_string);
+ i_free(db->host);
+ i_free(db->error);
+ array_free(&db->api.module_contexts);
+ i_free(db);
+}
+
+static enum sql_db_flags driver_pgsql_get_flags(struct sql_db *db)
+{
+ switch (db->state) {
+ case SQL_DB_STATE_DISCONNECTED:
+ if (sql_connect(db) < 0)
+ break;
+ /* fall through */
+ case SQL_DB_STATE_CONNECTING:
+ /* Wait for connection to finish, so we can get the flags
+ reliably. */
+ sql_wait(db);
+ break;
+ case SQL_DB_STATE_IDLE:
+ case SQL_DB_STATE_BUSY:
+ break;
+ }
+ return db->flags;
+}
+
+static int driver_pgsql_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r ATTR_UNUSED)
+{
+ struct pgsql_db *db;
+
+ db = i_new(struct pgsql_db, 1);
+ db->connect_string = i_strdup(set->connect_string);
+ db->api = driver_pgsql_db;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_pgsql);
+
+ /* NOTE: Connection string will be parsed by pgsql itself
+ We only pick the host part here */
+ T_BEGIN {
+ const char *const *arg = t_strsplit(db->connect_string, " ");
+
+ for (; *arg != NULL; arg++) {
+ if (str_begins(*arg, "host="))
+ db->host = i_strdup(*arg + 5);
+
+ }
+ } T_END;
+
+ event_set_append_log_prefix(db->api.event, t_strdup_printf("pgsql(%s): ", db->host));
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_pgsql_deinit_v(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ driver_pgsql_disconnect(_db);
+ driver_pgsql_free(&db);
+}
+
+static void driver_pgsql_set_idle(struct pgsql_db *db)
+{
+ i_assert(db->api.state == SQL_DB_STATE_BUSY);
+
+ if (db->fatal_error)
+ driver_pgsql_close(db);
+ else if (!driver_pgsql_next_callback(db))
+ driver_pgsql_set_state(db, SQL_DB_STATE_IDLE);
+}
+
+static void consume_results(struct pgsql_db *db)
+{
+ PGresult *pgres;
+
+ driver_pgsql_stop_io(db);
+
+ while (PQconsumeInput(db->pg) != 0) {
+ if (PQisBusy(db->pg) != 0) {
+ db->io = io_add(PQsocket(db->pg), IO_READ,
+ consume_results, db);
+ db->io_dir = IO_READ;
+ return;
+ }
+
+ pgres = PQgetResult(db->pg);
+ if (pgres == NULL)
+ break;
+ PQclear(pgres);
+ }
+
+ if (PQstatus(db->pg) == CONNECTION_BAD)
+ driver_pgsql_close(db);
+ else
+ driver_pgsql_set_idle(db);
+}
+
+static void driver_pgsql_result_free(struct sql_result *_result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ bool success;
+
+ i_assert(!result->api.callback);
+ i_assert(db->cur_result == result);
+ i_assert(result->callback == NULL);
+
+ if (_result == db->sync_result)
+ db->sync_result = NULL;
+ db->cur_result = NULL;
+
+ success = result->pgres != NULL && !db->fatal_error;
+ if (result->pgres != NULL) {
+ PQclear(result->pgres);
+ result->pgres = NULL;
+ }
+
+ if (success) {
+ /* we'll have to read the rest of the results as well */
+ i_assert(db->io == NULL);
+ consume_results(db);
+ } else {
+ driver_pgsql_set_idle(db);
+ }
+
+ if (array_is_created(&result->binary_values)) {
+ struct pgsql_binary_value *value;
+
+ array_foreach_modifiable(&result->binary_values, value)
+ PQfreemem(value->value);
+ array_free(&result->binary_values);
+ }
+
+ event_unref(&result->api.event);
+ i_free(result->query);
+ i_free(result->fields);
+ i_free(result->values);
+ i_free(result);
+}
+
+static void result_finish(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ bool free_result = TRUE;
+ int duration;
+
+ i_assert(db->io == NULL);
+ timeout_remove(&result->to);
+ DLLIST_REMOVE(&db->pending_results, result);
+
+ /* if connection to server was lost, we don't yet see that the
+ connection is bad. we only see the fatal error, so assume it also
+ means disconnection. */
+ if (PQstatus(db->pg) == CONNECTION_BAD || result->pgres == NULL ||
+ PQresultStatus(result->pgres) == PGRES_FATAL_ERROR)
+ db->fatal_error = TRUE;
+
+ if (db->fatal_error) {
+ result->api.failed = TRUE;
+ result->api.failed_try_retry = TRUE;
+ }
+
+ /* emit event */
+ if (result->api.failed) {
+ const char *error = result->timeout ? "Timed out" : last_error(db);
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->api.event,
+ result->query, TRUE, &duration);
+ e->add_str("error", error);
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", result->query,
+ duration, error);
+ } else {
+ e_debug(sql_query_finished_event(&db->api, result->api.event,
+ result->query, FALSE, &duration)->
+ event(),
+ SQL_QUERY_FINISHED_FMT, result->query, duration);
+ }
+ result->api.callback = TRUE;
+ T_BEGIN {
+ if (result->callback != NULL)
+ result->callback(&result->api, result->context);
+ } T_END;
+ result->api.callback = FALSE;
+
+ free_result = db->sync_result != &result->api;
+ if (db->ioloop != NULL)
+ io_loop_stop(db->ioloop);
+
+ i_assert(!free_result || result->api.refcount > 0);
+ result->callback = NULL;
+ if (free_result)
+ sql_result_unref(&result->api);
+}
+
+static void get_result(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+
+ driver_pgsql_stop_io(db);
+
+ if (PQconsumeInput(db->pg) == 0) {
+ result_finish(result);
+ return;
+ }
+
+ if (PQisBusy(db->pg) != 0) {
+ db->io = io_add(PQsocket(db->pg), IO_READ,
+ get_result, result);
+ db->io_dir = IO_READ;
+ return;
+ }
+
+ result->pgres = PQgetResult(db->pg);
+ result_finish(result);
+}
+
+static void flush_callback(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ int ret;
+
+ driver_pgsql_stop_io(db);
+
+ ret = PQflush(db->pg);
+ if (ret > 0) {
+ db->io = io_add(PQsocket(db->pg), IO_WRITE,
+ flush_callback, result);
+ db->io_dir = IO_WRITE;
+ return;
+ }
+
+ if (ret < 0) {
+ result_finish(result);
+ } else {
+ /* all flushed */
+ get_result(result);
+ }
+}
+
+static void query_timeout(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+
+ driver_pgsql_stop_io(db);
+
+ result->timeout = TRUE;
+ result_finish(result);
+}
+
+static void do_query(struct pgsql_result *result, const char *query)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ int ret;
+
+ i_assert(SQL_DB_IS_READY(&db->api));
+ i_assert(db->cur_result == NULL);
+ i_assert(db->io == NULL);
+
+ driver_pgsql_set_state(db, SQL_DB_STATE_BUSY);
+ db->cur_result = result;
+ DLLIST_PREPEND(&db->pending_results, result);
+ result->to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ query_timeout, result);
+ result->query = i_strdup(query);
+
+ if (PQsendQuery(db->pg, query) == 0 ||
+ (ret = PQflush(db->pg)) < 0) {
+ /* failed to send query */
+ result_finish(result);
+ return;
+ }
+
+ if (ret > 0) {
+ /* write blocks */
+ db->io = io_add(PQsocket(db->pg), IO_WRITE,
+ flush_callback, result);
+ db->io_dir = IO_WRITE;
+ } else {
+ get_result(result);
+ }
+}
+
+static const char *
+driver_pgsql_escape_string(struct sql_db *_db, const char *string)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ size_t len = strlen(string);
+ char *to;
+
+#ifdef HAVE_PQESCAPE_STRING_CONN
+ if (db->api.state == SQL_DB_STATE_DISCONNECTED) {
+ /* try connecting again */
+ (void)sql_connect(&db->api);
+ }
+ if (db->api.state != SQL_DB_STATE_DISCONNECTED) {
+ int error;
+
+ to = t_buffer_get(len * 2 + 1);
+ len = PQescapeStringConn(db->pg, to, string, len, &error);
+ } else
+#endif
+ {
+ to = t_buffer_get(len * 2 + 1);
+ len = PQescapeString(to, string, len);
+ }
+ t_buffer_alloc(len + 1);
+ return to;
+}
+
+static void exec_callback(struct sql_result *_result,
+ void *context ATTR_UNUSED)
+{
+ struct pgsql_result *result = (struct pgsql_result*)_result;
+ result_finish(result);
+}
+
+static void driver_pgsql_exec(struct sql_db *db, const char *query)
+{
+ struct pgsql_result *result;
+
+ result = i_new(struct pgsql_result, 1);
+ result->api = driver_pgsql_result;
+ result->api.db = db;
+ result->api.refcount = 1;
+ result->api.event = event_create(db->event);
+ result->callback = exec_callback;
+ do_query(result, query);
+}
+
+static void driver_pgsql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct pgsql_result *result;
+
+ result = i_new(struct pgsql_result, 1);
+ result->api = driver_pgsql_result;
+ result->api.db = db;
+ result->api.refcount = 1;
+ result->api.event = event_create(db->event);
+ result->callback = callback;
+ result->context = context;
+ do_query(result, query);
+}
+
+static void pgsql_query_s_callback(struct sql_result *result, void *context)
+{
+ struct pgsql_db *db = context;
+
+ db->sync_result = result;
+}
+
+static void driver_pgsql_sync_init(struct pgsql_db *db)
+{
+ bool add_to_connect;
+
+ db->orig_ioloop = current_ioloop;
+ if (db->io == NULL) {
+ db->ioloop = io_loop_create();
+ return;
+ }
+
+ i_assert(db->api.state == SQL_DB_STATE_CONNECTING);
+
+ /* have to move our existing I/O and timeout handlers to new I/O loop */
+ io_remove(&db->io);
+
+ add_to_connect = (db->to_connect != NULL);
+ timeout_remove(&db->to_connect);
+
+ db->ioloop = io_loop_create();
+ if (add_to_connect) {
+ db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000,
+ driver_pgsql_connect_timeout, db);
+ }
+ db->io = io_add(PQsocket(db->pg), db->io_dir, connect_callback, db);
+ /* wait for connecting to finish */
+ io_loop_run(db->ioloop);
+}
+
+static void driver_pgsql_sync_deinit(struct pgsql_db *db)
+{
+ io_loop_destroy(&db->ioloop);
+}
+
+static struct sql_result *
+driver_pgsql_sync_query(struct pgsql_db *db, const char *query)
+{
+ struct sql_result *result;
+
+ i_assert(db->sync_result == NULL);
+
+ switch (db->api.state) {
+ case SQL_DB_STATE_CONNECTING:
+ case SQL_DB_STATE_BUSY:
+ i_unreached();
+ case SQL_DB_STATE_DISCONNECTED:
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ case SQL_DB_STATE_IDLE:
+ break;
+ }
+
+ driver_pgsql_query(&db->api, query, pgsql_query_s_callback, db);
+ if (db->sync_result == NULL)
+ io_loop_run(db->ioloop);
+
+ i_assert(db->io == NULL);
+
+ result = db->sync_result;
+ if (result == &sql_not_connected_result) {
+ /* we don't end up in pgsql's free function, so sync_result
+ won't be set to NULL if we don't do it here. */
+ db->sync_result = NULL;
+ } else if (result == NULL) {
+ result = &sql_not_connected_result;
+ result->refcount++;
+ }
+
+ i_assert(db->io == NULL);
+ return result;
+}
+
+static struct sql_result *
+driver_pgsql_query_s(struct sql_db *_db, const char *query)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ struct sql_result *result;
+
+ driver_pgsql_sync_init(db);
+ result = driver_pgsql_sync_query(db, query);
+ driver_pgsql_sync_deinit(db);
+ return result;
+}
+
+static int driver_pgsql_result_next_row(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+
+ if (result->rows != 0) {
+ /* second time we're here */
+ if (++result->rownum < result->rows)
+ return 1;
+
+ /* end of this packet. see if there's more. FIXME: this may
+ block, but the current API doesn't provide a non-blocking
+ way to do this.. */
+ PQclear(result->pgres);
+ result->pgres = PQgetResult(db->pg);
+ if (result->pgres == NULL)
+ return 0;
+ }
+
+ if (result->pgres == NULL) {
+ _result->failed = TRUE;
+ return -1;
+ }
+
+ switch (PQresultStatus(result->pgres)) {
+ case PGRES_COMMAND_OK:
+ /* no rows returned */
+ return 0;
+ case PGRES_TUPLES_OK:
+ result->rows = PQntuples(result->pgres);
+ return result->rows > 0 ? 1 : 0;
+ case PGRES_EMPTY_QUERY:
+ case PGRES_NONFATAL_ERROR:
+ /* nonfatal error */
+ _result->failed = TRUE;
+ return -1;
+ default:
+ /* treat as fatal error */
+ _result->failed = TRUE;
+ db->fatal_error = TRUE;
+ return -1;
+ }
+}
+
+static void driver_pgsql_result_fetch_fields(struct pgsql_result *result)
+{
+ unsigned int i;
+
+ if (result->fields != NULL)
+ return;
+
+ /* @UNSAFE */
+ result->fields_count = PQnfields(result->pgres);
+ result->fields = i_new(const char *, result->fields_count);
+ for (i = 0; i < result->fields_count; i++)
+ result->fields[i] = PQfname(result->pgres, i);
+}
+
+static unsigned int
+driver_pgsql_result_get_fields_count(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ driver_pgsql_result_fetch_fields(result);
+ return result->fields_count;
+}
+
+static const char *
+driver_pgsql_result_get_field_name(struct sql_result *_result, unsigned int idx)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ driver_pgsql_result_fetch_fields(result);
+ i_assert(idx < result->fields_count);
+ return result->fields[idx];
+}
+
+static int driver_pgsql_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ unsigned int i;
+
+ driver_pgsql_result_fetch_fields(result);
+ for (i = 0; i < result->fields_count; i++) {
+ if (strcmp(result->fields[i], field_name) == 0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_pgsql_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ if (PQgetisnull(result->pgres, result->rownum, idx) != 0)
+ return NULL;
+
+ return PQgetvalue(result->pgres, result->rownum, idx);
+}
+
+static const unsigned char *
+driver_pgsql_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ const char *value;
+ struct pgsql_binary_value *binary_value;
+
+ if (PQgetisnull(result->pgres, result->rownum, idx) != 0) {
+ *size_r = 0;
+ return NULL;
+ }
+
+ value = PQgetvalue(result->pgres, result->rownum, idx);
+
+ if (!array_is_created(&result->binary_values))
+ i_array_init(&result->binary_values, idx + 1);
+
+ binary_value = array_idx_get_space(&result->binary_values, idx);
+ if (binary_value->value == NULL) {
+ binary_value->value =
+ PQunescapeBytea((const unsigned char *)value,
+ &binary_value->size);
+ }
+
+ *size_r = binary_value->size;
+ return binary_value->value;
+}
+
+static const char *
+driver_pgsql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_pgsql_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_pgsql_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_pgsql_result_get_values(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ unsigned int i;
+
+ if (result->values == NULL) {
+ driver_pgsql_result_fetch_fields(result);
+ result->values = i_new(const char *, result->fields_count);
+ }
+
+ /* @UNSAFE */
+ for (i = 0; i < result->fields_count; i++) {
+ result->values[i] =
+ driver_pgsql_result_get_field_value(_result, i);
+ }
+
+ return result->values;
+}
+
+static const char *driver_pgsql_result_get_error(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+ const char *msg;
+ size_t len;
+
+ i_free_and_null(db->error);
+
+ if (result->timeout) {
+ db->error = i_strdup("Query timed out");
+ } else if (result->pgres == NULL) {
+ /* connection error */
+ db->error = i_strdup(last_error(db));
+ } else {
+ msg = PQresultErrorMessage(result->pgres);
+ if (msg == NULL)
+ return "(no error set)";
+
+ /* Error message should contain trailing \n, we don't want it */
+ len = strlen(msg);
+ db->error = len == 0 || msg[len-1] != '\n' ?
+ i_strdup(msg) : i_strndup(msg, len-1);
+ }
+ return db->error;
+}
+
+static struct sql_transaction_context *
+driver_pgsql_transaction_begin(struct sql_db *db)
+{
+ struct pgsql_transaction_context *ctx;
+
+ ctx = i_new(struct pgsql_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->ctx.event = event_create(db->event);
+ /* we need to be able to handle multiple open transactions, so at least
+ for now just keep them in memory until commit time. */
+ ctx->query_pool = pool_alloconly_create("pgsql transaction", 1024);
+ return &ctx->ctx;
+}
+
+static void
+driver_pgsql_transaction_free(struct pgsql_transaction_context *ctx)
+{
+ pool_unref(&ctx->query_pool);
+ event_unref(&ctx->ctx.event);
+ i_free(ctx);
+}
+
+static void
+transaction_commit_callback(struct sql_result *result,
+ struct pgsql_transaction_context *ctx)
+{
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ if (sql_result_next_row(result) < 0) {
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ }
+ ctx->callback(&commit_result, ctx->context);
+ driver_pgsql_transaction_free(ctx);
+}
+
+static bool transaction_send_next(void *context)
+{
+ struct pgsql_transaction_context *ctx = context;
+
+ i_assert(!ctx->failed);
+
+ if (ctx->ctx.db->state == SQL_DB_STATE_BUSY) {
+ /* kludgy.. */
+ ctx->ctx.db->state = SQL_DB_STATE_IDLE;
+ } else if (!SQL_DB_IS_READY(ctx->ctx.db)) {
+ struct sql_commit_result commit_result = {
+ .error = "Not connected"
+ };
+ ctx->callback(&commit_result, ctx->context);
+ return FALSE;
+ }
+
+ if (ctx->ctx.head != NULL) {
+ struct sql_transaction_query *query = ctx->ctx.head;
+
+ ctx->ctx.head = ctx->ctx.head->next;
+ sql_query(ctx->ctx.db, query->query,
+ transaction_update_callback, query);
+ } else {
+ sql_query(ctx->ctx.db, "COMMIT",
+ transaction_commit_callback, ctx);
+ }
+ return TRUE;
+}
+
+static void
+transaction_commit_error_callback(struct pgsql_transaction_context *ctx,
+ struct sql_result *result)
+{
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed: %s", commit_result.error);
+ ctx->callback(&commit_result, ctx->context);
+}
+
+static void
+transaction_begin_callback(struct sql_result *result,
+ struct pgsql_transaction_context *ctx)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->db;
+
+ i_assert(result->db == ctx->ctx.db);
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+ i_assert(db->next_callback == NULL);
+ db->next_callback = transaction_send_next;
+ db->next_context = ctx;
+}
+
+static void
+transaction_update_callback(struct sql_result *result,
+ struct sql_transaction_query *query)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)query->trans;
+ struct pgsql_db *db = (struct pgsql_db *)result->db;
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result = (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ i_assert(db->next_callback == NULL);
+ db->next_callback = transaction_send_next;
+ db->next_context = ctx;
+}
+
+static void
+transaction_trans_query_callback(struct sql_result *result,
+ struct sql_transaction_query *query)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)query->trans;
+ struct sql_commit_result commit_result;
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result = (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->event(),
+ "Transaction committed");
+ i_zero(&commit_result);
+ ctx->callback(&commit_result, ctx->context);
+ driver_pgsql_transaction_free(ctx);
+}
+
+static void
+driver_pgsql_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ struct sql_commit_result result;
+
+ i_zero(&result);
+ ctx->callback = callback;
+ ctx->context = context;
+
+ if (ctx->failed || _ctx->head == NULL) {
+ if (ctx->failed) {
+ result.error = ctx->error;
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", ctx->error)->event(),
+ "Transaction failed: %s", ctx->error);
+ } else {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ }
+ callback(&result, context);
+ driver_pgsql_transaction_free(ctx);
+ } else if (_ctx->head->next == NULL) {
+ /* just a single query, send it */
+ sql_query(_ctx->db, _ctx->head->query,
+ transaction_trans_query_callback, _ctx->head);
+ } else {
+ /* multiple queries, use a transaction */
+ i_assert(_ctx->db->v.query == driver_pgsql_query);
+ sql_query(_ctx->db, "BEGIN", transaction_begin_callback, ctx);
+ }
+}
+
+static void
+commit_multi_fail(struct pgsql_transaction_context *ctx,
+ struct sql_result *result, const char *query)
+{
+ ctx->failed = TRUE;
+ ctx->error = t_strdup_printf("%s (query: %s)",
+ sql_result_get_error(result), query);
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_pgsql_transaction_commit_multi(struct pgsql_transaction_context *ctx)
+{
+ struct pgsql_db *db = (struct pgsql_db *)ctx->ctx.db;
+ struct sql_result *result;
+ struct sql_transaction_query *query;
+
+ result = driver_pgsql_sync_query(db, "BEGIN");
+ if (sql_result_next_row(result) < 0) {
+ commit_multi_fail(ctx, result, "BEGIN");
+ return NULL;
+ }
+ sql_result_unref(result);
+
+ /* send queries */
+ for (query = ctx->ctx.head; query != NULL; query = query->next) {
+ result = driver_pgsql_sync_query(db, query->query);
+ if (sql_result_next_row(result) < 0) {
+ commit_multi_fail(ctx, result, query->query);
+ break;
+ }
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result =
+ (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ sql_result_unref(result);
+ }
+
+ return driver_pgsql_sync_query(db, ctx->failed ?
+ "ROLLBACK" : "COMMIT");
+}
+
+static void
+driver_pgsql_try_commit_s(struct pgsql_transaction_context *ctx,
+ const char **error_r)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ struct pgsql_db *db = (struct pgsql_db *)_ctx->db;
+ struct sql_transaction_query *single_query = NULL;
+ struct sql_result *result;
+
+ if (_ctx->head->next == NULL) {
+ /* just a single query, send it */
+ single_query = _ctx->head;
+ result = sql_query_s(_ctx->db, single_query->query);
+ } else {
+ /* multiple queries, use a transaction */
+ driver_pgsql_sync_init(db);
+ result = driver_pgsql_transaction_commit_multi(ctx);
+ driver_pgsql_sync_deinit(db);
+ }
+
+ if (ctx->failed) {
+ i_assert(ctx->error != NULL);
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", ctx->error)->event(),
+ "Transaction failed: %s", ctx->error);
+ *error_r = ctx->error;
+ } else if (result != NULL) {
+ if (sql_result_next_row(result) < 0)
+ *error_r = sql_result_get_error(result);
+ else if (single_query != NULL &&
+ single_query->affected_rows != NULL) {
+ struct pgsql_result *pg_result =
+ (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ single_query->affected_rows) < 0)
+ i_unreached();
+ }
+ }
+
+ if (!ctx->failed) {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ }
+
+ if (result != NULL)
+ sql_result_unref(result);
+}
+
+static int
+driver_pgsql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ struct pgsql_db *db = (struct pgsql_db *)_ctx->db;
+
+ *error_r = NULL;
+
+ if (_ctx->head != NULL) {
+ driver_pgsql_try_commit_s(ctx, error_r);
+ if (_ctx->db->state == SQL_DB_STATE_DISCONNECTED) {
+ *error_r = t_strdup(*error_r);
+ e_info(db->api.event, "Disconnected from database, "
+ "retrying commit");
+ if (sql_connect(_ctx->db) >= 0) {
+ ctx->failed = FALSE;
+ *error_r = NULL;
+ driver_pgsql_try_commit_s(ctx, error_r);
+ }
+ }
+ }
+
+ driver_pgsql_transaction_free(ctx);
+ return *error_r == NULL ? 0 : -1;
+}
+
+static void
+driver_pgsql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+
+ driver_pgsql_transaction_free(ctx);
+}
+
+static void
+driver_pgsql_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+
+ sql_transaction_add_query(_ctx, ctx->query_pool, query, affected_rows);
+}
+
+static const char *
+driver_pgsql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "E'\\\\x");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+static bool driver_pgsql_have_work(struct pgsql_db *db)
+{
+ return db->next_callback != NULL || db->pending_results != NULL ||
+ db->api.state == SQL_DB_STATE_CONNECTING;
+}
+
+static void driver_pgsql_wait(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ if (!driver_pgsql_have_work(db))
+ return;
+
+ db->orig_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ db->io = io_loop_move_io(&db->io);
+ while (driver_pgsql_have_work(db))
+ io_loop_run(db->ioloop);
+
+ io_loop_set_current(db->orig_ioloop);
+ db->io = io_loop_move_io(&db->io);
+ io_loop_set_current(db->ioloop);
+ io_loop_destroy(&db->ioloop);
+}
+
+const struct sql_db driver_pgsql_db = {
+ .name = "pgsql",
+ .flags = SQL_DB_FLAG_POOLED,
+
+ .v = {
+ .get_flags = driver_pgsql_get_flags,
+ .init_full = driver_pgsql_init_full_v,
+ .deinit = driver_pgsql_deinit_v,
+ .connect = driver_pgsql_connect,
+ .disconnect = driver_pgsql_disconnect,
+ .escape_string = driver_pgsql_escape_string,
+ .exec = driver_pgsql_exec,
+ .query = driver_pgsql_query,
+ .query_s = driver_pgsql_query_s,
+ .wait = driver_pgsql_wait,
+
+ .transaction_begin = driver_pgsql_transaction_begin,
+ .transaction_commit = driver_pgsql_transaction_commit,
+ .transaction_commit_s = driver_pgsql_transaction_commit_s,
+ .transaction_rollback = driver_pgsql_transaction_rollback,
+
+ .update = driver_pgsql_update,
+
+ .escape_blob = driver_pgsql_escape_blob,
+ }
+};
+
+const struct sql_result driver_pgsql_result = {
+ .v = {
+ .free = driver_pgsql_result_free,
+ .next_row = driver_pgsql_result_next_row,
+ .get_fields_count = driver_pgsql_result_get_fields_count,
+ .get_field_name = driver_pgsql_result_get_field_name,
+ .find_field = driver_pgsql_result_find_field,
+ .get_field_value = driver_pgsql_result_get_field_value,
+ .get_field_value_binary = driver_pgsql_result_get_field_value_binary,
+ .find_field_value = driver_pgsql_result_find_field_value,
+ .get_values = driver_pgsql_result_get_values,
+ .get_error = driver_pgsql_result_get_error,
+ }
+};
+
+const char *driver_pgsql_version = DOVECOT_ABI_VERSION;
+
+void driver_pgsql_init(void);
+void driver_pgsql_deinit(void);
+
+void driver_pgsql_init(void)
+{
+ sql_driver_register(&driver_pgsql_db);
+}
+
+void driver_pgsql_deinit(void)
+{
+ sql_driver_unregister(&driver_pgsql_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-sqlite.c b/src/lib-sql/driver-sqlite.c
new file mode 100644
index 0000000..e05ed18
--- /dev/null
+++ b/src/lib-sql/driver-sqlite.c
@@ -0,0 +1,555 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_SQLITE
+#include <sqlite3.h>
+
+/* retry time if db is busy (in ms) */
+static const int sqlite_busy_timeout = 1000;
+
+struct sqlite_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const char *dbfile;
+ sqlite3 *sqlite;
+ bool connected:1;
+ int rc;
+};
+
+struct sqlite_result {
+ struct sql_result api;
+ sqlite3_stmt *stmt;
+ unsigned int cols;
+ const char **row;
+};
+
+struct sqlite_transaction_context {
+ struct sql_transaction_context ctx;
+ bool failed:1;
+};
+
+extern const struct sql_db driver_sqlite_db;
+extern const struct sql_result driver_sqlite_result;
+extern const struct sql_result driver_sqlite_error_result;
+
+static struct event_category event_category_sqlite = {
+ .parent = &event_category_sql,
+ .name = "sqlite"
+};
+
+static int driver_sqlite_connect(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ if (db->connected)
+ return 1;
+
+ db->rc = sqlite3_open(db->dbfile, &db->sqlite);
+
+ if (db->rc == SQLITE_OK) {
+ db->connected = TRUE;
+ sqlite3_busy_timeout(db->sqlite, sqlite_busy_timeout);
+ return 1;
+ } else {
+ e_error(_db->event, "open(%s) failed: %s", db->dbfile,
+ sqlite3_errmsg(db->sqlite));
+ sqlite3_close(db->sqlite);
+ db->sqlite = NULL;
+ return -1;
+ }
+}
+
+static void driver_sqlite_disconnect(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ sqlite3_close(db->sqlite);
+ db->sqlite = NULL;
+}
+
+static int driver_sqlite_init_full_v(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct sqlite_db *db;
+ pool_t pool;
+
+ pool = pool_alloconly_create("sqlite driver", 512);
+ db = p_new(pool, struct sqlite_db, 1);
+ db->pool = pool;
+ db->api = driver_sqlite_db;
+ db->dbfile = p_strdup(db->pool, set->connect_string);
+ db->connected = FALSE;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_sqlite);
+ event_set_append_log_prefix(db->api.event, "sqlite: ");
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_sqlite_deinit_v(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ _db->no_reconnect = TRUE;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+
+ sqlite3_close(db->sqlite);
+ sql_connection_log_finished(_db);
+ event_unref(&_db->event);
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static const char *
+driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED,
+ const char *string)
+{
+ const char *p;
+ char *dest, *destbegin;
+
+ /* find the first ' */
+ for (p = string; *p != '\''; p++) {
+ if (*p == '\0')
+ return t_strdup_noconst(string);
+ }
+
+ /* @UNSAFE: escape ' with '' */
+ dest = destbegin = t_buffer_get((p - string) + strlen(string) * 2 + 1);
+
+ memcpy(dest, string, p - string);
+ dest += p - string;
+
+ for (; *p != '\0'; p++) {
+ *dest++ = *p;
+ if (*p == '\'')
+ *dest++ = *p;
+ }
+ *dest++ = '\0';
+ t_buffer_alloc(dest - destbegin);
+
+ return destbegin;
+}
+
+static void driver_sqlite_result_log(const struct sql_result *result, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)result->db;
+ bool success = db->connected && db->rc == SQLITE_OK;
+ int duration;
+ const char *suffix = "";
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->event, query, success,
+ &duration);
+ io_loop_time_refresh();
+
+ if (!db->connected) {
+ suffix = ": Cannot connect to database";
+ e->add_str("error", "Cannot connect to database");
+ } else if (db->rc != SQLITE_OK) {
+ suffix = t_strdup_printf(": %s (%d)", sqlite3_errmsg(db->sqlite),
+ db->rc);
+ e->add_str("error", sqlite3_errmsg(db->sqlite));
+ e->add_int("error_code", db->rc);
+ }
+
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT"%s", query, duration, suffix);
+}
+
+static void driver_sqlite_exec(struct sql_db *_db, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+ struct sql_result result;
+
+ i_zero(&result);
+ result.db = _db;
+ result.event = event_create(_db->event);
+
+ /* Other drivers do not include time spent connecting
+ but this simplifies error logging, so we include
+ it here. */
+ if (driver_sqlite_connect(_db) < 0) {
+ driver_sqlite_result_log(&result, query);
+ } else {
+ db->rc = sqlite3_exec(db->sqlite, query, NULL, NULL, NULL);
+ driver_sqlite_result_log(&result, query);
+ }
+
+ event_unref(&result.event);
+}
+
+static void driver_sqlite_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result;
+
+ result = sql_query_s(db, query);
+ result->callback = TRUE;
+ callback(result, context);
+ result->callback = FALSE;
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_sqlite_query_s(struct sql_db *_db, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+ struct sqlite_result *result;
+ struct event *event;
+
+ result = i_new(struct sqlite_result, 1);
+ result->api.db = _db;
+ /* Temporarily store the event since result->api gets
+ * overwritten later here and we need to reset it. */
+ event = event_create(_db->event);
+ result->api.event = event;
+
+ if (driver_sqlite_connect(_db) < 0) {
+ driver_sqlite_result_log(&result->api, query);
+ result->api = driver_sqlite_error_result;
+ result->stmt = NULL;
+ result->cols = 0;
+ } else {
+ db->rc = sqlite3_prepare(db->sqlite, query, -1, &result->stmt, NULL);
+ driver_sqlite_result_log(&result->api, query);
+ if (db->rc == SQLITE_OK) {
+ result->api = driver_sqlite_result;
+ result->cols = sqlite3_column_count(result->stmt);
+ result->row = i_new(const char *, result->cols);
+ } else {
+ result->api = driver_sqlite_error_result;
+ result->stmt = NULL;
+ result->cols = 0;
+ }
+ }
+
+ result->api.db = _db;
+ result->api.refcount = 1;
+ result->api.event = event;
+ return &result->api;
+}
+
+static void driver_sqlite_result_free(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ struct sqlite_db *db = (struct sqlite_db *) result->api.db;
+ int rc;
+
+ if (_result->callback)
+ return;
+
+ if (result->stmt != NULL) {
+ if ((rc = sqlite3_finalize(result->stmt)) != SQLITE_OK) {
+ e_warning(_result->event, "finalize failed: %s (%d)",
+ sqlite3_errmsg(db->sqlite), rc);
+ }
+ i_free(result->row);
+ }
+ event_unref(&result->api.event);
+ i_free(result);
+}
+
+static int driver_sqlite_result_next_row(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ switch (sqlite3_step(result->stmt)) {
+ case SQLITE_ROW:
+ return 1;
+ case SQLITE_DONE:
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+static unsigned int
+driver_sqlite_result_get_fields_count(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return result->cols;
+}
+
+static const char *
+driver_sqlite_result_get_field_name(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return sqlite3_column_name(result->stmt, idx);
+}
+
+static int driver_sqlite_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ unsigned int i;
+
+ for (i = 0; i < result->cols; ++i) {
+ const char *col = sqlite3_column_name(result->stmt, i);
+
+ if (strcmp(col, field_name) == 0)
+ return i;
+ }
+
+ return -1;
+}
+
+static const char *
+driver_sqlite_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return (const char*)sqlite3_column_text(result->stmt, idx);
+}
+
+static const unsigned char *
+driver_sqlite_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ *size_r = sqlite3_column_bytes(result->stmt, idx);
+ return sqlite3_column_blob(result->stmt, idx);
+}
+
+static const char *
+driver_sqlite_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_sqlite_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_sqlite_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_sqlite_result_get_values(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ unsigned int i;
+
+ for (i = 0; i < result->cols; ++i) {
+ result->row[i] =
+ driver_sqlite_result_get_field_value(_result, i);
+ }
+
+ return (const char *const *)result->row;
+}
+
+static const char *driver_sqlite_result_get_error(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ struct sqlite_db *db = (struct sqlite_db *)result->api.db;
+
+ if (db->connected)
+ return sqlite3_errmsg(db->sqlite);
+ else
+ return "Cannot connect to database";
+}
+
+static struct sql_transaction_context *
+driver_sqlite_transaction_begin(struct sql_db *_db)
+{
+ struct sqlite_transaction_context *ctx;
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ ctx = i_new(struct sqlite_transaction_context, 1);
+ ctx->ctx.db = _db;
+ ctx->ctx.event = event_create(_db->event);
+
+ sql_exec(_db, "BEGIN TRANSACTION");
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+
+ return &ctx->ctx;
+}
+
+static void
+driver_sqlite_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+
+ if (!ctx->failed) {
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+ }
+ sql_exec(_ctx->db, "ROLLBACK");
+ event_unref(&_ctx->event);
+ i_free(ctx);
+}
+
+static void
+driver_sqlite_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db;
+ struct sql_commit_result commit_result;
+
+ if (!ctx->failed) {
+ sql_exec(_ctx->db, "COMMIT");
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+ }
+
+ i_zero(&commit_result);
+ if (ctx->failed) {
+ commit_result.error = sqlite3_errmsg(db->sqlite);
+ callback(&commit_result, context);
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed");
+ /* From SQLite manual: It is recommended that applications
+ respond to the errors listed above by explicitly issuing a
+ ROLLBACK command. If the transaction has already been rolled
+ back automatically by the error response, then the ROLLBACK
+ command will fail with an error, but no harm is caused by
+ this. */
+ driver_sqlite_transaction_rollback(_ctx);
+ } else {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ callback(&commit_result, context);
+ event_unref(&_ctx->event);
+ i_free(ctx);
+ }
+}
+
+static int
+driver_sqlite_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *) ctx->ctx.db;
+
+ if (ctx->failed) {
+ /* also does i_free(ctx) */
+ driver_sqlite_transaction_rollback(_ctx);
+ return -1;
+ }
+
+ sql_exec(_ctx->db, "COMMIT");
+ *error_r = sqlite3_errmsg(db->sqlite);
+ i_free(ctx);
+ return 0;
+}
+
+static void
+driver_sqlite_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db;
+
+ if (ctx->failed)
+ return;
+
+ sql_exec(_ctx->db, query);
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+ else if (affected_rows != NULL)
+ *affected_rows = sqlite3_changes(db->sqlite);
+}
+
+static const char *
+driver_sqlite_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "x'");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+const struct sql_db driver_sqlite_db = {
+ .name = "sqlite",
+ .flags =
+#if SQLITE_VERSION_NUMBER >= 3024000
+ SQL_DB_FLAG_ON_CONFLICT_DO |
+#endif
+ SQL_DB_FLAG_BLOCKING,
+
+ .v = {
+ .init_full = driver_sqlite_init_full_v,
+ .deinit = driver_sqlite_deinit_v,
+ .connect = driver_sqlite_connect,
+ .disconnect = driver_sqlite_disconnect,
+ .escape_string = driver_sqlite_escape_string,
+ .exec = driver_sqlite_exec,
+ .query = driver_sqlite_query,
+ .query_s = driver_sqlite_query_s,
+
+ .transaction_begin = driver_sqlite_transaction_begin,
+ .transaction_commit = driver_sqlite_transaction_commit,
+ .transaction_commit_s = driver_sqlite_transaction_commit_s,
+ .transaction_rollback = driver_sqlite_transaction_rollback,
+
+ .update = driver_sqlite_update,
+
+ .escape_blob = driver_sqlite_escape_blob,
+ }
+};
+
+const struct sql_result driver_sqlite_result = {
+ .v = {
+ .free = driver_sqlite_result_free,
+ .next_row = driver_sqlite_result_next_row,
+ .get_fields_count = driver_sqlite_result_get_fields_count,
+ .get_field_name = driver_sqlite_result_get_field_name,
+ .find_field = driver_sqlite_result_find_field,
+ .get_field_value = driver_sqlite_result_get_field_value,
+ .get_field_value_binary = driver_sqlite_result_get_field_value_binary,
+ .find_field_value = driver_sqlite_result_find_field_value,
+ .get_values = driver_sqlite_result_get_values,
+ .get_error = driver_sqlite_result_get_error,
+ }
+};
+
+static int
+driver_sqlite_result_error_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+const struct sql_result driver_sqlite_error_result = {
+ .v = {
+ .free = driver_sqlite_result_free,
+ .next_row = driver_sqlite_result_error_next_row,
+ .get_error = driver_sqlite_result_get_error,
+ }
+};
+
+const char *driver_sqlite_version = DOVECOT_ABI_VERSION;
+
+void driver_sqlite_init(void);
+void driver_sqlite_deinit(void);
+
+void driver_sqlite_init(void)
+{
+ sql_driver_register(&driver_sqlite_db);
+}
+
+void driver_sqlite_deinit(void)
+{
+ sql_driver_unregister(&driver_sqlite_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-sqlpool.c b/src/lib-sql/driver-sqlpool.c
new file mode 100644
index 0000000..553b2a0
--- /dev/null
+++ b/src/lib-sql/driver-sqlpool.c
@@ -0,0 +1,934 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "sql-api-private.h"
+
+#include <time.h>
+
+#define QUERY_TIMEOUT_SECS 6
+
+/* sqlpool events are separate from category:sql, because
+ they are usually not very interesting, and would only
+ make logging too noisy. They can be enabled explicitly.
+*/
+static struct event_category event_category_sqlpool = {
+ .name = "sqlpool",
+};
+
+struct sqlpool_host {
+ char *connect_string;
+
+ unsigned int connection_count;
+};
+
+struct sqlpool_connection {
+ struct sql_db *db;
+ unsigned int host_idx;
+};
+
+struct sqlpool_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const struct sql_db *driver;
+ unsigned int connection_limit;
+
+ ARRAY(struct sqlpool_host) hosts;
+ /* all connections from all hosts */
+ ARRAY(struct sqlpool_connection) all_connections;
+ /* index of last connection in all_connections that was used to
+ send a query. */
+ unsigned int last_query_conn_idx;
+
+ /* queued requests */
+ struct sqlpool_request *requests_head, *requests_tail;
+ struct timeout *request_to;
+};
+
+struct sqlpool_request {
+ struct sqlpool_request *prev, *next;
+
+ struct sqlpool_db *db;
+ time_t created;
+
+ unsigned int host_idx;
+ unsigned int retry_count;
+
+ struct event *event;
+
+ /* requests are a) queries */
+ char *query;
+ sql_query_callback_t *callback;
+ void *context;
+
+ /* b) transaction waiters */
+ struct sqlpool_transaction_context *trans;
+};
+
+struct sqlpool_transaction_context {
+ struct sql_transaction_context ctx;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ pool_t query_pool;
+ struct sqlpool_request *commit_request;
+};
+
+extern struct sql_db driver_sqlpool_db;
+
+static struct sqlpool_connection *
+sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host,
+ unsigned int host_idx);
+static void
+driver_sqlpool_query_callback(struct sql_result *result,
+ struct sqlpool_request *request);
+static void
+driver_sqlpool_commit_callback(const struct sql_commit_result *result,
+ struct sqlpool_transaction_context *ctx);
+static void driver_sqlpool_deinit(struct sql_db *_db);
+
+static struct sqlpool_request * ATTR_NULL(2)
+sqlpool_request_new(struct sqlpool_db *db, const char *query)
+{
+ struct sqlpool_request *request;
+
+ request = i_new(struct sqlpool_request, 1);
+ request->db = db;
+ request->created = time(NULL);
+ request->query = i_strdup(query);
+ request->event = event_create(db->api.event);
+ return request;
+}
+
+static void
+sqlpool_request_free(struct sqlpool_request **_request)
+{
+ struct sqlpool_request *request = *_request;
+
+ *_request = NULL;
+
+ i_assert(request->prev == NULL && request->next == NULL);
+ event_unref(&request->event);
+ i_free(request->query);
+ i_free(request);
+}
+
+static void
+sqlpool_request_abort(struct sqlpool_request **_request)
+{
+ struct sqlpool_request *request = *_request;
+
+ *_request = NULL;
+
+ if (request->callback != NULL)
+ request->callback(&sql_not_connected_result, request->context);
+
+ i_assert(request->prev != NULL ||
+ request->db->requests_head == request);
+ DLLIST2_REMOVE(&request->db->requests_head,
+ &request->db->requests_tail, request);
+ sqlpool_request_free(&request);
+}
+
+static struct sql_transaction_context *
+driver_sqlpool_new_conn_trans(struct sqlpool_transaction_context *trans,
+ struct sql_db *conndb)
+{
+ struct sql_transaction_context *conn_trans;
+ struct sql_transaction_query *query;
+
+ conn_trans = sql_transaction_begin(conndb);
+ /* backend will use our queries list (we might still append more
+ queries to the list) */
+ conn_trans->head = trans->ctx.head;
+ conn_trans->tail = trans->ctx.tail;
+ for (query = conn_trans->head; query != NULL; query = query->next)
+ query->trans = conn_trans;
+ return conn_trans;
+}
+
+static void
+sqlpool_request_handle_transaction(struct sql_db *conndb,
+ struct sqlpool_transaction_context *trans)
+{
+ struct sql_transaction_context *conn_trans;
+
+ sqlpool_request_free(&trans->commit_request);
+ conn_trans = driver_sqlpool_new_conn_trans(trans, conndb);
+ sql_transaction_commit(&conn_trans,
+ driver_sqlpool_commit_callback, trans);
+}
+
+static void
+sqlpool_request_send_next(struct sqlpool_db *db, struct sql_db *conndb)
+{
+ struct sqlpool_request *request;
+
+ if (db->requests_head == NULL || !SQL_DB_IS_READY(conndb))
+ return;
+
+ request = db->requests_head;
+ DLLIST2_REMOVE(&db->requests_head, &db->requests_tail, request);
+ timeout_reset(db->request_to);
+
+ if (request->query != NULL) {
+ sql_query(conndb, request->query,
+ driver_sqlpool_query_callback, request);
+ } else if (request->trans != NULL) {
+ sqlpool_request_handle_transaction(conndb, request->trans);
+ } else {
+ i_unreached();
+ }
+}
+
+static void sqlpool_reconnect(struct sql_db *conndb)
+{
+ timeout_remove(&conndb->to_reconnect);
+ (void)sql_connect(conndb);
+}
+
+static struct sqlpool_host *
+sqlpool_find_host_with_least_connections(struct sqlpool_db *db,
+ unsigned int *host_idx_r)
+{
+ struct sqlpool_host *hosts, *min = NULL;
+ unsigned int i, count;
+
+ hosts = array_get_modifiable(&db->hosts, &count);
+ i_assert(count > 0);
+
+ min = &hosts[0];
+ *host_idx_r = 0;
+
+ for (i = 1; i < count; i++) {
+ if (min->connection_count > hosts[i].connection_count) {
+ min = &hosts[i];
+ *host_idx_r = i;
+ }
+ }
+ return min;
+}
+
+static bool sqlpool_have_successful_connections(struct sqlpool_db *db)
+{
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn) {
+ if (conn->db->state >= SQL_DB_STATE_IDLE)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+sqlpool_handle_connect_failed(struct sqlpool_db *db, struct sql_db *conndb)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ if (conndb->connect_failure_count > 0) {
+ /* increase delay between reconnections to this
+ server */
+ conndb->connect_delay *= 5;
+ if (conndb->connect_delay > SQL_CONNECT_MAX_DELAY)
+ conndb->connect_delay = SQL_CONNECT_MAX_DELAY;
+ }
+ conndb->connect_failure_count++;
+
+ /* reconnect after the delay */
+ timeout_remove(&conndb->to_reconnect);
+ conndb->to_reconnect = timeout_add(conndb->connect_delay * 1000,
+ sqlpool_reconnect, conndb);
+
+ /* if we have zero successful hosts and there still are hosts
+ without connections, connect to one of them. */
+ if (!sqlpool_have_successful_connections(db)) {
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count == 0)
+ (void)sqlpool_add_connection(db, host, host_idx);
+ }
+}
+
+static void
+sqlpool_state_changed(struct sql_db *conndb, enum sql_db_state prev_state,
+ void *context)
+{
+ struct sqlpool_db *db = context;
+
+ if (conndb->state == SQL_DB_STATE_IDLE) {
+ conndb->connect_failure_count = 0;
+ conndb->connect_delay = SQL_CONNECT_MIN_DELAY;
+ sqlpool_request_send_next(db, conndb);
+ }
+
+ if (prev_state == SQL_DB_STATE_CONNECTING &&
+ conndb->state == SQL_DB_STATE_DISCONNECTED &&
+ !conndb->no_reconnect)
+ sqlpool_handle_connect_failed(db, conndb);
+}
+
+static struct sqlpool_connection *
+sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host,
+ unsigned int host_idx)
+{
+ struct sql_db *conndb;
+ struct sqlpool_connection *conn;
+ const char *error;
+ int ret = 0;
+
+ host->connection_count++;
+
+ e_debug(db->api.event, "Creating new connection");
+
+ if (db->driver->v.init_full == NULL) {
+ conndb = db->driver->v.init(host->connect_string);
+ } else {
+ struct sql_settings set = {
+ .connect_string = host->connect_string,
+ .event_parent = event_get_parent(db->api.event),
+ };
+ ret = db->driver->v.init_full(&set, &conndb, &error);
+ }
+ if (ret < 0)
+ i_fatal("sqlpool: %s", error);
+
+ sql_init_common(conndb);
+
+ conndb->state_change_callback = sqlpool_state_changed;
+ conndb->state_change_context = db;
+ conndb->connect_delay = SQL_CONNECT_MIN_DELAY;
+
+ conn = array_append_space(&db->all_connections);
+ conn->host_idx = host_idx;
+ conn->db = conndb;
+ return conn;
+}
+
+static struct sqlpool_connection *
+sqlpool_add_new_connection(struct sqlpool_db *db)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count >= db->connection_limit)
+ return NULL;
+ else
+ return sqlpool_add_connection(db, host, host_idx);
+}
+
+static const struct sqlpool_connection *
+sqlpool_find_available_connection(struct sqlpool_db *db,
+ unsigned int unwanted_host_idx,
+ bool *all_disconnected_r)
+{
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ *all_disconnected_r = TRUE;
+
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ unsigned int idx = (i + db->last_query_conn_idx + 1) % count;
+ struct sql_db *conndb = conns[idx].db;
+
+ if (conns[idx].host_idx == unwanted_host_idx)
+ continue;
+
+ if (!SQL_DB_IS_READY(conndb) && conndb->to_reconnect == NULL) {
+ /* see if we could reconnect to it immediately */
+ (void)sql_connect(conndb);
+ }
+ if (SQL_DB_IS_READY(conndb)) {
+ db->last_query_conn_idx = idx;
+ *all_disconnected_r = FALSE;
+ return &conns[idx];
+ }
+ if (conndb->state != SQL_DB_STATE_DISCONNECTED)
+ *all_disconnected_r = FALSE;
+ }
+ return NULL;
+}
+
+static bool
+driver_sqlpool_get_connection(struct sqlpool_db *db,
+ unsigned int unwanted_host_idx,
+ const struct sqlpool_connection **conn_r)
+{
+ const struct sqlpool_connection *conn, *conns;
+ unsigned int i, count;
+ bool all_disconnected;
+
+ conn = sqlpool_find_available_connection(db, unwanted_host_idx,
+ &all_disconnected);
+ if (conn == NULL && unwanted_host_idx != UINT_MAX) {
+ /* maybe there are no wanted hosts. use any of them. */
+ conn = sqlpool_find_available_connection(db, UINT_MAX,
+ &all_disconnected);
+ }
+ if (conn == NULL && all_disconnected) {
+ /* no connected connections. connect_delays may have gotten too
+ high, reset all of them to see if some are still alive. */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ struct sql_db *conndb = conns[i].db;
+
+ if (conndb->connect_delay > SQL_CONNECT_RESET_DELAY)
+ conndb->connect_delay = SQL_CONNECT_RESET_DELAY;
+ }
+ conn = sqlpool_find_available_connection(db, UINT_MAX,
+ &all_disconnected);
+ }
+ if (conn == NULL) {
+ /* still nothing. try creating new connections */
+ conn = sqlpool_add_new_connection(db);
+ if (conn != NULL)
+ (void)sql_connect(conn->db);
+ if (conn == NULL || !SQL_DB_IS_READY(conn->db))
+ return FALSE;
+ }
+ *conn_r = conn;
+ return TRUE;
+}
+
+static bool
+driver_sqlpool_get_sync_connection(struct sqlpool_db *db,
+ const struct sqlpool_connection **conn_r)
+{
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ if (driver_sqlpool_get_connection(db, UINT_MAX, conn_r))
+ return TRUE;
+
+ /* no idling connections, but maybe we can find one that's trying to
+ connect to server, and we can use it once it's finished */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i].db->state == SQL_DB_STATE_CONNECTING) {
+ *conn_r = &conns[i];
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+driver_sqlpool_get_connected_flags(struct sqlpool_db *db,
+ enum sql_db_flags *flags_r)
+{
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn) {
+ if (conn->db->state > SQL_DB_STATE_CONNECTING) {
+ *flags_r = sql_get_flags(conn->db);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static enum sql_db_flags driver_sqlpool_get_flags(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ enum sql_db_flags flags;
+
+ /* try to use a connected db */
+ if (driver_sqlpool_get_connected_flags(db, &flags))
+ return flags;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ /* Failed to connect to database. Just use the first
+ connection. */
+ conn = array_idx(&db->all_connections, 0);
+ }
+ return sql_get_flags(conn->db);
+}
+
+static int
+driver_sqlpool_parse_hosts(struct sqlpool_db *db, const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *key, *value, *hostname;
+ struct sqlpool_host *host;
+ ARRAY_TYPE(const_string) hostnames, connect_args;
+
+ t_array_init(&hostnames, 8);
+ t_array_init(&connect_args, 32);
+
+ /* connect string is a space separated list. it may contain
+ backend-specific strings which we'll pass as-is. we'll only care
+ about our own settings, plus the host settings. */
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ key = *args;
+ value = "";
+ } else {
+ key = t_strdup_until(*args, value);
+ value++;
+ }
+
+ if (strcmp(key, "maxconns") == 0) {
+ if (str_to_uint(value, &db->connection_limit) < 0) {
+ *error_r = t_strdup_printf("Invalid value for maxconns: %s",
+ value);
+ return -1;
+ }
+ } else if (strcmp(key, "host") == 0) {
+ array_push_back(&hostnames, &value);
+ } else {
+ array_push_back(&connect_args, args);
+ }
+ }
+
+ /* build a new connect string without our settings or hosts */
+ array_append_zero(&connect_args);
+ connect_string = t_strarray_join(array_front(&connect_args), " ");
+
+ if (array_count(&hostnames) == 0) {
+ /* no hosts specified. create a default one. */
+ host = array_append_space(&db->hosts);
+ host->connect_string = i_strdup(connect_string);
+ } else {
+ if (*connect_string == '\0')
+ connect_string = NULL;
+
+ array_foreach_elem(&hostnames, hostname) {
+ host = array_append_space(&db->hosts);
+ host->connect_string =
+ i_strconcat("host=", hostname, " ",
+ connect_string, NULL);
+ }
+ }
+
+ if (db->connection_limit == 0)
+ db->connection_limit = SQL_DEFAULT_CONNECTION_LIMIT;
+ return 0;
+}
+
+static void sqlpool_add_all_once(struct sqlpool_db *db)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ for (;;) {
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count > 0)
+ break;
+ (void)sqlpool_add_connection(db, host, host_idx);
+ }
+}
+
+int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct sqlpool_db *db;
+ int ret;
+
+ db = i_new(struct sqlpool_db, 1);
+ db->driver = driver;
+ db->api = driver_sqlpool_db;
+ db->api.flags = driver->flags;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_sqlpool);
+ event_set_append_log_prefix(db->api.event,
+ t_strdup_printf("sqlpool(%s): ", driver->name));
+ i_array_init(&db->hosts, 8);
+
+ T_BEGIN {
+ ret = driver_sqlpool_parse_hosts(db, set->connect_string,
+ error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+
+ if (ret < 0) {
+ driver_sqlpool_deinit(&db->api);
+ return ret;
+ }
+ i_array_init(&db->all_connections, 16);
+ /* connect to all databases so we can do load balancing immediately */
+ sqlpool_add_all_once(db);
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_sqlpool_abort_requests(struct sqlpool_db *db)
+{
+ while (db->requests_head != NULL) {
+ struct sqlpool_request *request = db->requests_head;
+
+ sqlpool_request_abort(&request);
+ }
+ timeout_remove(&db->request_to);
+}
+
+static void driver_sqlpool_deinit(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ struct sqlpool_host *host;
+ struct sqlpool_connection *conn;
+
+ array_foreach_modifiable(&db->all_connections, conn)
+ sql_unref(&conn->db);
+ array_clear(&db->all_connections);
+
+ driver_sqlpool_abort_requests(db);
+
+ array_foreach_modifiable(&db->hosts, host)
+ i_free(host->connect_string);
+
+ i_assert(array_count(&db->all_connections) == 0);
+ array_free(&db->hosts);
+ array_free(&db->all_connections);
+ array_free(&_db->module_contexts);
+ event_unref(&_db->event);
+ i_free(db);
+}
+
+static int driver_sqlpool_connect(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ int ret = -1, ret2;
+
+ array_foreach(&db->all_connections, conn) {
+ ret2 = conn->db->to_reconnect != NULL ? -1 :
+ sql_connect(conn->db);
+ if (ret2 > 0)
+ ret = 1;
+ else if (ret2 == 0 && ret < 0)
+ ret = 0;
+ }
+ return ret;
+}
+
+static void driver_sqlpool_disconnect(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn)
+ sql_disconnect(conn->db);
+ driver_sqlpool_abort_requests(db);
+}
+
+static const char *
+driver_sqlpool_escape_string(struct sql_db *_db, const char *string)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ /* use the first ready connection */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (SQL_DB_IS_READY(conns[i].db))
+ return sql_escape_string(conns[i].db, string);
+ }
+ /* no ready connections. just use the first one (we're guaranteed
+ to always have one) */
+ return sql_escape_string(conns[0].db, string);
+}
+
+static void driver_sqlpool_timeout(struct sqlpool_db *db)
+{
+ int duration;
+
+ while (db->requests_head != NULL) {
+ struct sqlpool_request *request = db->requests_head;
+
+ if (request->created + SQL_QUERY_TIMEOUT_SECS > ioloop_time)
+ break;
+
+
+ if (request->query != NULL) {
+ e_error(sql_query_finished_event(&db->api, request->event,
+ request->query, FALSE,
+ &duration)->
+ add_str("error", "Query timed out")->
+ event(),
+ SQL_QUERY_FINISHED_FMT": Query timed out "
+ "(no free connections for %u secs)",
+ request->query, duration,
+ (unsigned int)(ioloop_time - request->created));
+ } else {
+ e_error(event_create_passthrough(request->event)->
+ add_str("error", "Timed out")->
+ set_name(SQL_TRANSACTION_FINISHED)->event(),
+ "Transaction timed out "
+ "(no free connections for %u secs)",
+ (unsigned int)(ioloop_time - request->created));
+ }
+ sqlpool_request_abort(&request);
+ }
+
+ if (db->requests_head == NULL)
+ timeout_remove(&db->request_to);
+}
+
+static void
+driver_sqlpool_prepend_request(struct sqlpool_db *db,
+ struct sqlpool_request *request)
+{
+ DLLIST2_PREPEND(&db->requests_head, &db->requests_tail, request);
+ if (db->request_to == NULL) {
+ db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ driver_sqlpool_timeout, db);
+ }
+}
+
+static void
+driver_sqlpool_append_request(struct sqlpool_db *db,
+ struct sqlpool_request *request)
+{
+ DLLIST2_APPEND(&db->requests_head, &db->requests_tail, request);
+ if (db->request_to == NULL) {
+ db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ driver_sqlpool_timeout, db);
+ }
+}
+
+static void
+driver_sqlpool_query_callback(struct sql_result *result,
+ struct sqlpool_request *request)
+{
+ struct sqlpool_db *db = request->db;
+ const struct sqlpool_connection *conn = NULL;
+ struct sql_db *conndb;
+
+ if (result->failed_try_retry &&
+ request->retry_count < array_count(&db->hosts)) {
+ e_warning(db->api.event, "Query failed, retrying: %s",
+ sql_result_get_error(result));
+ request->retry_count++;
+ driver_sqlpool_prepend_request(db, request);
+
+ if (driver_sqlpool_get_connection(request->db,
+ request->host_idx, &conn)) {
+ request->host_idx = conn->host_idx;
+ sqlpool_request_send_next(db, conn->db);
+ }
+ } else {
+ if (result->failed) {
+ e_error(db->api.event, "Query failed, aborting: %s",
+ request->query);
+ }
+ conndb = result->db;
+
+ if (request->callback != NULL)
+ request->callback(result, request->context);
+ sqlpool_request_free(&request);
+
+ sqlpool_request_send_next(db, conndb);
+ }
+}
+
+static void ATTR_NULL(3, 4)
+driver_sqlpool_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ struct sqlpool_request *request;
+ const struct sqlpool_connection *conn;
+
+ request = sqlpool_request_new(db, query);
+ request->callback = callback;
+ request->context = context;
+
+ if (!driver_sqlpool_get_connection(db, UINT_MAX, &conn))
+ driver_sqlpool_append_request(db, request);
+ else {
+ request->host_idx = conn->host_idx;
+ sql_query(conn->db, query, driver_sqlpool_query_callback,
+ request);
+ }
+}
+
+static void driver_sqlpool_exec(struct sql_db *_db, const char *query)
+{
+ driver_sqlpool_query(_db, query, NULL, NULL);
+}
+
+static struct sql_result *
+driver_sqlpool_query_s(struct sql_db *_db, const char *query)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ struct sql_result *result;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ }
+
+ result = sql_query_s(conn->db, query);
+ if (result->failed_try_retry) {
+ if (!driver_sqlpool_get_sync_connection(db, &conn))
+ return result;
+
+ sql_result_unref(result);
+ result = sql_query_s(conn->db, query);
+ }
+ return result;
+}
+
+static struct sql_transaction_context *
+driver_sqlpool_transaction_begin(struct sql_db *_db)
+{
+ struct sqlpool_transaction_context *ctx;
+
+ ctx = i_new(struct sqlpool_transaction_context, 1);
+ ctx->ctx.db = _db;
+
+ /* queue changes until commit. even if we did have a free connection
+ now, don't use it or multiple open transactions could tie up all
+ connections. */
+ ctx->query_pool = pool_alloconly_create("sqlpool transaction", 1024);
+ return &ctx->ctx;
+}
+
+static void
+driver_sqlpool_transaction_free(struct sqlpool_transaction_context *ctx)
+{
+ if (ctx->commit_request != NULL)
+ sqlpool_request_abort(&ctx->commit_request);
+ pool_unref(&ctx->query_pool);
+ i_free(ctx);
+}
+
+static void
+driver_sqlpool_commit_callback(const struct sql_commit_result *result,
+ struct sqlpool_transaction_context *ctx)
+{
+ ctx->callback(result, ctx->context);
+ driver_sqlpool_transaction_free(ctx);
+}
+
+static void
+driver_sqlpool_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback,
+ void *context)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+ struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db;
+ const struct sqlpool_connection *conn;
+
+ ctx->callback = callback;
+ ctx->context = context;
+
+ ctx->commit_request = sqlpool_request_new(db, NULL);
+ ctx->commit_request->trans = ctx;
+
+ if (driver_sqlpool_get_connection(db, UINT_MAX, &conn))
+ sqlpool_request_handle_transaction(conn->db, ctx);
+ else
+ driver_sqlpool_append_request(db, ctx->commit_request);
+}
+
+static int
+driver_sqlpool_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+ struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db;
+ const struct sqlpool_connection *conn;
+ struct sql_transaction_context *conn_trans;
+ int ret;
+
+ *error_r = NULL;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ *error_r = SQL_ERRSTR_NOT_CONNECTED;
+ driver_sqlpool_transaction_free(ctx);
+ return -1;
+ }
+
+ conn_trans = driver_sqlpool_new_conn_trans(ctx, conn->db);
+ ret = sql_transaction_commit_s(&conn_trans, error_r);
+ driver_sqlpool_transaction_free(ctx);
+ return ret;
+}
+
+static void
+driver_sqlpool_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+
+ driver_sqlpool_transaction_free(ctx);
+}
+
+static void
+driver_sqlpool_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+
+ /* we didn't get a connection for transaction immediately.
+ queue updates until commit transfers all of these */
+ sql_transaction_add_query(&ctx->ctx, ctx->query_pool,
+ query, affected_rows);
+}
+
+static const char *
+driver_sqlpool_escape_blob(struct sql_db *_db,
+ const unsigned char *data, size_t size)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ /* use the first ready connection */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (SQL_DB_IS_READY(conns[i].db))
+ return sql_escape_blob(conns[i].db, data, size);
+ }
+ /* no ready connections. just use the first one (we're guaranteed
+ to always have one) */
+ return sql_escape_blob(conns[0].db, data, size);
+}
+
+static void driver_sqlpool_wait(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn)
+ sql_wait(conn->db);
+}
+
+struct sql_db driver_sqlpool_db = {
+ "",
+
+ .v = {
+ .get_flags = driver_sqlpool_get_flags,
+ .deinit = driver_sqlpool_deinit,
+ .connect = driver_sqlpool_connect,
+ .disconnect = driver_sqlpool_disconnect,
+ .escape_string = driver_sqlpool_escape_string,
+ .exec = driver_sqlpool_exec,
+ .query = driver_sqlpool_query,
+ .query_s = driver_sqlpool_query_s,
+ .wait = driver_sqlpool_wait,
+
+ .transaction_begin = driver_sqlpool_transaction_begin,
+ .transaction_commit = driver_sqlpool_transaction_commit,
+ .transaction_commit_s = driver_sqlpool_transaction_commit_s,
+ .transaction_rollback = driver_sqlpool_transaction_rollback,
+
+ .update = driver_sqlpool_update,
+
+ .escape_blob = driver_sqlpool_escape_blob,
+ }
+};
diff --git a/src/lib-sql/driver-test.c b/src/lib-sql/driver-test.c
new file mode 100644
index 0000000..9d4db76
--- /dev/null
+++ b/src/lib-sql/driver-test.c
@@ -0,0 +1,514 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "sql-api-private.h"
+#include "driver-test.h"
+#include "array.h"
+#include "hex-binary.h"
+
+struct test_sql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ ARRAY(struct test_driver_result) expected;
+ const char *error;
+ bool failed:1;
+};
+
+struct test_sql_result {
+ struct sql_result api;
+ struct test_driver_result *result;
+ const char *error;
+};
+
+static struct sql_db *driver_test_mysql_init(const char *connect_string);
+static struct sql_db *driver_test_cassandra_init(const char *connect_string);
+static struct sql_db *driver_test_sqlite_init(const char *connect_string);
+static void driver_test_deinit(struct sql_db *_db);
+static int driver_test_connect(struct sql_db *_db);
+static void driver_test_disconnect(struct sql_db *_db);
+static const char *
+driver_test_mysql_escape_string(struct sql_db *_db, const char *string);
+static const char *
+driver_test_escape_string(struct sql_db *_db, const char *string);
+static void driver_test_exec(struct sql_db *_db, const char *query);
+static void driver_test_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context);
+static struct sql_result *
+driver_test_query_s(struct sql_db *_db, const char *query);
+static struct sql_transaction_context *
+driver_test_transaction_begin(struct sql_db *_db);
+static void driver_test_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback,
+ void *context);
+static int
+driver_test_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r);
+static void
+driver_test_transaction_rollback(struct sql_transaction_context *ctx);
+static void
+driver_test_update(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+static const char *
+driver_test_mysql_escape_blob(struct sql_db *_db, const unsigned char *data,
+ size_t size);
+static const char *
+driver_test_escape_blob(struct sql_db *_db, const unsigned char *data,
+ size_t size);
+
+static void driver_test_result_free(struct sql_result *result);
+static int driver_test_result_next_row(struct sql_result *result);
+
+static unsigned int
+driver_test_result_get_fields_count(struct sql_result *result);
+static const char *
+driver_test_result_get_field_name(struct sql_result *result, unsigned int idx);
+static int
+driver_test_result_find_field(struct sql_result *result, const char *field_name);
+
+static const char *
+driver_test_result_get_field_value(struct sql_result *result, unsigned int idx);
+static const unsigned char *
+driver_test_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r);
+static const char *
+driver_test_result_find_field_value(struct sql_result *result,
+ const char *field_name);
+static const char *const *
+driver_test_result_get_values(struct sql_result *result);
+
+const char *driver_test_result_get_error(struct sql_result *result);
+
+
+const struct sql_db driver_test_mysql_db = {
+ .name = "mysql",
+ .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_ON_DUPLICATE_KEY,
+
+ .v = {
+ .init = driver_test_mysql_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_mysql_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_mysql_escape_blob,
+ }
+};
+
+const struct sql_db driver_test_cassandra_db = {
+ .name = "cassandra",
+
+ .v = {
+ .init = driver_test_cassandra_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_escape_blob,
+ }
+};
+
+const struct sql_db driver_test_sqlite_db = {
+ .name = "sqlite",
+ .flags = SQL_DB_FLAG_ON_CONFLICT_DO | SQL_DB_FLAG_BLOCKING,
+
+ .v = {
+ .init = driver_test_sqlite_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_escape_blob,
+ }
+};
+
+
+const struct sql_result driver_test_result = {
+ .v = {
+ .free = driver_test_result_free,
+ .next_row = driver_test_result_next_row,
+ .get_fields_count = driver_test_result_get_fields_count,
+ .get_field_name = driver_test_result_get_field_name,
+ .find_field = driver_test_result_find_field,
+ .get_field_value = driver_test_result_get_field_value,
+ .get_field_value_binary = driver_test_result_get_field_value_binary,
+ .find_field_value = driver_test_result_find_field_value,
+ .get_values = driver_test_result_get_values,
+ .get_error = driver_test_result_get_error,
+ }
+};
+
+void sql_driver_test_register(void)
+{
+ sql_driver_register(&driver_test_mysql_db);
+ sql_driver_register(&driver_test_cassandra_db);
+ sql_driver_register(&driver_test_sqlite_db);
+}
+
+void sql_driver_test_unregister(void)
+{
+ sql_driver_unregister(&driver_test_mysql_db);
+ sql_driver_unregister(&driver_test_cassandra_db);
+ sql_driver_unregister(&driver_test_sqlite_db);
+}
+
+static struct sql_db *driver_test_init(const struct sql_db *driver,
+ const char *connect_string ATTR_UNUSED)
+{
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING" test sql driver", 2048);
+ struct test_sql_db *ret = p_new(pool, struct test_sql_db, 1);
+ ret->pool = pool;
+ ret->api = *driver;
+ p_array_init(&ret->expected, pool, 8);
+ return &ret->api;
+}
+
+static struct sql_db *driver_test_mysql_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_mysql_db, connect_string);
+}
+
+static struct sql_db *driver_test_cassandra_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_cassandra_db, connect_string);
+}
+
+static struct sql_db *driver_test_sqlite_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_sqlite_db, connect_string);
+}
+
+static void driver_test_deinit(struct sql_db *_db ATTR_UNUSED)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static int driver_test_connect(struct sql_db *_db ATTR_UNUSED)
+{
+ /* nix */
+ return 0;
+}
+
+static void driver_test_disconnect(struct sql_db *_db ATTR_UNUSED)
+{ }
+
+static const char *
+driver_test_mysql_escape_string(struct sql_db *_db ATTR_UNUSED,
+ const char *string)
+{
+ string_t *esc = t_str_new(strlen(string));
+ for(const char *ptr = string; *ptr != '\0'; ptr++) {
+ if (*ptr == '\n' || *ptr == '\r' || *ptr == '\\' ||
+ *ptr == '\'' || *ptr == '\"' || *ptr == '\x1a')
+ str_append_c(esc, '\\');
+ str_append_c(esc, *ptr);
+ }
+ return str_c(esc);
+}
+
+static const char *
+driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string)
+{
+ return string;
+}
+
+static void driver_test_exec(struct sql_db *_db, const char *query)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ i_assert(result->cur < result->nqueries);
+
+/* i_debug("DUMMY EXECUTE: %s", query);
+ i_debug("DUMMY EXPECT : %s", result->queries[result->cur]); */
+
+ test_assert_strcmp(result->queries[result->cur], query);
+
+ if (strcmp(result->queries[result->cur], query) != 0) {
+ db->error = "Invalid query";
+ db->failed = TRUE;
+ }
+
+ result->cur++;
+}
+
+static void
+driver_test_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result = driver_test_query_s(_db, query);
+ if (callback != NULL)
+ callback(result, context);
+}
+
+static struct sql_result *
+driver_test_query_s(struct sql_db *_db, const char *query)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ struct test_sql_result *res = i_new(struct test_sql_result, 1);
+
+ driver_test_exec(_db, query);
+
+ if (db->failed) {
+ res->api.failed = TRUE;
+ }
+
+ res->api.v = driver_test_result.v;
+ res->api.db = _db;
+ if (result->result != NULL) {
+ res->result = i_new(struct test_driver_result, 1);
+ memcpy(res->result, result, sizeof(*result));
+ }
+ res->api.refcount = 1;
+
+ /* drop it from array if it's used up */
+ if (result->cur == result->nqueries)
+ array_pop_front(&db->expected);
+
+ return &res->api;
+}
+
+static struct sql_transaction_context *
+driver_test_transaction_begin(struct sql_db *_db)
+{
+ struct sql_transaction_context *ctx =
+ i_new(struct sql_transaction_context, 1);
+ ctx->db = _db;
+ return ctx;
+}
+
+static void
+driver_test_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_commit_result res;
+ res.error_type = driver_test_transaction_commit_s(ctx, &res.error);
+ callback(&res, context);
+}
+
+static int
+driver_test_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r)
+{
+ struct test_sql_db *db = (struct test_sql_db*)ctx->db;
+ int ret = 0;
+
+ if (db->error != NULL) {
+ *error_r = db->error;
+ ret = -1;
+ }
+ i_free(ctx);
+ db->error = NULL;
+ db->failed = FALSE;
+
+ return ret;
+}
+
+static void
+driver_test_transaction_rollback(struct sql_transaction_context *ctx)
+{
+ struct test_sql_db *db = (struct test_sql_db*)ctx->db;
+ i_free(ctx);
+ db->error = NULL;
+ db->failed = FALSE;
+}
+
+static void
+driver_test_update(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct test_sql_db *db= (struct test_sql_db*)ctx->db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ driver_test_exec(ctx->db, query);
+
+ if (affected_rows != NULL)
+ *affected_rows = result->affected_rows;
+
+ /* drop it from array if it's used up */
+ if (result->cur == result->nqueries)
+ array_pop_front(&db->expected);
+}
+
+static const char *
+driver_test_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ return t_strdup_printf("X'%s'", binary_to_hex(data,size));
+}
+
+static const char *
+driver_test_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ return t_strdup_printf("X'%s'", binary_to_hex(data,size));
+}
+
+static void driver_test_result_free(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ if (tsr->result != NULL)
+ i_free(tsr->result);
+ i_free(result);
+}
+
+static int driver_test_result_next_row(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+
+ if (r == NULL) return 0;
+
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ if (rs->cur <= rs->rows) {
+ rs->cur++;
+ }
+
+ return rs->cur <= rs->rows ? 1 : 0;
+}
+
+static unsigned int
+driver_test_result_get_fields_count(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ return rs->cols;
+}
+
+static const char *
+driver_test_result_get_field_name(struct sql_result *result, unsigned int idx)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ i_assert(idx < rs->cols);
+ return rs->col_names[idx];
+}
+
+static int
+driver_test_result_find_field(struct sql_result *result, const char *field_name)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ for(size_t i = 0; i < rs->cols; i++) {
+ if (strcmp(field_name, rs->col_names[i])==0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_test_result_get_field_value(struct sql_result *result, unsigned int idx)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+
+ i_assert(idx < rs->cols);
+ i_assert(rs->cur <= rs->rows);
+
+ return rs->row_data[rs->cur-1][idx];
+}
+static const unsigned char *
+driver_test_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r)
+{
+ buffer_t *buf = t_buffer_create(64);
+ const char *value = driver_test_result_get_field_value(result, idx);
+ /* expect it hex encoded */
+ if (hex_to_binary(value, buf) < 0) {
+ *size_r = 0;
+ return NULL;
+ }
+ *size_r = buf->used;
+ return buf->data;
+}
+static const char *
+driver_test_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx = driver_test_result_find_field(result, field_name);
+ if (idx < 0) return NULL;
+ return driver_test_result_get_field_value(result, idx);
+}
+static const char *const *
+driver_test_result_get_values(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ i_assert(rs->cur <= rs->rows);
+ return rs->row_data[rs->cur-1];
+}
+
+const char *driver_test_result_get_error(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ return tsr->error;
+}
+
+
+void sql_driver_test_add_expected_result(struct sql_db *_db,
+ const struct test_driver_result *result)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_push_back(&db->expected, result);
+}
+
+void sql_driver_test_clear_expected_results(struct sql_db *_db)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_clear(&db->expected);
+}
diff --git a/src/lib-sql/driver-test.h b/src/lib-sql/driver-test.h
new file mode 100644
index 0000000..49915ad
--- /dev/null
+++ b/src/lib-sql/driver-test.h
@@ -0,0 +1,28 @@
+#ifndef DRIVER_TEST_H
+#define DRIVER_TEST_H 1
+
+struct test_driver_result_set {
+ size_t rows, cols, cur;
+ const char *const *col_names;
+ const char ***row_data;
+};
+
+struct test_driver_result {
+ /* expected queries */
+ size_t nqueries;
+ size_t cur;
+ unsigned int affected_rows;
+ const char *const *queries;
+
+ /* test result, rows and columns */
+ struct test_driver_result_set *result;
+};
+
+void sql_driver_test_register(void);
+void sql_driver_test_unregister(void);
+
+void sql_driver_test_add_expected_result(struct sql_db *_db,
+ const struct test_driver_result *result);
+void sql_driver_test_clear_expected_results(struct sql_db *_db);
+
+#endif
diff --git a/src/lib-sql/sql-api-private.h b/src/lib-sql/sql-api-private.h
new file mode 100644
index 0000000..3026512
--- /dev/null
+++ b/src/lib-sql/sql-api-private.h
@@ -0,0 +1,255 @@
+#ifndef SQL_API_PRIVATE_H
+#define SQL_API_PRIVATE_H
+
+#include "sql-api.h"
+#include "module-context.h"
+
+enum sql_db_state {
+ /* not connected to database */
+ SQL_DB_STATE_DISCONNECTED,
+ /* waiting for connection attempt to succeed or fail */
+ SQL_DB_STATE_CONNECTING,
+ /* connected, allowing more queries */
+ SQL_DB_STATE_IDLE,
+ /* connected, no more queries allowed */
+ SQL_DB_STATE_BUSY
+};
+
+/* Minimum delay between reconnecting to same server */
+#define SQL_CONNECT_MIN_DELAY 1
+/* Maximum time to avoiding reconnecting to same server */
+#define SQL_CONNECT_MAX_DELAY (60*30)
+/* If no servers are connected but a query is requested, try reconnecting to
+ next server which has been disconnected longer than this (with a single
+ server setup this is really the "max delay" and the SQL_CONNECT_MAX_DELAY
+ is never used). */
+#define SQL_CONNECT_RESET_DELAY 15
+/* Abort connect() if it can't connect within this time. */
+#define SQL_CONNECT_TIMEOUT_SECS 5
+/* Abort queries after this many seconds */
+#define SQL_QUERY_TIMEOUT_SECS 60
+/* Default max. number of connections to create per host */
+#define SQL_DEFAULT_CONNECTION_LIMIT 5
+
+#define SQL_DB_IS_READY(db) \
+ ((db)->state == SQL_DB_STATE_IDLE)
+#define SQL_ERRSTR_NOT_CONNECTED "Not connected to database"
+
+/* What is considered slow query */
+#define SQL_SLOW_QUERY_MSEC 1000
+
+#define SQL_QUERY_FINISHED "sql_query_finished"
+#define SQL_CONNECTION_FINISHED "sql_connection_finished"
+#define SQL_TRANSACTION_FINISHED "sql_transaction_finished"
+
+#define SQL_QUERY_FINISHED_FMT "Finished query '%s' in %u msecs"
+
+struct sql_db_module_register {
+ unsigned int id;
+};
+
+union sql_db_module_context {
+ struct sql_db_module_register *reg;
+};
+
+extern struct sql_db_module_register sql_db_module_register;
+
+extern struct event_category event_category_sql;
+
+struct sql_transaction_query {
+ struct sql_transaction_query *next;
+ struct sql_transaction_context *trans;
+
+ const char *query;
+ unsigned int *affected_rows;
+};
+
+struct sql_db_vfuncs {
+ struct sql_db *(*init)(const char *connect_string);
+ int (*init_full)(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error);
+ void (*deinit)(struct sql_db *db);
+ void (*unref)(struct sql_db *db);
+ void (*wait) (struct sql_db *db);
+
+ enum sql_db_flags (*get_flags)(struct sql_db *db);
+
+ int (*connect)(struct sql_db *db);
+ void (*disconnect)(struct sql_db *db);
+ const char *(*escape_string)(struct sql_db *db, const char *string);
+
+ void (*exec)(struct sql_db *db, const char *query);
+ void (*query)(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context);
+ struct sql_result *(*query_s)(struct sql_db *db, const char *query);
+
+ struct sql_transaction_context *(*transaction_begin)(struct sql_db *db);
+ void (*transaction_commit)(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback,
+ void *context);
+ int (*transaction_commit_s)(struct sql_transaction_context *ctx,
+ const char **error_r);
+ void (*transaction_rollback)(struct sql_transaction_context *ctx);
+
+ void (*update)(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+ const char *(*escape_blob)(struct sql_db *db,
+ const unsigned char *data, size_t size);
+
+ struct sql_prepared_statement *
+ (*prepared_statement_init)(struct sql_db *db,
+ const char *query_template);
+ void (*prepared_statement_deinit)(struct sql_prepared_statement *prep_stmt);
+
+
+ struct sql_statement *
+ (*statement_init)(struct sql_db *db, const char *query_template);
+ struct sql_statement *
+ (*statement_init_prepared)(struct sql_prepared_statement *prep_stmt);
+ void (*statement_abort)(struct sql_statement *stmt);
+ void (*statement_set_timestamp)(struct sql_statement *stmt,
+ const struct timespec *ts);
+ void (*statement_bind_str)(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value);
+ void (*statement_bind_binary)(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size);
+ void (*statement_bind_int64)(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value);
+ void (*statement_query)(struct sql_statement *stmt,
+ sql_query_callback_t *callback, void *context);
+ struct sql_result *(*statement_query_s)(struct sql_statement *stmt);
+ void (*update_stmt)(struct sql_transaction_context *ctx,
+ struct sql_statement *stmt,
+ unsigned int *affected_rows);
+};
+
+struct sql_db {
+ const char *name;
+ enum sql_db_flags flags;
+ int refcount;
+
+ struct sql_db_vfuncs v;
+ ARRAY(union sql_db_module_context *) module_contexts;
+
+ void (*state_change_callback)(struct sql_db *db,
+ enum sql_db_state prev_state,
+ void *context);
+ void *state_change_context;
+
+ struct event *event;
+ HASH_TABLE(char *, struct sql_prepared_statement *) prepared_stmt_hash;
+
+ enum sql_db_state state;
+ /* last time we started connecting to this server
+ (which may or may not have succeeded) */
+ time_t last_connect_try;
+ unsigned int connect_delay;
+ unsigned int connect_failure_count;
+ struct timeout *to_reconnect;
+
+ uint64_t succeeded_queries;
+ uint64_t failed_queries;
+ /* includes both succeeded and failed */
+ uint64_t slow_queries;
+
+ bool no_reconnect:1;
+};
+
+struct sql_result_vfuncs {
+ void (*free)(struct sql_result *result);
+ int (*next_row)(struct sql_result *result);
+
+ unsigned int (*get_fields_count)(struct sql_result *result);
+ const char *(*get_field_name)(struct sql_result *result,
+ unsigned int idx);
+ int (*find_field)(struct sql_result *result, const char *field_name);
+
+ const char *(*get_field_value)(struct sql_result *result,
+ unsigned int idx);
+ const unsigned char *
+ (*get_field_value_binary)(struct sql_result *result,
+ unsigned int idx,
+ size_t *size_r);
+ const char *(*find_field_value)(struct sql_result *result,
+ const char *field_name);
+ const char *const *(*get_values)(struct sql_result *result);
+
+ const char *(*get_error)(struct sql_result *result);
+ void (*more)(struct sql_result **result, bool async,
+ sql_query_callback_t *callback, void *context);
+};
+
+struct sql_prepared_statement {
+ struct sql_db *db;
+ int refcount;
+ char *query_template;
+};
+
+struct sql_statement {
+ struct sql_db *db;
+
+ pool_t pool;
+ const char *query_template;
+ ARRAY_TYPE(const_string) args;
+
+ /* Tell the driver to not log this query with expanded values. */
+ bool no_log_expanded_values;
+};
+
+struct sql_field_map {
+ enum sql_field_type type;
+ size_t offset;
+};
+
+struct sql_result {
+ struct sql_result_vfuncs v;
+ int refcount;
+
+ struct sql_db *db;
+ const struct sql_field_def *fields;
+
+ unsigned int map_size;
+ struct sql_field_map *map;
+ void *fetch_dest;
+ struct event *event;
+ size_t fetch_dest_size;
+ enum sql_result_error_type error_type;
+
+ bool failed:1;
+ bool failed_try_retry:1;
+ bool callback:1;
+};
+
+struct sql_transaction_context {
+ struct sql_db *db;
+ struct event *event;
+
+ /* commit() must use this query list if head is non-NULL. */
+ struct sql_transaction_query *head, *tail;
+};
+
+ARRAY_DEFINE_TYPE(sql_drivers, const struct sql_db *);
+
+extern ARRAY_TYPE(sql_drivers) sql_drivers;
+extern struct sql_result sql_not_connected_result;
+
+void sql_init_common(struct sql_db *db);
+struct sql_db *
+driver_sqlpool_init(const char *connect_string, const struct sql_db *driver);
+int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver,
+ struct sql_db **db_r, const char **error_r);
+
+void sql_db_set_state(struct sql_db *db, enum sql_db_state state);
+
+void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool,
+ const char *query, unsigned int *affected_rows);
+const char *sql_statement_get_log_query(struct sql_statement *stmt);
+const char *sql_statement_get_query(struct sql_statement *stmt);
+
+void sql_connection_log_finished(struct sql_db *db);
+struct event_passthrough *
+sql_query_finished_event(struct sql_db *db, struct event *event, const char *query,
+ bool success, int *duration_r);
+struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx);
+#endif
diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c
new file mode 100644
index 0000000..cd16a5f
--- /dev/null
+++ b/src/lib-sql/sql-api.c
@@ -0,0 +1,846 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+
+#include <time.h>
+
+struct event_category event_category_sql = {
+ .name = "sql",
+};
+
+struct sql_db_module_register sql_db_module_register = { 0 };
+ARRAY_TYPE(sql_drivers) sql_drivers;
+
+void sql_drivers_init(void)
+{
+ i_array_init(&sql_drivers, 8);
+}
+
+void sql_drivers_deinit(void)
+{
+ array_free(&sql_drivers);
+}
+
+static const struct sql_db *sql_driver_lookup(const char *name)
+{
+ const struct sql_db *const *drivers;
+ unsigned int i, count;
+
+ drivers = array_get(&sql_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(drivers[i]->name, name) == 0)
+ return drivers[i];
+ }
+ return NULL;
+}
+
+void sql_driver_register(const struct sql_db *driver)
+{
+ if (sql_driver_lookup(driver->name) != NULL) {
+ i_fatal("sql_driver_register(%s): Already registered",
+ driver->name);
+ }
+ array_push_back(&sql_drivers, &driver);
+}
+
+void sql_driver_unregister(const struct sql_db *driver)
+{
+ const struct sql_db *const *drivers;
+ unsigned int i, count;
+
+ drivers = array_get(&sql_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (drivers[i] == driver) {
+ array_delete(&sql_drivers, i, 1);
+ break;
+ }
+ }
+}
+
+struct sql_db *sql_init(const char *db_driver, const char *connect_string)
+{
+ const char *error;
+ struct sql_db *db;
+ struct sql_settings set = {
+ .driver = db_driver,
+ .connect_string = connect_string,
+ };
+
+ if (sql_init_full(&set, &db, &error) < 0)
+ i_fatal("%s", error);
+ return db;
+}
+
+int sql_init_full(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r)
+{
+ const struct sql_db *driver;
+ struct sql_db *db;
+ int ret = 0;
+
+ i_assert(set->connect_string != NULL);
+
+ driver = sql_driver_lookup(set->driver);
+ if (driver == NULL) {
+ *error_r = t_strdup_printf("Unknown database driver '%s'", set->driver);
+ return -1;
+ }
+
+ if ((driver->flags & SQL_DB_FLAG_POOLED) == 0) {
+ if (driver->v.init_full == NULL) {
+ db = driver->v.init(set->connect_string);
+ } else
+ ret = driver->v.init_full(set, &db, error_r);
+ } else
+ ret = driver_sqlpool_init_full(set, driver, &db, error_r);
+
+ if (ret < 0)
+ return -1;
+
+ sql_init_common(db);
+ *db_r = db;
+ return 0;
+}
+
+void sql_init_common(struct sql_db *db)
+{
+ db->refcount = 1;
+ i_array_init(&db->module_contexts, 5);
+ hash_table_create(&db->prepared_stmt_hash, default_pool, 0,
+ str_hash, strcmp);
+}
+
+void sql_ref(struct sql_db *db)
+{
+ i_assert(db->refcount > 0);
+ db->refcount++;
+}
+
+static void
+default_sql_prepared_statement_deinit(struct sql_prepared_statement *prep_stmt)
+{
+ i_free(prep_stmt->query_template);
+ i_free(prep_stmt);
+}
+
+static void sql_prepared_statements_free(struct sql_db *db)
+{
+ struct hash_iterate_context *iter;
+ struct sql_prepared_statement *prep_stmt;
+ char *query;
+
+ iter = hash_table_iterate_init(db->prepared_stmt_hash);
+ while (hash_table_iterate(iter, db->prepared_stmt_hash, &query, &prep_stmt)) {
+ i_assert(prep_stmt->refcount == 0);
+ if (prep_stmt->db->v.prepared_statement_deinit != NULL)
+ prep_stmt->db->v.prepared_statement_deinit(prep_stmt);
+ else
+ default_sql_prepared_statement_deinit(prep_stmt);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_clear(db->prepared_stmt_hash, TRUE);
+}
+
+void sql_unref(struct sql_db **_db)
+{
+ struct sql_db *db = *_db;
+
+ *_db = NULL;
+
+ i_assert(db->refcount > 0);
+ if (db->v.unref != NULL)
+ db->v.unref(db);
+ if (--db->refcount > 0)
+ return;
+
+ timeout_remove(&db->to_reconnect);
+ sql_prepared_statements_free(db);
+ hash_table_destroy(&db->prepared_stmt_hash);
+ db->v.deinit(db);
+}
+
+enum sql_db_flags sql_get_flags(struct sql_db *db)
+{
+ if (db->v.get_flags != NULL)
+ return db->v.get_flags(db);
+ else
+ return db->flags;
+}
+
+int sql_connect(struct sql_db *db)
+{
+ time_t now;
+
+ switch (db->state) {
+ case SQL_DB_STATE_DISCONNECTED:
+ break;
+ case SQL_DB_STATE_CONNECTING:
+ return 0;
+ default:
+ return 1;
+ }
+
+ /* don't try reconnecting more than once a second */
+ now = time(NULL);
+ if (db->last_connect_try + (time_t)db->connect_delay > now)
+ return -1;
+ db->last_connect_try = now;
+
+ return db->v.connect(db);
+}
+
+void sql_disconnect(struct sql_db *db)
+{
+ timeout_remove(&db->to_reconnect);
+ db->v.disconnect(db);
+}
+
+const char *sql_escape_string(struct sql_db *db, const char *string)
+{
+ return db->v.escape_string(db, string);
+}
+
+const char *sql_escape_blob(struct sql_db *db,
+ const unsigned char *data, size_t size)
+{
+ return db->v.escape_blob(db, data, size);
+}
+
+void sql_exec(struct sql_db *db, const char *query)
+{
+ db->v.exec(db, query);
+}
+
+#undef sql_query
+void sql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ db->v.query(db, query, callback, context);
+}
+
+struct sql_result *sql_query_s(struct sql_db *db, const char *query)
+{
+ return db->v.query_s(db, query);
+}
+
+static struct sql_prepared_statement *
+default_sql_prepared_statement_init(struct sql_db *db,
+ const char *query_template)
+{
+ struct sql_prepared_statement *prep_stmt;
+
+ prep_stmt = i_new(struct sql_prepared_statement, 1);
+ prep_stmt->db = db;
+ prep_stmt->refcount = 1;
+ prep_stmt->query_template = i_strdup(query_template);
+ return prep_stmt;
+}
+
+static struct sql_statement *
+default_sql_statement_init_prepared(struct sql_prepared_statement *stmt)
+{
+ return sql_statement_init(stmt->db, stmt->query_template);
+}
+
+const char *sql_statement_get_log_query(struct sql_statement *stmt)
+{
+ if (stmt->no_log_expanded_values)
+ return stmt->query_template;
+ return sql_statement_get_query(stmt);
+}
+
+const char *sql_statement_get_query(struct sql_statement *stmt)
+{
+ string_t *query = t_str_new(128);
+ const char *const *args;
+ unsigned int i, args_count, arg_pos = 0;
+
+ args = array_get(&stmt->args, &args_count);
+
+ for (i = 0; stmt->query_template[i] != '\0'; i++) {
+ if (stmt->query_template[i] == '?') {
+ if (arg_pos >= args_count ||
+ args[arg_pos] == NULL) {
+ i_panic("lib-sql: Missing bind for arg #%u in statement: %s",
+ arg_pos, stmt->query_template);
+ }
+ str_append(query, args[arg_pos++]);
+ } else {
+ str_append_c(query, stmt->query_template[i]);
+ }
+ }
+ if (arg_pos != args_count) {
+ i_panic("lib-sql: Too many bind args (%u) for statement: %s",
+ args_count, stmt->query_template);
+ }
+ return str_c(query);
+}
+
+static void
+default_sql_statement_query(struct sql_statement *stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ sql_query(stmt->db, sql_statement_get_query(stmt),
+ callback, context);
+ pool_unref(&stmt->pool);
+}
+
+static struct sql_result *
+default_sql_statement_query_s(struct sql_statement *stmt)
+{
+ struct sql_result *result =
+ sql_query_s(stmt->db, sql_statement_get_query(stmt));
+ pool_unref(&stmt->pool);
+ return result;
+}
+
+static void default_sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement *stmt,
+ unsigned int *affected_rows)
+{
+ ctx->db->v.update(ctx, sql_statement_get_query(stmt),
+ affected_rows);
+ pool_unref(&stmt->pool);
+}
+
+struct sql_prepared_statement *
+sql_prepared_statement_init(struct sql_db *db, const char *query_template)
+{
+ struct sql_prepared_statement *stmt;
+
+ stmt = hash_table_lookup(db->prepared_stmt_hash, query_template);
+ if (stmt != NULL) {
+ stmt->refcount++;
+ return stmt;
+ }
+
+ if (db->v.prepared_statement_init != NULL)
+ stmt = db->v.prepared_statement_init(db, query_template);
+ else
+ stmt = default_sql_prepared_statement_init(db, query_template);
+
+ hash_table_insert(db->prepared_stmt_hash, stmt->query_template, stmt);
+ return stmt;
+}
+
+void sql_prepared_statement_unref(struct sql_prepared_statement **_prep_stmt)
+{
+ struct sql_prepared_statement *prep_stmt = *_prep_stmt;
+
+ *_prep_stmt = NULL;
+
+ i_assert(prep_stmt->refcount > 0);
+ prep_stmt->refcount--;
+}
+
+static void
+sql_statement_init_fields(struct sql_statement *stmt, struct sql_db *db)
+{
+ stmt->db = db;
+ p_array_init(&stmt->args, stmt->pool, 8);
+}
+
+struct sql_statement *
+sql_statement_init(struct sql_db *db, const char *query_template)
+{
+ struct sql_statement *stmt;
+
+ if (db->v.statement_init != NULL)
+ stmt = db->v.statement_init(db, query_template);
+ else {
+ pool_t pool = pool_alloconly_create("sql statement", 1024);
+ stmt = p_new(pool, struct sql_statement, 1);
+ stmt->pool = pool;
+ }
+ stmt->query_template = p_strdup(stmt->pool, query_template);
+ sql_statement_init_fields(stmt, db);
+ return stmt;
+}
+
+struct sql_statement *
+sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt)
+{
+ struct sql_statement *stmt;
+
+ if (prep_stmt->db->v.statement_init_prepared == NULL)
+ return default_sql_statement_init_prepared(prep_stmt);
+
+ stmt = prep_stmt->db->v.statement_init_prepared(prep_stmt);
+ sql_statement_init_fields(stmt, prep_stmt->db);
+ return stmt;
+}
+
+void sql_statement_abort(struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_abort != NULL)
+ stmt->db->v.statement_abort(stmt);
+ pool_unref(&stmt->pool);
+}
+
+void sql_statement_set_timestamp(struct sql_statement *stmt,
+ const struct timespec *ts)
+{
+ if (stmt->db->v.statement_set_timestamp != NULL)
+ stmt->db->v.statement_set_timestamp(stmt, ts);
+}
+
+void sql_statement_set_no_log_expanded_values(struct sql_statement *stmt,
+ bool no_expand)
+{
+ stmt->no_log_expanded_values = no_expand;
+}
+
+void sql_statement_bind_str(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value)
+{
+ const char *escaped_value =
+ p_strdup_printf(stmt->pool, "'%s'",
+ sql_escape_string(stmt->db, value));
+ array_idx_set(&stmt->args, column_idx, &escaped_value);
+
+ if (stmt->db->v.statement_bind_str != NULL)
+ stmt->db->v.statement_bind_str(stmt, column_idx, value);
+}
+
+void sql_statement_bind_binary(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size)
+{
+ const char *value_str =
+ p_strdup_printf(stmt->pool, "%s",
+ sql_escape_blob(stmt->db, value, value_size));
+ array_idx_set(&stmt->args, column_idx, &value_str);
+
+ if (stmt->db->v.statement_bind_binary != NULL) {
+ stmt->db->v.statement_bind_binary(stmt, column_idx,
+ value, value_size);
+ }
+}
+
+void sql_statement_bind_int64(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value)
+{
+ const char *value_str = p_strdup_printf(stmt->pool, "%"PRId64, value);
+ array_idx_set(&stmt->args, column_idx, &value_str);
+
+ if (stmt->db->v.statement_bind_int64 != NULL)
+ stmt->db->v.statement_bind_int64(stmt, column_idx, value);
+}
+
+#undef sql_statement_query
+void sql_statement_query(struct sql_statement **_stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_query != NULL)
+ stmt->db->v.statement_query(stmt, callback, context);
+ else
+ default_sql_statement_query(stmt, callback, context);
+}
+
+struct sql_result *sql_statement_query_s(struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_query_s != NULL)
+ return stmt->db->v.statement_query_s(stmt);
+ else
+ return default_sql_statement_query_s(stmt);
+}
+
+void sql_result_ref(struct sql_result *result)
+{
+ result->refcount++;
+}
+
+void sql_result_unref(struct sql_result *result)
+{
+ i_assert(result->refcount > 0);
+ if (--result->refcount > 0)
+ return;
+
+ i_free(result->map);
+ result->v.free(result);
+}
+
+static const struct sql_field_def *
+sql_field_def_find(const struct sql_field_def *fields, const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; fields[i].name != NULL; i++) {
+ if (strcasecmp(fields[i].name, name) == 0)
+ return &fields[i];
+ }
+ return NULL;
+}
+
+static void
+sql_result_build_map(struct sql_result *result,
+ const struct sql_field_def *fields, size_t dest_size)
+{
+ const struct sql_field_def *def;
+ const char *name;
+ unsigned int i, count, field_size = 0;
+
+ count = sql_result_get_fields_count(result);
+
+ result->map_size = count;
+ result->map = i_new(struct sql_field_map, result->map_size);
+ for (i = 0; i < count; i++) {
+ name = sql_result_get_field_name(result, i);
+ def = sql_field_def_find(fields, name);
+ if (def != NULL) {
+ result->map[i].type = def->type;
+ result->map[i].offset = def->offset;
+ switch (def->type) {
+ case SQL_TYPE_STR:
+ field_size = sizeof(const char *);
+ break;
+ case SQL_TYPE_UINT:
+ field_size = sizeof(unsigned int);
+ break;
+ case SQL_TYPE_ULLONG:
+ field_size = sizeof(unsigned long long);
+ break;
+ case SQL_TYPE_BOOL:
+ field_size = sizeof(bool);
+ break;
+ }
+ i_assert(def->offset + field_size <= dest_size);
+ } else {
+ result->map[i].offset = SIZE_MAX;
+ }
+ }
+}
+
+void sql_result_setup_fetch(struct sql_result *result,
+ const struct sql_field_def *fields,
+ void *dest, size_t dest_size)
+{
+ if (result->map == NULL)
+ sql_result_build_map(result, fields, dest_size);
+ result->fetch_dest = dest;
+ result->fetch_dest_size = dest_size;
+}
+
+static void sql_result_fetch(struct sql_result *result)
+{
+ unsigned int i, count;
+ const char *value;
+ void *ptr;
+
+ memset(result->fetch_dest, 0, result->fetch_dest_size);
+ count = result->map_size;
+ for (i = 0; i < count; i++) {
+ if (result->map[i].offset == SIZE_MAX)
+ continue;
+
+ value = sql_result_get_field_value(result, i);
+ ptr = STRUCT_MEMBER_P(result->fetch_dest,
+ result->map[i].offset);
+
+ switch (result->map[i].type) {
+ case SQL_TYPE_STR: {
+ *((const char **)ptr) = value;
+ break;
+ }
+ case SQL_TYPE_UINT: {
+ if (value != NULL &&
+ str_to_uint(value, (unsigned int *)ptr) < 0)
+ i_error("sql: Value not uint: %s", value);
+ break;
+ }
+ case SQL_TYPE_ULLONG: {
+ if (value != NULL &&
+ str_to_ullong(value, (unsigned long long *)ptr) < 0)
+ i_error("sql: Value not ullong: %s", value);
+ break;
+ }
+ case SQL_TYPE_BOOL: {
+ if (value != NULL && (*value == 't' || *value == '1'))
+ *((bool *)ptr) = TRUE;
+ break;
+ }
+ }
+ }
+}
+
+int sql_result_next_row(struct sql_result *result)
+{
+ int ret;
+
+ if ((ret = result->v.next_row(result)) <= 0)
+ return ret;
+
+ if (result->fetch_dest != NULL)
+ sql_result_fetch(result);
+ return 1;
+}
+
+#undef sql_result_more
+void sql_result_more(struct sql_result **result,
+ sql_query_callback_t *callback, void *context)
+{
+ i_assert((*result)->v.more != NULL);
+
+ (*result)->v.more(result, TRUE, callback, context);
+}
+
+static void
+sql_result_more_sync_callback(struct sql_result *result, void *context)
+{
+ struct sql_result **dest_result = context;
+
+ *dest_result = result;
+}
+
+void sql_result_more_s(struct sql_result **result)
+{
+ i_assert((*result)->v.more != NULL);
+
+ (*result)->v.more(result, FALSE, sql_result_more_sync_callback, result);
+ /* the callback must have been called */
+ i_assert(*result != NULL);
+}
+
+unsigned int sql_result_get_fields_count(struct sql_result *result)
+{
+ return result->v.get_fields_count(result);
+}
+
+const char *sql_result_get_field_name(struct sql_result *result,
+ unsigned int idx)
+{
+ return result->v.get_field_name(result, idx);
+}
+
+int sql_result_find_field(struct sql_result *result, const char *field_name)
+{
+ return result->v.find_field(result, field_name);
+}
+
+const char *sql_result_get_field_value(struct sql_result *result,
+ unsigned int idx)
+{
+ return result->v.get_field_value(result, idx);
+}
+
+const unsigned char *
+sql_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r)
+{
+ return result->v.get_field_value_binary(result, idx, size_r);
+}
+
+const char *sql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ return result->v.find_field_value(result, field_name);
+}
+
+const char *const *sql_result_get_values(struct sql_result *result)
+{
+ return result->v.get_values(result);
+}
+
+const char *sql_result_get_error(struct sql_result *result)
+{
+ return result->v.get_error(result);
+}
+
+enum sql_result_error_type sql_result_get_error_type(struct sql_result *result)
+{
+ return result->error_type;
+}
+
+static void
+sql_result_not_connected_free(struct sql_result *result ATTR_UNUSED)
+{
+}
+
+static int
+sql_result_not_connected_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+static const char *
+sql_result_not_connected_get_error(struct sql_result *result ATTR_UNUSED)
+{
+ return SQL_ERRSTR_NOT_CONNECTED;
+}
+
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db)
+{
+ return db->v.transaction_begin(db);
+}
+
+#undef sql_transaction_commit
+void sql_transaction_commit(struct sql_transaction_context **_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ ctx->db->v.transaction_commit(ctx, callback, context);
+}
+
+int sql_transaction_commit_s(struct sql_transaction_context **_ctx,
+ const char **error_r)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ return ctx->db->v.transaction_commit_s(ctx, error_r);
+}
+
+void sql_transaction_rollback(struct sql_transaction_context **_ctx)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ ctx->db->v.transaction_rollback(ctx);
+}
+
+void sql_update(struct sql_transaction_context *ctx, const char *query)
+{
+ ctx->db->v.update(ctx, query, NULL);
+}
+
+void sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (ctx->db->v.update_stmt != NULL)
+ ctx->db->v.update_stmt(ctx, stmt, NULL);
+ else
+ default_sql_update_stmt(ctx, stmt, NULL);
+}
+
+void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ ctx->db->v.update(ctx, query, affected_rows);
+}
+
+void sql_update_stmt_get_rows(struct sql_transaction_context *ctx,
+ struct sql_statement **_stmt,
+ unsigned int *affected_rows)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (ctx->db->v.update_stmt != NULL)
+ ctx->db->v.update_stmt(ctx, stmt, affected_rows);
+ else
+ default_sql_update_stmt(ctx, stmt, affected_rows);
+}
+
+void sql_db_set_state(struct sql_db *db, enum sql_db_state state)
+{
+ enum sql_db_state old_state = db->state;
+
+ if (db->state == state)
+ return;
+
+ db->state = state;
+ if (db->state_change_callback != NULL) {
+ db->state_change_callback(db, old_state,
+ db->state_change_context);
+ }
+}
+
+void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool,
+ const char *query, unsigned int *affected_rows)
+{
+ struct sql_transaction_query *tquery;
+
+ tquery = p_new(pool, struct sql_transaction_query, 1);
+ tquery->trans = ctx;
+ tquery->query = p_strdup(pool, query);
+ tquery->affected_rows = affected_rows;
+
+ if (ctx->head == NULL)
+ ctx->head = tquery;
+ else
+ ctx->tail->next = tquery;
+ ctx->tail = tquery;
+}
+
+void sql_connection_log_finished(struct sql_db *db)
+{
+ struct event_passthrough *e = event_create_passthrough(db->event)->
+ set_name(SQL_CONNECTION_FINISHED);
+ e_debug(e->event(),
+ "Connection finished (queries=%"PRIu64", slow queries=%"PRIu64")",
+ db->succeeded_queries + db->failed_queries,
+ db->slow_queries);
+}
+
+struct event_passthrough *
+sql_query_finished_event(struct sql_db *db, struct event *event, const char *query,
+ bool success, int *duration_r)
+{
+ int diff;
+ struct timeval tv;
+ event_get_create_time(event, &tv);
+ struct event_passthrough *e = event_create_passthrough(event)->
+ set_name(SQL_QUERY_FINISHED)->
+ add_str("query_first_word", t_strcut(query, ' '));
+ diff = timeval_diff_msecs(&ioloop_timeval, &tv);
+
+ if (!success) {
+ db->failed_queries++;
+ } else {
+ db->succeeded_queries++;
+ }
+
+ if (diff >= SQL_SLOW_QUERY_MSEC) {
+ e->add_str("slow_query", "y");
+ db->slow_queries++;
+ }
+
+ if (duration_r != NULL)
+ *duration_r = diff;
+
+ return e;
+}
+
+struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx)
+{
+ return event_create_passthrough(ctx->event)->
+ set_name(SQL_TRANSACTION_FINISHED);
+}
+
+void sql_wait(struct sql_db *db)
+{
+ if (db->v.wait != NULL)
+ db->v.wait(db);
+}
+
+
+struct sql_result sql_not_connected_result = {
+ .v = {
+ sql_result_not_connected_free,
+ sql_result_not_connected_next_row,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ sql_result_not_connected_get_error,
+ NULL,
+ },
+ .failed_try_retry = TRUE
+};
diff --git a/src/lib-sql/sql-api.h b/src/lib-sql/sql-api.h
new file mode 100644
index 0000000..669a851
--- /dev/null
+++ b/src/lib-sql/sql-api.h
@@ -0,0 +1,251 @@
+#ifndef SQL_API_H
+#define SQL_API_H
+
+struct timespec;
+
+/* This SQL API is designed to work asynchronously. The underlying drivers
+ however may not. */
+
+enum sql_db_flags {
+ /* Set if queries are not executed asynchronously */
+ SQL_DB_FLAG_BLOCKING = 0x01,
+ /* Set if database wants to use connection pooling */
+ SQL_DB_FLAG_POOLED = 0x02,
+ /* Prepared statements are supported by the database. If they aren't,
+ the functions can still be used, but they're just internally
+ convered into regular statements. */
+ SQL_DB_FLAG_PREP_STATEMENTS = 0x04,
+ /* Database supports INSERT .. ON DUPLICATE KEY syntax. */
+ SQL_DB_FLAG_ON_DUPLICATE_KEY = 0x08,
+ /* Database supports INSERT .. ON CONFLICT DO UPDATE syntax. */
+ SQL_DB_FLAG_ON_CONFLICT_DO = 0x10,
+};
+
+enum sql_field_type {
+ SQL_TYPE_STR,
+ SQL_TYPE_UINT,
+ SQL_TYPE_ULLONG,
+ SQL_TYPE_BOOL
+};
+
+struct sql_field_def {
+ enum sql_field_type type;
+ const char *name;
+ size_t offset;
+};
+
+enum sql_result_error_type {
+ SQL_RESULT_ERROR_TYPE_UNKNOWN = 0,
+ /* It's unknown whether write succeeded or not. This could be due to
+ a timeout or a disconnection from server. */
+ SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN
+};
+
+enum sql_result_next {
+ /* Row was returned */
+ SQL_RESULT_NEXT_OK = 1,
+ /* There are no more rows */
+ SQL_RESULT_NEXT_LAST = 0,
+ /* Error occurred - see sql_result_get_error*() */
+ SQL_RESULT_NEXT_ERROR = -1,
+ /* There are more results - call sql_result_more() */
+ SQL_RESULT_NEXT_MORE = -99
+};
+
+#define SQL_DEF_STRUCT(name, struct_name, type, c_type) \
+ { (type) + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((struct struct_name *)0)->name, c_type), \
+ #name, offsetof(struct struct_name, name) }
+
+#define SQL_DEF_STRUCT_STR(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_STR, const char *)
+#define SQL_DEF_STRUCT_UINT(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_UINT, unsigned int)
+#define SQL_DEF_STRUCT_ULLONG(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_ULLONG, unsigned long long)
+#define SQL_DEF_STRUCT_BOOL(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_BOOL, bool)
+
+struct sql_db;
+struct sql_result;
+
+struct sql_commit_result {
+ const char *error;
+ enum sql_result_error_type error_type;
+};
+
+struct sql_settings {
+ const char *driver;
+ const char *connect_string;
+ struct event *event_parent;
+};
+
+typedef void sql_query_callback_t(struct sql_result *result, void *context);
+typedef void sql_commit_callback_t(const struct sql_commit_result *result, void *context);
+
+void sql_drivers_init(void);
+void sql_drivers_deinit(void);
+
+/* register all built-in SQL drivers */
+void sql_drivers_register_all(void);
+
+void sql_driver_register(const struct sql_db *driver);
+void sql_driver_unregister(const struct sql_db *driver);
+
+/* Initialize database connections. db_driver is the database driver name,
+ eg. "mysql" or "pgsql". connect_string is driver-specific. */
+struct sql_db *sql_init(const char *db_driver, const char *connect_string);
+int sql_init_full(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r);
+
+void sql_ref(struct sql_db *db);
+void sql_unref(struct sql_db **db);
+
+/* Returns SQL database state flags. */
+enum sql_db_flags sql_get_flags(struct sql_db *db);
+
+/* Explicitly connect to the database. It's not required to call this function
+ though. Returns -1 if we're not connected, 0 if we started connecting or
+ 1 if we are fully connected now. */
+int sql_connect(struct sql_db *db);
+/* Explicitly disconnect from database and abort pending auth requests. */
+void sql_disconnect(struct sql_db *db);
+
+/* Escape the given string if needed and return it. */
+const char *sql_escape_string(struct sql_db *db, const char *string);
+/* Escape the given data as a string. */
+const char *sql_escape_blob(struct sql_db *db,
+ const unsigned char *data, size_t size);
+
+/* Execute SQL query without waiting for results. */
+void sql_exec(struct sql_db *db, const char *query);
+/* Execute SQL query and return result in callback. If fields list is given,
+ the returned fields are validated to be of correct type, and you can use
+ sql_result_next_row_get() */
+void sql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context);
+#define sql_query(db, query, callback, context) \
+ sql_query(db, query - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))), \
+ (sql_query_callback_t *)callback, context)
+/* Execute blocking SQL query and return result. */
+struct sql_result *sql_query_s(struct sql_db *db, const char *query);
+
+struct sql_prepared_statement *
+sql_prepared_statement_init(struct sql_db *db, const char *query_template);
+void sql_prepared_statement_unref(struct sql_prepared_statement **prep_stmt);
+
+struct sql_statement *
+sql_statement_init(struct sql_db *db, const char *query_template);
+struct sql_statement *
+sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt);
+void sql_statement_abort(struct sql_statement **stmt);
+void sql_statement_set_timestamp(struct sql_statement *stmt,
+ const struct timespec *ts);
+void sql_statement_set_no_log_expanded_values(struct sql_statement *stmt,
+ bool no_expand);
+void sql_statement_bind_str(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value);
+void sql_statement_bind_binary(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size);
+void sql_statement_bind_int64(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value);
+void sql_statement_query(struct sql_statement **stmt,
+ sql_query_callback_t *callback, void *context);
+#define sql_statement_query(stmt, callback, context) \
+ sql_statement_query(stmt, \
+ (sql_query_callback_t *)callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))))
+struct sql_result *sql_statement_query_s(struct sql_statement **stmt);
+
+void sql_result_setup_fetch(struct sql_result *result,
+ const struct sql_field_def *fields,
+ void *dest, size_t dest_size);
+
+/* Go to next row. See enum sql_result_next. */
+int sql_result_next_row(struct sql_result *result);
+
+/* If sql_result_next_row() returned SQL_RESULT_NEXT_MORE, this can be called
+ to continue returning more results. The result is freed with this call, so
+ it must not be accesed anymore until the callback is finished. */
+void sql_result_more(struct sql_result **result,
+ sql_query_callback_t *callback, void *context);
+#define sql_result_more(result, callback, context) \
+ sql_result_more(result - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))), \
+ (sql_query_callback_t *)callback, context)
+/* Synchronous version of sql_result_more(). The result will be replaced with
+ the new result. */
+void sql_result_more_s(struct sql_result **result);
+
+void sql_result_ref(struct sql_result *result);
+/* Needs to be called only with sql_query_s() or when result has been
+ explicitly referenced. */
+void sql_result_unref(struct sql_result *result);
+
+/* Return number of fields in result. */
+unsigned int sql_result_get_fields_count(struct sql_result *result);
+/* Return name of the given field index. */
+const char *sql_result_get_field_name(struct sql_result *result,
+ unsigned int idx);
+/* Return field index for given name, or -1 if not found. */
+int sql_result_find_field(struct sql_result *result, const char *field_name);
+
+/* Returns value of given field as string. Note that it can be NULL. */
+const char *sql_result_get_field_value(struct sql_result *result,
+ unsigned int idx);
+/* Returns a binary value. Note that a NULL is returned as NULL with size=0,
+ while empty string returns non-NULL with size=0. */
+const unsigned char *
+sql_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r);
+/* Find the field and return its value. NULL return value can mean that either
+ the field didn't exist or that its value is NULL. */
+const char *sql_result_find_field_value(struct sql_result *result,
+ const char *field_name);
+/* Return all values of current row. Note that this array is not
+ NULL-terminated - you must use sql_result_get_fields_count() to find out
+ the array's length. It's also possible that some of the values inside the
+ array are NULL. */
+const char *const *sql_result_get_values(struct sql_result *result);
+
+/* Return last error message in result. */
+const char *sql_result_get_error(struct sql_result *result);
+enum sql_result_error_type sql_result_get_error_type(struct sql_result *result);
+
+/* Begin a new transaction. Currently you're limited to only one open
+ transaction at a time. */
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db);
+/* Commit transaction. */
+void sql_transaction_commit(struct sql_transaction_context **ctx,
+ sql_commit_callback_t *callback, void *context);
+#define sql_transaction_commit(ctx, callback, context) \
+ sql_transaction_commit(ctx - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct sql_commit_result *, typeof(context))), \
+ (sql_commit_callback_t *)callback, context)
+/* Synchronous commit. Returns 0 if ok, -1 if error. */
+int sql_transaction_commit_s(struct sql_transaction_context **ctx,
+ const char **error_r);
+void sql_transaction_rollback(struct sql_transaction_context **ctx);
+
+/* Execute query in given transaction. */
+void sql_update(struct sql_transaction_context *ctx, const char *query);
+void sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement **stmt);
+/* Save the number of rows updated by this query. The value is set before
+ commit callback is called. */
+void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+void sql_update_stmt_get_rows(struct sql_transaction_context *ctx,
+ struct sql_statement **stmt,
+ unsigned int *affected_rows);
+
+/* Wait for SQL query results. */
+void sql_wait(struct sql_db *db);
+
+#endif
diff --git a/src/lib-sql/sql-db-cache.c b/src/lib-sql/sql-db-cache.c
new file mode 100644
index 0000000..b2fb9fb
--- /dev/null
+++ b/src/lib-sql/sql-db-cache.c
@@ -0,0 +1,156 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "sql-api-private.h"
+#include "sql-db-cache.h"
+
+#define SQL_DB_CACHE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, sql_db_cache_module)
+
+struct sql_db_cache_context {
+ union sql_db_module_context module_ctx;
+ struct sql_db *prev, *next; /* These are set while refcount=0 */
+
+ struct sql_db_cache *cache;
+ int refcount;
+ char *key;
+ void (*orig_deinit)(struct sql_db *db);
+};
+
+struct sql_db_cache {
+ HASH_TABLE(char *, struct sql_db *) dbs;
+ unsigned int unused_count, max_unused_connections;
+ struct sql_db *unused_tail, *unused_head;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(sql_db_cache_module, &sql_db_module_register);
+
+static void sql_db_cache_db_unref(struct sql_db *db)
+{
+ struct sql_db_cache_context *ctx = SQL_DB_CACHE_CONTEXT(db);
+ struct sql_db_cache_context *head_ctx;
+
+ if (--ctx->refcount > 0)
+ return;
+
+ i_assert(db->refcount == 2);
+
+ ctx->cache->unused_count++;
+ if (ctx->cache->unused_tail == NULL)
+ ctx->cache->unused_tail = db;
+ else {
+ head_ctx = SQL_DB_CACHE_CONTEXT(ctx->cache->unused_head);
+ head_ctx->next = db;
+ }
+ ctx->prev = ctx->cache->unused_head;
+ ctx->cache->unused_head = db;
+}
+
+static void sql_db_cache_unlink(struct sql_db_cache_context *ctx)
+{
+ struct sql_db_cache_context *prev_ctx, *next_ctx;
+
+ i_assert(ctx->refcount == 0);
+
+ if (ctx->prev == NULL)
+ ctx->cache->unused_tail = ctx->next;
+ else {
+ prev_ctx = SQL_DB_CACHE_CONTEXT(ctx->prev);
+ prev_ctx->next = ctx->next;
+ }
+ if (ctx->next == NULL)
+ ctx->cache->unused_head = ctx->prev;
+ else {
+ next_ctx = SQL_DB_CACHE_CONTEXT(ctx->next);
+ next_ctx->prev = ctx->prev;
+ }
+ ctx->cache->unused_count--;
+}
+
+static void sql_db_cache_free_tail(struct sql_db_cache *cache)
+{
+ struct sql_db *db;
+ struct sql_db_cache_context *ctx;
+
+ db = cache->unused_tail;
+ i_assert(db->refcount == 1);
+
+ ctx = SQL_DB_CACHE_CONTEXT(db);
+ sql_db_cache_unlink(ctx);
+ hash_table_remove(cache->dbs, ctx->key);
+
+ i_free(ctx->key);
+ i_free(ctx);
+
+ db->v.unref = NULL;
+ sql_unref(&db);
+}
+
+static void sql_db_cache_drop_oldest(struct sql_db_cache *cache)
+{
+ while (cache->unused_count >= cache->max_unused_connections)
+ sql_db_cache_free_tail(cache);
+}
+
+int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct sql_db_cache_context *ctx;
+ struct sql_db *db;
+ char *key;
+
+ key = i_strdup_printf("%s\t%s", set->driver, set->connect_string);
+ db = hash_table_lookup(cache->dbs, key);
+ if (db != NULL) {
+ ctx = SQL_DB_CACHE_CONTEXT(db);
+ if (ctx->refcount == 0) {
+ sql_db_cache_unlink(ctx);
+ ctx->prev = ctx->next = NULL;
+ }
+ i_free(key);
+ } else {
+ sql_db_cache_drop_oldest(cache);
+
+ if (sql_init_full(set, &db, error_r) < 0) {
+ i_free(key);
+ return -1;
+ }
+
+ ctx = i_new(struct sql_db_cache_context, 1);
+ ctx->cache = cache;
+ ctx->key = key;
+ ctx->orig_deinit = db->v.deinit;
+ db->v.unref = sql_db_cache_db_unref;
+
+ MODULE_CONTEXT_SET(db, sql_db_cache_module, ctx);
+ hash_table_insert(cache->dbs, ctx->key, db);
+ }
+
+ ctx->refcount++;
+ sql_ref(db);
+ *db_r = db;
+ return 0;
+}
+
+struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections)
+{
+ struct sql_db_cache *cache;
+
+ cache = i_new(struct sql_db_cache, 1);
+ hash_table_create(&cache->dbs, default_pool, 0, str_hash, strcmp);
+ cache->max_unused_connections = max_unused_connections;
+ return cache;
+}
+
+void sql_db_cache_deinit(struct sql_db_cache **_cache)
+{
+ struct sql_db_cache *cache = *_cache;
+
+ *_cache = NULL;
+ while (cache->unused_tail != NULL)
+ sql_db_cache_free_tail(cache);
+ hash_table_destroy(&cache->dbs);
+ i_free(cache);
+}
diff --git a/src/lib-sql/sql-db-cache.h b/src/lib-sql/sql-db-cache.h
new file mode 100644
index 0000000..1517f5f
--- /dev/null
+++ b/src/lib-sql/sql-db-cache.h
@@ -0,0 +1,13 @@
+#ifndef SQL_DB_CACHE_H
+#define SQL_DB_CACHE_H
+
+struct sql_db_cache;
+
+/* Like sql_init(), but use a connection pool. */
+int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r);
+
+struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections);
+void sql_db_cache_deinit(struct sql_db_cache **cache);
+
+#endif