diff options
author | Terry Wilson <twilson@digium.com> | 2011-09-11 17:09:36 +0000 |
---|---|---|
committer | Terry Wilson <twilson@digium.com> | 2011-09-11 17:09:36 +0000 |
commit | 1fed068bae88eea78912d0c6686a05f291b32478 (patch) | |
tree | f17d8dde2e8c930e1aa9100b79a269d74fe58e5e /res/res_config_sqlite3.c | |
parent | 8b5ba33fe0e69e8d31f27139daa03e2d05d0b6d2 (diff) |
Add SQLite 3 realtime support
git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@335129 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'res/res_config_sqlite3.c')
-rw-r--r-- | res/res_config_sqlite3.c | 1190 |
1 files changed, 1190 insertions, 0 deletions
diff --git a/res/res_config_sqlite3.c b/res/res_config_sqlite3.c new file mode 100644 index 000000000..76cb8d9ce --- /dev/null +++ b/res/res_config_sqlite3.c @@ -0,0 +1,1190 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2011, Terry Wilson + * + * Terry Wilson <twilson@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + * + * Please follow coding guidelines + * http://svn.digium.com/view/asterisk/trunk/doc/CODING-GUIDELINES + */ + +/*! \file + * + * \brief SQLite 3 configuration engine + * + * \author\verbatim Terry Wilson <twilson@digium.com> \endverbatim + * + * This is a realtime configuration engine for the SQLite 3 Database + * \ingroup resources + */ + +/*** MODULEINFO + <depend>sqlite3</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include <sqlite3.h> + +#include "asterisk/module.h" +#include "asterisk/config.h" +#include "asterisk/paths.h" +#include "asterisk/astobj2.h" +#include "asterisk/lock.h" +#include "asterisk/utils.h" +#include "asterisk/app.h" + +/*** DOCUMENTATION + ***/ + +static struct ast_config *realtime_sqlite3_load(const char *database, const char *table, const char *configfile, struct ast_config *config, struct ast_flags flags, const char *suggested_include_file, const char *who_asked); +static struct ast_variable *realtime_sqlite3(const char *database, const char *table, va_list ap); +static struct ast_config *realtime_sqlite3_multi(const char *database, const char *table, va_list ap); +static int realtime_sqlite3_update(const char *database, const char *table, const char *keyfield, const char *entity, va_list ap); +static int realtime_sqlite3_update2(const char *database, const char *table, va_list ap); +static int realtime_sqlite3_store(const char *database, const char *table, va_list ap); +static int realtime_sqlite3_destroy(const char *database, const char *table, const char *keyfield, const char *entity, va_list ap); +static int realtime_sqlite3_require(const char *database, const char *table, va_list ap); +static int realtime_sqlite3_unload(const char *database, const char *table); + +struct ast_config_engine sqlite3_config_engine = { + .name = "sqlite3", + .load_func = realtime_sqlite3_load, + .realtime_func = realtime_sqlite3, + .realtime_multi_func = realtime_sqlite3_multi, + .update_func = realtime_sqlite3_update, + .update2_func = realtime_sqlite3_update2, + .store_func = realtime_sqlite3_store, + .destroy_func = realtime_sqlite3_destroy, + .require_func = realtime_sqlite3_require, + .unload_func = realtime_sqlite3_unload, +}; + +enum { + REALTIME_SQLITE3_REQ_WARN, + REALTIME_SQLITE3_REQ_CLOSE, + REALTIME_SQLITE3_REQ_CHAR, +}; + +struct realtime_sqlite3_db { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(name); + AST_STRING_FIELD(filename); + ); + sqlite3 *handle; + pthread_t syncthread; + ast_cond_t cond; + unsigned int requirements:2; + unsigned int dirty:1; + unsigned int debug:1; + unsigned int exiting:1; + unsigned int wakeup:1; + unsigned int batch; +}; + +struct ao2_container *databases; +#define DB_BUCKETS 7 + +AST_MUTEX_DEFINE_STATIC(config_lock); + +/* We need a separate buffer for each field we might use concurrently */ +AST_THREADSTORAGE(escape_table_buf); +AST_THREADSTORAGE(escape_column_buf); +AST_THREADSTORAGE(escape_value_buf); + +static int realtime_sqlite3_execute_handle(struct realtime_sqlite3_db *db, const char *sql, int (*callback)(void*, int, char **, char **), void *arg, int sync); +void db_start_batch(struct realtime_sqlite3_db *db); +void db_stop_batch(struct realtime_sqlite3_db *db); + +static inline const char *sqlite3_escape_string_helper(struct ast_threadstorage *ts, const char *param) +{ + size_t maxlen = strlen(param) * 2 + sizeof("\"\""); + /* It doesn't appear that sqlite3_snprintf will do more than double the + * length of a string with %q as an option. %Q could double and possibly + * add two quotes, and convert NULL pointers to the word "NULL", but we + * don't allow those anyway. Just going to use %q for now. */ + struct ast_str *buf = ast_str_thread_get(ts, maxlen); + char *tmp = ast_str_buffer(buf); + char q = ts == &escape_value_buf ? '\'' : '"'; + + ast_str_reset(buf); + *tmp++ = q; /* Initial quote */ + while ((*tmp++ = *param++)) { + /* Did we just copy a quote? Then double it. */ + if (*(tmp - 1) == q) { + *tmp++ = q; + } + } + *tmp = '\0'; /* Terminate past NULL from copy */ + *(tmp - 1) = q; /* Replace original NULL with the quote */ + ast_str_update(buf); + + return ast_str_buffer(buf); +} + +static inline const char *sqlite3_escape_table(const char *param) +{ + return sqlite3_escape_string_helper(&escape_table_buf, param); +} + +static inline const char *sqlite3_escape_column(const char *param) +{ + return sqlite3_escape_string_helper(&escape_column_buf, param); +} + +/* Not inlining this function because it uses strdupa and I don't know if the compiler would be dumb */ +static const char *sqlite3_escape_column_op(const char *param) +{ + size_t maxlen = strlen(param) * 2 + sizeof("\"\" ="); + struct ast_str *buf = ast_str_thread_get(&escape_column_buf, maxlen); + char *tmp = ast_str_buffer(buf); + int space = 0; + + ast_str_reset(buf); + *tmp++ = '"'; + while ((*tmp++ = *param++)) { + /* If we have seen a space, don't double quotes. XXX If we ever make the column/op field + * available to users via an API, we will definitely need to avoid allowing special + * characters like ';' in the data past the space as it will be unquoted data */ + if (space) { + continue; + } + if (*(tmp - 1) == ' ') { + *(tmp - 1) = '"'; + *tmp++ = ' '; + space = 1; + } else if (*(tmp - 1) == '"') { + *tmp++ = '"'; + } + } + if (!space) { + strcpy(tmp - 1, "\" ="); + } + + ast_str_update(buf); + + return ast_str_buffer(buf); +} + +static inline const char *sqlite3_escape_value(const char *param) +{ + return sqlite3_escape_string_helper(&escape_value_buf, param); +} + +static int db_hash_fn(const void *obj, const int flags) +{ + const struct realtime_sqlite3_db *db = obj; + + return ast_str_hash(flags & OBJ_KEY ? (const char *) obj : db->name); +} + +static int db_cmp_fn(void *obj, void *arg, int flags) { + struct realtime_sqlite3_db *db = obj, *other = arg; + const char *name = arg; + + return !strcasecmp(db->name, flags & OBJ_KEY ? name : other->name) ? CMP_MATCH | CMP_STOP : 0; +} + +static void db_destructor(void *obj) +{ + struct realtime_sqlite3_db *db = obj; + + ast_debug(1, "Destroying db: %s\n", db->name); + ast_string_field_free_memory(db); + db_stop_batch(db); + if (db->handle) { + ao2_lock(db); + sqlite3_close(db->handle); + ao2_unlock(db); + } +} + +static struct realtime_sqlite3_db *find_database(const char *database) +{ + return ao2_find(databases, database, OBJ_KEY); +} + +static void unref_db(struct realtime_sqlite3_db **db) +{ + ao2_ref(*db, -1); + *db = NULL; +} + +static int stop_batch_cb(void *obj, void *arg, int flags) +{ + struct realtime_sqlite3_db *db = obj; + + db_stop_batch(db); + return CMP_MATCH; +} + +static int mark_dirty_cb(void *obj, void *arg, int flags) +{ + struct realtime_sqlite3_db *db = obj; + db->dirty = 1; + return CMP_MATCH; +} + +static void mark_all_databases_dirty(void) +{ + ao2_callback(databases, OBJ_MULTIPLE | OBJ_NODATA, mark_dirty_cb, NULL); +} + +static int is_dirty_cb(void *obj, void *arg, int flags) +{ + struct realtime_sqlite3_db *db = obj; + if (db->dirty) { + db_stop_batch(db); + return CMP_MATCH; + } + return 0; +} + +static void unlink_dirty_databases(void) +{ + ao2_callback(databases, OBJ_MULTIPLE | OBJ_NODATA | OBJ_UNLINK, is_dirty_cb, NULL); +} + +static int str_to_requirements(const char *data) +{ + if (!strcasecmp(data, "createclose")) { + return REALTIME_SQLITE3_REQ_CLOSE; + } else if (!strcasecmp(data, "createchar")) { + return REALTIME_SQLITE3_REQ_CHAR; + } + /* default */ + return REALTIME_SQLITE3_REQ_WARN; +} + +/*! \note Since this is called while a query is executing, we should already hold the db lock */ +static void trace_cb(void *arg, const char *sql) +{ + struct realtime_sqlite3_db *db = arg; + ast_debug(3, "DB: %s SQL: %s\n", db->name, sql); +} + +/*! \brief Wrap commands in transactions increased write performance */ +static void *db_sync_thread(void *data) +{ + struct realtime_sqlite3_db *db = data; + ao2_lock(db); + realtime_sqlite3_execute_handle(db, "BEGIN TRANSACTION", NULL, NULL, 0); + for (;;) { + if (!db->wakeup) { + ast_cond_wait(&db->cond, ao2_object_get_lockaddr(db)); + } + db->wakeup = 0; + if (realtime_sqlite3_execute_handle(db, "COMMIT", NULL, NULL, 0) < 0) { + realtime_sqlite3_execute_handle(db, "ROLLBACK", NULL, NULL, 0); + } + if (db->exiting) { + ao2_unlock(db); + break; + } + realtime_sqlite3_execute_handle(db, "BEGIN TRANSACTION", NULL, NULL, 0); + ao2_unlock(db); + usleep(1000 * db->batch); + ao2_lock(db); + } + + unref_db(&db); + + return NULL; +} + +/*! \brief Open a database and appropriately set debugging on the db handle */ +static int db_open(struct realtime_sqlite3_db *db) +{ + ao2_lock(db); + if (sqlite3_open(db->filename, &db->handle) != SQLITE_OK) { + ast_log(LOG_WARNING, "Could not open %s: %s\n", db->filename, sqlite3_errmsg(db->handle)); + ao2_unlock(db); + return -1; + } + + if (db->debug) { + sqlite3_trace(db->handle, trace_cb, db); + } else { + sqlite3_trace(db->handle, NULL, NULL); + } + + ao2_unlock(db); + + return 0; +} + +static void db_sync(struct realtime_sqlite3_db *db) +{ + db->wakeup = 1; + ast_cond_signal(&db->cond); +} + +void db_start_batch(struct realtime_sqlite3_db *db) +{ + if (db->batch) { + ast_cond_init(&db->cond, NULL); + ao2_ref(db, +1); + ast_pthread_create_background(&db->syncthread, NULL, db_sync_thread, db); + } +} + +void db_stop_batch(struct realtime_sqlite3_db *db) +{ + if (db->batch) { + db->exiting = 1; + db_sync(db); + pthread_join(db->syncthread, NULL); + } +} + +/*! \brief Create a db object based on a config category + * \note Opening the db handle and linking to databases must be handled outside of this function + */ +static struct realtime_sqlite3_db *new_realtime_sqlite3_db(struct ast_config *config, const char *cat) +{ + struct ast_variable *var; + struct realtime_sqlite3_db *db; + + if (!(db = ao2_alloc(sizeof(*db), db_destructor))) { + return NULL; + } + + if (ast_string_field_init(db, 64)) { + unref_db(&db); + return NULL; + } + + /* Set defaults */ + db->requirements = REALTIME_SQLITE3_REQ_WARN; + db->batch = 100; + ast_string_field_set(db, name, cat); + + for (var = ast_variable_browse(config, cat); var; var = var->next) { + if (!strcasecmp(var->name, "dbfile")) { + ast_string_field_set(db, filename, var->value); + } else if (!strcasecmp(var->name, "requirements")) { + db->requirements = str_to_requirements(var->value); + } else if (!strcasecmp(var->name, "batch")) { + ast_app_parse_timelen(var->value, (int *) &db->batch, TIMELEN_MILLISECONDS); + } else if (!strcasecmp(var->name, "debug")) { + db->debug = ast_true(var->value); + } + } + + if (ast_strlen_zero(db->filename)) { + ast_log(LOG_WARNING, "Must specify dbfile in res_config_sqlite3.conf\n"); + unref_db(&db); + return NULL; + } + + return db; +} + +/*! \brief Update an existing db object based on config data + * \param db The database object to update + * \param config The configuration data with which to update the db + * \param cat The config category (which becomes db->name) + */ +static int update_realtime_sqlite3_db(struct realtime_sqlite3_db *db, struct ast_config *config, const char *cat) +{ + struct realtime_sqlite3_db *new; + + if (!(new = new_realtime_sqlite3_db(config, cat))) { + return -1; + } + + /* Copy fields that don't need anything special done on change */ + db->requirements = new->requirements; + + /* Handle changes that require immediate behavior modification */ + if (db->debug != new->debug) { + if (db->debug) { + sqlite3_trace(db->handle, NULL, NULL); + } else { + sqlite3_trace(db->handle, trace_cb, db); + } + db->debug = new->debug; + } + + if (strcmp(db->filename, new->filename)) { + sqlite3_close(db->handle); + ast_string_field_set(db, filename, new->filename); + db_open(db); /* Also handles setting appropriate debug on new handle */ + } + + if (db->batch != new->batch) { + if (db->batch == 0) { + db->batch = new->batch; + db_start_batch(db); + } else if (new->batch == 0) { + db->batch = new->batch; + db_stop_batch(db); + } + db->batch = new->batch; + } + + db->dirty = 0; + unref_db(&new); + + return 0; +} + +/*! \brief Create a varlist from a single sqlite3 result row */ +static int row_to_varlist(void *arg, int num_columns, char **values, char **columns) +{ + struct ast_variable **head = arg, *tail; + int i; + struct ast_variable *new; + + if (!(new = ast_variable_new(columns[0], S_OR(values[0], ""), ""))) { + return SQLITE_ABORT; + } + *head = tail = new; + + for (i = 1; i < num_columns; i++) { + if (!(new = ast_variable_new(columns[i], S_OR(values[i], ""), ""))) { + ast_variables_destroy(*head); + *head = NULL; + return SQLITE_ABORT; + } + tail->next = new; + tail = new; + } + + return 0; +} + +/*! \brief Callback for creating an ast_config from a successive sqlite3 result rows */ +static int append_row_to_cfg(void *arg, int num_columns, char **values, char **columns) +{ + struct ast_config *cfg = arg; + struct ast_category *cat; + int i; + + if (!(cat = ast_category_new("", "", 99999))) { + return SQLITE_ABORT; + } + + for (i = 0; i < num_columns; i++) { + struct ast_variable *var; + if (!(var = ast_variable_new(columns[i], S_OR(values[i], ""), ""))) { + ast_log(LOG_ERROR, "Could not create new variable for '%s: %s', throwing away list\n", columns[i], values[i]); + continue; + } + ast_variable_append(cat, var); + } + ast_category_append(cfg, cat); + + return 0; +} + +/*! + * Structure sent to the SQLite 3 callback function for static configuration. + * + * \see static_realtime_cb() + */ +struct cfg_entry_args { + struct ast_config *cfg; + struct ast_category *cat; + char *cat_name; + struct ast_flags flags; + const char *who_asked; +}; + +/*! Exeute an SQL statement given the database object + * + * \retval -1 ERROR + * \retval > -1 Number of rows changed + */ +static int realtime_sqlite3_execute_handle(struct realtime_sqlite3_db *db, const char *sql, int (*callback)(void*, int, char **, char **), void *arg, int sync) +{ + int res = 0; + char *errmsg; + + ao2_lock(db); + if (sqlite3_exec(db->handle, sql, callback, arg, &errmsg) != SQLITE_OK) { + ast_log(LOG_WARNING, "Could not execute '%s': %s\n", sql, errmsg); + sqlite3_free(errmsg); + res = -1; + } else { + res = sqlite3_changes(db->handle); + } + ao2_unlock(db); + + if (sync) { + db_sync(db); + } + + return res; +} + +/*! Exeute an SQL statement give the database name + * + * \retval -1 ERROR + * \retval > -1 Number of rows changed + */ +static int realtime_sqlite3_execute(const char *database, const char *sql, int (*callback)(void*, int, char **, char **), void *arg, int sync) +{ + struct realtime_sqlite3_db *db; + int res; + + if (!(db = find_database(database))) { + ast_log(LOG_WARNING, "Could not find database: %s\n", database); + return -1; + } + + res = realtime_sqlite3_execute_handle(db, sql, callback, arg, sync); + ao2_ref(db, -1); + + return res; +} + +/*! \note It is important that the COL_* enum matches the order of the columns selected in static_sql */ +static const char *static_sql = "SELECT category, var_name, var_val FROM \"%q\" WHERE filename = %Q AND commented = 0 ORDER BY cat_metric ASC, var_metric ASC"; +enum { + COL_CATEGORY, + COL_VAR_NAME, + COL_VAR_VAL, + COL_COLUMNS, +}; + +static int static_realtime_cb(void *arg, int num_columns, char **values, char **columns) +{ + struct cfg_entry_args *args = arg; + struct ast_variable *var; + + if (!strcmp(values[COL_VAR_NAME], "#include")) { + struct ast_config *cfg; + char *val; + + val = values[COL_VAR_VAL]; + if (!(cfg = ast_config_internal_load(val, args->cfg, args->flags, "", args->who_asked))) { + ast_log(LOG_WARNING, "Unable to include %s\n", val); + return SQLITE_ABORT; + } else { + args->cfg = cfg; + return 0; + } + } + + if (!args->cat_name || strcmp(args->cat_name, values[COL_CATEGORY])) { + if (!(args->cat = ast_category_new(values[COL_CATEGORY], "", 99999))) { + ast_log(LOG_WARNING, "Unable to allocate category\n"); + return SQLITE_ABORT; + } + + ast_free(args->cat_name); + + if (!(args->cat_name = ast_strdup(values[COL_CATEGORY]))) { + ast_category_destroy(args->cat); + return SQLITE_ABORT; + } + + ast_category_append(args->cfg, args->cat); + } + + if (!(var = ast_variable_new(values[COL_VAR_NAME], values[COL_VAR_VAL], ""))) { + ast_log(LOG_WARNING, "Unable to allocate variable"); + return SQLITE_ABORT; + } + + ast_variable_append(args->cat, var); + + return 0; +} + +/*! \brief Realtime callback for static realtime + * \return ast_config on success, NULL on failure + */ +static struct ast_config *realtime_sqlite3_load(const char *database, const char *table, const char *configfile, struct ast_config *config, struct ast_flags flags, const char *suggested_include_file, const char *who_asked) +{ + char *sql; + struct cfg_entry_args args; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return NULL; + } + + if (!(sql = sqlite3_mprintf(static_sql, table, configfile))) { + ast_log(LOG_WARNING, "Couldn't allocate query\n"); + return NULL; + }; + + args.cfg = config; + args.cat = NULL; + args.cat_name = NULL; + args.flags = flags; + args.who_asked = who_asked; + + realtime_sqlite3_execute(database, sql, static_realtime_cb, &args, 0); + + sqlite3_free(sql); + + return config; +} + +/*! \brief Helper function for single and multi-row realtime load functions */ +static int realtime_sqlite3_helper(const char *database, const char *table, va_list ap, int is_multi, void *arg) +{ + struct ast_str *sql; + const char *param, *value; + int first = 1; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(sql = ast_str_create(128))) { + return -1; + } + + while ((param = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s %s", sqlite3_escape_table(table), + sqlite3_escape_column_op(param), sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&sql, 0, " AND %s %s", sqlite3_escape_column_op(param), + sqlite3_escape_value(value)); + } + } + + if (!is_multi) { + ast_str_append(&sql, 0, "%s", " LIMIT 1"); + } + + if (realtime_sqlite3_execute(database, ast_str_buffer(sql), is_multi ? append_row_to_cfg : row_to_varlist, arg, 0) < 0) { + ast_free(sql); + return -1; + } + + ast_free(sql); + + return 0; +} + +/*! \brief Realtime callback for a single row query + * \return ast_variable list for single result on success, NULL on empty/failure + */ +static struct ast_variable *realtime_sqlite3(const char *database, const char *table, va_list ap) +{ + struct ast_variable *result_row = NULL; + + realtime_sqlite3_helper(database, table, ap, 0, &result_row); + + return result_row; +} + +/*! \brief Realtime callback for a multi-row query + * \return ast_config containing possibly many results on success, NULL on empty/failure + */ +static struct ast_config *realtime_sqlite3_multi(const char *database, const char *table, va_list ap) +{ + struct ast_config *cfg; + + if (!(cfg = ast_config_new())) { + return NULL; + } + + if (realtime_sqlite3_helper(database, table, ap, 1, cfg)) { + ast_config_destroy(cfg); + return NULL; + } + + return cfg; +} + +/*! \brief Realtime callback for updating a row based on a single criteria + * \return Number of rows affected or -1 on error + */ +static int realtime_sqlite3_update(const char *database, const char *table, const char *keyfield, const char *entity, va_list ap) +{ + struct ast_str *sql; + const char *key, *value; + int first = 1, res; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(sql = ast_str_create(128))) { + return -1; + } + + while ((key = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&sql, 0, "UPDATE %s SET %s = %s", + sqlite3_escape_table(table), sqlite3_escape_column(key), sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&sql, 0, ", %s = %s", sqlite3_escape_column(key), sqlite3_escape_value(value)); + } + } + + ast_str_append(&sql, 0, " WHERE %s %s", sqlite3_escape_column_op(keyfield), sqlite3_escape_value(entity)); + + res = realtime_sqlite3_execute(database, ast_str_buffer(sql), NULL, NULL, 1); + ast_free(sql); + + return res; +} + +/*! \brief Realtime callback for updating a row based on multiple criteria + * \return Number of rows affected or -1 on error + */ +static int realtime_sqlite3_update2(const char *database, const char *table, va_list ap) +{ + struct ast_str *sql; + struct ast_str *where_clause; + const char *key, *value; + int first = 1, res; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(sql = ast_str_create(128))) { + return -1; + } + + if (!(where_clause = ast_str_create(128))) { + ast_free(sql); + return -1; + } + + while ((key = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&where_clause, 0, " WHERE %s %s", sqlite3_escape_column_op(key), sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&where_clause, 0, " AND %s %s", sqlite3_escape_column_op(key), sqlite3_escape_value(value)); + } + } + + first = 1; + while ((key = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&sql, 0, "UPDATE %s SET %s = %s", sqlite3_escape_table(table), sqlite3_escape_column(key), sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&sql, 0, ", %s = %s", sqlite3_escape_column(key), sqlite3_escape_value(value)); + } + } + + ast_str_append(&sql, 0, "%s", ast_str_buffer(where_clause)); + + res = realtime_sqlite3_execute(database, ast_str_buffer(sql), NULL, NULL, 1); + + ast_free(sql); + ast_free(where_clause); + + return res; +} + +/*! \brief Realtime callback for inserting a row + * \return Number of rows affected or -1 on error + */ +static int realtime_sqlite3_store(const char *database, const char *table, va_list ap) +{ + struct ast_str *sql, *values; + const char *column, *value; + int first = 1, res; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(sql = ast_str_create(128))) { + return -1; + } + + if (!(values = ast_str_create(128))) { + ast_free(sql); + return -1; + } + + while ((column = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&sql, 0, "INSERT INTO %s (%s", sqlite3_escape_table(table), sqlite3_escape_column(column)); + ast_str_set(&values, 0, ") VALUES (%s", sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&sql, 0, ", %s", sqlite3_escape_column(column)); + ast_str_append(&values, 0, ", %s", sqlite3_escape_value(value)); + } + } + + ast_str_append(&sql, 0, "%s)", ast_str_buffer(values)); + + res = realtime_sqlite3_execute(database, ast_str_buffer(sql), NULL, NULL, 1); + + ast_free(sql); + ast_free(values); + + return res; +} + +/*! \brief Realtime callback for deleting a row + * \return Number of rows affected or -1 on error + */ +static int realtime_sqlite3_destroy(const char *database, const char *table, const char *keyfield, const char *entity, va_list ap) +{ + struct ast_str *sql; + const char *param, *value; + int first = 1, res; + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(sql = ast_str_create(128))) { + return -1; + } + + while ((param = va_arg(ap, const char *)) && (value = va_arg(ap, const char *))) { + if (first) { + ast_str_set(&sql, 0, "DELETE FROM %s WHERE %s %s", sqlite3_escape_table(table), + sqlite3_escape_column_op(param), sqlite3_escape_value(value)); + first = 0; + } else { + ast_str_append(&sql, 0, " AND %s %s", sqlite3_escape_column_op(param), sqlite3_escape_value(value)); + } + } + + res = realtime_sqlite3_execute(database, ast_str_buffer(sql), NULL, NULL, 1); + + ast_free(sql); + + return res; +} + +/*! \brief Convert Asterisk realtime types to SQLite 3 types + * \note SQLite 3 has NULL, INTEGER, REAL, TEXT, and BLOB types. Any column other than + * an INTEGER PRIMARY KEY will actually store any kind of data due to its dynamic + * typing. When we create columns, we'll go ahead and use these base types instead + * of messing with column widths, etc. */ + +static const char *get_sqlite_column_type(int type) +{ + switch(type) { + case RQ_INTEGER1 : + case RQ_UINTEGER1 : + case RQ_INTEGER2 : + case RQ_UINTEGER2 : + case RQ_INTEGER3 : + case RQ_UINTEGER3 : + case RQ_INTEGER4 : + case RQ_UINTEGER4 : + case RQ_INTEGER8 : + return "INTEGER"; + case RQ_UINTEGER8 : /* SQLite3 stores INTEGER as signed 8-byte */ + case RQ_CHAR : + case RQ_DATE : + case RQ_DATETIME : + return "TEXT"; + case RQ_FLOAT : + return "REAL"; + default : + return "TEXT"; + } + + return "TEXT"; +} + +/*! \brief Create a table if ast_realtime_require shows that we are configured to handle the data + */ +static int handle_missing_table(struct realtime_sqlite3_db *db, const char *table, va_list ap) +{ + const char *column; + int type, first = 1, res; + size_t sz; + struct ast_str *sql; + + if (!(sql = ast_str_create(128))) { + return -1; + } + + while ((column = va_arg(ap, typeof(column))) && (type = va_arg(ap, typeof(type))) && (sz = va_arg(ap, typeof(sz)))) { + if (first) { + ast_str_set(&sql, 0, "CREATE TABLE IF NOT EXISTS %s (%s %s", sqlite3_escape_table(table), + sqlite3_escape_column(column), get_sqlite_column_type(type)); + first = 0; + } else { + ast_str_append(&sql, 0, ", %s %s", sqlite3_escape_column(column), get_sqlite_column_type(type)); + } + } + + ast_str_append(&sql, 0, ")"); + + res = realtime_sqlite3_execute_handle(db, ast_str_buffer(sql), NULL, NULL, 1) < 0 ? -1 : 0; + ast_free(sql); + + return res; +} + +/*! \brief If ast_realtime_require sends info about a column we don't have, create it + */ +static int handle_missing_column(struct realtime_sqlite3_db *db, const char *table, const char *column, int type, size_t sz) +{ + char *sql; + const char *sqltype = get_sqlite_column_type(type); + int res; + + if (db->requirements == REALTIME_SQLITE3_REQ_WARN) { + ast_log(LOG_WARNING, "Missing column '%s' of type '%s' in %s.%s\n", column, sqltype, db->name, table); + return -1; + } else if (db->requirements == REALTIME_SQLITE3_REQ_CHAR) { + sqltype = "TEXT"; + } + + if (!(sql = sqlite3_mprintf("ALTER TABLE \"%q\" ADD COLUMN \"%q\" %s", table, column, sqltype))) { + return -1; + } + + if (!(res = realtime_sqlite3_execute_handle(db, sql, NULL, NULL, 1) < 0 ? -1 : 0)) { + ast_log(LOG_NOTICE, "Creating column '%s' type %s for table %s\n", column, sqltype, table); + } + + sqlite3_free(sql); + + return res; +} + +static int str_hash_fn(const void *obj, const int flags) +{ + return ast_str_hash((const char *) obj); +} + +static int str_cmp_fn(void *obj, void *arg, int flags) { + return !strcasecmp((const char *) obj, (const char *) arg); +} + +/*! \brief Callback for creating a hash of column names for comparison in realtime_sqlite3_require + */ +static int add_column_name(void *arg, int num_columns, char **values, char **columns) +{ + char *column; + struct ao2_container *cnames = arg; + + + if (!(column = ao2_alloc(strlen(values[1]) + 1, NULL))) { + return -1; + } + + strcpy(column, values[1]); + + ao2_link(cnames, column); + ao2_ref(column, -1); + + return 0; +} + +/*! \brief Callback for ast_realtime_require + * \retval 0 Required fields met specified standards + * \retval -1 One or more fields was missing or insufficient + */ +static int realtime_sqlite3_require(const char *database, const char *table, va_list ap) +{ + const char *column; + char *sql; + int type; + int res; + size_t sz; + struct ao2_container *columns; + struct realtime_sqlite3_db *db; + + /* SQLite3 columns are dynamically typed, with type affinity. Built-in functions will + * return the results as char * anyway. The only field that that cannot contain text + * data is an INTEGER PRIMARY KEY, which must be a 64-bit signed integer. So, for + * the purposes here we really only care whether the column exists and not what its + * type or length is. */ + + if (ast_strlen_zero(table)) { + ast_log(LOG_WARNING, "Must have a table to query!\n"); + return -1; + } + + if (!(db = find_database(database))) { + return -1; + } + + if (!(columns = ao2_container_alloc(31, str_hash_fn, str_cmp_fn))) { + unref_db(&db); + return -1; + } + + if (!(sql = sqlite3_mprintf("PRAGMA table_info(\"%q\")", table))) { + unref_db(&db); + ao2_ref(columns, -1); + return -1; + } + + if ((res = realtime_sqlite3_execute_handle(db, sql, add_column_name, columns, 0)) < 0) { + unref_db(&db); + ao2_ref(columns, -1); + sqlite3_free(sql); + return -1; + } else if (res == 0) { + /* Table does not exist */ + sqlite3_free(sql); + res = handle_missing_table(db, table, ap); + ao2_ref(columns, -1); + unref_db(&db); + return res; + } + + sqlite3_free(sql); + + while ((column = va_arg(ap, typeof(column))) && (type = va_arg(ap, typeof(type))) && (sz = va_arg(ap, typeof(sz)))) { + char *found; + if (!(found = ao2_find(columns, column, OBJ_POINTER | OBJ_UNLINK))) { + if (handle_missing_column(db, table, column, type, sz)) { + unref_db(&db); + ao2_ref(columns, -1); + return -1; + } + } else { + ao2_ref(found, -1); + } + } + + ao2_ref(columns, -1); + unref_db(&db); + + return 0; +} + +/*! \brief Callback for clearing any cached info + * \note We don't currently cache anything + * \retval 0 If any cache was purged + * \retval -1 If no cache was found + */ +static int realtime_sqlite3_unload(const char *database, const char *table) +{ + /* We currently do no caching */ + return -1; +} + +/*! \brief Parse the res_config_sqlite3 config file + */ +static int parse_config(int reload) +{ + struct ast_config *config; + struct ast_flags config_flags = { CONFIG_FLAG_NOREALTIME | (reload ? CONFIG_FLAG_FILEUNCHANGED : 0) }; + static const char *config_filename = "res_config_sqlite3.conf"; + + config = ast_config_load(config_filename, config_flags); + + if (config == CONFIG_STATUS_FILEUNCHANGED) { + ast_debug(1, "%s was unchanged, skipping parsing\n", config_filename); + return 0; + } + + ast_mutex_lock(&config_lock); + + if (config == CONFIG_STATUS_FILEMISSING || config == CONFIG_STATUS_FILEINVALID) { + ast_log(LOG_ERROR, "%s config file '%s'\n", + config == CONFIG_STATUS_FILEMISSING ? "Missing" : "Invalid", config_filename); + } else { + const char *cat; + struct realtime_sqlite3_db *db; + + mark_all_databases_dirty(); + for (cat = ast_category_browse(config, NULL); cat; cat = ast_category_browse(config, cat)) { + if (!strcasecmp(cat, "general")) { + continue; + } + if (!(db = find_database(cat))) { + if (!(db = new_realtime_sqlite3_db(config, cat))) { + ast_log(LOG_WARNING, "Could not allocate new db for '%s' - skipping.\n", cat); + continue; + } + if (db_open(db)) { + unref_db(&db); + continue; + } + db_start_batch(db); + ao2_link(databases, db); + unref_db(&db); + } else { + if (update_realtime_sqlite3_db(db, config, cat)) { + unref_db(&db); + continue; + } + unref_db(&db); + } + } + unlink_dirty_databases(); + } + + ast_mutex_unlock(&config_lock); + + ast_config_destroy(config); + + return 0; +} + +static int reload(void) +{ + parse_config(1); + return 0; +} + +static int unload_module(void) +{ + ast_mutex_lock(&config_lock); + ao2_callback(databases, OBJ_MULTIPLE | OBJ_NODATA | OBJ_UNLINK, stop_batch_cb, NULL); + ao2_ref(databases, -1); + databases = NULL; + ast_config_engine_deregister(&sqlite3_config_engine); + ast_mutex_unlock(&config_lock); + + return 0; +} + +static int load_module(void) +{ + if (!((databases = ao2_container_alloc(DB_BUCKETS, db_hash_fn, db_cmp_fn)))) { + return AST_MODULE_LOAD_FAILURE; + } + + if (parse_config(0)) { + ao2_ref(databases, -1); + return AST_MODULE_LOAD_FAILURE; + } + + if (!(ast_config_engine_register(&sqlite3_config_engine))) { + ast_log(LOG_ERROR, "The config API must have changed, this shouldn't happen.\n"); + ao2_ref(databases, -1); + return AST_MODULE_LOAD_FAILURE; + } + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SQLite 3 realtime config engine", + .load = load_module, + .unload = unload_module, + .reload = reload, + .load_pri = AST_MODPRI_REALTIME_DRIVER, +); |