Merge lp:~allenap/storm/value-columns-by-name into lp:storm

Proposed by Gavin Panella
Status: Work in progress
Proposed branch: lp:~allenap/storm/value-columns-by-name
Merge into: lp:storm
Diff against target: 182 lines (+107/-9)
2 files modified
storm/store.py (+40/-8)
tests/store/base.py (+67/-1)
To merge this branch: bzr merge lp:~allenap/storm/value-columns-by-name
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Disapprove
Jamu Kakar (community) Needs Fixing
James Henstridge Needs Fixing
Review via email: mp+23480@code.launchpad.net

Commit message

ResultSet.values() can now accept column names as well as columns themselves.

Description of the change

In an environment like Launchpad where a lot of the code only uses Storm via a Zope prophylactic, and there is also an importfascist that complains bitterly when model code is imported by non-model modules, it would be extremely handy to be able to pass names into ResultSet.values() rather than columns.

I might have done this all the wrong way, but it's a start and I'm happy to learn the right way to get this branch landed.

To post a comment you must log in.
Revision history for this message
Jamu Kakar (jkakar) wrote :

Can you please file a bug and link this branch to it? All Storm
branches should have an associated bug.

Revision history for this message
James Henstridge (jamesh) wrote :

If we want to support this feature, it should probably exclude all tuple finds. For example:

  result = store.find((Company, Employee), Company.id == Employee.company_id)
  ids = result.values('id')

Assuming both tables have an 'id' column, which one am I referring to? It seems better to limit the feature to the simple situations where we can handle it unambiguously.

review: Needs Fixing
Revision history for this message
James Henstridge (jamesh) wrote :

And a follow up: to be consistent with Store.find(), I'd suggest using getattr(find_spec.default_cls, name) to convert names to column objects.

In the tuple find case, default_cls will be None, so you should check for this and raise FeatureError.

Revision history for this message
Jamu Kakar (jkakar) wrote :

[1]

Can you please add docstrings to the tests. Also, the @param for
'columns' in the docstring for ResultSet.values needs to be updated.

[2]

+ def test_find_values_by_column_name(self):

There are several different cases being exercised in this test. I
recommend you break it up into several smaller tests.

[3]

+ # If more than one column in the result set has the same name,
+ # the first will be chosen.
+ result = self.store.find(
+ (Foo.id, FooValue.id), FooValue.foo_id == Foo.id)
+ result = result.config(distinct=True).order_by(Foo.id)
+ values = result.values('id')
+ self.assertEquals(list(values), [10, 20])

I agree with James that this case should raise FeatureError. We
shouldn't be making guesses about which column the user is
specifying if the choice is ambiguous.

[4]

+ def test_find_multiple_values_by_column_name(self):

This test could also be split into two, so that each test exercises
one specific behaviour (multiple columns, and mixed string and Column
values).

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

Getting columns from names is now done by getting the columns from the
find set, using it to construct a dict of name -> [columns] and
mapping with that. When the result of the mapping is a list of length
2 or more then it raises FeatureError. In this way it will work
naturally with tuple finds, and allow values() to (not quite, there's
another issue) yield expression values.

I've also changed it to copy the select before modifying it. I don't
know how necessary this is, but the code before looked a bit suspect
to my untrained eye.

Also, when variable_factory is not found on the column, it defaults to
Variable. This means that it's possible to do things like
results.values(Sum(Table.price)).

Okay, having said all that, if this is too fancy, or liable to cause
issues, I'll go for the approach in get_where_for_args().

However, if this approach is okay, a couple of notes:

- Either or both of the new methods ResultSet._get_column_name_map()
  and ResultSet._map_column_names() could be moved to FindSpec. Would
  that be a more natural home for them?

- Should _map_column_names() be use in order_by() and group_by()?

