Browse Source

Begin implementation of cross datasource reference query

- The set_query_filter() method handles subquery creation
- The run isn't written yet...
Yann Weber 9 years ago
parent
commit
a91d25e392
2 changed files with 81 additions and 37 deletions
  1. 76
    33
      lodel/leapi/query.py
  2. 5
    4
      tests/leapi/query/test_filtered.py

+ 76
- 33
lodel/leapi/query.py View File

@@ -90,6 +90,9 @@ class LeQuery(object):
90 90
 ##@brief Abstract class handling query with filters
91 91
 #
92 92
 #@todo add handling of inter-datasource queries
93
+#
94
+#@warning relationnal filters on multiple classes from different datasource
95
+# will generate a lot of subqueries
93 96
 class LeFilteredQuery(LeQuery):
94 97
     
95 98
     ##@brief The available operators used in query definitions
@@ -117,16 +120,80 @@ class LeFilteredQuery(LeQuery):
117 120
         super().__init__(target_class)
118 121
         ##@brief The query filter tuple(std_filter, relational_filters)
119 122
         self.__query_filter = None
123
+        ##@brief Stores potential subqueries (used when a query implies
124
+        # more than one datasource.
125
+        #
126
+        # Subqueries are tuple(target_class_ref_field, LeGetQuery)
127
+        self.subqueries = None
120 128
         self.set_query_filter(query_filters)
121 129
     
122 130
     ##@brief Add filter(s) to the query
131
+    #
132
+    # This method is also able to slice query if different datasources are
133
+    # implied in the request
134
+    #
123 135
     #@param query_filter list|tuple|str : A single filter or a list of filters
124 136
     #@see LeFilteredQuery._prepare_filters()
125 137
     def set_query_filter(self, query_filter):
126 138
         if isinstance(query_filter, str):
127 139
             query_filter = [query_filter]
128
-        self.__query_filter = self._prepare_filters(query_filter)
129
-
140
+        #Query filter prepration
141
+        filters_orig , rel_filters = self._prepare_filters(query_filter)
142
+        # Here we now that each relational filter concern only one datasource
143
+        # thank's to _prepare_relational_fields
144
+
145
+        #Multiple datasources detection
146
+        self_ds_name = self._target_class._datasource_name
147
+        result_rel_filters = list() # The filters that will stay in the query
148
+        other_ds_filters = dict()
149
+        for rfilter in rel_filters:
150
+            (rfield, ref_dict), op, value = rfilter
151
+            #rfield : the field in self._target_class
152
+            tmp_rel_filter = dict() #designed to stores rel_field of same DS
153
+            # First step : simplification
154
+            # Trying to delete relational filters done on referenced class uid
155
+            for tclass, tfield in ref_dict.items():
156
+                #tclass : reference target class
157
+                #tfield : referenced field from target class
158
+                if tfield == tclass.uid_fieldname:
159
+                    #This relational filter can be simplified as 
160
+                    # ref_field, op, value
161
+                    # Note : we will have to dedup filters_orig
162
+                    filters_orig.append((rfield, op, value))
163
+                    del(ref_dict[tclass])
164
+            #Determine what to do with other relational filters given 
165
+            # referenced class datasource
166
+            #Remember : each class in a relational filter has the same 
167
+            # datasource
168
+            tclass = list(ref_dict.keys())[0]
169
+            cur_ds = tclass._datasource_name
170
+            if cur_ds == self_ds_name:
171
+                # Same datasource, the filter stay is self query
172
+                result_rel_filters.append(((rfield, ref_dict), op, value))
173
+            else:
174
+                # Different datasource, we will have to create a subquery
175
+                if cur_ds not in other_ds_filters:
176
+                    other_ds_filters[cur_ds] = list()
177
+                other_ds_filters[cur_ds].append(
178
+                    ((rfield, ref_dict), op, value))
179
+        #deduplication of std filters
180
+        filters_orig = list(set(filters_orig))
181
+        # Sets __query_filter attribute of self query
182
+        self.__query_filter = (filters_orig, result_rel_filters)
183
+
184
+        #Sub queries creation
185
+        subq = list()
186
+        for ds, rfilters in other_ds_filters.items():
187
+            for rfilter in rfilters:
188
+                (rfield, ref_dict), op, value = rfilter
189
+                for tclass, tfield in ref_dict.items():
190
+                    query = LeGetQuery(
191
+                        target_class = tclass,
192
+                        query_filter = [(rfield, op, value)],
193
+                        field_list = [tfield])
194
+                    subq.append((rfield, query))
195
+    
196
+    ##@return informations
130 197
     def dump_infos(self):
131 198
         ret = super().dump_infos()
132 199
         ret['query_filter'] = self.__query_filter
@@ -243,34 +310,6 @@ field to use for the relational filter"
243 310
                                         "Error while preparing filters : ",
