From 4d64e707bcb645d92787c718b6ee50cd2941158d Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Sun, 20 Jul 2014 23:13:56 +0200 Subject: [PATCH] Add AgDb class --- configure.ac | 1 + src/Makefile.am | 5 +- src/ag-db.c | 460 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ag-db.h | 37 ++++ 4 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 src/ag-db.c create mode 100644 src/ag-db.h diff --git a/configure.ac b/configure.ac index c7eb783..d5dfccb 100644 --- a/configure.ac +++ b/configure.ac @@ -35,6 +35,7 @@ PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.8]) PKG_CHECK_MODULES([LIBXML], [libxml-2.0]) PKG_CHECK_MODULES([LIBXSLT], [libexslt]) PKG_CHECK_MODULES([WEBKIT], [webkit2gtk-3.0]) +PKG_CHECK_MODULES([GDA], [libgda-5.0 libgda-sqlite-5.0]) PKG_CHECK_MODULES([SWE_GLIB], [swe-glib >= 2.1.0]) AC_CONFIG_FILES([ diff --git a/src/Makefile.am b/src/Makefile.am index f1e551c..94116d3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -18,6 +18,7 @@ astrognome_source_files = \ ag-chart.c \ ag-settings.c \ ag-preferences.c \ + ag-db.c \ astrognome.c \ $(NULL) @@ -30,7 +31,7 @@ AM_CPPFLAGS = -DG_LOG_DOMAIN=\"Astrognome\" -DLOCALEDIR=\"$(localedir)\" -DPKGDA bin_PROGRAMS = astrognome astrognome_SOURCES = $(astrognome_source_files) $(BUILT_SOURCES) -astrognome_LDADD = $(SWE_GLIB_LIBS) $(GTK_LIBS) $(LIBXML_LIBS) $(LIBXSLT_LIBS) $(WEBKIT_LIBS) +astrognome_LDADD = $(SWE_GLIB_LIBS) $(GTK_LIBS) $(LIBXML_LIBS) $(LIBXSLT_LIBS) $(WEBKIT_LIBS) $(GDA_LIBS) astrognome_LDFLAGS = -rdynamic -astrognome_CFLAGS = $(SWE_GLIB_CFLAGS) $(CFLAGS) $(GTK_CFLAGS) $(LIBXML_CFLAGS) $(LIBXSLT_CFLAGS) $(WEBKIT_CFLAGS) -Wall +astrognome_CFLAGS = $(SWE_GLIB_CFLAGS) $(CFLAGS) $(GTK_CFLAGS) $(LIBXML_CFLAGS) $(LIBXSLT_CFLAGS) $(WEBKIT_CFLAGS) $(GDA_CFLAGS) -Wall diff --git a/src/ag-db.c b/src/ag-db.c new file mode 100644 index 0000000..2e8c953 --- /dev/null +++ b/src/ag-db.c @@ -0,0 +1,460 @@ +#include +#include +#include +#include +#include + +#include "config.h" +#include "ag-db.h" + +#define SCHEMA_VERSION 1 + +static AgDb *singleton = NULL; + +typedef struct _AgDbPrivate { + gchar *dsn; + GdaConnection *conn; +} AgDbPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE(AgDb, ag_db, G_TYPE_OBJECT); + +static void +ag_db_non_select(AgDb *db, const gchar *sql) +{ + GdaStatement *sth; + gint nrows; + const gchar *remain; + GdaSqlParser *parser; + AgDbPrivate *priv = ag_db_get_instance_private(db); + GError *err = NULL; + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + g_assert(GDA_IS_SQL_PARSER(parser)); + + if ((sth = gda_sql_parser_parse_string( + parser, + sql, + &remain, + &err + )) == NULL) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + + if ((nrows = gda_connection_statement_execute_non_select( + priv->conn, + sth, + NULL, + NULL, + &err + )) == -1) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no details" + ); + } + + g_object_unref(sth); +} + +static gboolean +ag_db_table_exists(AgDb *db, const gchar *table) +{ + GdaSqlParser *parser; + GdaStatement *sth; + GdaDataModel *result; + GdaSet *params; + GdaHolder *holder; + const gchar *remain; + gboolean ret; + GValue table_value = G_VALUE_INIT; + AgDbPrivate *priv = ag_db_get_instance_private(db); + GError *err = NULL; + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + + if ((sth = gda_sql_parser_parse_string( + parser, + "SELECT type \n" \ + " FROM sqlite_master \n" \ + " WHERE type = 'table' \n" \ + " AND name = ##name::string", + &remain, + &err + )) == NULL) { + g_error( + "SQL error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + if (gda_statement_get_parameters(sth, ¶ms, &err) == FALSE) { + g_error( + "Params error: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + holder = gda_set_get_holder(params, "name"); + g_value_init(&table_value, G_TYPE_STRING); + g_value_set_string(&table_value, table); + gda_holder_set_value(holder, &table_value, &err); + g_value_unset(&table_value); + + result = gda_connection_statement_execute_select( + priv->conn, + sth, + params, + &err + ); + + if (gda_data_model_get_n_rows(result) > 0) { + ret = TRUE; + } else { + ret = FALSE; + } + + g_object_unref(result); + g_object_unref(sth); + + return ret; +} + +/** + * ag_db_select: + * @db: the database object to work on + * @err: a #GError or NULL + * @sql: the query to execute + * @...: a NULL terminated list of key-value pairs of the query parameters + * + * Returns: (transfer full): the #GdaDataModel as the result of the query + */ +static GdaDataModel * +ag_db_select(AgDb *db, GError **err, const gchar *sql, ...) +{ + GdaSqlParser *parser; + const gchar *remain; + GdaSet *params; + GdaStatement *sth; + GdaDataModel *ret; + gchar *error = NULL; + AgDbPrivate *priv = ag_db_get_instance_private(db); + + parser = g_object_get_data(G_OBJECT(priv->conn), "parser"); + + if ((sth = gda_sql_parser_parse_string( + parser, + sql, + &remain, + err + )) == NULL) { + g_error( + "SQL error: %s", + (*err && (*err)->message) + ? (*err)->message + : "no reason" + ); + } + + if (!gda_statement_get_parameters(sth, ¶ms, err)) { + g_error( + "SQL error: %s", + (*err && (*err)->message) + ? (*err)->message + : "no reason" + ); + } + + if (params) { + va_list ap; + + va_start(ap, sql); + while (TRUE) { + gchar *key; + GdaHolder *holder; + GType type; + GValue value = G_VALUE_INIT; + + if ((key = va_arg(ap, gchar *)) == NULL) { + break; + } + + if ((holder = gda_set_get_holder( + params, + (const gchar *)key + )) == NULL) { + g_error("Error: holder %s is not defined in query.", key); + } + + type = gda_holder_get_g_type(holder); + g_value_init(&value, type); + G_VALUE_COLLECT_INIT(&value, type, ap, 0, &error); + + if (error) { + g_error("SQL GValue error: %s", error); + } + + if (!gda_holder_set_value(holder, (const GValue *)&value, err)) { + g_error( + "SQL GdaHolder error: %s", + (*err && (*err)->message) + ? (*err)->message + : "no reason" + ); + } + } + } + + ret = gda_connection_statement_execute_select(priv->conn, sth, params, err); + g_object_unref(sth); + + return ret; +} + +static void +ag_db_check_version_table(AgDb *db) +{ + if (ag_db_table_exists(db, "version")) { + GdaDataModel *result; + gint ret; + + g_debug( + "Version table exists. " \ + "Checking if db_version is %d and app_version is %s", + SCHEMA_VERSION, + PACKAGE_VERSION + ); + result = ag_db_select( + db, + NULL, + "SELECT db_version, app_version FROM version", + NULL + ); + + ret = gda_data_model_get_n_rows(result); + + if (ret < 0) { + g_error("No number of rows?"); + } else if (ret > 1) { + g_error("Error in database. Version table has more than one rows!"); + } else if (ret == 0) { + // TODO: Check schema against current one maybe? If it’s fine, we + // may just add a row here. + g_error("Error in database. Version table has no rows!"); + } else { + // Version table has one row + const GValue *value; + GdaDataModelIter *iter = gda_data_model_create_iter(result); + gint version; + + gda_data_model_iter_move_next(iter); + value = gda_data_model_iter_get_value_at(iter, 0); + + if (!G_VALUE_HOLDS_INT(value)) { + g_error( + "Database is invalid. " \ + "version.db_version should be an integer." + ); + } + + version = g_value_get_int(value); + + if (version < SCHEMA_VERSION) { + // TODO + g_error("Update required!"); + } else if (version > SCHEMA_VERSION) { + const GValue *app_version_value; + const gchar *app_version; + + app_version_value = gda_data_model_iter_get_value_at( + iter, + 1 + ); + app_version = g_value_get_string( + app_version_value + ); + + g_error( + "The version of your database is from the future. " \ + "It seems it was created by Astrognome v%s.", + app_version + ); + } else { + g_object_unref(result); + } + } + } else { + GValue db_version = G_VALUE_INIT, + app_version = G_VALUE_INIT; + AgDbPrivate *priv = ag_db_get_instance_private(db); + + g_value_init(&db_version, G_TYPE_INT); + g_value_init(&app_version, G_TYPE_STRING); + + g_value_set_int(&db_version, SCHEMA_VERSION); + g_value_set_static_string(&app_version, PACKAGE_VERSION); + + ag_db_non_select( + db, + "CREATE TABLE version (" \ + "id INTEGER PRIMARY KEY, " \ + "db_version INTEGER UNIQUE NOT NULL, " \ + "app_version TEXT UNIQUE NOT NULL" \ + ")" + ); + + gda_connection_insert_row_into_table( + priv->conn, + "version", + NULL, + "db_version", &db_version, + "app_version", &app_version, + NULL + ); + } +} + +static void +ag_db_check_chart_table(AgDb *db) +{ + ag_db_non_select( + db, + "CREATE TABLE IF NOT EXISTS chart (" \ + "id INTEGER PRIMARY KEY, " \ + "name TEXT NOT NULL, " \ + "country_name TEXT, " \ + "city_name TEXT, " \ + "longitude DOUBLE NOT NULL, " \ + "latitude DOUBLE NOT NULL, " \ + "altitude DOUBLE, " \ + "year INTEGER NOT NULL, " \ + "month UNSIGNED INTEGER NOT NULL, " \ + "day UNSIGNED INTEGER NOT NULL, " \ + "hour UNSIGNED INTEGER NOT NULL, " \ + "minute UNSIGNED INTEGER NOT NULL, " \ + "second UNSIGNED INTEGER NOT NULL, " \ + "timezone DOUBLE NOT NULL, " \ + "house_system TEXT NOT NULL, " \ + "note TEXT" \ + ")" + ); +} + +static gint +ag_db_verify(AgDb *db) +{ + ag_db_check_version_table(db); + ag_db_check_chart_table(db); + + return 0; +} + +static void +ag_db_init(AgDb *db) +{ + GdaSqlParser *parser; + GFile *user_data_dir = g_file_new_for_path(g_get_user_data_dir()), + *ag_data_dir = g_file_get_child(user_data_dir, "astrognome"); + AgDbPrivate *priv = ag_db_get_instance_private(db); + gchar *path = g_file_get_path(ag_data_dir); + GError *err = NULL; + + gda_init(); + + if (!g_file_query_exists(ag_data_dir, NULL)) { + gchar *path = g_file_get_path(ag_data_dir); + + if (g_mkdir_with_parents(path, 0700) != 0) { + g_error( + "Data directory %s does not exist and can not be created.", + path + ); + } + } + + priv->dsn = g_strdup_printf("SQLite://DB_DIR=%s;DB_NAME=charts", path); + + g_free(path); + g_object_unref(user_data_dir); + g_object_unref(ag_data_dir); + + priv->conn = gda_connection_open_from_string( + NULL, + priv->dsn, + NULL, + GDA_CONNECTION_OPTIONS_NONE, + &err + ); + + if (priv->conn == NULL) { + g_error( + "Unable to initialize database: %s", + (err && err->message) + ? err->message + : "no reason" + ); + } + + if ((parser = gda_connection_create_parser(priv->conn)) == NULL) { + parser = gda_sql_parser_new(); + } + + g_object_set_data_full( + G_OBJECT(priv->conn), + "parser", + parser, + g_object_unref + ); + + ag_db_verify(db); +} + +static void +ag_db_dispose(GObject *gobject) +{ + AgDbPrivate *priv = ag_db_get_instance_private(AG_DB(gobject)); + + g_object_unref(priv->conn); + g_free(priv->dsn); + G_OBJECT_CLASS(ag_db_parent_class)->dispose(gobject); +} + +static void +ag_db_finalize(GObject *gobject) +{ + singleton = NULL; + + G_OBJECT_CLASS(ag_db_parent_class)->finalize(gobject); +} + +static void +ag_db_class_init(AgDbClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + gobject_class->dispose = ag_db_dispose; + gobject_class->finalize = ag_db_finalize; +} + +AgDb * +ag_db_get(void) +{ + if (!singleton) { + singleton = AG_DB(g_object_new(AG_TYPE_DB, NULL)); + } else { + g_object_ref(singleton); + } + + g_assert(singleton); + + return singleton; +} diff --git a/src/ag-db.h b/src/ag-db.h new file mode 100644 index 0000000..5b99040 --- /dev/null +++ b/src/ag-db.h @@ -0,0 +1,37 @@ +#ifndef __AG_DB_H__ +#define __AG_DB_H__ + +#include + +G_BEGIN_DECLS + +#define AG_TYPE_DB (ag_db_get_type()) +#define AG_DB(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + AG_TYPE_DB, \ + AgDb)) +#define AG_DB_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), \ + AG_TYPE_DB, \ + AgDbClass)) +#define AG_IS_DB(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), AG_TYPE_DB)) +#define AG_IS_DB_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), AG_TYPE_DB)) +#define AG_DB_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), \ + AG_TYPE_DB, \ + AgDbClass)) + +typedef struct _AgDb AgDb; +typedef struct _AgDbClass AgDbClass; + +struct _AgDb { + GObject parent_instance; +}; + +struct _AgDbClass { + GObjectClass parent_class; +}; + +GType ag_db_get_type(void) G_GNUC_CONST; +AgDb *ag_db_get(void); + +G_END_DECLS + +#endif /* __AG_DB_H__ */