=== modified file 'storm/store.py'
--- storm/store.py 2010-04-15 13:31:08 +0000
+++ storm/store.py 2010-04-22 09:32:10 +0000
@@ -540,7 +540,6 @@
540 else:540 else:
541541
542 cached_primary_vars = obj_info["primary_vars"]542 cached_primary_vars = obj_info["primary_vars"]
543 primary_key_idx = cls_info.primary_key_idx
544543
545 changes = self._get_changes_map(obj_info)544 changes = self._get_changes_map(obj_info)
546545
@@ -1238,34 +1237,63 @@
1238 """Get the sum of all values in an expression."""1237 """Get the sum of all values in an expression."""
1239 return self._aggregate(Sum, expr, expr)1238 return self._aggregate(Sum, expr, expr)
12401239
1240 def _get_column_name_map(self):
1241 """Return a mapping of column name to lists of columns."""
1242 columns, tables = self._find_spec.get_columns_and_tables()
1243 column_map = {}
1244 for column in columns:
1245 if isinstance(column, (Alias, Column)):
1246 if column.name in column_map:
1247 column_map[column.name].append(column)
1248 else:
1249 column_map[column.name] = [column]
1250 return column_map
1251
1252 def _map_column_names(self, columns):
1253 """Attempt to map column names to actual columns."""
1254 column_map = self._get_column_name_map()
1255 for column in columns:
1256 if column in column_map:
1257 column_name, column_list = column, column_map[column]
1258 if len(column_list) != 1:
1259 raise FeatureError("Ambiguous column: %s" % column_name)
1260 [column] = column_list
1261 if isinstance(column, Alias):
1262 column = column.expr
1263 yield column
1264
1241 def values(self, *columns):1265 def values(self, *columns):
1242 """Retrieve only the specified columns.1266 """Retrieve only the specified columns.
12431267
1244 This does not load full objects from the database into Python.1268 This does not load full objects from the database into Python.
12451269
1246 @param columns: One or more L{storm.expr.Column} objects whose1270 @param columns: One or more L{storm.expr.Column} objects, or
1247 values will be fetched.1271 column names, whose values will be fetched. If column
1272 names are given, each must refer unambiguously to a named
1273 column or alias in the result set.
1248 @return: An iterator of tuples of the values for each column1274 @return: An iterator of tuples of the values for each column
1249 from each matching row in the database.1275 from each matching row in the database.
1250 """1276 """
1251 if not columns:1277 if not columns:
1252 raise FeatureError("values() takes at least one column "1278 raise FeatureError("values() takes at least one column "
1253 "as argument")1279 "as argument")
1254 select = self._get_select()1280 columns = list(self._map_column_names(columns))
1255 column_map = dict(1281 # replace_columns() can lose ordering so it's not good here.
1256 (column.name, column)1282 select = copy(self._get_select())
1257 for column in reversed(select.columns)
1258 if isinstance(column, Column))
1259 columns = [column_map.get(column, column) for column in columns]
1260 select.columns = columns1283 select.columns = columns
1261 result = self._store._connection.execute(select)1284 result = self._store._connection.execute(select)
1262 if len(columns) == 1:1285 variable_factories = [
1263 variable = columns[0].variable_factory()1286 getattr(column, 'variable_factory', Variable)
1287 for column in columns]
1288 variables = [
1289 variable_factory()
1290 for variable_factory in variable_factories]
1291 if len(variables) == 1:
1292 [variable] = variables
1264 for values in result:1293 for values in result:
1265 result.set_variable(variable, values[0])1294 result.set_variable(variable, values[0])
1266 yield variable.get()1295 yield variable.get()
1267 else:1296 else:
1268 variables = [column.variable_factory() for column in columns]
1269 for values in result:1297 for values in result:
1270 for variable, value in zip(variables, values):1298 for variable, value in zip(variables, values):
1271 result.set_variable(variable, value)1299 result.set_variable(variable, value)
@@ -1370,7 +1398,6 @@
1370 def get_column(column):1398 def get_column(column):
1371 return obj_info.variables[column].get()1399 return obj_info.variables[column].get()
1372 objects = []1400 objects = []
1373 cls = self._find_spec.default_cls_info.cls
1374 for obj_info in self._store._iter_alive():1401 for obj_info in self._store._iter_alive():
1375 try:1402 try:
1376 if (obj_info.cls_info is self._find_spec.default_cls_info and1403 if (obj_info.cls_info is self._find_spec.default_cls_info and
13771404
=== modified file 'tests/store/base.py'
--- tests/store/base.py 2010-04-15 13:31:08 +0000
+++ tests/store/base.py 2010-04-22 10:30:27 +0000
@@ -1002,25 +1002,51 @@
1002 self.assertEquals([type(value) for value in values],1002 self.assertEquals([type(value) for value in values],
1003 [unicode, unicode, unicode])1003 [unicode, unicode, unicode])
10041004
1005 def test_find_values_with_expression(self):
1006 """
1007 An expression can be passed to ResultSet.values().
1008 """
1009 values = self.store.find(Foo.id).values(Sum(Foo.id))
1010 self.assertEquals(list(values), [60])
1011
1005 def test_find_values_by_column_name(self):1012 def test_find_values_by_column_name(self):
1013 """
1014 ResultSet.values() can accept column names which are mapped to
1015 columns in the find spec.
1016 """
1006 result = self.store.find(Foo).order_by(Foo.id)1017 result = self.store.find(Foo).order_by(Foo.id)
1007 values = result.values('id')1018 values = result.values('id')
1008 self.assertEquals(list(values), [10, 20, 30])1019 self.assertEquals(list(values), [10, 20, 30])
1009 values = result.values('title')1020 values = result.values('title')
1010 self.assertEquals(list(values), ["Title 30", "Title 20", "Title 10"])1021 self.assertEquals(list(values), ["Title 30", "Title 20", "Title 10"])
1011 # If more than one column in the result set has the same name,1022
1012 # the first will be chosen.1023 def test_find_values_by_alias_name(self):
1024 """
1025 Alias names can also be passed to ResultSet.values().
1026 """
1027 result = self.store.find(Alias(Foo.id, 'foo')).order_by(Foo.id)
1028 values = result.values('foo')
1029 self.assertEquals(list(values), [10, 20, 30])
1030
1031 def test_find_values_by_alias_name_to_expression(self):
1032 """
1033 Alias names can be passed to ResultSet.values(), even if the
1034 aliased column is actually an expression.
1035 """
1036 result = self.store.find(Alias(Sum(Foo.id), 'foo')).order_by(Foo.id)
1037 values = result.values('foo')
1038 self.assertEquals(list(values), [60])
1039
1040 def test_find_values_by_ambiguous_column_name(self):
1041 """
1042 If more than one column in the find spec has the same name,
1043 FeatureError is raised.
1044 """
1013 result = self.store.find(1045 result = self.store.find(
1014 (Foo.id, FooValue.id), FooValue.foo_id == Foo.id)1046 (Foo.id, FooValue.id), FooValue.foo_id == Foo.id)
1015 result = result.config(distinct=True).order_by(Foo.id)1047 result = result.config(distinct=True).order_by(Foo.id)
1016 values = result.values('id')1048 values = result.values('id')
1017 self.assertEquals(list(values), [10, 20])1049 self.assertRaises(FeatureError, list, values)
1018 # The name is only matched against columns, not aliases or
1019 # other expressions.
1020 result = self.store.find(
1021 (Alias(SQL('1'), 'id'), Foo.id)).order_by(Foo.id)
1022 values = result.values('id')
1023 self.assertEquals(list(values), [10, 20, 30])
10241050
1025 def test_find_multiple_values(self):1051 def test_find_multiple_values(self):
1026 result = self.store.find(Foo).order_by(Foo.id)1052 result = self.store.find(Foo).order_by(Foo.id)
@@ -1031,12 +1057,22 @@
1031 (30, "Title 10")])1057 (30, "Title 10")])
10321058
1033 def test_find_multiple_values_by_column_name(self):1059 def test_find_multiple_values_by_column_name(self):
1060 """
1061 More than one column name can be given to ResultSet.values();
1062 it will map them all to columns.
1063 """
1034 result = self.store.find(Foo).order_by(Foo.id)1064 result = self.store.find(Foo).order_by(Foo.id)
1035 values = result.values('id', 'title')1065 values = result.values('id', 'title')
1036 expected = [(10, "Title 30"), (20, "Title 20"), (30, "Title 10")]1066 expected = [(10, "Title 30"), (20, "Title 20"), (30, "Title 10")]
1037 self.assertEquals(list(values), expected)1067 self.assertEquals(list(values), expected)
1038 # Columns and column names can be mixed.1068
1069 def test_find_multiple_values_by_column_and_column_name(self):
1070 """
1071 Columns and column names can be mixed.
1072 """
1073 result = self.store.find(Foo).order_by(Foo.id)
1039 values = result.values(Foo.id, 'title')1074 values = result.values(Foo.id, 'title')
1075 expected = [(10, "Title 30"), (20, "Title 20"), (30, "Title 10")]
1040 self.assertEquals(list(values), expected)1076 self.assertEquals(list(values), expected)
10411077
1042 def test_find_values_with_no_arguments(self):1078 def test_find_values_with_no_arguments(self):
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Gavin,