244 311
                                         err_l)
245 312
         return (res_filters, rel_filters)
246
-    
247
-    ##@brief Prepare & check relational field
248
-    #
249
-    # The result is a tuple with (field, ref_field, concerned_classes), with :
250
-    # - field the target_class field name
251
-    # - ref_field the concerned_classes field names
252
-    # - concerned_classes a set of concerned LeObject classes
253
-    #@param field str : The target_class field name
254
-    #@param ref_field str : The referenced class field name
255
-    #@return a tuple(field, concerned_classes, ref_field) or an Exception
256
-    # class instance
257
-    def _prepare_relational_fields(self,field, ref_field):
258
-        field_dh = self._target_class.field(field)
259
-        concerned_classes = []
260
-        linked_classes = field_dh.linked_classes
261
-        if linked_classes is None:
262
-            linked_classes = []
263
-        for l_class in linked_classes:
264
-            try:
265
-                l_class.field(ref_field)
266
-                concerned_classes.append(l_class)
267
-            except KeyError:
268
-                pass
269
-        if len(concerned_classes) > 0:
270
-            return (field, ref_field, concerned_classes)
271
-        else:
272
-            msg = "None of the linked class of field %s has a field named '%s'"
273
-            return ValueError(msg % (field, ref_field))
274 313
 
275 314
     ## @brief Check and split a query filter
276 315
     # @note The query_filter format is "FIELD OPERATOR VALUE"
@@ -356,8 +395,7 @@ field to use for the relational filter"
356 395
     #@param ref_field str|None : The referenced field name (if None use
357 396
     #uniq identifiers as referenced field
358 397
     #@return a well formed relational filter tuple or an Exception instance
359
-    @classmethod
360
-    def __prepare_relational_fields(cls, fieldname, ref_field = None):
398
+    def _prepare_relational_fields(self, fieldname, ref_field = None):
361 399
         datahandler = self._target_class.field(fieldname)
362 400
         # now we are going to fetch the referenced class to see if the
363 401
         # reference field is valid
@@ -367,7 +405,12 @@ field to use for the relational filter"
367 405
             for ref_class in ref_classes:
368 406
                 ref_dict[ref_class] = ref_class.uid_fieldname
369 407
         else:
408
+            r_ds = None
370 409
             for ref_class in ref_classes:
410
+                if r_ds is None:
411
+                    r_ds = ref_class._datasource_name
412
+                elif ref_class._datasource_name != r_ds:
413
+                    return RuntimeError("All referenced class doesn't have the same datasource. Query not possible")
371 414
                 if ref_field in ref_class.fieldnames(True):
372 415
                     ref_dict[ref_class] = ref_field
373 416
                 else:
@@ -376,7 +419,7 @@ the relational filter %s" % ref_class.__name__)
376 419
         if len(ref_dict) == 0:
377 420
             return NameError(   "No field named '%s' in referenced objects"
378 421
                                 % ref_field)
379
-        return ( (fieldname, ref_dict), op, value)
422
+        return (fieldname, ref_dict)
380 423
  
381 424
 
382 425
 ##@brief A query to insert a new object

+ 5
- 4
tests/leapi/query/test_filtered.py View File

@@ -32,8 +32,9 @@ class LeFilteredQueryTestCase(unittest.TestCase):
32 32
         for q_class in self.q_classes:
33 33
             for q_filter_arg, e_qfilter in test_datas:
34 34
                 get_q = q_class(dyncode.Publication, q_filter_arg)
35
-                self.assertEqual(   get_q.dump_infos()['query_filter'],
36
-                                    e_qfilter)
35
+                self.assertEqual(
36
+                    sorted(get_q.dump_infos()['query_filter'][0]),
37
+                    sorted(e_qfilter[0]))
37 38
 
38 39
     def test_invalid_filters(self):
39 40
         """ Testing invalid filters detection """
@@ -91,11 +92,11 @@ class LeFilteredQueryTestCase(unittest.TestCase):
91 92
         test_datas = [  (   dyncode.Subsection,
92 93
                             'parent.lodel_id = 42',
93 94
                             (   [],
94
-                                [(('parent', 'lodel_id', [dyncode.Section]),'=','42')])),
95
+                                [(('parent', {dyncode.Section: 'lodel_id'}), '=', '42')])),
95 96
                         (   dyncode.Section,
96 97
                             'childs.lodel_id = 42',
97 98
                             (   [],
98
-                                [(('childs', 'lodel_id', [dyncode.Subsection]),'=','42')]))
99
+                                [(('childs', {dyncode.Subsection: 'lodel_id'}), '=', '42')]))
99 100
                         ]
100 101
 
101 102
         for le_class, q_filter_arg, e_qfilter in test_datas:

Loading…
Cancel
Save