/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

#include "dbconn.h"
#include "zbxcommon.h"
#include "zbxdb.h"
#include "zbxcfg.h"
#include "zbxmutexs.h"
#include "zbxshmem.h"
#include "zbxalgo.h"
#include "zbxdbschema.h"
#include "zbxnum.h"
#include "zbxstr.h"
#include "zbxtypes.h"
#if defined(HAVE_POSTGRESQL)
#	include "zbx_dbversion_constants.h"
#endif

#define ZBX_MAX_SQL_SIZE	262144	/* 256KB */
#ifndef ZBX_MAX_OVERFLOW_SQL_SIZE
#	define ZBX_MAX_OVERFLOW_SQL_SIZE	ZBX_MAX_SQL_SIZE
#elif 0 != ZBX_MAX_OVERFLOW_SQL_SIZE && \
	(1024 > ZBX_MAX_OVERFLOW_SQL_SIZE || ZBX_MAX_OVERFLOW_SQL_SIZE > ZBX_MAX_SQL_SIZE)
#error ZBX_MAX_OVERFLOW_SQL_SIZE is out of range
#endif

ZBX_CONST_PTR_VECTOR_IMPL(const_db_field_ptr, const zbx_db_field_t *)
ZBX_PTR_VECTOR_IMPL(db_value_ptr, zbx_db_value_t *)

const char	*idcache_tables[] = {"events", "event_tag", "problem_tag", "dservices", "dhosts", "alerts",
					"escalations", "autoreg_host", "event_suppress", "trigger_queue",
					"proxy_history", "proxy_dhistory", "proxy_autoreg_host", "host_proxy"};

#define ZBX_IDS_SIZE	ARRSIZE(idcache_tables)

static int	compare_table_names(const void *d1, const void *d2)
{
	const char *n1 = *(const char * const *)d1;
	const char *n2 = *(const char * const *)d2;

	return strcmp(n1, n2);
}

typedef struct
{
	zbx_uint64_t	lastids[ZBX_IDS_SIZE];
}
zbx_db_idcache_t;

/* nextid cache for tables updated only by server/proxy */
static zbx_mutex_t	idcache_mutex = ZBX_MUTEX_NULL;
zbx_shmem_info_t	*idcache_mem;
static zbx_db_idcache_t	*idcache = NULL;

int	zbx_db_init(char **error)
{
	qsort(idcache_tables, ZBX_IDS_SIZE, sizeof(idcache_tables[0]), compare_table_names);

	if (SUCCEED != dbconn_init(error))
		return FAIL;

	if (SUCCEED != zbx_mutex_create(&idcache_mutex, ZBX_MUTEX_CACHE_IDS, error))
		return FAIL;

	if (SUCCEED != zbx_shmem_create_min(&idcache_mem, sizeof(zbx_db_idcache_t), "table ids cache", NULL, 0, error))
		return FAIL;

	idcache = idcache_mem->base;
	memset(idcache, 0, sizeof(zbx_db_idcache_t));

	return SUCCEED;
}

void	zbx_db_deinit(void)
{
	dbconn_deinit();

	zbx_shmem_destroy(idcache_mem);
	idcache_mem = NULL;

	zbx_mutex_destroy(&idcache_mutex);
}

/******************************************************************************
 *                                                                            *
 * Purpose: get next id for requested table from cache                        *
 *                                                                            *
 ******************************************************************************/
static zbx_uint64_t	dbconn_get_cached_nextid(zbx_dbconn_t *db, size_t index, zbx_uint64_t num)
{
	zbx_uint64_t	nextid, lastid;
	const char	*table_name = idcache_tables[index];

	zabbix_log(LOG_LEVEL_DEBUG, "In %s() table:'%s' num:" ZBX_FS_UI64, __func__, table_name, num);

	zbx_mutex_lock(idcache_mutex);

	if (0 == idcache->lastids[index])
	{
		zbx_db_result_t		result;
		zbx_db_row_t		row;
		const zbx_db_table_t	*table;
		zbx_uint64_t		min = 0, max = ZBX_DB_MAX_ID;

		if (NULL == (table = zbx_db_get_table(table_name)))
		{
			zbx_mutex_unlock(idcache_mutex);
			THIS_SHOULD_NEVER_HAPPEN_MSG("unknown table: %s", table_name);
			exit(EXIT_FAILURE);
		}

		result = zbx_dbconn_select(db, "select max(%s) from %s where %s between " ZBX_FS_UI64 " and "
				ZBX_FS_UI64, table->recid, table_name, table->recid, min, max);

		if (NULL == result)
		{
			lastid = 0;
			nextid = 0;
			goto out;
		}

		if (NULL == (row = zbx_db_fetch(result)) || SUCCEED == zbx_db_is_null(row[0]))
			idcache->lastids[index] = min;
		else
			ZBX_STR2UINT64(idcache->lastids[index], row[0]);

		zbx_db_free_result(result);
	}

	nextid = idcache->lastids[index] + 1;
	idcache->lastids[index] += num;
	lastid = idcache->lastids[index];
out:
	zbx_mutex_unlock(idcache_mutex);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s() table:'%s' [" ZBX_FS_UI64 ":" ZBX_FS_UI64 "]",
			__func__, table_name, nextid, lastid);

	return nextid;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get next id for requested table from database                     *
 *                                                                            *
 ******************************************************************************/