I don't quite see the motivation for this feature. Using the actual class attributes is how it works pretty much everywhere else in Storm, and doesn't look like a big burden when compared to the column names.

If you have a huge class name, you can easily do something like this:

    C = MyHugeClassName

And then have exactly the same call length:

    resultset.values(C.id, C.name)

vs.

    resultset.values("id", "name")

With the advantage that the former, in case of errors, will blow up as syntax errors early, rather than SQL exceptions.

The import fascist isn't a great reason to add support like this in Storm either. Having a result set in the code means you have access to the model objects. What would be the reason of preventing access to the object's class in a situation where you have the object and a *result set* (which allows one to change column values without any checks).

Then, there's a significant additional cost being introduced in a place which used to be pretty lightweight.

For these reasons, I don't feel like it's an improvement over how it works today.

review: Disapprove
Revision history for this message
Gavin Panella (allenap) wrote :

> Gavin,
>
> I don't quite see the motivation for this feature. Using the actual class
> attributes is how it works pretty much everywhere else in Storm, and doesn't
> look like a big burden when compared to the column names.

This patch also lets you specify an aliased column, which could be an
expression rather than a plain column on a table. Perhaps there is a
better way of achieving this.

>
> If you have a huge class name, you can easily do something like this:
>
> C = MyHugeClassName
>
> And then have exactly the same call length:
>
> resultset.values(C.id, C.name)
>
> vs.
>
> resultset.values("id", "name")

