Merge lp:~cjwatson/launchpad/better-gitref-bulk-loading into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18777
Proposed branch: lp:~cjwatson/launchpad/better-gitref-bulk-loading
Merge into: lp:launchpad
Diff against target: 125 lines (+60/-8)
2 files modified
lib/lp/services/database/bulk.py (+33/-5)
lib/lp/services/database/tests/test_bulk.py (+27/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/better-gitref-bulk-loading
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+354112@code.launchpad.net

Commit message

Use a more compact query to load objects with compound primary keys.

Description of the change

Mentioned in passing in bug 1790047, although it's not really essential to that bug.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/services/database/bulk.py'
--- lib/lp/services/database/bulk.py 2013-08-23 05:36:46 +0000
+++ lib/lp/services/database/bulk.py 2018-09-12 09:02:58 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Optimized bulk operations against the database."""4"""Optimized bulk operations against the database."""
@@ -16,7 +16,10 @@
1616
17from collections import defaultdict17from collections import defaultdict
18from functools import partial18from functools import partial
19from itertools import chain19from itertools import (
20 chain,
21 groupby,
22 )
20from operator import (23from operator import (
21 attrgetter,24 attrgetter,
22 itemgetter,25 itemgetter,
@@ -99,6 +102,32 @@
99 return primary_key102 return primary_key
100103
101104
105def _make_compound_load_clause(primary_key, values_list):
106 """Construct a bulk-loading query clause for a compound primary key.
107
108 When the primary key is compound, we expect that in practice we will
109 often want to load objects with common leading elements of the key: for
110 example, we often want to load many `GitRef` objects from the same
111 repository. This helper returns a query clause optimised to be compact
112 in this case.
113
114 :param primary_key: The primary key columns.
115 :param values_list: A sequence of values of the primary key whose
116 corresponding rows should be loaded. This must be sorted by the
117 caller (because this function calls itself recursively, and it's
118 usually more efficient to sort the whole sequence in one go).
119 """
120 if len(primary_key) > 1:
121 return Or(*(
122 And(
123 primary_key[0] == leading_value,
124 _make_compound_load_clause(
125 primary_key[1:], [values[1:] for values in group]))
126 for leading_value, group in groupby(values_list, itemgetter(0))))
127 else:
128 return primary_key[0].is_in([values[0] for values in values_list])
129
130
102def load(object_type, primary_keys, store=None):131def load(object_type, primary_keys, store=None):
103 """Load a large number of objects efficiently."""132 """Load a large number of objects efficiently."""
104 primary_key = _primary_key(object_type, allow_compound=True)133 primary_key = _primary_key(object_type, allow_compound=True)
@@ -107,9 +136,8 @@
107 if not primary_keys:136 if not primary_keys:
108 return []137 return []
109 if isinstance(primary_key, tuple):138 if isinstance(primary_key, tuple):
110 condition = Or(*(139 condition = _make_compound_load_clause(
111 And(*(key == value for (key, value) in zip(primary_key, values)))140 primary_key, sorted(primary_keys))
112 for values in primary_keys))
113 else:141 else:
114 condition = primary_key.is_in(primary_keys)142 condition = primary_key.is_in(primary_keys)
115 if store is None:143 if store is None:
116144
=== modified file 'lib/lp/services/database/tests/test_bulk.py'
--- lib/lp/services/database/tests/test_bulk.py 2018-04-19 00:02:19 +0000
+++ lib/lp/services/database/tests/test_bulk.py 2018-09-12 09:02:58 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test the bulk database functions."""4"""Test the bulk database functions."""
@@ -35,7 +35,10 @@
35 ISlaveStore,35 ISlaveStore,
36 IStore,36 IStore,
37 )37 )
38from lp.services.database.sqlbase import get_transaction_timestamp38from lp.services.database.sqlbase import (
39 convert_storm_clause_to_string,
40 get_transaction_timestamp,
41 )
39from lp.services.features.model import (42from lp.services.features.model import (
40 FeatureFlag,43 FeatureFlag,
41 getFeatureStore,44 getFeatureStore,
@@ -172,6 +175,27 @@
172 bulk.reload([db_object])175 bulk.reload([db_object])
173 self.assertIsNone(db_object_info.get('invalidated'))176 self.assertIsNone(db_object_info.get('invalidated'))
174177
178 def test__make_compound_load_clause(self):
179 # The query constructed by _make_compound_load_clause has the
180 # correct structure.
181 clause = bulk._make_compound_load_clause(
182 # This primary key is fictional, but lets us do a more elaborate
183 # test.
184 (FeatureFlag.scope, FeatureFlag.priority, FeatureFlag.flag),
185 sorted(
186 [(u'foo', 0, u'bar'), (u'foo', 0, u'baz'),
187 (u'foo', 1, u'bar'), (u'foo', 1, u'quux'),
188 (u'bar', 0, u'foo')]))
189 self.assertEqual(
190 "FeatureFlag.scope = E'bar' AND ("
191 "FeatureFlag.priority = 0 AND FeatureFlag.flag IN (E'foo')) OR "
192 "FeatureFlag.scope = E'foo' AND ("
193 "FeatureFlag.priority = 0 AND "
194 "FeatureFlag.flag IN (E'bar', E'baz') OR "
195 "FeatureFlag.priority = 1 AND "
196 "FeatureFlag.flag IN (E'bar', E'quux'))",
197 convert_storm_clause_to_string(clause))
198
175 def test_load(self):199 def test_load(self):
176 # load() loads objects of the given type by their primary keys.200 # load() loads objects of the given type by their primary keys.
177 db_objects = [201 db_objects = [
@@ -189,7 +213,7 @@
189 ClassInfoError, bulk.load, str, [])213 ClassInfoError, bulk.load, str, [])
190214
191 def test_load_with_compound_primary_keys(self):215 def test_load_with_compound_primary_keys(self):
192 # load() does not like compound primary keys.216 # load() can load objects with compound primary keys.
193 flags = [217 flags = [
194 FeatureFlag(u'foo', 0, u'bar', u'true'),218 FeatureFlag(u'foo', 0, u'bar', u'true'),
195 FeatureFlag(u'foo', 0, u'baz', u'false'),219 FeatureFlag(u'foo', 0, u'baz', u'false'),