static zbx_uint64_t	dbconn_get_nextid(zbx_dbconn_t *db, const char *tablename, zbx_uint64_t num)
{
	zbx_db_result_t		result;
	zbx_db_row_t		row;
	zbx_uint64_t		ret1, ret2;
	zbx_uint64_t		min = 0, max = ZBX_DB_MAX_ID;
	int			found = FAIL, dbres;
	const zbx_db_table_t	*table;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s() tablename:'%s'", __func__, tablename);

	if (NULL == (table = zbx_db_get_table(tablename)))
	{
		zabbix_log(LOG_LEVEL_CRIT, "Error getting table: %s", tablename);
		THIS_SHOULD_NEVER_HAPPEN;
		exit(EXIT_FAILURE);
	}

	while (FAIL == found)
	{
		/* avoid eternal loop within failed transaction */
		if (0 < db->txn_level && 0 != db->txn_error)
		{
			zabbix_log(LOG_LEVEL_DEBUG, "End of %s() transaction failed", __func__);
			return 0;
		}

		result = zbx_dbconn_select(db, "select nextid from ids where table_name='%s' and field_name='%s'",
				table->table, table->recid);

		if (NULL == (row = zbx_db_fetch(result)))
		{
			zbx_db_free_result(result);

			result = zbx_dbconn_select(db, "select max(%s) from %s where %s between " ZBX_FS_UI64 " and "
					ZBX_FS_UI64, table->recid, table->table, table->recid, min, max);

			if (NULL == (row = zbx_db_fetch(result)) || SUCCEED == zbx_db_is_null(row[0]))
			{
				ret1 = min;
			}
			else
			{
				ZBX_STR2UINT64(ret1, row[0]);
				if (ret1 >= max)
				{
					zabbix_log(LOG_LEVEL_CRIT, "maximum number of id's exceeded"
							" [table:%s, field:%s, id:" ZBX_FS_UI64 "]",
							table->table, table->recid, ret1);
					exit(EXIT_FAILURE);
				}
			}

			zbx_db_free_result(result);

			dbres = zbx_dbconn_execute(db, "insert into ids (table_name,field_name,nextid)"
					" values ('%s','%s'," ZBX_FS_UI64 ")",
					table->table, table->recid, ret1);

			if (ZBX_DB_OK > dbres)
			{
				/* solving the problem of an invisible record created in a parallel transaction */
				zbx_dbconn_execute(db, "update ids set nextid=nextid+1 where table_name='%s' and"
						" field_name='%s'", table->table, table->recid);
			}

			continue;
		}
		else
		{
			ZBX_STR2UINT64(ret1, row[0]);
			zbx_db_free_result(result);

			if (ret1 < min || ret1 >= max)
			{
				zbx_dbconn_execute(db, "delete from ids where table_name='%s' and field_name='%s'",
						table->table, table->recid);
				continue;
			}

			zbx_dbconn_execute(db, "update ids set nextid=nextid+" ZBX_FS_UI64 " where table_name='%s' and"
					" field_name='%s'", num, table->table, table->recid);

			result = zbx_dbconn_select(db, "select nextid from ids where table_name='%s' and"
					" field_name='%s'", table->table, table->recid);

			if (NULL != (row = zbx_db_fetch(result)) && SUCCEED != zbx_db_is_null(row[0]))
			{
				ZBX_STR2UINT64(ret2, row[0]);

				if (ret1 + num == ret2)
					found = SUCCEED;
			}
			else
				THIS_SHOULD_NEVER_HAPPEN;

			zbx_db_free_result(result);
		}
	}

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():" ZBX_FS_UI64 " table:'%s' recid:'%s'",
			__func__, ret2 - num + 1, table->table, table->recid);

	return ret2 - num + 1;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get next id for requested table                                   *
 *                                                                            *
 ******************************************************************************/
zbx_uint64_t	zbx_dbconn_get_maxid_num(zbx_dbconn_t *db, const char *tablename, int num)
{
	const char	**ptr;

	if (NULL != (ptr = (const char **)bsearch(&tablename, idcache_tables, ZBX_IDS_SIZE, sizeof(idcache_tables[0]),
			compare_table_names)))
	{
		return dbconn_get_cached_nextid(db, (size_t)(ptr - idcache_tables), (zbx_uint64_t)num);
	}

	return dbconn_get_nextid(db, tablename, (zbx_uint64_t)num);
}

/******************************************************************************
 *                                                                            *
 * Purpose: flush SQL request                                                 *
 *                                                                            *
 ******************************************************************************/
int	zbx_dbconn_flush_overflowed_sql(zbx_dbconn_t *db, char *sql, size_t sql_offset)
{
	if (0 != sql_offset)
		return zbx_dbconn_execute(db, "%s", sql);

	return ZBX_DB_OK;
}

/******************************************************************************
 *                                                                            *
 * Purpose: execute a set of SQL statements IF it is big enough               *
 *                                                                            *
 ******************************************************************************/