Brevity wasn't my concern in this patch :)

> With the advantage that the former, in case of errors, will blow up as syntax
> errors early, rather than SQL exceptions.

Actually, somewhat amusingly, you'll get the string for the invalid
column back instead. So:

  list(store.find(Foo.id).values('frooty'))

returns:

  ['frooty', 'frooty', 'frooty']

But that's possible to remedy.

>
> The import fascist isn't a great reason to add support like this in Storm
> either. Having a result set in the code means you have access to the model
> objects. What would be the reason of preventing access to the object's class
> in a situation where you have the object and a *result set* (which allows one
> to change column values without any checks).

In Launchpad result sets are often returned from methods called on a
secured utility, so browser code and script code does not have access
to the model class.

Any code that uses the objects materialized from a result set must
know the name of the attributes its interested in, so it makes sense
that it could ask for the values of those attributes across the rows
defined by the result set.

My use-case is a script with lots of transactions. A result set
defining the interesting rows is obtained early on from a secured
utility and is used in many separate transactions. Currently there can
be as many as ~16000 interesting rows. I want to avoid materializing
these rows into model objects until they're absolutely needed, because
it's slow and the transaction killer is merciless, but sometimes the
code does need one or two attributes from the whole set.

Anyway, that was my reasoning, but, as earlier, there may be a better
way to do it. I could, for example, add more methods on the secured
utility to return different information from the result set for
me. It's not really the way I'd like to structure the code but it only
offends my taste a little bit ;)

>
> Then, there's a significant additional cost being introduced in a place which
> used to be pretty lightweight.

I haven't measured it, but I guess that the cost is still small
compared to compiling the query and doing a round-trip to the
database.

>
> For these reasons, I don't feel like it's an improvement over how it works
> today.

Fair enough, I'm not blocked on this. Thanks for looking at it! I've
learnt a lot about Storm from doing this.

Unmerged revisions

362. By Gavin Panella

Make ResultSet.values() work with expressions.

361. By Gavin Panella

Add docstrings to new private methods.

360. By Gavin Panella

Fix some lint.

359. By Gavin Panella

Make the implementation of _get_column_name_map() more readable and obvious.

358. By Gavin Panella

Update the docstring for ResultSet.values().

357. By Gavin Panella

Raise an error when the column choice is ambiguous, copy the select before mutating it, and break up tests.

356. By Gavin Panella

