#include #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" }, }; /** * ag_db_non_select: * @db: the AgDb to operate on * @sql: the SQL query to execute * * Executes a non-SELECT query on @db. No result is returned right now (TODO) */ 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); } /** * ag_db_table_exists: * @db: the #AgDb object to operate on * @table: the nawe of the table to check for * * Checks if the specified table exists. It is done by querying the * sqlite_master system table. * * Returns: TRUE if the table exists, FALSE otherwise */ 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" ); } } va_end(ap); } ret = gda_connection_statement_execute_select(priv->conn, sth, params, err); g_object_unref(sth); return ret; } /** * ag_db_check_version_table: * @db: the #AgDb object to operate on * * Checks if the version table exists, and creates it if necessary. It doesn't * check if the structure is valid! */ 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 ); } } /** * ag_db_check_chart_table: * @db: the #AgDb object to operate on * * Checks if the chart table exists, and creates it if necessary. It doesn't * check if the structure is valid! */ 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" \ ")" ); } /** * ag_db_verify: * @db: the #AgDb object to operate on * * Checks if the database file is sane. * * Returns: the status of the database (TODO: make this an enum!) */ 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; } /** * ag_db_save_data_free: * @save_data: the #AgDbSave struct to free * * Frees @save_data and all its fields */ 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); } /** * ag_db_save_chart: * @db: the #AgDb object to operate on * @save_data: the data to save. * @err: a #GError for storing errors * * Saves @save_data to the database. If its db_id field is -1, a new record is * created; otherwise the row with the given ID will be updated. * * Returns: TRUE if the save succeeds, FALSE otherwise */ gboolean ag_db_save_chart(AgDb *db, AgDbSave *save_data, GError **err) { GError *local_err = NULL; gboolean save_success = TRUE; 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); /* It is possible to get 0 here, which is as non-existant as -1 */ if (save_data->db_id < 0) { if (!gda_connection_insert_row_into_table( priv->conn, "chart", &local_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 )) { g_set_error( err, AG_DB_ERROR, AG_DB_ERROR_DATABASE_ERROR, "%s", (local_err && local_err->message) ? local_err->message : _("Reason unknown") ); save_success = FALSE; } } 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, &local_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 )) { g_set_error( err, AG_DB_ERROR, AG_DB_ERROR_DATABASE_ERROR, "%s", (local_err && local_err->message) ? local_err->message : _("Reason unknown") ); save_success = FALSE; } 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 save_success; } /** * ag_db_get_chart_list: * @db: the #AgDb object to operate on * @err: a #GError * * Creates a list of all charts in the database, ordered by name. As the return * value may be NULL even if there are no charts or if there was an error, you * may want to check @err if the return value is NULL. * * Please be aware that the #AgDbSave objects of the returned value are not * fully realised chart records. To get one, you need to call * ag_db_get_chart_data_by_id() * * Returns: (element-type AgDbSave) (transfer full): the list of all charts, or * or NULL if there are none, or if there is an error. */ 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); } /** * ag_db_get_chart_data_by_id: * @db: the #AgDb object to operate on * @row_id: the ID field of the requested chart * @err: a #GError * * Fetches the specified row from the chart table. * * Returns: (transfer full): A fully filled #AgDbSave record of the chart */ 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; } /** * string_collate: * @str1: the first string * @str2: the second string * * A wrapper function around g_utf8_collate() that can handle NULL values. NULL * precedes any strings (even ""). * * Returns: -1 if str1 is ordered before str2, 1 if str2 comes first, or 0 if * they are identical */ 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); } /** * ag_db_save_identical: * @a: the first #AgDbSave structure * @b: the second #AgDbSave structure * * Compares two #AgDbSave structures and their contents. * * Returns: TRUE if the two structs hold equal values (strings are also compared * with string_collate()), FALSE otherwise */ 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; }