int	zbx_dbconn_execute_overflowed_sql(zbx_dbconn_t *db, char **sql, size_t *sql_alloc, size_t *sql_offset,
			const char *clause)
{
	int	ret = SUCCEED;

	if (ZBX_MAX_OVERFLOW_SQL_SIZE < *sql_offset)
	{
#ifdef HAVE_MULTIROW_INSERT
		if (',' == (*sql)[*sql_offset - 1])
		{
			(*sql_offset)--;

			if (NULL != clause)
				zbx_strcpy_alloc(sql, sql_alloc, sql_offset, clause);

			zbx_strcpy_alloc(sql, sql_alloc, sql_offset, ";\n");
		}
#else
		ZBX_UNUSED(sql_alloc);
		ZBX_UNUSED(clause);
#endif
		/* For Oracle with max_overflow_sql_size == 0, jump over "begin\n" */
		/* before execution. ZBX_SQL_EXEC_FROM is 0 for all other cases. */
		if (ZBX_DB_OK > zbx_dbconn_execute(db, "%s", *sql))
			ret = FAIL;

		*sql_offset = 0;
	}

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Return value: escaped string                                               *
 *                                                                            *
 * Comments: sync changes with 'db_get_escape_string_len'                     *
 *           and 'zbx_db_dyn_escape_string'                                   *
 *                                                                            *
 ******************************************************************************/
static void	db_escape_string(const char *src, char *dst, size_t len, zbx_escape_sequence_t flag)
{
	const char	*s;
	char		*d;

	assert(dst);

	len--;	/* '\0' */

	for (s = src, d = dst; NULL != s && '\0' != *s && 0 < len; s++)
	{
		if (ESCAPE_SEQUENCE_ON == flag && SUCCEED == db_is_escape_sequence(*s))
		{
			if (2 > len)
				break;

#if defined(HAVE_MYSQL)
			*d++ = '\\';
#elif defined(HAVE_POSTGRESQL)
			*d++ = *s;
#else
			*d++ = '\'';
#endif
			len--;
		}
		*d++ = *s;
		len--;
	}
	*d = '\0';
}

/******************************************************************************
 *                                                                            *
 * Purpose: to calculate escaped string length limited by bytes or characters *
 *          whichever is reached first.                                       *
 *                                                                            *
 * Parameters: s         - [IN] string to escape                              *
 *             max_bytes - [IN] limit in bytes                                *
 *             max_chars - [IN] limit in characters                           *
 *             flag      - [IN] sequences need to be escaped on/off           *
 *                                                                            *
 * Return value: return length in bytes of escaped string                     *
 *               with terminating '\0'                                        *
 *                                                                            *
 ******************************************************************************/
static size_t	db_get_escape_string_len(const char *s, size_t max_bytes, size_t max_chars,
		zbx_escape_sequence_t flag)
{
	size_t	csize, len = 1;	/* '\0' */

	if (NULL == s)
		return len;

	while ('\0' != *s && 0 < max_chars)
	{
		csize = zbx_utf8_char_len(s);

		/* process non-UTF-8 characters as single byte characters */
		if (0 == csize)
			csize = 1;

		if (max_bytes < csize)
			break;

		if (ESCAPE_SEQUENCE_ON == flag && SUCCEED == db_is_escape_sequence(*s))
			len++;

		s += csize;
		len += csize;
		max_bytes -= csize;
		max_chars--;
	}

	return len;
}

/******************************************************************************
 *                                                                            *
 * Purpose: to escape string limited by bytes or characters, whichever limit  *
 *          is reached first.                                                 *
 *                                                                            *
 * Parameters: src       - [IN] string to escape                              *
 *             max_bytes - [IN] limit in bytes                                *
 *             max_chars - [IN] limit in characters                           *
 *             flag      - [IN] sequences need to be escaped on/off           *
 *                                                                            *
 * Return value: escaped string                                               *
 *                                                                            *
 ******************************************************************************/
char	*db_dyn_escape_string(const char *src, size_t max_bytes, size_t max_chars, zbx_escape_sequence_t flag)
{
	char	*dst = NULL;
	size_t	len;

	len = db_get_escape_string_len(src, max_bytes, max_chars, flag);

	dst = (char *)zbx_malloc(dst, len);

	db_escape_string(src, dst, len, flag);

	return dst;
}

char	*zbx_db_dyn_escape_string_len(const char *src, size_t length)
{
	return db_dyn_escape_string(src, ZBX_SIZE_T_MAX, length, ESCAPE_SEQUENCE_ON);
}

char	*zbx_db_dyn_escape_string(const char *src)
{
	return db_dyn_escape_string(src, ZBX_SIZE_T_MAX, ZBX_SIZE_T_MAX, ESCAPE_SEQUENCE_ON);
}

/******************************************************************************
 *                                                                            *
 * Return value: return length of escaped LIKE pattern with terminating '\0'  *
 *                                                                            *
 * Comments: sync changes with 'db_escape_like_pattern'                       *
 *                                                                            *
 ******************************************************************************/
static size_t	db_get_escape_like_pattern_len(const char *src)
{
	size_t		len;
	const char	*s;

	len = db_get_escape_string_len(src, ZBX_SIZE_T_MAX, ZBX_SIZE_T_MAX, ESCAPE_SEQUENCE_ON) - 1; /* minus '\0' */

	for (s = src; s && *s; s++)
	{
		len += (*s == '_' || *s == '%' || *s == ZBX_SQL_LIKE_ESCAPE_CHAR);
		len += 1;
	}

	len++; /* '\0' */

	return len;
}

/******************************************************************************
 *                                                                            *
 * Return value: escaped string to be used as pattern in LIKE                 *
 *                                                                            *
 * Comments: sync changes with 'db_get_escape_like_pattern_len'               *
 *                                                                            *
 *           For instance, we wish to find string a_b%c\d'e!f in our database *
 *           using '!' as escape character. Our queries then become:          *
 *                                                                            *
 *           ... LIKE 'a!_b!%c\\d\'e!!f' ESCAPE '!' (MySQL, PostgreSQL)       *
 *           ... LIKE 'a!_b!%c\d''e!!f' ESCAPE '!' (Oracle, SQLite3)          *
 *                                                                            *
 *           Using backslash as escape character in LIKE would be too much    *
 *           trouble, because escaping backslashes would have to be escaped   *
 *           as well, like so:                                                *
 *                                                                            *
 *           ... LIKE 'a\\_b\\%c\\\\d\'e!f' ESCAPE '\\' or                    *
 *           ... LIKE 'a\\_b\\%c\\\\d\\\'e!f' ESCAPE '\\' (MySQL, PostgreSQL) *
 *           ... LIKE 'a\_b\%c\\d''e!f' ESCAPE '\' (Oracle, SQLite3)          *
 *                                                                            *
 *           Hence '!' instead of backslash.                                  *
 *                                                                            *
 ******************************************************************************/
static void	db_escape_like_pattern(const char *src, char *dst, size_t len)
{
	char		*d;
	char		*tmp = NULL;
	const char	*t;

	assert(dst);

	tmp = (char *)zbx_malloc(tmp, len);

	db_escape_string(src, tmp, len, ESCAPE_SEQUENCE_ON);

	len--; /* '\0' */

	for (t = tmp, d = dst; t && *t && len; t++)
	{
		if (*t == '_' || *t == '%' || *t == ZBX_SQL_LIKE_ESCAPE_CHAR)
		{
			if (len <= 1)
				break;
			*d++ = ZBX_SQL_LIKE_ESCAPE_CHAR;
			len--;
		}
		*d++ = *t;
		len--;
	}

	*d = '\0';

	zbx_free(tmp);
}

/******************************************************************************
 *                                                                            *
 * Return value: escaped string to be used as pattern in LIKE                 *
 *                                                                            *
 ******************************************************************************/
char	*zbx_db_dyn_escape_like_pattern(const char *src)
{
	size_t	len;
	char	*dst = NULL;

	len = db_get_escape_like_pattern_len(src);

	dst = (char *)zbx_malloc(dst, len);

	db_escape_like_pattern(src, dst, len);

	return dst;
}

/******************************************************************************
 *                                                                            *
 * Purpose: return the string length to fit into a database field of the      *
 *          specified size                                                    *
 *                                                                            *
 * Return value: the string length in bytes                                   *
 *                                                                            *
 ******************************************************************************/
size_t	zbx_db_strlen_n(const char *text_loc, size_t maxlen)
{
	return zbx_strlen_utf8_nchars(text_loc, maxlen);
}

static zbx_db_table_t	*db_get_table(const char *tablename)
{
	zbx_db_table_t	*tables = zbx_dbschema_get_tables();

	for (int t = 0; NULL != tables[t].table; t++)
	{
		if (0 == strcmp(tables[t].table, tablename))
			return &tables[t];
	}

	return NULL;
}

static const zbx_db_field_t	*db_get_field(const zbx_db_table_t *table, const char *fieldname)
{
	int	f;

	for (f = 0; NULL != table->fields[f].name; f++)
	{
		if (0 == strcmp(table->fields[f].name, fieldname))
			return &table->fields[f];
	}

	return NULL;
}

const zbx_db_table_t	*zbx_db_get_table(const char *tablename)
{
	return db_get_table(tablename);
}

const zbx_db_field_t	*zbx_db_get_field(const zbx_db_table_t *table, const char *fieldname)
{
	return db_get_field(table, fieldname);
}

#ifdef HAVE_MYSQL
static size_t	get_string_field_size(const zbx_db_field_t *field)
{
	switch (field->type)
	{
		case ZBX_TYPE_BLOB:
		case ZBX_TYPE_LONGTEXT:
			return 4294967295ul;
		case ZBX_TYPE_CHAR:
		case ZBX_TYPE_TEXT:
			return 65535u;
		case ZBX_TYPE_CUID:
			return CUID_LEN - 1;
		default:
			THIS_SHOULD_NEVER_HAPPEN;
			exit(EXIT_FAILURE);
	}
}
#endif

static size_t	get_string_field_chars(const zbx_db_field_t *field)
{
	if ((ZBX_TYPE_LONGTEXT == field->type || ZBX_TYPE_BLOB == field->type) && 0 == field->length)
		return ZBX_SIZE_T_MAX;
	else if (ZBX_TYPE_CUID == field->type)
		return CUID_LEN - 1;
	else
		return field->length;
}

int	zbx_db_validate_field_size(const char *tablename, const char *fieldname, const char *str)
{
	const zbx_db_table_t	*table;
	const zbx_db_field_t	*field;
	size_t			max_bytes, max_chars;

	if (NULL == (table = zbx_db_get_table(tablename)) || NULL == (field = zbx_db_get_field(table, fieldname)))
	{
		zabbix_log(LOG_LEVEL_CRIT, "invalid table: \"%s\" field: \"%s\"", tablename, fieldname);
		return FAIL;
	}

#if defined(HAVE_MYSQL) || defined(HAVE_ORACLE)
	max_bytes = get_string_field_size(field);
#else
	max_bytes = ZBX_SIZE_T_MAX;
#endif
	max_chars = get_string_field_chars(field);

	if (max_bytes < strlen(str))
		return FAIL;

	if (ZBX_SIZE_T_MAX == max_chars)
		return SUCCEED;

	if (max_chars != max_bytes && max_chars < zbx_strlen_utf8(str))
		return FAIL;

	return SUCCEED;
}

char	*db_dyn_escape_field_len(const zbx_db_field_t *field, const char *src, zbx_escape_sequence_t flag)
{
#if defined(HAVE_MYSQL)
	return db_dyn_escape_string(src, get_string_field_size(field), get_string_field_chars(field), flag);
#else
	return db_dyn_escape_string(src, ZBX_SIZE_T_MAX, get_string_field_chars(field), flag);
#endif
}

char	*zbx_db_dyn_escape_field(const char *table_name, const char *field_name, const char *src)
{
	const zbx_db_table_t	*table;
	const zbx_db_field_t	*field;

	if (NULL == (table = zbx_db_get_table(table_name)) || NULL == (field = zbx_db_get_field(table, field_name)))
	{
		zabbix_log(LOG_LEVEL_CRIT, "invalid table: \"%s\" field: \"%s\"", table_name, field_name);
		exit(EXIT_FAILURE);
	}

	return db_dyn_escape_field_len(field, src, ESCAPE_SEQUENCE_ON);
}

int	zbx_db_is_null(const char *field)
{
	if (NULL == field)
		return SUCCEED;

	return FAIL;
}

zbx_db_config_t	*zbx_db_config_create(void)
{
	zbx_db_config_t	*config;

	config = (zbx_db_config_t *)zbx_malloc(NULL, sizeof(zbx_db_config_t));
	memset(config, 0, sizeof(zbx_db_config_t));

	return config;
}

void	zbx_db_config_free(zbx_db_config_t *config)
{
	zbx_free(config->dbhost);
	zbx_free(config->dbname);
	zbx_free(config->dbschema);
	zbx_free(config->dbuser);
	zbx_free(config->dbpassword);
	zbx_free(config->dbsocket);
	zbx_free(config->db_tls_connect);
	zbx_free(config->db_tls_cert_file);
	zbx_free(config->db_tls_key_file);
	zbx_free(config->db_tls_ca_file);
	zbx_free(config->db_tls_cipher);
	zbx_free(config->db_tls_cipher_13);

	zbx_free(config);
}

/******************************************************************************
 *                                                                            *
 * Return value: validate database configuration parameters depending on      *
 *               component type (server/proxy)                                *
 *                                                                            *
 ******************************************************************************/
int	zbx_db_config_validate_features(zbx_db_config_t *config, unsigned char program_type)
{
	int	err = 0;

#if !(defined(HAVE_MYSQL_TLS) || defined(HAVE_MARIADB_TLS) || defined(HAVE_POSTGRESQL))
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSConnect", config->db_tls_connect,
			"PostgreSQL or MySQL library version that support TLS"));
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSCAFile", config->db_tls_ca_file,
			"PostgreSQL or MySQL library version that support TLS"));
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSCertFile", config->db_tls_cert_file,
			"PostgreSQL or MySQL library version that support TLS"));
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSKeyFile", config->db_tls_key_file,
			"PostgreSQL or MySQL library version that support TLS"));