ResultSet.values() can now accept column names as well as columns themselves.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storm/store.py'
--- storm/store.py 2010-02-10 11:29:39 +0000
+++ storm/store.py 2010-04-22 10:53:24 +0000
@@ -540,7 +540,6 @@
540 else:540 else:
541541
542 cached_primary_vars = obj_info["primary_vars"]542 cached_primary_vars = obj_info["primary_vars"]
543 primary_key_idx = cls_info.primary_key_idx
544543
545 changes = self._get_changes_map(obj_info)544 changes = self._get_changes_map(obj_info)
546545
@@ -1246,29 +1245,63 @@
1246 """Get the sum of all values in an expression."""1245 """Get the sum of all values in an expression."""
1247 return self._aggregate(Sum, expr, expr)1246 return self._aggregate(Sum, expr, expr)
12481247
1248 def _get_column_name_map(self):
1249 """Return a mapping of column name to lists of columns."""
1250 columns, tables = self._find_spec.get_columns_and_tables()
1251 column_map = {}
1252 for column in columns:
1253 if isinstance(column, (Alias, Column)):
1254 if column.name in column_map:
1255 column_map[column.name].append(column)
1256 else:
1257 column_map[column.name] = [column]
1258 return column_map
1259
1260 def _map_column_names(self, columns):
1261 """Attempt to map column names to actual columns."""
1262 column_map = self._get_column_name_map()
1263 for column in columns:
1264 if column in column_map:
1265 column_name, column_list = column, column_map[column]
1266 if len(column_list) != 1:
1267 raise FeatureError("Ambiguous column: %s" % column_name)
1268 [column] = column_list
1269 if isinstance(column, Alias):
1270 column = column.expr
1271 yield column
1272
1249 def values(self, *columns):1273 def values(self, *columns):
1250 """Retrieve only the specified columns.1274 """Retrieve only the specified columns.
12511275
1252 This does not load full objects from the database into Python.1276 This does not load full objects from the database into Python.
12531277
1254 @param columns: One or more L{storm.expr.Column} objects whose1278 @param columns: One or more L{storm.expr.Column} objects, or
1255 values will be fetched.1279 column names, whose values will be fetched. If column
1280 names are given, each must refer unambiguously to a named
1281 column or alias in the result set.
1256 @return: An iterator of tuples of the values for each column1282 @return: An iterator of tuples of the values for each column
1257 from each matching row in the database.1283 from each matching row in the database.
1258 """1284 """
1259 if not columns:1285 if not columns:
1260 raise FeatureError("values() takes at least one column "1286 raise FeatureError("values() takes at least one column "
1261 "as argument")1287 "as argument")
1262 select = self._get_select()1288 columns = list(self._map_column_names(columns))
1289 # replace_columns() can lose ordering so it's not good here.
1290 select = copy(self._get_select())
1263 select.columns = columns1291 select.columns = columns
1264 result = self._store._connection.execute(select)1292 result = self._store._connection.execute(select)
1265 if len(columns) == 1:1293 variable_factories = [
1266 variable = columns[0].variable_factory()1294 getattr(column, 'variable_factory', Variable)
1295 for column in columns]
1296 variables = [
1297 variable_factory()
1298 for variable_factory in variable_factories]
1299 if len(variables) == 1:
1300 [variable] = variables
1267 for values in result:1301 for values in result:
1268 result.set_variable(variable, values[0])1302 result.set_variable(variable, values[0])
1269 yield variable.get()1303 yield variable.get()
1270 else:1304 else:
1271 variables = [column.variable_factory() for column in columns]
1272 for values in result:1305 for values in result:
1273 for variable, value in zip(variables, values):1306 for variable, value in zip(variables, values):
1274 result.set_variable(variable, value)1307 result.set_variable(variable, value)
@@ -1373,7 +1406,6 @@
1373 def get_column(column):1406 def get_column(column):
1374 return obj_info.variables[column].get()1407 return obj_info.variables[column].get()
1375 objects = []1408 objects = []
1376 cls = self._find_spec.default_cls_info.cls
1377 for obj_info in self._store._iter_alive():1409 for obj_info in self._store._iter_alive():
1378 try:1410 try:
1379 if (obj_info.cls_info is self._find_spec.default_cls_info and1411 if (obj_info.cls_info is self._find_spec.default_cls_info and
13801412
=== modified file 'tests/store/base.py'
--- tests/store/base.py 2010-04-16 07:12:13 +0000
+++ tests/store/base.py 2010-04-22 10:53:24 +0000
@@ -30,7 +30,8 @@
30from storm.properties import PropertyPublisherMeta, Decimal30from storm.properties import PropertyPublisherMeta, Decimal
31from storm.variables import PickleVariable31from storm.variables import PickleVariable
32from storm.expr import (32from storm.expr import (
33 Asc, Desc, Select, LeftJoin, SQL, Count, Sum, Avg, And, Or, Eq, Lower)33 Alias, And, Asc, Avg, Count, Desc, Eq, LeftJoin, Lower, Or, SQL, Select,
34 Sum)
34from storm.variables import Variable, UnicodeVariable, IntVariable35from storm.variables import Variable, UnicodeVariable, IntVariable
35from storm.info import get_obj_info, ClassAlias36from storm.info import get_obj_info, ClassAlias
36from storm.exceptions import *37from storm.exceptions import *
@@ -1001,6 +1002,52 @@
1001 self.assertEquals([type(value) for value in values],1002 self.assertEquals([type(value) for value in values],
1002 [unicode, unicode, unicode])1003 [unicode, unicode, unicode])
10031004
1005 def test_find_values_with_expression(self):
1006 """
1007 An expression can be passed to ResultSet.values().
1008 """
1009 values = self.store.find(Foo.id).values(Sum(Foo.id))
1010 self.assertEquals(list(values), [60])
1011
1012 def test_find_values_by_column_name(self):
1013 """
1014 ResultSet.values() can accept column names which are mapped to
1015 columns in the find spec.
1016 """
1017 result = self.store.find(Foo).order_by(Foo.id)
1018 values = result.values('id')
1019 self.assertEquals(list(values), [10, 20, 30])
1020 values = result.values('title')
1021 self.assertEquals(list(values), ["Title 30", "Title 20", "Title 10"])
1022
1023 def test_find_values_by_alias_name(self):
1024 """
1025 Alias names can also be passed to ResultSet.values().
1026 """
1027 result = self.store.find(Alias(Foo.id, 'foo')).order_by(Foo.id)
1028 values = result.values('foo')
1029 self.assertEquals(list(values), [10, 20, 30])
1030
1031 def test_find_values_by_alias_name_to_expression(self):
1032 """
1033 Alias names can be passed to ResultSet.values(), even if the
1034 aliased column is actually an expression.
1035 """
1036 result = self.store.find(Alias(Sum(Foo.id), 'foo')).order_by(Foo.id)
1037 values = result.values('foo')
1038 self.assertEquals(list(values), [60])
1039
1040 def test_find_values_by_ambiguous_column_name(self):
1041 """
1042 If more than one column in the find spec has the same name,
1043 FeatureError is raised.
1044 """
1045 result = self.store.find(
1046 (Foo.id, FooValue.id), FooValue.foo_id == Foo.id)
1047 result = result.config(distinct=True).order_by(Foo.id)
1048 values = result.values('id')
1049 self.assertRaises(FeatureError, list, values)
1050
1004 def test_find_multiple_values(self):1051 def test_find_multiple_values(self):
1005 result = self.store.find(Foo).order_by(Foo.id)1052 result = self.store.find(Foo).order_by(Foo.id)
1006 values = result.values(Foo.id, Foo.title)1053 values = result.values(Foo.id, Foo.title)
@@ -1009,6 +1056,25 @@
1009 (20, "Title 20"),1056 (20, "Title 20"),
1010 (30, "Title 10")])1057 (30, "Title 10")])
10111058
1059 def test_find_multiple_values_by_column_name(self):
1060 """
1061 More than one column name can be given to ResultSet.values();
1062 it will map them all to columns.
1063 """
1064 result = self.store.find(Foo).order_by(Foo.id)
1065 values = result.values('id', 'title')
1066 expected = [(10, "Title 30"), (20, "Title 20"), (30, "Title 10")]
1067 self.assertEquals(list(values), expected)
1068
1069 def test_find_multiple_values_by_column_and_column_name(self):
1070 """
1071 Columns and column names can be mixed.
1072 """
1073 result = self.store.find(Foo).order_by(Foo.id)
1074 values = result.values(Foo.id, 'title')
1075 expected = [(10, "Title 30"), (20, "Title 20"), (30, "Title 10")]
1076 self.assertEquals(list(values), expected)
1077
1012 def test_find_values_with_no_arguments(self):1078 def test_find_values_with_no_arguments(self):
1013 result = self.store.find(Foo).order_by(Foo.id)1079 result = self.store.find(Foo).order_by(Foo.id)
1014 self.assertRaises(FeatureError, result.values().next)1080 self.assertRaises(FeatureError, result.values().next)

Subscribers

People subscribed via source and target branches

to status/vote changes: