diff --git a/UPGRADE.txt b/UPGRADE.txt index 5250793484..8092140f04 100644 --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -19,6 +19,7 @@ === UPGRADE-10.txt -- Upgrade info for 1.8 to 10 === =========================================================== + from 11.8 to 11.9 * res_fax now returns the correct rates for V.27ter (4800 or 9600 bit/s). Because of this the default settings would not load, so the minrate (minimum @@ -29,6 +30,14 @@ from 11.8 to 11.9 and the fix involved using a different method to achieve the same goal. The new method to achieve this functionality is by using sound_begin to play a sound to the conference when waitmarked users are moved into the conference. +* A compatibility setting, allow_empty_string_in_nontext, has been added to + res_odbc.conf. When enabled (default behavior), empty column values are + stored as empty strings during realtime updates. Disabling this option + causes empty column values to be stored as NULLs for non-text columns. + + Disable it for PostgreSQL backends in order to avoid errors caused by + updating integer columns with an empty string instead of NULL + (sipppeers,sipregs) From 11.7 to 11.8: * The per console verbose level feature as previously implemented caused a diff --git a/configs/res_odbc.conf.sample b/configs/res_odbc.conf.sample index 66659ae42e..581389fda1 100644 --- a/configs/res_odbc.conf.sample +++ b/configs/res_odbc.conf.sample @@ -64,6 +64,14 @@ pre-connect => yes ; MS SQL Server, the answer is no. ;backslash_is_escape => yes ; +; When enabled (default behavior), empty column values are stored as empty strings +; during realtime updates. Disabling this option causes empty column values to be +; stored as NULLs for non-text columns. +; Disable it for PostgreSQL backend in order to avoid errors caused by updating +; integer columns with an empty string instead of NULL (sipppeers,sipregs) +; NOTE: This option will be removed in asterisk 13. At that point, it will always behave as if it was set to 'no'. +;allow_empty_string_in_nontext => yes +; ; How long (in seconds) should we attempt to connect before considering the ; connection dead? The default is 10 seconds, but you may wish to reduce it, ; to increase responsiveness. diff --git a/include/asterisk/res_odbc.h b/include/asterisk/res_odbc.h index 76d57f74c1..2f63ec54a4 100644 --- a/include/asterisk/res_odbc.h +++ b/include/asterisk/res_odbc.h @@ -221,4 +221,10 @@ int ast_odbc_clear_cache(const char *database, const char *tablename); */ SQLRETURN ast_odbc_ast_str_SQLGetData(struct ast_str **buf, int pmaxlen, SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLSMALLINT TargetType, SQLLEN *StrLen_or_Ind); +/*! \brief Checks if the database natively supports implicit conversion from an empty string to a number (0). + * \param obj The ODBC object + * \return Returns 1 if the implicit conversion is valid and non-text columns can take empty strings, 0 if the conversion is not valid and NULLs should be used instead '\' + */ +int ast_odbc_allow_empty_string_in_nontext(struct odbc_obj *obj); + #endif /* _ASTERISK_RES_ODBC_H */ diff --git a/res/res_config_odbc.c b/res/res_config_odbc.c index 66e15a02be..0f522e5ccf 100644 --- a/res/res_config_odbc.c +++ b/res/res_config_odbc.c @@ -69,6 +69,12 @@ static void decode_chunk(char *chunk) } } +static inline int is_text(const struct odbc_cache_columns *column) +{ + return column->type == SQL_CHAR || column->type == SQL_VARCHAR || column->type == SQL_LONGVARCHAR + || column->type == SQL_WCHAR || column->type == SQL_WVARCHAR || column->type == SQL_WLONGVARCHAR; +} + static SQLHSTMT custom_prepare(struct odbc_obj *obj, void *data) { int res, x = 1, count = 0; @@ -484,15 +490,15 @@ static int update_odbc(const char *database, const char *table, const char *keyf SQLHSTMT stmt; char sql[256]; SQLLEN rowcount=0; - const char *newparam; - int res, count = 1; + const char *newparam, *newval; + int res, count = 0, paramcount = 0; va_list aq; struct custom_prepare_struct cps = { .sql = sql, .extra = lookup }; struct odbc_cache_tables *tableptr; struct odbc_cache_columns *column = NULL; struct ast_flags connected_flag = { RES_ODBC_CONNECTED }; - if (!table) { + if (!table || !keyfield) { return -1; } @@ -507,39 +513,31 @@ static int update_odbc(const char *database, const char *table, const char *keyf return -1; } + if (tableptr && !ast_odbc_find_column(tableptr, keyfield)) { + ast_log(LOG_WARNING, "Key field '%s' does not exist in table '%s@%s'. Update will fail\n", keyfield, table, database); + } + va_copy(aq, ap); - newparam = va_arg(aq, const char *); - if (!newparam) { - va_end(aq); - ast_odbc_release_obj(obj); - ast_odbc_release_table(tableptr); - ast_string_field_free_memory(&cps); - return -1; - } - va_arg(aq, const char *); - if (tableptr && !ast_odbc_find_column(tableptr, newparam)) { - ast_log(LOG_WARNING, "Key field '%s' does not exist in table '%s@%s'. Update will fail\n", newparam, table, database); - } - - snprintf(sql, sizeof(sql), "UPDATE %s SET %s=?", table, newparam); + snprintf(sql, sizeof(sql), "UPDATE %s SET ", table); while((newparam = va_arg(aq, const char *))) { - va_arg(aq, const char *); - if ((tableptr && (column = ast_odbc_find_column(tableptr, newparam))) || count > 63) { - /* NULL test for integer-based columns */ - if (ast_strlen_zero(newparam) && tableptr && column && column->nullable && count < 64 && - (column->type == SQL_INTEGER || column->type == SQL_BIGINT || - column->type == SQL_SMALLINT || column->type == SQL_TINYINT || - column->type == SQL_NUMERIC || column->type == SQL_DECIMAL)) { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ", %s=NULL", newparam); + newval = va_arg(aq, const char *); + if ((tableptr && (column = ast_odbc_find_column(tableptr, newparam))) || count >= 64) { + if (paramcount++) { + snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ", "); + } + /* NULL test for non-text columns */ + if (count < 64 && ast_strlen_zero(newval) && column->nullable && !is_text(column) && !ast_odbc_allow_empty_string_in_nontext(obj)) { + snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s=NULL", newparam); cps.skip |= (1LL << count); } else { - snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ", %s=?", newparam); + /* Value is not an empty string, or column accepts empty strings, or we couldn't fit any more into cps.skip (count >= 64 ?!). */ + snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%s=?", newparam); } } else { /* the column does not exist in the table */ cps.skip |= (1LL << count); } - count++; + ++count; } va_end(aq); snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), " WHERE %s=?", keyfield); diff --git a/res/res_odbc.c b/res/res_odbc.c index 797f31ece8..3de4737be5 100644 --- a/res/res_odbc.c +++ b/res/res_odbc.c @@ -128,6 +128,7 @@ struct odbc_class unsigned int delme:1; /*!< Purge the class */ unsigned int backslash_is_escape:1; /*!< On this database, the backslash is a native escape sequence */ unsigned int forcecommit:1; /*!< Should uncommitted transactions be auto-committed on handle release? */ + unsigned int allow_empty_strings:1; /*!< Implicit conversion from an empty string to a number is valid for this database */ unsigned int isolation; /*!< Flags for how the DB should deal with data in other, uncommitted transactions */ unsigned int limit; /*!< Maximum number of database handles we will allow */ int count; /*!< Running count of pooled connections */ @@ -772,7 +773,7 @@ static int load_odbc_config(void) struct ast_variable *v; char *cat; const char *dsn, *username, *password, *sanitysql; - int enabled, pooling, limit, bse, conntimeout, forcecommit, isolation; + int enabled, pooling, limit, bse, conntimeout, forcecommit, isolation, allow_empty_strings; struct timeval ncache = { 0, 0 }; unsigned int idlecheck; int preconnect = 0, res = 0; @@ -801,6 +802,7 @@ static int load_odbc_config(void) bse = 1; conntimeout = 10; forcecommit = 0; + allow_empty_strings = 1; isolation = SQL_TXN_READ_COMMITTED; for (v = ast_variable_browse(config, cat); v; v = v->next) { if (!strcasecmp(v->name, "pooling")) { @@ -836,6 +838,8 @@ static int load_odbc_config(void) sanitysql = v->value; } else if (!strcasecmp(v->name, "backslash_is_escape")) { bse = ast_true(v->value); + } else if (!strcasecmp(v->name, "allow_empty_string_in_nontext")) { + allow_empty_strings = ast_true(v->value); } else if (!strcasecmp(v->name, "connect_timeout")) { if (sscanf(v->value, "%d", &conntimeout) != 1 || conntimeout < 1) { ast_log(LOG_WARNING, "connect_timeout must be a positive integer\n"); @@ -897,6 +901,7 @@ static int load_odbc_config(void) new->idlecheck = idlecheck; new->conntimeout = conntimeout; new->negative_connection_cache = ncache; + new->allow_empty_strings = allow_empty_strings ? 1 : 0; if (cat) ast_copy_string(new->name, cat, sizeof(new->name)); @@ -1114,6 +1119,11 @@ int ast_odbc_backslash_is_escape(struct odbc_obj *obj) return obj->parent->backslash_is_escape; } +int ast_odbc_allow_empty_string_in_nontext(struct odbc_obj *obj) +{ + return obj->parent->allow_empty_strings; +} + static int commit_exec(struct ast_channel *chan, const char *data) { struct odbc_txn_frame *tx; diff --git a/res/res_odbc.exports.in b/res/res_odbc.exports.in index ad674beb17..991daccaf6 100644 --- a/res/res_odbc.exports.in +++ b/res/res_odbc.exports.in @@ -2,6 +2,7 @@ global: LINKER_SYMBOL_PREFIXast_odbc_ast_str_SQLGetData; LINKER_SYMBOL_PREFIXast_odbc_backslash_is_escape; + LINKER_SYMBOL_PREFIXast_odbc_allow_empty_string_in_nontext; LINKER_SYMBOL_PREFIXast_odbc_clear_cache; LINKER_SYMBOL_PREFIXast_odbc_direct_execute; LINKER_SYMBOL_PREFIXast_odbc_find_column;