[gom] sorting: Support ordering queries



commit 423e1dc364c4c031129a41f4f665eea4e9627125
Author: Mathieu Bridon <bochecha daitauha fr>
Date:   Sat May 16 15:28:07 2015 +0200

    sorting: Support ordering queries
    
    https://bugzilla.gnome.org/show_bug.cgi?id=730581

 .gitignore                |    1 +
 gom/Makefile.include      |    2 +
 gom/gom-command-builder.c |   48 +++++
 gom/gom-repository.c      |   36 ++++
 gom/gom-repository.h      |    6 +
 gom/gom-resource-group.c  |   44 +++++
 gom/gom-sorting.c         |  226 ++++++++++++++++++++++
 gom/gom-sorting.h         |   68 +++++++
 gom/gom.h                 |    1 +
 tests/Makefile.include    |    6 +
 tests/test-gom-sorting.c  |  453 +++++++++++++++++++++++++++++++++++++++++++++
 11 files changed, 891 insertions(+), 0 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 635a89e..4b3a1dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ test-gom-transform
 test-gom-migration
 test-gom-constraints
 test-gom-update
+test-gom-sorting
 *.typelib
 *.gmo
 *.pot
diff --git a/gom/Makefile.include b/gom/Makefile.include
index eeb2162..4eef987 100644
--- a/gom/Makefile.include
+++ b/gom/Makefile.include
@@ -16,6 +16,7 @@ INST_H_FILES += gom/gom-repository.h
 INST_H_FILES += gom/gom-resource-group.h
 INST_H_FILES += gom/gom-resource.h
 INST_H_FILES += gom/gom-autocleanups.h
+INST_H_FILES += gom/gom-sorting.h
 
 NOINST_H_FILES = gom/gom-resource-priv.h
 
@@ -31,6 +32,7 @@ libgom_1_0_la_SOURCES += gom/gom-filter.c
 libgom_1_0_la_SOURCES += gom/gom-repository.c
 libgom_1_0_la_SOURCES += gom/gom-resource.c
 libgom_1_0_la_SOURCES += gom/gom-resource-group.c
+libgom_1_0_la_SOURCES += gom/gom-sorting.c
 
 libgom_1_0_la_CPPFLAGS =
 libgom_1_0_la_CPPFLAGS += '-DG_LOG_DOMAIN="Gom"'
diff --git a/gom/gom-command-builder.c b/gom/gom-command-builder.c
index 393aa13..4534246 100644
--- a/gom/gom-command-builder.c
+++ b/gom/gom-command-builder.c
@@ -24,6 +24,7 @@
 #include "gom-filter.h"
 #include "gom-resource.h"
 #include "gom-resource-priv.h"
+#include "gom-sorting.h"
 
 G_DEFINE_TYPE(GomCommandBuilder, gom_command_builder, G_TYPE_OBJECT)
 