#endif

#if !(defined(HAVE_MYSQL_TLS) || defined(HAVE_POSTGRESQL))
	if (NULL != config->db_tls_connect && 0 == strcmp(config->db_tls_connect,
			ZBX_DB_TLS_CONNECT_VERIFY_CA_TXT))
	{
		zbx_error("\"DBTLSConnect\" configuration parameter value '%s' cannot be used: Zabbix %s was compiled"
			" without PostgreSQL or MySQL library version that support this value",
			ZBX_DB_TLS_CONNECT_VERIFY_CA_TXT, get_program_type_string(program_type));
		err |= 1;
	}
#else
	ZBX_UNUSED(program_type);
	ZBX_UNUSED(config);
#endif

#if !(defined(HAVE_MYSQL_TLS) || defined(HAVE_MARIADB_TLS))
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSCipher", config->db_tls_cipher,
			"MySQL library version that support configuration of cipher"));
#endif

#if !defined(HAVE_MYSQL_TLS_CIPHERSUITES)
	err |= (FAIL == zbx_check_cfg_feature_str("DBTLSCipher13", config->db_tls_cipher_13,
			"MySQL library version that support configuration of TLSv1.3 ciphersuites"));
#endif

	return 0 != err ? FAIL : SUCCEED;
}

#if defined(HAVE_MYSQL) || defined(HAVE_POSTGRESQL)
static void	check_cfg_empty_str(const char *parameter, const char *value)
{
	if (NULL != value && 0 == strlen(value))
	{
		zabbix_log(LOG_LEVEL_CRIT, "configuration parameter \"%s\" is defined but empty", parameter);
		exit(EXIT_FAILURE);
	}
}

