#include #include #include #include #include #include "config.h" #include "ag-app.h" #include "ag-db.h" #define SCHEMA_VERSION 1 static AgDb *singleton = NULL; typedef struct _AgDbPrivate { gchar *dsn; GdaConnection *conn; } AgDbPrivate; G_DEFINE_QUARK(ag_db_error_quark, ag_db_error); G_DEFINE_TYPE_WITH_PRIVATE(AgDb, ag_db, G_TYPE_OBJECT); typedef enum { COLUMN_ID, COLUMN_NAME, COLUMN_COUNTRY, COLUMN_CITY, COLUMN_LONGITUDE, COLUMN_LATITUDE, COLUMN_ALTITUDE, COLUMN_YEAR, COLUMN_MONTH, COLUMN_DAY, COLUMN_HOUR, COLUMN_MINUTE, COLUMN_SECOND, COLUMN_TIMEZONE, COLUMN_HOUSE_SYSTEM, COLUMN_NOTE, /* Leave this as the last element */ COLUMN_COUNT } ChartTableColumn; typedef struct { ChartTableColumn id; gchar *name; } ChartTableColumnDef; static ChartTableColumnDef chart_table_column[] = { { COLUMN_ID, "id" }, { COLUMN_NAME, "name" }, { COLUMN_COUNTRY, "country_name" }, { COLUMN_CITY, "city_name" }, { COLUMN_LONGITUDE, "longitude" }, { COLUMN_LATITUDE, "latitude" }, { COLUMN_ALTITUDE, "altitude" }, { COLUMN_YEAR, "year" }, { COLUMN_MONTH, "month" }, { COLUMN_DAY, "day" }, { COLUMN_HOUR, "hour" }, { COLUMN_MINUTE, "minute" }, { COLUMN_SECOND, "second" }, { COLUMN_TIMEZONE, "timezone" }, { COLUMN_HOUSE_SYSTEM, "house_system" }, { COLUMN_NOTE, "note" }, }; 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; GError *local_err = 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, &local_err )) == NULL) { g_error( "SQL error: %s", (local_err && local_err->message) ? local_err->message : "no reason" ); } if (!gda_statement_get_parameters(sth, ¶ms, &local_err)) { g_error( "SQL error: %s", (local_err && local_err->message) ? local_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; } void ag_db_save_data_free(AgDbSave *save_data) { if (!save_data) { return; } if (save_data->name) { g_free(save_data->name); } if (save_data->country) { g_free(save_data->country); } if (save_data->city) { g_free(save_data->city); } if (save_data->house_system) { g_free(save_data->house_system); } if (save_data->note) { g_free(save_data->note); } g_free(save_data); } gboolean ag_db_save_chart(AgDb *db, AgDbSave *save_data, GtkWidget *window, GError **err) { GValue db_id = G_VALUE_INIT, name = G_VALUE_INIT, country = G_VALUE_INIT, city = G_VALUE_INIT, longitude = G_VALUE_INIT, latitude = G_VALUE_INIT, altitude = G_VALUE_INIT, year = G_VALUE_INIT, month = G_VALUE_INIT, day = G_VALUE_INIT, hour = G_VALUE_INIT, minute = G_VALUE_INIT, second = G_VALUE_INIT, timezone = G_VALUE_INIT, house_system = G_VALUE_INIT, note = G_VALUE_INIT; AgDbPrivate *priv = ag_db_get_instance_private(db); g_value_init(&name, G_TYPE_STRING); g_value_set_string(&name, save_data->name); g_value_init(&country, G_TYPE_STRING); g_value_set_string(&country, save_data->country); g_value_init(&city, G_TYPE_STRING); g_value_set_string(&city, save_data->city); g_value_init(&longitude, G_TYPE_DOUBLE); g_value_set_double(&longitude, save_data->longitude); g_value_init(&latitude, G_TYPE_DOUBLE); g_value_set_double(&latitude, save_data->latitude); g_value_init(&altitude, G_TYPE_DOUBLE); g_value_set_double(&altitude, save_data->altitude); g_value_init(&year, G_TYPE_INT); g_value_set_int(&year, save_data->year); g_value_init(&month, G_TYPE_UINT); g_value_set_uint(&month, save_data->month); g_value_init(&day, G_TYPE_UINT); g_value_set_uint(&day, save_data->day); g_value_init(&hour, G_TYPE_UINT); g_value_set_uint(&hour, save_data->hour); g_value_init(&minute, G_TYPE_UINT); g_value_set_uint(&minute, save_data->minute); g_value_init(&second, G_TYPE_UINT); g_value_set_uint(&second, save_data->second); g_value_init(&timezone, G_TYPE_DOUBLE); g_value_set_double(&timezone, save_data->timezone); g_value_init(&house_system, G_TYPE_STRING); g_value_set_string(&house_system, save_data->house_system); g_value_init(¬e, G_TYPE_STRING); g_value_set_string(¬e, save_data->note); if (save_data->db_id == -1) { if (!gda_connection_insert_row_into_table( priv->conn, "chart", err, "name", &name, "country_name", &country, "city_name", &city, "longitude", &longitude, "latitude", &latitude, "altitude", &altitude, "year", &year, "month", &month, "day", &day, "hour", &hour, "minute", &minute, "second", &second, "timezone", &timezone, "house_system", &house_system, "note", ¬e, NULL )) { ag_app_message_dialog( window, GTK_MESSAGE_ERROR, "Unable to save: %s", (*err && (*err)->message) ? (*err)->message : "no reason" ); } } else { g_value_init(&db_id, G_TYPE_INT); g_value_set_int(&db_id, save_data->db_id); if (!gda_connection_update_row_in_table( priv->conn, "chart", "id", &db_id, err, "name", &name, "country_name", &country, "city_name", &city, "longitude", &longitude, "latitude", &latitude, "altitude", &altitude, "year", &year, "month", &month, "day", &day, "hour", &hour, "minute", &minute, "second", &second, "timezone", &timezone, "house_system", &house_system, "note", ¬e, NULL )) { ag_app_message_dialog( window, GTK_MESSAGE_ERROR, "Unable to save: %s", (*err && (*err)->message) ? (*err)->message : "no reason" ); } g_value_unset(&db_id); } g_value_unset(¬e); g_value_unset(&house_system); g_value_unset(&timezone); g_value_unset(&second); g_value_unset(&minute); g_value_unset(&hour); g_value_unset(&day); g_value_unset(&month); g_value_unset(&year); g_value_unset(&altitude); g_value_unset(&latitude); g_value_unset(&longitude); g_value_unset(&city); g_value_unset(&country); g_value_unset(&name); return FALSE; } GList * ag_db_get_chart_list(AgDb *db, GError **err) { GdaDataModelIter *iter; GList *ret = NULL; GdaDataModel *result = ag_db_select( db, err, "SELECT id, name FROM chart ORDER BY name", NULL ); if (result == NULL) { return NULL; } iter = gda_data_model_create_iter(result); while (gda_data_model_iter_move_next(iter)) { const GValue *value; AgDbSave *save_data = g_new0(AgDbSave, 1); value = gda_data_model_iter_get_value_at(iter, 0); save_data->db_id = g_value_get_int(value); value = gda_data_model_iter_get_value_at(iter, 1); save_data->name = g_strdup(g_value_get_string(value)); ret = g_list_prepend(ret, save_data); } return g_list_reverse(ret); } AgDbSave * ag_db_get_chart_data_by_id(AgDb *db, guint row_id, GError **err) { AgDbSave *save_data; const GValue *value; GdaValueAttribute attributes; gchar *query, *columns; guint i; GdaDataModel *result; GError *local_err = NULL; columns = NULL; for (i = 1; i < COLUMN_COUNT; i++) { gchar *tmp; if (i == 1) { columns = g_strjoin( ", ", chart_table_column[0].name, chart_table_column[1].name, NULL ); } else { tmp = g_strjoin(", ", columns, chart_table_column[i].name, NULL); g_free(columns); columns = tmp; } } query = g_strdup_printf( "SELECT %s FROM chart WHERE id = ##id::gint", columns ); g_free(columns); result = ag_db_select(db, &local_err, query, "id", row_id, NULL); g_free(query); if (local_err && (local_err->message)) { return NULL; } if (gda_data_model_get_n_rows(result) < 1) { g_set_error( err, AG_DB_ERROR, AG_DB_ERROR_NO_CHART, "Chart does not exist" ); return NULL; } save_data = g_new0(AgDbSave, 1); /* id */ value = gda_data_model_get_value_at(result, COLUMN_ID, 0, NULL); save_data->db_id = g_value_get_int(value); /* name */ value = gda_data_model_get_value_at(result, COLUMN_NAME, 0, NULL); save_data->name = g_strdup(g_value_get_string(value)); /* country */ value = gda_data_model_get_value_at(result, COLUMN_COUNTRY, 0, NULL); attributes = gda_data_model_get_attributes_at(result, COLUMN_COUNTRY, 0); if (attributes | GDA_VALUE_ATTR_IS_NULL) { save_data->country = NULL; } else { save_data->country = g_strdup(g_value_get_string(value)); } value = gda_data_model_get_value_at(result, COLUMN_CITY, 0, NULL); attributes = gda_data_model_get_attributes_at(result, COLUMN_CITY, 0); if (attributes | GDA_VALUE_ATTR_IS_NULL) { save_data->city = NULL; } else { save_data->city = g_strdup(g_value_get_string(value)); } value = gda_data_model_get_value_at( result, COLUMN_LONGITUDE, 0, NULL ); save_data->longitude = g_value_get_double(value); value = gda_data_model_get_value_at( result, COLUMN_LATITUDE, 0, NULL ); save_data->latitude = g_value_get_double(value); value = gda_data_model_get_value_at(result, COLUMN_ALTITUDE, 0, NULL); attributes = gda_data_model_get_attributes_at(result, COLUMN_ALTITUDE, 0); if (attributes | GDA_VALUE_ATTR_IS_NULL) { /* TODO: this value should be macroified */ save_data->altitude = 280.0; } else { save_data->altitude = g_value_get_double(value); } value = gda_data_model_get_value_at(result, COLUMN_YEAR, 0, NULL); save_data->year = g_value_get_int(value); value = gda_data_model_get_value_at( result, COLUMN_MONTH, 0, NULL ); save_data->month = g_value_get_uint(value); value = gda_data_model_get_value_at(result, COLUMN_DAY, 0, NULL); save_data->day = g_value_get_uint(value); value = gda_data_model_get_value_at(result, COLUMN_HOUR, 0, NULL); save_data->hour = g_value_get_uint(value); value = gda_data_model_get_value_at( result, COLUMN_MINUTE, 0, NULL ); save_data->minute = g_value_get_uint(value); value = gda_data_model_get_value_at( result, COLUMN_SECOND, 0, NULL ); save_data->second = g_value_get_uint(value); value = gda_data_model_get_value_at( result, COLUMN_TIMEZONE, 0, NULL ); save_data->timezone = g_value_get_double(value); value = gda_data_model_get_value_at( result, COLUMN_HOUSE_SYSTEM, 0, NULL ); save_data->house_system = g_strdup(g_value_get_string(value)); value = gda_data_model_get_value_at(result, COLUMN_NOTE, 0, NULL); attributes = gda_data_model_get_attributes_at(result, 15, 0); if (attributes | GDA_VALUE_ATTR_IS_NULL) { save_data->note = NULL; } else { save_data->note = g_strdup(g_value_get_string(value)); } g_object_unref(result); return save_data; } static gint string_collate(const gchar *str1, const gchar *str2) { if (((str1 == NULL) || (str2 == NULL)) && (str1 != str2)) { return (str1 == NULL) ? -1 : 1; } if (str1 == str2) { return 0; } return g_utf8_collate(str1, str2); } gboolean ag_db_save_identical(const AgDbSave *a, const AgDbSave *b) { if (a == b) { return TRUE; } if ((a == NULL) || (b == NULL)) { return FALSE; } if (string_collate(a->name, b->name) != 0) { return FALSE; } if (string_collate(a->country, b->country) != 0) { return FALSE; } if (string_collate(a->city, b->city) != 0) { return FALSE; } if (a->longitude != b->longitude) { return FALSE; } if (a->latitude != b->latitude) { return FALSE; } if (a->altitude != b->altitude) { return FALSE; } if (a->year != b->year) { return FALSE; } if (a->month != b->month) { return FALSE; } if (a->day != b->day) { return FALSE; } if (a->hour != b->hour) { return FALSE; } if (a->minute != b->minute) { return FALSE; } if (a->second != b->second) { return FALSE; } if (a->timezone != b->timezone) { return FALSE; } if (string_collate(a->house_system, b->house_system) != 0) { return FALSE; } if (string_collate(a->note, b->note) != 0) { return FALSE; } return TRUE; }