@@ -31,6 +32,7 @@ struct _GomCommandBuilderPrivate
 {
    GomAdapter *adapter;
    GomFilter *filter;
+   GomSorting *sorting;
    GType resource_type;
    guint limit;
    guint offset;
@@ -43,6 +45,7 @@ enum
    PROP_0,
    PROP_ADAPTER,
    PROP_FILTER,
+   PROP_SORTING,
    PROP_LIMIT,
    PROP_M2M_TABLE,
    PROP_M2M_TYPE,
@@ -350,6 +353,32 @@ add_where (GString     *str,
 }
 
 static void
+add_order_by (GString *str,
+              GType        m2m_type,
+              const gchar *m2m_table,
+              GomSorting *sorting)
+{
+   GHashTable *table_map = NULL;
+   gchar *sql;
+
+   if (sorting) {
+      if (m2m_type) {
+         table_map = g_hash_table_new_full(g_str_hash, g_str_equal,
+                                           g_free, g_free);
+         build_map(table_map, m2m_type, m2m_table);
+      }
+
+      sql = gom_sorting_get_sql(sorting, table_map);
+      g_string_append_printf(str, " ORDER BY %s ", sql);
+      g_free(sql);
+
+      if (table_map) {
+         g_hash_table_destroy(table_map);
+      }
+   }
+}
+
+static void
 add_limit (GString *str,
            guint    limit)
 {
@@ -511,6 +540,7 @@ gom_command_builder_build_select (GomCommandBuilder *builder)
    add_joins(str, klass);
    add_m2m(str, klass, priv->m2m_table, priv->m2m_type);
    add_where(str, priv->m2m_type, priv->m2m_table, priv->filter);
+   add_order_by(str, priv->m2m_type, priv->m2m_table, priv->sorting);
    add_limit(str, priv->limit);
    add_offset(str, priv->offset);
 
@@ -869,6 +899,7 @@ gom_command_builder_finalize (GObject *object)
 
    g_clear_object(&priv->adapter);
    g_clear_object(&priv->filter);
+   g_clear_object(&priv->sorting);
    g_free(priv->m2m_table);
 
    G_OBJECT_CLASS(gom_command_builder_parent_class)->finalize(object);
@@ -898,6 +929,9 @@ gom_command_builder_get_property (GObject    *object,
    case PROP_FILTER:
       g_value_set_object(value, builder->priv->filter);
       break;
+   case PROP_SORTING:
+      g_value_set_object(value, builder->priv->sorting);
+      break;
    case PROP_LIMIT:
       g_value_set_uint(value, builder->priv->limit);
       break;
@@ -945,6 +979,11 @@ gom_command_builder_set_property (GObject      *object,
       builder->priv->filter = g_value_dup_object(value);
       g_object_notify_by_pspec(object, pspec);
       break;
+   case PROP_SORTING:
+      g_clear_object(&builder->priv->sorting);
+      builder->priv->sorting = g_value_dup_object(value);
+      g_object_notify_by_pspec(object, pspec);
+      break;
    case PROP_LIMIT:
       builder->priv->limit = g_value_get_uint(value);
       g_object_notify_by_pspec(object, pspec);
@@ -1005,6 +1044,15 @@ gom_command_builder_class_init (GomCommandBuilderClass *klass)
    g_object_class_install_property(object_class, PROP_FILTER,
                                    gParamSpecs[PROP_FILTER]);
 
+   gParamSpecs[PROP_SORTING] =
+      g_param_spec_object("sorting",
+                          _("Sorting"),
+                          _("The sorting for the command."),
+                          GOM_TYPE_SORTING,
+                          G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_SORTING,
+                                   gParamSpecs[PROP_SORTING]);
+
    gParamSpecs[PROP_LIMIT] =
       g_param_spec_uint("limit",
                         _("Limit"),
diff --git a/gom/gom-repository.c b/gom/gom-repository.c
index 28e723c..ae396ae 100644
--- a/gom/gom-repository.c
+++ b/gom/gom-repository.c
@@ -468,6 +468,7 @@ gom_repository_find_cb (GomAdapter *adapter,
    GomCommand *command;
    GomCursor *cursor;
    GomFilter *filter;
+   GomSorting *sorting;
    GError *error = NULL;
    GType resource_type;
    GAsyncQueue *queue;
@@ -485,12 +486,16 @@ gom_repository_find_cb (GomAdapter *adapter,
    filter = g_object_get_data(G_OBJECT(simple), "filter");
    g_assert(!filter || GOM_IS_FILTER(filter));
 
+   sorting = g_object_get_data(G_OBJECT(simple), "sorting");
+   g_assert(!sorting || GOM_IS_SORTING(sorting));
+
    queue = g_object_get_data(G_OBJECT(simple), "queue");
 
    builder = g_object_new(GOM_TYPE_COMMAND_BUILDER,
                           "adapter", adapter,
                           "resource-type", resource_type,
                           "filter", filter,
+                          "sorting", sorting,
                           NULL);
 
    command = gom_command_builder_build_count(builder);
@@ -511,6 +516,7 @@ gom_repository_find_cb (GomAdapter *adapter,
    ret = g_object_new(GOM_TYPE_RESOURCE_GROUP,
                       "count", count,
                       "filter", filter,
+                      "sorting", sorting,
                       "repository", repository,
                       "resource-type", resource_type,
                       NULL);
@@ -546,6 +552,32 @@ gom_repository_find_sync (GomRepository  *repository,
                           GomFilter      *filter,
                           GError        **error)
 {
+   return gom_repository_find_sorted_sync(repository, resource_type, filter,
+                                          NULL, error);
+}
+
+/**
+ * gom_repository_find_sorted_sync:
+ * @repository: (in): A #GomRepository.
+ * @resource_type: (in): The #GType of the resources to query.
+ * @filter: (in) (allow-none): An optional filter for the query.
+ * @sorting: (in) (allow-none): An optional #GomSorting to order the query
+ *                              results.
+ * @error: (out): A location for a #GError, or %NULL.
+ *
+ * Synchronously queries the #GomRepository for objects matching the
+ * requested query. This must only be run from a callback provided to
+ * gom_adapter_queue_read().
+ *
+ * Returns: (transfer full): A #GomResourceGroup or %NULL.
+ */
+GomResourceGroup *
+gom_repository_find_sorted_sync (GomRepository  *repository,
+                                 GType           resource_type,
+                                 GomFilter      *filter,
+                                 GomSorting     *sorting,
+                                 GError        **error)
+{
    GomRepositoryPrivate *priv;
    GSimpleAsyncResult *simple;
    GomResourceGroup *ret;
@@ -555,6 +587,7 @@ gom_repository_find_sync (GomRepository  *repository,
    g_return_val_if_fail(g_type_is_a(resource_type, GOM_TYPE_RESOURCE), NULL);
    g_return_val_if_fail(resource_type != GOM_TYPE_RESOURCE, NULL);
    g_return_val_if_fail(!filter || GOM_IS_FILTER(filter), NULL);
+   g_return_val_if_fail(!sorting || GOM_IS_SORTING(sorting), NULL);
 
    priv = repository->priv;
 
@@ -567,6 +600,9 @@ gom_repository_find_sync (GomRepository  *repository,
    g_object_set_data_full(G_OBJECT(simple), "filter",
                           filter ? g_object_ref(filter) : NULL,
                           filter ? g_object_unref : NULL);
+   g_object_set_data_full(G_OBJECT(simple), "sorting",
+                          sorting ? g_object_ref(sorting) : NULL,
+                          sorting ? g_object_unref : NULL);
    g_object_set_data(G_OBJECT(simple), "queue", queue);
 
    gom_adapter_queue_read(priv->adapter, gom_repository_find_cb, simple);
diff --git a/gom/gom-repository.h b/gom/gom-repository.h
index 3f7e55b..113cccd 100644
--- a/gom/gom-repository.h
+++ b/gom/gom-repository.h
@@ -24,6 +24,7 @@
 #include "gom-adapter.h"
 #include "gom-filter.h"
 #include "gom-resource-group.h"
+#include "gom-sorting.h"
 
 G_BEGIN_DECLS
 
@@ -106,6 +107,11 @@ GomResourceGroup *gom_repository_find_sync       (GomRepository          *reposi
                                                   GType                   resource_type,
                                                   GomFilter              *filter,
                                                   GError                **error);
+GomResourceGroup *gom_repository_find_sorted_sync (GomRepository          *repository,
+                                                   GType                   resource_type,
+                                                   GomFilter              *filter,
+                                                   GomSorting             *sorting,
+                                                   GError                **error);
 void              gom_repository_find_async      (GomRepository          *repository,
                                                   GType                   resource_type,
                                                   GomFilter              *filter,
diff --git a/gom/gom-resource-group.c b/gom/gom-resource-group.c
index f6bbfa3..ad83b23 100644
--- a/gom/gom-resource-group.c
+++ b/gom/gom-resource-group.c
@@ -27,6 +27,7 @@
 #include "gom-resource.h"
 #include "gom-resource-priv.h"
 #include "gom-resource-group.h"
+#include "gom-sorting.h"
 
 G_DEFINE_TYPE(GomResourceGroup, gom_resource_group, G_TYPE_OBJECT)
 
@@ -37,6 +38,7 @@ struct _GomResourceGroupPrivate
    /* Read group */
    guint count;
    GomFilter *filter;
+   GomSorting *sorting;
    GType resource_type;
    GHashTable *items;
    gchar *m2m_table;
@@ -52,6 +54,7 @@ enum
    PROP_0,
    PROP_COUNT,
    PROP_FILTER,
+   PROP_SORTING,
    PROP_M2M_TABLE,
    PROP_M2M_TYPE,
    PROP_RESOURCE_TYPE,
@@ -438,6 +441,26 @@ gom_resource_group_set_filter (GomResourceGroup *group,
    }
 }
 
+static GomSorting *
+gom_resource_group_get_sorting (GomResourceGroup *group)
+{
+   g_return_val_if_fail(GOM_IS_RESOURCE_GROUP(group), NULL);
+   return group->priv->sorting;
+}
+
+static void
+gom_resource_group_set_sorting (GomResourceGroup *group,
+                                GomSorting       *sorting)
+{
+   g_return_if_fail(GOM_IS_RESOURCE_GROUP(group));
+   g_return_if_fail(!sorting || GOM_IS_SORTING(sorting));
+
+   if (sorting) {
+      group->priv->sorting = g_object_ref(sorting);
+      g_object_notify_by_pspec(G_OBJECT(group), gParamSpecs[PROP_SORTING]);
+   }
+}
+
 guint
 gom_resource_group_get_count (GomResourceGroup *group)
 {
@@ -640,6 +663,7 @@ gom_resource_group_fetch_cb (GomAdapter *adapter,
    GomCommand *command = NULL;
    GomCursor *cursor = NULL;
    GomFilter *filter = NULL;
+   GomSorting *sorting = NULL;
    GError *error = NULL;
    GType resource_type;
    gchar *m2m_table = NULL;
@@ -655,6 +679,7 @@ gom_resource_group_fetch_cb (GomAdapter *adapter,
    group = GOM_RESOURCE_GROUP(g_async_result_get_source_object(G_ASYNC_RESULT(simple)));
    g_object_get(group,
                 "filter", &filter,
+                "sorting", &sorting,
                 "m2m-table", &m2m_table,
                 "m2m-type", &m2m_type,
                 "repository", &repository,
@@ -662,6 +687,7 @@ gom_resource_group_fetch_cb (GomAdapter *adapter,
                 NULL);
    g_assert(GOM_IS_ADAPTER(adapter));
    g_assert(!filter || GOM_IS_FILTER(filter));
+   g_assert(!sorting || GOM_IS_SORTING(sorting));
    g_assert(GOM_IS_REPOSITORY(repository));
    g_assert(g_type_is_a(resource_type, GOM_TYPE_RESOURCE));
 
@@ -672,6 +698,7 @@ gom_resource_group_fetch_cb (GomAdapter *adapter,
    builder = g_object_new(GOM_TYPE_COMMAND_BUILDER,
                           "adapter", gom_repository_get_adapter(repository),
                           "filter", filter,
+                          "sorting", sorting,
                           "limit", limit,
                           "m2m-table", m2m_table,
                           "m2m-type", m2m_type,
@@ -717,6 +744,7 @@ out:
    g_clear_object(&command);
    g_clear_object(&cursor);
    g_clear_object(&filter);
+   g_clear_object(&sorting);
    g_clear_object(&repository);
    if (!queue)
       g_simple_async_result_complete_in_idle(simple);
@@ -865,6 +893,7 @@ gom_resource_group_finalize (GObject *object)
 
    g_clear_object(&priv->repository);
    g_clear_object(&priv->filter);
+   g_clear_object(&priv->sorting);
    g_clear_pointer(&priv->items, g_hash_table_unref);
    g_clear_pointer(&priv->to_write, g_ptr_array_unref);
 
@@ -895,6 +924,9 @@ gom_resource_group_get_property (GObject    *object,
    case PROP_FILTER:
       g_value_set_object(value, gom_resource_group_get_filter(group));
       break;
+   case PROP_SORTING:
+      g_value_set_object(value, gom_resource_group_get_sorting(group));
+      break;
    case PROP_M2M_TABLE:
       g_value_set_string(value, gom_resource_group_get_m2m_table(group));
       break;
@@ -939,6 +971,9 @@ gom_resource_group_set_property (GObject      *object,
    case PROP_FILTER:
       gom_resource_group_set_filter(group, g_value_get_object(value));
       break;
+   case PROP_SORTING:
+      gom_resource_group_set_sorting(group, g_value_get_object(value));
+      break;
    case PROP_M2M_TABLE:
       gom_resource_group_set_m2m_table(group, g_value_get_string(value));
       break;
@@ -996,6 +1031,15 @@ gom_resource_group_class_init (GomResourceGroupClass *klass)
    g_object_class_install_property(object_class, PROP_FILTER,
                                    gParamSpecs[PROP_FILTER]);
 
+   gParamSpecs[PROP_SORTING] =
+      g_param_spec_object("sorting",
+                          _("Sorting"),
+                          _("The query sorting."),
+                          GOM_TYPE_SORTING,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+   g_object_class_install_property(object_class, PROP_SORTING,
+                                   gParamSpecs[PROP_SORTING]);
+
    gParamSpecs[PROP_M2M_TABLE] =
       g_param_spec_string("m2m-table",
                           _("Many-to-Many Table"),
diff --git a/gom/gom-sorting.c b/gom/gom-sorting.c
new file mode 100644
index 0000000..915a0d5
--- /dev/null
+++ b/gom/gom-sorting.c
@@ -0,0 +1,226 @@
+/* gom-sorting.c
+ *
+ * Copyright (C) 2015 Mathieu Bridon <bochecha daitauha fr>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdarg.h>
+
+#include <glib/gi18n.h>
+
+#include "gom-sorting.h"
+#include "gom-resource.h"
+
+G_DEFINE_TYPE(GomSorting, gom_sorting, G_TYPE_INITIALLY_UNOWNED)
+
+struct _GomSortingPrivate
+{
+   GQueue *order_by_terms;
+};
+
+typedef struct GomOrderByTerm
+{
+   GType resource_type;
+   gchar *property_name;
+   GomSortingMode mode;
+} GomOrderByTerm;
+
+static void
+gom_sorting_finalize (GObject *object)
+{
+   GomSortingPrivate *priv = GOM_SORTING(object)->priv;
+
+   if (priv->order_by_terms != NULL)
+      g_queue_free_full(priv->order_by_terms, g_free);
+
+   G_OBJECT_CLASS(gom_sorting_parent_class)->finalize(object);
+}
+
+static void
+gom_sorting_class_init (GomSortingClass *klass)
+{
+   GObjectClass *object_class;
+
+   object_class = G_OBJECT_CLASS(klass);
+   object_class->finalize = gom_sorting_finalize;
+
+   g_type_class_add_private(object_class, sizeof(GomSortingPrivate));
+}
+
+static void
+gom_sorting_init (GomSorting *sorting)
+{
+   sorting->priv = G_TYPE_INSTANCE_GET_PRIVATE(sorting, GOM_TYPE_SORTING,
+                                               GomSortingPrivate);
+}
+
+GType
+gom_sorting_mode_get_type (void)
+{
+   static GType g_type = 0;
+   static gsize initialized = FALSE;
+   static const GEnumValue values[] = {
+      { GOM_SORTING_ASCENDING,  "GOM_SORTING_ASCENDING",  "" },
+      { GOM_SORTING_DESCENDING, "GOM_SORTING_DESCENDING", "DESC" },
+      { 0 }
+   };
+
+   if (g_once_init_enter(&initialized)) {
+      g_type = g_enum_register_static("GomSortingMode", values);
+      g_once_init_leave(&initialized, TRUE);
+   }
+
+   return g_type;
+}
+
+static gchar *
+get_table (GType       type,
+           GHashTable *table_map)
+{
+   GomResourceClass *klass;
+   gchar *table;
+   gchar *key;
+
+   g_return_val_if_fail(g_type_is_a(type, GOM_TYPE_RESOURCE), NULL);
+
+   klass = g_type_class_ref(type);
+   key = g_strdup_printf("%s.%s", g_type_name(type), klass->table);
+   if (table_map && (table = g_hash_table_lookup(table_map, key))) {
+      table = g_strdup(table);
+   } else {
+      table = g_strdup(klass->table);
+   }
+   g_free(key);
+   g_type_class_unref(klass);
+
+   return table;
+}
+
+/**
+ * gom_sorting_new: (constructor)
+ * @first_resource_type: A subclass of #GomResource.
+ * @first_property_name: A pointer to a const gchar.
+ * @first_sorting_mode: A GomSortingMode.
+ * @...: Additional triples of resource_type/property_name/sorting_mode,
+ *       followed by %NULL.
+ *
+ * Creates a new #GomSorting to instance.
+ *
+ * This is useful to sort query results, as #GomSorting knows how to return
+ * the proper "ORDER BY" SQL statements.
+ *
+ * Example:
+ *
+ *     GomSorting *sorting = gom_sorting_new(EPISODE_TYPE_RESOURCE,
+ *                                           "season-number",
+ *                                           GOM_SORTING_DESCENDING,
+ *                                           EPISODE_TYPE_RESOURCE,
+ *                                           "episode-number",
+ *                                           GOM_SORTING_ASCENDING);
+ *
+ * The above example maps to the following SQL statement:
+ *
+ *     ORDER BY 'episodes'.'season-number' DESC, 'episodes'.'episode-number'
+ *
+ * Returns: (transfer full): A #GomSorting.
+ */
+GomSorting *
+gom_sorting_new (GType           first_resource_type,
+                 const gchar    *first_property_name,
+                 GomSortingMode  first_sorting_mode,
+                 ...)
+{
+   GomSorting *sorting;
+   va_list args;
+   GType resource_type;
+   const gchar *property_name;
+   GomSortingMode sorting_mode;
+
+   g_return_val_if_fail(g_type_is_a(first_resource_type, GOM_TYPE_RESOURCE),
+                        NULL);
+
+   sorting = g_object_new(GOM_TYPE_SORTING, NULL);
+   sorting->priv->order_by_terms = g_queue_new();
+
+   resource_type = first_resource_type;
+   property_name = first_property_name;
+   sorting_mode = first_sorting_mode;
+
+   va_start(args, first_sorting_mode);
+
+   while TRUE {
+      GomOrderByTerm *o = g_new(GomOrderByTerm, 1);
+
+      g_return_val_if_fail(g_type_is_a(resource_type, GOM_TYPE_RESOURCE),
+                           NULL);
+      g_return_val_if_fail(property_name != NULL, NULL);
+      g_return_val_if_fail(sorting_mode, NULL);
+
+      o->resource_type = resource_type;
+      o->property_name = strdup(property_name);
+      o->mode = sorting_mode;
+      g_queue_push_tail(sorting->priv->order_by_terms, o);
+
+      resource_type = va_arg(args, GType);
+
+      if (!resource_type)
+         break;
+
+      property_name = va_arg(args, const gchar*);
+      sorting_mode = va_arg(args, GomSortingMode);
+   }
+
+   va_end(args);
+
+   return sorting;
+}
+
+/**
+ * gom_sorting_get_sql:
+ * @sorting: (in): A #GomSorting.
+ * @table_map: (in): A #GHashTable.
+ *
+ * Returns: (transfer full): A string containing the SQL query corresponding
+ *                           to this @sorting.
+ */
+gchar *
+gom_sorting_get_sql (GomSorting *sorting,
+                     GHashTable *table_map)
+{
+   GomSortingPrivate *priv;
+   gchar *table;
+   gchar **sqls;
+   gint i, len;
+   gchar *ret;
+
+   g_return_val_if_fail(GOM_IS_SORTING(sorting), NULL);
+
+   priv = sorting->priv;
+   len = g_queue_get_length(priv->order_by_terms);
+   sqls = g_new(gchar *, len + 1);
+
+   for (i = 0; i < len; i++) {
+      GomOrderByTerm *o = g_queue_peek_nth(priv->order_by_terms, i);
+      table = get_table(o->resource_type, table_map);
+
+      sqls[i] = g_strdup_printf("'%s'.'%s'%s", table, o->property_name, o->mode == GOM_SORTING_DESCENDING ? 
" DESC" : "");
+   }
+   sqls[i] = NULL;
+
+   ret = g_strjoinv(", ", sqls);
+   g_strfreev(sqls);
+
+   return ret;
+}
diff --git a/gom/gom-sorting.h b/gom/gom-sorting.h
new file mode 100644
index 0000000..87fb1db
--- /dev/null
+++ b/gom/gom-sorting.h
@@ -0,0 +1,68 @@
+/* gom-sorting.h
+ *
+ * Copyright (C) 2015 Mathieu Bridon <bochecha daitauha fr>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GOM_SORTING_H
+#define GOM_SORTING_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GOM_TYPE_SORTING            (gom_sorting_get_type())
+#define GOM_TYPE_SORTING_MODE       (gom_sorting_mode_get_type())
+#define GOM_SORTING(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GOM_TYPE_SORTING, GomSorting))
+#define GOM_SORTING_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GOM_TYPE_SORTING, GomSorting const))
+#define GOM_SORTING_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GOM_TYPE_SORTING, GomSortingClass))
+#define GOM_IS_SORTING(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GOM_TYPE_SORTING))
+#define GOM_IS_SORTING_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GOM_TYPE_SORTING))
+#define GOM_SORTING_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GOM_TYPE_SORTING, GomSortingClass))
+
+typedef struct _GomSorting        GomSorting;
+typedef struct _GomSortingClass   GomSortingClass;
+typedef struct _GomSortingPrivate GomSortingPrivate;
+typedef enum   _GomSortingMode    GomSortingMode;
+
+struct _GomSorting
+{
+   GObject parent;
+   GomSortingPrivate *priv;
+};
+
+struct _GomSortingClass
+{
+   GObjectClass parent_class;
+};
+
+enum _GomSortingMode
+{
+   GOM_SORTING_ASCENDING = 1,
+   GOM_SORTING_DESCENDING
+};
+
+GType       gom_sorting_get_type      (void) G_GNUC_CONST;
+GType       gom_sorting_mode_get_type (void) G_GNUC_CONST;
+gchar      *gom_sorting_get_sql       (GomSorting     *sorting,
+                                       GHashTable     *table_map);
+GomSorting *gom_sorting_new           (GType           first_resource_type,
+                                       const gchar    *first_property_name,
+                                       GomSortingMode  first_sorting_mode,
+                                       ...);
+
+G_END_DECLS
+
+#endif /* GOM_SORTING_H */
diff --git a/gom/gom.h b/gom/gom.h
index 9b67da7..c8cb85b 100644
--- a/gom/gom.h
+++ b/gom/gom.h
@@ -34,6 +34,7 @@ G_BEGIN_DECLS
 #include "gom-repository.h"
 #include "gom-resource-group.h"
 #include "gom-resource.h"
+#include "gom-sorting.h"
 
 #include "gom-autocleanups.h"
 
diff --git a/tests/Makefile.include b/tests/Makefile.include
index 28b6c4c..f0d25c7 100644
--- a/tests/Makefile.include
+++ b/tests/Makefile.include
@@ -9,6 +9,7 @@ noinst_PROGRAMS += test-gom-migration
 noinst_PROGRAMS += test-gom-constraints
 noinst_PROGRAMS += test-gom-insert
 noinst_PROGRAMS += test-gom-update
+noinst_PROGRAMS += test-gom-sorting
 
 TEST_PROGS += test-gom-adapter
 TEST_PROGS += test-gom-repository
@@ -20,6 +21,7 @@ TEST_PROGS += test-gom-migration
 TEST_PROGS += test-gom-constraints
 TEST_PROGS += test-gom-insert
 TEST_PROGS += test-gom-update
+TEST_PROGS += test-gom-sorting
 
 test_gom_adapter_SOURCES = tests/test-gom-adapter.c
 test_gom_adapter_CPPFLAGS = $(GIO_CFLAGS) $(GOBJECT_CFLAGS) $(WARN_CFLAGS)
@@ -61,4 +63,8 @@ test_gom_update_SOURCES = tests/test-gom-update.c
 test_gom_update_CPPFLAGS = $(GIO_CFLAGS) $(GOBJECT_CFLAGS) $(WARN_CFLAGS)
 test_gom_update_LDADD = $(GIO_LIBS) $(GOBJECT_LIBS) $(top_builddir)/libgom-1.0.la
 
+test_gom_sorting_SOURCES = tests/test-gom-sorting.c
+test_gom_sorting_CPPFLAGS = $(GIO_CFLAGS) $(GOBJECT_CFLAGS) $(WARN_CFLAGS)
+test_gom_sorting_LDADD = $(GIO_LIBS) $(GOBJECT_LIBS) $(top_builddir)/libgom-1.0.la
+
 EXTRA_DIST += tests/grl-bookmarks.db tests/gnome.png
diff --git a/tests/test-gom-sorting.c b/tests/test-gom-sorting.c
new file mode 100644
index 0000000..2b8eb19
--- /dev/null
+++ b/tests/test-gom-sorting.c
@@ -0,0 +1,453 @@
+#include <gom/gom.h>
+
+
+static GMainLoop *gMainLoop;
+
+
+#define EPISODE_TYPE_RESOURCE              (episode_resource_get_type())
+#define EPISODE_TYPE_TYPE                  (episode_type_get_type())
+#define EPISODE_RESOURCE(obj)              (G_TYPE_CHECK_INSTANCE_CAST ((obj), EPISODE_TYPE_RESOURCE, 
EpisodeResource))
+#define EPISODE_RESOURCE_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST ((klass), EPISODE_TYPE_RESOURCE, 
EpisodeResourceClass))
+#define EPISODE_IS_RESOURCE(obj)           (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EPISODE_TYPE_RESOURCE))
+#define EPISODE_IS_RESOURCE_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE ((klass), EPISODE_TYPE_RESOURCE))
+#define EPISODE_RESOURCE_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS ((obj), EPISODE_TYPE_RESOURCE, 
EpisodeResourceClass))
+
+typedef struct {
+   gint64  db_id;
+   gchar  *series_id;
+   gchar  *imdb_id;
+   guint8  season_number;
+   guint8  episode_number;
+   gchar  *episode_name;
+} EpisodeResourcePrivate;
+
+typedef struct
+{
+   GomResource             parent;
+   EpisodeResourcePrivate *priv;
+} EpisodeResource;
+
+typedef struct
+{
+   GomResourceClass parent_class;
+} EpisodeResourceClass;
+
+GType episode_resource_get_type(void);
+
+G_DEFINE_TYPE(EpisodeResource, episode_resource, GOM_TYPE_RESOURCE)
+
+enum {
+   PROP_0,
+   PROP_DB_ID,
+   PROP_SERIES_ID,
+   PROP_IMDB_ID,
+   PROP_SEASON_NUMBER,
+   PROP_EPISODE_NUMBER,
+   PROP_EPISODE_NAME,
+   LAST_PROP
+};
+
+static GParamSpec *specs[LAST_PROP];
+
+static struct {
+   const gchar *series_id;
+   const gchar *imdb_id;
+   guint8       season_number;
+   guint8       episode_number;
+   const gchar *episode_name;
+} values[] = {
+   { "84947", "tt2483070", 4, 1, "New York Sour" },
+   { "84947", "tt2778300", 4, 2, "Resignation" },
+   { "84947", "tt3216480", 5, 1, "Golden Days for Boys and Girls" },
+   { "84947", "tt3767076", 5, 2, "The Good Listener" }
+};
+
+static void
+episode_resource_finalize (GObject *object)
+{
+   EpisodeResourcePrivate *priv = EPISODE_RESOURCE(object)->priv;
+
+   g_clear_pointer(&priv->series_id, g_free);
+   g_clear_pointer(&priv->imdb_id, g_free);
+   g_clear_pointer(&priv->episode_name, g_free);
+
+   G_OBJECT_CLASS(episode_resource_parent_class)->finalize(object);
+}
+
+static void
+episode_resource_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+   EpisodeResource *resource = EPISODE_RESOURCE(object);
+
+   switch (prop_id) {
+   case PROP_DB_ID:
+      g_value_set_int64(value, resource->priv->db_id);
+      break;
+   case PROP_SERIES_ID:
+      g_value_set_string(value, resource->priv->series_id);
+      break;
+   case PROP_IMDB_ID:
+      g_value_set_string(value, resource->priv->imdb_id);
+      break;
+   case PROP_SEASON_NUMBER:
+      g_value_set_uchar(value, resource->priv->season_number);
+      break;
+   case PROP_EPISODE_NUMBER:
+      g_value_set_uchar(value, resource->priv->episode_number);
+      break;
+   case PROP_EPISODE_NAME:
+      g_value_set_string(value, resource->priv->episode_name);
+      break;
+   default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+   }
+}
+
+static void
+episode_resource_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+   EpisodeResource *resource = EPISODE_RESOURCE(object);
+
+   switch (prop_id) {
+   case PROP_DB_ID:
+      resource->priv->db_id = g_value_get_int64(value);
+      break;
+   case PROP_SERIES_ID:
+      g_clear_pointer(&resource->priv->series_id, g_free);
+      resource->priv->series_id = g_value_dup_string(value);
+      break;
+   case PROP_IMDB_ID:
+      g_clear_pointer(&resource->priv->imdb_id, g_free);
+      resource->priv->imdb_id = g_value_dup_string(value);
+      break;
+   case PROP_SEASON_NUMBER:
+      resource->priv->season_number = g_value_get_uchar(value);
+      break;
+   case PROP_EPISODE_NUMBER:
+      resource->priv->episode_number = g_value_get_uchar(value);
+      break;
+   case PROP_EPISODE_NAME:
+      g_clear_pointer(&resource->priv->episode_name, g_free);
+      resource->priv->episode_name = g_value_dup_string(value);
+      break;
+   default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+   }
+}
+
+static void
+episode_resource_class_init (EpisodeResourceClass *klass)
+{
+   GObjectClass *object_class;
+   GomResourceClass *resource_class;
+
+   object_class = G_OBJECT_CLASS(klass);
+   object_class->finalize = episode_resource_finalize;
+   object_class->get_property = episode_resource_get_property;
+   object_class->set_property = episode_resource_set_property;
+   g_type_class_add_private(object_class, sizeof(EpisodeResourcePrivate));
+
+   resource_class = GOM_RESOURCE_CLASS(klass);
+   gom_resource_class_set_table(resource_class, "episodes");
+
+   specs[PROP_DB_ID] = g_param_spec_int64("id", NULL, NULL, 0, G_MAXINT64,
+                                          0, G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_DB_ID,
+                                   specs[PROP_DB_ID]);
+   gom_resource_class_set_primary_key(resource_class, "id");
+
+   specs[PROP_SERIES_ID] = g_param_spec_string("series-id", NULL, NULL, NULL,
+                                               G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_SERIES_ID,
+                                   specs[PROP_SERIES_ID]);
+
+   specs[PROP_IMDB_ID] = g_param_spec_string("imdb-id", NULL, NULL, NULL,
+                                             G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_IMDB_ID,
+                                   specs[PROP_IMDB_ID]);
+
+   specs[PROP_SEASON_NUMBER] = g_param_spec_uchar("season-number", NULL, NULL,
+                                                  0, G_MAXUINT8, 0,
+                                                  G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_SEASON_NUMBER,
+                                   specs[PROP_SEASON_NUMBER]);
+
+   specs[PROP_EPISODE_NUMBER] = g_param_spec_uchar("episode-number", NULL,
+                                                   NULL, 0, G_MAXUINT8, 0,
+                                                   G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_EPISODE_NUMBER,
+                                   specs[PROP_EPISODE_NUMBER]);
+
+   specs[PROP_EPISODE_NAME] = g_param_spec_string("episode-name", NULL, NULL,
+                                                  NULL, G_PARAM_READWRITE);
+   g_object_class_install_property(object_class, PROP_EPISODE_NAME,
+                                   specs[PROP_EPISODE_NAME]);
+
+}
+
+static void
+episode_resource_init (EpisodeResource *resource)
+{
+   resource->priv = G_TYPE_INSTANCE_GET_PRIVATE(resource,
+                                                EPISODE_TYPE_RESOURCE,
+                                                EpisodeResourcePrivate);
+}
+
+static void
+create_memory_db (GomAdapter    **adapter,
+                  GomRepository **repository)
+{
+   gboolean ret;
+   GError *error = NULL;
+   GList *object_types;
+   EpisodeResource *eres;
+   guint i;
+
+   *adapter = gom_adapter_new();
+   ret = gom_adapter_open_sync(*adapter, ":memory:", &error);
+   g_assert_no_error(error);
+   g_assert(ret);
+
+   *repository = gom_repository_new(*adapter);
+
+   object_types = g_list_prepend(NULL,
+                                 GINT_TO_POINTER(EPISODE_TYPE_RESOURCE));
+   ret = gom_repository_automatic_migrate_sync(*repository, 1, object_types,
+                                               &error);
+   g_assert_no_error(error);
+   g_assert(ret);
+
+   for (i = 0; i < G_N_ELEMENTS(values); i++) {
+      eres = g_object_new(EPISODE_TYPE_RESOURCE, "repository", *repository,
+                          "series-id", values[i].series_id,
+                          "imdb-id", values[i].imdb_id,
+                          "season-number", values[i].season_number,
+                          "episode-number", values[i].episode_number,
+                          "episode-name", values[i].episode_name,
+                          NULL);
+      ret = gom_resource_save_sync(GOM_RESOURCE(eres), &error);
+      g_assert(ret);
+      g_assert_no_error(error);
+      g_object_unref(eres);
+   }
+}
+
+static void
+free_memory_db (GomAdapter    *adapter,
+                GomRepository *repository)
+{
+   gboolean ret;
+   GError *error = NULL;
+
+   ret = gom_adapter_close_sync(adapter, &error);
+   g_assert_no_error(error);
+   g_assert(ret);
+
+   g_object_unref(repository);
+   g_object_unref(adapter);
+}
+
+
+static void
+find_order_by_asc (void)
+{
+   GomAdapter *adapter;
+   GomRepository *repository;
+   GomFilter *filter;
+   GomSorting *sorting;
+   GValue value = { 0, };
+   GError *error = NULL;
+   GomResourceGroup *group;
+   EpisodeResource *eres;
+   guint count;
+   guint8 i;
+
+   create_memory_db(&adapter, &repository);
+
+   /* Filter on season number */
+   g_value_init(&value, G_TYPE_INT64);
+   g_value_set_int64(&value, 4);
+   filter = gom_filter_new_eq(EPISODE_TYPE_RESOURCE, "season-number", &value);
+   g_value_unset(&value);
+
+   /* Order by episode */
+   sorting = gom_sorting_new(EPISODE_TYPE_RESOURCE, "episode-number",
+                             GOM_SORTING_ASCENDING, NULL);
+
+   group = gom_repository_find_sorted_sync(repository, EPISODE_TYPE_RESOURCE,
+                                           filter, sorting, &error);
+   g_assert_no_error(error);
+   g_object_unref(filter);
+   g_object_unref(sorting);
+
+   count = gom_resource_group_get_count(group);
+   g_assert_cmpuint(count, ==, 2);
+
+   gom_resource_group_fetch_sync(group, 0, count, &error);
+   g_assert_no_error(error);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 0));
+   g_assert(eres);
+   g_object_get(eres, "episode-number", &i, NULL);
+   g_assert_cmpuint(i, ==, 1);
+   g_object_unref(eres);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 1));
+   g_assert(eres);
+   g_object_get(eres, "episode-number", &i, NULL);
+   g_assert_cmpuint(i, ==, 2);
+   g_object_unref(eres);
+
+   free_memory_db(adapter, repository);
+}
+
+static void
+find_order_by_desc (void)
+{
+   GomAdapter *adapter;
+   GomRepository *repository;
+   GomFilter *filter;
+   GomSorting *sorting;
+   GValue value = { 0, };
+   GError *error = NULL;
+   GomResourceGroup *group;
+   EpisodeResource *eres;
+   guint count;
+   guint8 i;
+
+   create_memory_db(&adapter, &repository);
+
+   /* Filter on season number */
+   g_value_init(&value, G_TYPE_INT64);
+   g_value_set_int64(&value, 4);
+   filter = gom_filter_new_eq(EPISODE_TYPE_RESOURCE, "season-number", &value);
+   g_value_unset(&value);
+
+   /* Order by episode */
+   sorting = gom_sorting_new(EPISODE_TYPE_RESOURCE, "episode-number",
+                             GOM_SORTING_DESCENDING, NULL);
+
+   group = gom_repository_find_sorted_sync(repository, EPISODE_TYPE_RESOURCE,
+                                           filter, sorting, &error);
+   g_assert_no_error(error);
+   g_object_unref(filter);
+   g_object_unref(sorting);
+
+   count = gom_resource_group_get_count(group);
+   g_assert_cmpuint(count, ==, 2);
+
+   gom_resource_group_fetch_sync(group, 0, count, &error);
+   g_assert_no_error(error);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 0));
+   g_assert(eres);
+   g_object_get(eres, "episode-number", &i, NULL);
+   g_assert_cmpuint(i, ==, 2);
+   g_object_unref(eres);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 1));
+   g_assert(eres);
+   g_object_get(eres, "episode-number", &i, NULL);
+   g_assert_cmpuint(i, ==, 1);
+   g_object_unref(eres);
+
+   free_memory_db(adapter, repository);
+}
+
+static void
+find_order_by_complex (void)
+{
+   GomAdapter *adapter;
+   GomRepository *repository;
+   GomFilter *filter;
+   GomSorting *sorting;
+   GomResourceGroup *group;
+   GValue value = { 0, };
+   GError *error = NULL;
+   EpisodeResource *eres;
+   guint count;
+   gchar *id;
+   guint8 season, episode;
+
+   create_memory_db(&adapter, &repository);
+
+   /* Select only the episode for a single show */
+   g_value_init(&value, G_TYPE_STRING);
+   g_value_set_string(&value, "84947");
+   filter = gom_filter_new_eq(EPISODE_TYPE_RESOURCE, "series-id", &value);
+   g_value_unset(&value);
+
+   /* Order by season, then by episode */
+   sorting = gom_sorting_new(EPISODE_TYPE_RESOURCE, "season-number",
+                             GOM_SORTING_DESCENDING,
+                             EPISODE_TYPE_RESOURCE, "episode-number",
+                             GOM_SORTING_ASCENDING,
+                             NULL);
+
+   group = gom_repository_find_sorted_sync(repository, EPISODE_TYPE_RESOURCE,
+                                           filter, sorting, &error);
+   g_assert_no_error(error);
+   g_object_unref(sorting);
+
+   count = gom_resource_group_get_count(group);
+   g_assert_cmpuint(count, ==, 4);
+
+   gom_resource_group_fetch_sync(group, 0, count, &error);
+   g_assert_no_error(error);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 0));
+   g_assert(eres);
+   g_object_get(eres, "series-id", &id, "season-number", &season,
+                "episode-number", &episode, NULL);
+   g_assert_cmpstr(id, ==, "84947");
+   g_assert_cmpuint(season, ==, 5);
+   g_assert_cmpuint(episode, ==, 1);
+   g_object_unref(eres);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 1));
+   g_assert(eres);
+   g_object_get(eres, "series-id", &id, "season-number", &season,
+                "episode-number", &episode, NULL);
+   g_assert_cmpstr(id, ==, "84947");
+   g_assert_cmpuint(season, ==, 5);
+   g_assert_cmpuint(episode, ==, 2);
+   g_object_unref(eres);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 2));
+   g_assert(eres);
+   g_object_get(eres, "series-id", &id, "season-number", &season,
+                "episode-number", &episode, NULL);
+   g_assert_cmpstr(id, ==, "84947");
+   g_assert_cmpuint(season, ==, 4);
+   g_assert_cmpuint(episode, ==, 1);
+   g_object_unref(eres);
+
+   eres = EPISODE_RESOURCE(gom_resource_group_get_index(group, 3));
+   g_assert(eres);
+   g_object_get(eres, "series-id", &id, "season-number", &season,
+                "episode-number", &episode, NULL);
+   g_assert_cmpstr(id, ==, "84947");
+   g_assert_cmpuint(season, ==, 4);
+   g_assert_cmpuint(episode, ==, 2);
+   g_object_unref(eres);
+
+   free_memory_db(adapter, repository);
+}
+
+gint
+main (gint argc, gchar *argv[])
+{
+   g_test_init(&argc, &argv, NULL);
+   g_test_add_func("/GomRepository/find-order-by-asc", find_order_by_asc);
+   g_test_add_func("/GomRepository/find-order-by-desc", find_order_by_desc);
+   g_test_add_func("/GomRepository/find-order-by-complex",
+                   find_order_by_complex);
+   gMainLoop = g_main_loop_new(NULL, FALSE);
+   return g_test_run();
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]