/******************************************************************************
 *                                                                            *
 * Return value: validate database configuration parameters                   *
 *                                                                            *
 ******************************************************************************/
void	zbx_db_config_validate(zbx_db_config_t *config)
{
	check_cfg_empty_str("DBTLSConnect", config->db_tls_connect);
	check_cfg_empty_str("DBTLSCertFile", config->db_tls_cert_file);
	check_cfg_empty_str("DBTLSKeyFile", config->db_tls_key_file);
	check_cfg_empty_str("DBTLSCAFile", config->db_tls_ca_file);
	check_cfg_empty_str("DBTLSCipher", config->db_tls_cipher);
	check_cfg_empty_str("DBTLSCipher13", config->db_tls_cipher_13);

	if (NULL != config->db_tls_connect &&
			0 != strcmp(config->db_tls_connect, ZBX_DB_TLS_CONNECT_REQUIRED_TXT) &&
			0 != strcmp(config->db_tls_connect, ZBX_DB_TLS_CONNECT_VERIFY_CA_TXT) &&
			0 != strcmp(config->db_tls_connect, ZBX_DB_TLS_CONNECT_VERIFY_FULL_TXT))
	{
		zabbix_log(LOG_LEVEL_CRIT, "invalid \"DBTLSConnect\" configuration parameter: '%s'",
				config->db_tls_connect);
		exit(EXIT_FAILURE);
	}

	if (NULL != config->db_tls_connect &&
			(0 == strcmp(ZBX_DB_TLS_CONNECT_VERIFY_CA_TXT, config->db_tls_connect) ||
			0 == strcmp(ZBX_DB_TLS_CONNECT_VERIFY_FULL_TXT, config->db_tls_connect)) &&
			NULL == config->db_tls_ca_file)
	{
		zabbix_log(LOG_LEVEL_CRIT, "parameter \"DBTLSConnect\" value \"%s\" requires \"DBTLSCAFile\", but it"
				" is not defined", config->db_tls_connect);
		exit(EXIT_FAILURE);
	}

	if ((NULL != config->db_tls_cert_file || NULL != config->db_tls_key_file) &&
			(NULL == config->db_tls_cert_file ||
			NULL == config->db_tls_key_file || NULL == config->db_tls_ca_file))
	{
		zabbix_log(LOG_LEVEL_CRIT, "parameter \"DBTLSKeyFile\" or \"DBTLSCertFile\" is defined, but"
				" \"DBTLSKeyFile\", \"DBTLSCertFile\" or \"DBTLSCAFile\" is not defined");
		exit(EXIT_FAILURE);
	}
}
#endif

#ifdef HAVE_POSTGRESQL
/******************************************************************************
 *                                                                            *
 * Purpose: retrieves TimescaleDB (TSDB) license information                  *
 *                                                                            *
 * Return value: license information from datase as string                    *
 *               "apache"    for TimescaleDB Apache 2 Edition                 *
 *               "timescale" for TimescaleDB Community Edition                *
 *                                                                            *
 * Comments: returns a pointer to allocated memory                            *
 *                                                                            *
 ******************************************************************************/
static char	*dbconn_tsdb_get_license(zbx_dbconn_t *db)
{
	zbx_db_result_t	result;
	zbx_db_row_t	row;
	char		*tsdb_lic = NULL;

	result = zbx_dbconn_select(db, "show timescaledb.license");

	if ((zbx_db_result_t)ZBX_DB_DOWN != result && NULL != result && NULL != (row = zbx_db_fetch(result)))
	{
		tsdb_lic = zbx_strdup(NULL, row[0]);
	}

	zbx_db_free_result(result);

	return tsdb_lic;
}
#endif

int	zbx_dbconn_check_extension(zbx_dbconn_t *db, struct zbx_db_version_info_t *info, int allow_unsupported)
{
#ifdef HAVE_POSTGRESQL
	zbx_db_result_t	result;
	zbx_db_row_t	row;
	char		*tsdb_lic = NULL;
	int		ret = SUCCEED;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	if (SUCCEED == zbx_db_table_exists("settings"))
	{
		if (NULL == (result = zbx_dbconn_select(db,
				"select value_str from settings where name='db_extension'")))
		{
			goto out;
		}
	}
	else if (SUCCEED == zbx_db_field_exists("config", "db_extension"))
	{
		if (NULL == (result = zbx_dbconn_select(db, "select db_extension from config")))
			goto out;
	}
	else
		goto out;

	if (NULL == (row = zbx_db_fetch(result)) || '\0' == *row[0])
	{
		zbx_db_free_result(result);
		goto out;
	}

	info->extension = zbx_strdup(NULL, row[0]);

	zbx_db_free_result(result);

	if (0 != zbx_strcmp_null(info->extension, ZBX_DB_EXTENSION_TIMESCALEDB))
		goto out;

	/* at this point we know the TimescaleDB extension is enabled in Zabbix */

	zbx_tsdb_info_extract(info);

	if (DB_VERSION_FAILED_TO_RETRIEVE == info->ext_flag)
	{
		info->ext_err_code = ZBX_TIMESCALEDB_VERSION_FAILED_TO_RETRIEVE;
		ret = FAIL;
		goto out;
	}

	if (DB_VERSION_LOWER_THAN_MINIMUM == info->ext_flag)
	{
		zabbix_log(LOG_LEVEL_WARNING, "TimescaleDB version must be at least %d. Recommended version is at least"
				" %s %s.", ZBX_TIMESCALE_MIN_VERSION, ZBX_TIMESCALE_LICENSE_COMMUNITY_STR,
				ZBX_TIMESCALE_MIN_SUPPORTED_VERSION_STR);
		info->ext_err_code = ZBX_TIMESCALEDB_VERSION_LOWER_THAN_MINIMUM;
		ret = FAIL;
		goto out;
	}

	if (DB_VERSION_NOT_SUPPORTED_ERROR == info->ext_flag)
	{
		zabbix_log(LOG_LEVEL_WARNING, "TimescaleDB version %u is not officially supported. Recommended version"
				" is at least %s %s.", info->ext_current_version,
				ZBX_TIMESCALE_LICENSE_COMMUNITY_STR, ZBX_TIMESCALE_MIN_SUPPORTED_VERSION_STR);
		info->ext_err_code = ZBX_TIMESCALEDB_VERSION_NOT_SUPPORTED;

		if (0 == allow_unsupported)
		{
			ret = FAIL;
			goto out;
		}

		info->ext_flag = DB_VERSION_NOT_SUPPORTED_WARNING;
	}

	if (DB_VERSION_HIGHER_THAN_MAXIMUM == info->ext_flag)
	{
		zabbix_log(LOG_LEVEL_WARNING, "TimescaleDB version is too new. Recommended version is up to %s %s.",
				ZBX_TIMESCALE_LICENSE_COMMUNITY_STR, ZBX_TIMESCALE_MAX_VERSION_STR);
		info->ext_err_code = ZBX_TIMESCALEDB_VERSION_HIGHER_THAN_MAXIMUM;

		if (0 == allow_unsupported)
		{
			info->ext_flag = DB_VERSION_HIGHER_THAN_MAXIMUM_ERROR;
			ret = FAIL;
			goto out;
		}

		info->ext_flag = DB_VERSION_HIGHER_THAN_MAXIMUM_WARNING;
	}

	tsdb_lic = dbconn_tsdb_get_license(db);

	zbx_tsdb_extract_compressed_chunk_flags(info);

	zabbix_log(LOG_LEVEL_DEBUG, "TimescaleDB license: [%s]", ZBX_NULL2EMPTY_STR(tsdb_lic));

	if (0 != zbx_strcmp_null(tsdb_lic, ZBX_TIMESCALE_LICENSE_COMMUNITY))
	{
		zabbix_log(LOG_LEVEL_WARNING, "Detected license [%s] does not support compression. Compression is"
				" supported in %s.", ZBX_NULL2EMPTY_STR(tsdb_lic),
				ZBX_TIMESCALE_LICENSE_COMMUNITY_STR);
		info->ext_err_code = ZBX_TIMESCALEDB_LICENSE_NOT_COMMUNITY;
		goto out;
	}

	zabbix_log(LOG_LEVEL_DEBUG, "%s was detected. TimescaleDB compression is supported.",
			ZBX_TIMESCALE_LICENSE_COMMUNITY_STR);

	if (ZBX_EXT_ERR_UNDEFINED == info->ext_err_code)
		info->ext_err_code = ZBX_EXT_SUCCEED;

	zbx_tsdb_set_compression_availability(ON);
out:
	zbx_free(tsdb_lic);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret));

	return ret;
#else
	ZBX_UNUSED(db);
	ZBX_UNUSED(info);
	ZBX_UNUSED(allow_unsupported);

	return SUCCEED;
#endif
}

/******************************************************************************
 *                                                                            *
 * Purpose: locks a record in a table by its primary key and an optional      *
 *          constraint field                                                  *
 *                                                                            *
 * Parameters: db        - [IN] database connection                           *
 *             table     - [IN] the target table                              *
 *             id        - [IN] primary key value                             *
 *             add_field - [IN] additional constraint field name (optional)   *
 *             add_id    - [IN] constraint field value                        *
 *                                                                            *
 * Return value: SUCCEED - the record was successfully locked                 *
 *               FAIL    - the table does not contain the specified record    *
 *                                                                            *
 ******************************************************************************/
int	zbx_dbconn_lock_record(zbx_dbconn_t *db, const char *table, zbx_uint64_t id, const char *add_field,
		zbx_uint64_t add_id)
{
	zbx_db_result_t		result;
	const zbx_db_table_t	*t;
	int			ret;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	if (0 == db->txn_level)
		zabbix_log(LOG_LEVEL_DEBUG, "%s() called outside of transaction", __func__);

	t = zbx_db_get_table(table);

	if (NULL == add_field)
	{
		result = zbx_dbconn_select(db, "select null from %s where %s=" ZBX_FS_UI64 ZBX_FOR_UPDATE, table,
				t->recid, id);
	}
	else
	{
		result = zbx_dbconn_select(db, "select null from %s where %s=" ZBX_FS_UI64 " and %s=" ZBX_FS_UI64
				ZBX_FOR_UPDATE, table, t->recid, id, add_field, add_id);
	}

	if (NULL == zbx_db_fetch(result))
		ret = FAIL;
	else
		ret = SUCCEED;

	zbx_db_free_result(result);

	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret));

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Purpose: locks a records in a table by its primary key                     *
 *                                                                            *
 * Parameters: db        - [IN] database connection                           *
 *             table     - [IN] the target table                              *
 *             ids       - [IN] primary key values                            *
 *                                                                            *
 * Return value: SUCCEED - one or more of the specified records were          *
 *                         successfully locked                                *
 *               FAIL    - the table does not contain any of the specified    *
 *                         records or 'table' name not found                  *
 *                                                                            *
 ******************************************************************************/
int	zbx_dbconn_lock_records(zbx_dbconn_t *db, const char *table, const zbx_vector_uint64_t *ids)
{
	zbx_db_result_t		result;
	const zbx_db_table_t	*t;
	int			ret;
	char			*sql = NULL;
	size_t			sql_alloc = 0, sql_offset = 0;

	zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__);

	if (0 == db->txn_level)
		zabbix_log(LOG_LEVEL_DEBUG, "%s() called outside of transaction", __func__);

	if (NULL == (t = zbx_db_get_table(table)))
	{
		zabbix_log(LOG_LEVEL_CRIT, "%s(): cannot find table '%s'", __func__, table);
		THIS_SHOULD_NEVER_HAPPEN;
		ret = FAIL;
		goto out;
	}

	zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "select null from %s where", table);
	zbx_db_add_condition_alloc(&sql, &sql_alloc, &sql_offset, t->recid, ids->values, ids->values_num);

	result = zbx_dbconn_select(db, "%s" ZBX_FOR_UPDATE, sql);

	zbx_free(sql);

	if (NULL == zbx_db_fetch(result))
		ret = FAIL;
	else
		ret = SUCCEED;

	zbx_db_free_result(result);
out:
	zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret));

	return ret;
}

/******************************************************************************
 *                                                                            *
 * Purpose: locks a records in a table by field name                          *
 *                                                                            *
 * Parameters: table      - [IN] the target table                             *
 *             field_name - [IN] field name                                   *
 *             ids        - [IN/OUT] IN - sorted array of IDs to lock         *
 *                                   OUT - resulting array of locked IDs      *
 *                                                                            *
 * Return value: SUCCEED - one or more of the specified records were          *
 *                         successfully locked                                *
 *               FAIL    - no records were locked                             *
 *                                                                            *
 ******************************************************************************/
int	zbx_dbconn_lock_ids(zbx_dbconn_t *db, const char *table_name, const char *field_name, zbx_vector_uint64_t *ids)
{
	char		*sql = NULL;
	size_t		sql_alloc = 0, sql_offset = 0;
	zbx_uint64_t	id;
	int		i;
	zbx_db_result_t	result;
	zbx_db_row_t	row;

	if (0 == ids->values_num)
		return FAIL;

	zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "select %s from %s where", field_name, table_name);
	zbx_db_add_condition_alloc(&sql, &sql_alloc, &sql_offset, field_name, ids->values, ids->values_num);
	zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " order by %s" ZBX_FOR_UPDATE, field_name);
	result = zbx_dbconn_select(db, "%s", sql);
	zbx_free(sql);

	for (i = 0; NULL != (row = zbx_db_fetch(result)); i++)
	{
		ZBX_STR2UINT64(id, row[0]);

		while (id != ids->values[i])
			zbx_vector_uint64_remove(ids, i);
	}
	zbx_db_free_result(result);

	while (i != ids->values_num)
		zbx_vector_uint64_remove_noorder(ids, i);

	return (0 != ids->values_num ? SUCCEED : FAIL);
}

#if defined(HAVE_MYSQL) || defined(HAVE_POSTGRESQL)
#define MAX_EXPRESSIONS	1000	/* tune according to batch size to avoid unnecessary or conditions */
#else
#define MAX_EXPRESSIONS	950
#endif

/******************************************************************************
 *                                                                            *
 * Purpose: Takes an initial part of SQL query and appends a generated        *
 *          WHERE condition. The WHERE condition is generated from the given  *
 *          list of values as a mix of <fieldname> BETWEEN <id1> AND <idN>"   *
 *          and "<fieldname> IN (<id1>,<id2>,...,<idN>)" elements.            *
 *                                                                            *
 * Parameters: sql        - [IN/OUT] buffer for SQL query construction        *
 *             sql_alloc  - [IN/OUT] size of the 'sql' buffer                 *
 *             sql_offset - [IN/OUT] current position in the 'sql' buffer     *
 *             fieldname  - [IN] field name to be used in SQL WHERE condition *
 *             values     - [IN] array of numerical values sorted in          *
 *                               ascending order to be included in WHERE      *
 *             num        - [IN] number of elements in 'values' array         *
 *                                                                            *
 ******************************************************************************/
void	zbx_db_add_condition_alloc(char **sql, size_t *sql_alloc, size_t *sql_offset, const char *fieldname,
		const zbx_uint64_t *values, const int num)
{
	int		i, in_cnt;
#if defined(HAVE_SQLITE3)
	int		expr_num, expr_cnt = 0;
#endif
	if (0 == num)
		return;

	zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ' ');
	if (MAX_EXPRESSIONS < num)
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, '(');

#if defined(HAVE_SQLITE3)
	expr_num = (num + MAX_EXPRESSIONS - 1) / MAX_EXPRESSIONS;

	if (MAX_EXPRESSIONS < expr_num)
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, '(');
#endif

	if (1 < num)
		zbx_snprintf_alloc(sql, sql_alloc, sql_offset, "%s in (", fieldname);

	/* compose "in"s */
	for (i = 0, in_cnt = 0; i < num; i++)
	{
		if (1 == num)
		{
			zbx_snprintf_alloc(sql, sql_alloc, sql_offset, "%s=" ZBX_FS_UI64, fieldname,
					values[i]);
			break;
		}
		else
		{
			if (MAX_EXPRESSIONS == in_cnt)
			{
				in_cnt = 0;
				(*sql_offset)--;
#if defined(HAVE_SQLITE3)
				if (MAX_EXPRESSIONS == ++expr_cnt)
				{
					zbx_snprintf_alloc(sql, sql_alloc, sql_offset, ")) or (%s in (",
							fieldname);
					expr_cnt = 0;
				}
				else
				{
#endif
					zbx_snprintf_alloc(sql, sql_alloc, sql_offset, ") or %s in (",
							fieldname);
#if defined(HAVE_SQLITE3)
				}
#endif
			}

			in_cnt++;
			zbx_snprintf_alloc(sql, sql_alloc, sql_offset, ZBX_FS_UI64 ",",
					values[i]);
		}
	}

	if (1 < num)
	{
		(*sql_offset)--;
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');
	}

#if defined(HAVE_SQLITE3)
	if (MAX_EXPRESSIONS < expr_num)
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');
#endif
	if (MAX_EXPRESSIONS < num)
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');

#undef MAX_EXPRESSIONS
}

/*********************************************************************************
 *                                                                               *
 * Purpose: This function is similar to the zbx_db_add_condition_alloc(), except *
 *          it is designed for generating WHERE conditions for strings. Hence,   *
 *          this function is simpler, because only IN condition is possible.     *
 *                                                                               *
 * Parameters: sql        - [IN/OUT] buffer for SQL query construction           *
 *             sql_alloc  - [IN/OUT] size of the 'sql' buffer                    *
 *             sql_offset - [IN/OUT] current position in the 'sql' buffer        *
 *             fieldname  - [IN] field name to be used in SQL WHERE condition    *
 *             values     - [IN] array of string values                          *
 *             num        - [IN] number of elements in 'values' array            *
 *                                                                               *
 *                                                                               *
 *********************************************************************************/
void	zbx_db_add_str_condition_alloc(char **sql, size_t *sql_alloc, size_t *sql_offset, const char *fieldname,
		const char * const *values, const int num)
{
#if defined(HAVE_MYSQL) || defined(HAVE_POSTGRESQL)
#define MAX_EXPRESSIONS	1000	/* tune according to batch size to avoid unnecessary or conditions */
#else
#define MAX_EXPRESSIONS	950
#endif

	int	i, cnt = 0;
	char	*value_esc;
	int	values_num = 0, empty_num = 0;

	if (0 == num)
		return;

	zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ' ');

	for (i = 0; i < num; i++)
	{
		if ('\0' == *values[i])
			empty_num++;
		else
			values_num++;
	}

	if (MAX_EXPRESSIONS < values_num || (0 != values_num && 0 != empty_num))
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, '(');

	if (0 != empty_num)
	{
		zbx_snprintf_alloc(sql, sql_alloc, sql_offset, "%s" ZBX_SQL_STRCMP, fieldname, ZBX_SQL_STRVAL_EQ(""));

		if (0 == values_num)
			return;

		zbx_strcpy_alloc(sql, sql_alloc, sql_offset, " or ");
	}

	if (1 == values_num)
	{
		for (i = 0; i < num; i++)
		{
			if ('\0' == *values[i])
				continue;

			value_esc = zbx_db_dyn_escape_string(values[i]);
			zbx_snprintf_alloc(sql, sql_alloc, sql_offset, "%s='%s'", fieldname, value_esc);
			zbx_free(value_esc);
		}

		if (0 != empty_num)
			zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');
		return;
	}

	zbx_strcpy_alloc(sql, sql_alloc, sql_offset, fieldname);
	zbx_strcpy_alloc(sql, sql_alloc, sql_offset, " in (");

	for (i = 0; i < num; i++)
	{
		if ('\0' == *values[i])
			continue;

		if (MAX_EXPRESSIONS == cnt)
		{
			cnt = 0;
			(*sql_offset)--;
			zbx_strcpy_alloc(sql, sql_alloc, sql_offset, ") or ");
			zbx_strcpy_alloc(sql, sql_alloc, sql_offset, fieldname);
			zbx_strcpy_alloc(sql, sql_alloc, sql_offset, " in (");
		}

		value_esc = zbx_db_dyn_escape_string(values[i]);
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, '\'');
		zbx_strcpy_alloc(sql, sql_alloc, sql_offset, value_esc);
		zbx_strcpy_alloc(sql, sql_alloc, sql_offset, "',");
		zbx_free(value_esc);

		cnt++;
	}

	(*sql_offset)--;
	zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');

	if (MAX_EXPRESSIONS < values_num || 0 != empty_num)
		zbx_chrcpy_alloc(sql, sql_alloc, sql_offset, ')');

#undef MAX_EXPRESSIONS
}

/******************************************************************************
 *                                                                            *
 * Purpose: construct insert statement                                        *
 *                                                                            *
 * Return value: "<id>" if id not equal zero,                                 *
 *               otherwise "null"                                             *
 *                                                                            *
 ******************************************************************************/
const char	*zbx_db_sql_id_ins(zbx_uint64_t id)
{
	static unsigned char	n = 0;
	static char		buf[4][21];	/* 20 - value size, 1 - '\0' */
	static const char	null[5] = "null";

	if (0 == id)
		return null;

	n = (n + 1) & 3;

	zbx_snprintf(buf[n], sizeof(buf[n]), ZBX_FS_UI64, id);

	return buf[n];
}

/******************************************************************************
 *                                                                            *
 * Purpose: construct where condition                                         *
 *                                                                            *
 * Return value: "=<id>" if id not equal zero,                                *
 *               otherwise " is null"                                         *
 *                                                                            *
 * Comments: NB! Do not use this function more than once in same SQL query    *
 *                                                                            *
 ******************************************************************************/
const char	*zbx_db_sql_id_cmp(zbx_uint64_t id)
{
	static char		buf[22];	/* 1 - '=', 20 - value size, 1 - '\0' */
	static const char	is_null[9] = " is null";

	if (0 == id)
		return is_null;

	zbx_snprintf(buf, sizeof(buf), "=" ZBX_FS_UI64, id);

	return buf;
}