Merge lp:~jkakar/storm/resultset-expression into lp:storm

Proposed by Jamu Kakar
Status: Rejected
Rejected by: Jamu Kakar
Proposed branch: lp:~jkakar/storm/resultset-expression
Merge into: lp:storm
Diff against target: 104 lines (+46/-5)
2 files modified
storm/store.py (+11/-2)
tests/store/base.py (+35/-3)
To merge this branch: bzr merge lp:~jkakar/storm/resultset-expression
Reviewer Review Type Date Requested Status
James Henstridge Needs Fixing
Guilherme Salgado (community) Approve
Review via email: mp+16669@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jamu Kakar (jkakar) wrote :

This branch makes ResultSet a subclass of Expr. It also adds a
compiler specialization to generate SQL from a ResultSet. This
makes it easy to use a ResultSet as a subselect. Unfortunately, the
changes are not 100% backwards compatible. Code such as this:

  result1 = store.find(Foo)
  result2 = store.find(Foo, Foo.id.is_in(result1))

will now result in an OperationalError when run against all
currently supported databases. This is because, before the changes
made in this branch, result1 would have been iterated and yielded
Foo instances. The changes in this branch result in an invalid
query being generated for this case, because the subselect returns
all foo columns, instead of just the id column.

The current behaviour seems a bit odd, so I'm inclined to not worry
about the issue.

Revision history for this message
Guilherme Salgado (salgado) wrote :

At first I found it odd to think of a ResultSet as an Expr, but since it's actually just a container for a Select (which is an Expr), I guess it makes sense.

I think it'd be good to document the behaviour change in the NEWS file, what do you think?

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

I'm not sure if this branch actually helps with the relevant bug.

It doesn't look like it'd help with the example you've given above, which would generate SQL something like:

  SELECT Foo.id, Foo.column1, Foo.column2, ...
    FROM Foo
    WHERE Foo.id IN
      (SELECT Foo.id, Foo.column1, Foo.column2, ...
         FROM Foo);

This is obviously not what was intended. We can fix this by changing the first find call to "result1 = store.find(Foo.id)", but if we're in a position to do that, then we could just as easily have created a Select object directly.

My understanding is that the intended use is in cases where you are using an API that just returns a ResultSet, and these APIs rarely let you specify what is being selected.

Out of all the possible solutions to the problem, I liked the original ResultSet.select(columns) proposal most.

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

James:

Thanks for looking at the changes! I think you might have
misunderstood my original comment: I was saying the same thing you
are, that code that used to work no longer will. Also, I do think
this branch helps with the original problem, but agree that it can
be tricky to use.

That said, it's very easy to generate bad queries and, also, very
easy to inadvertently use result set from one store with a find on a
different store. I think the problems with this approach outweigh
the benefits. The more I think about it the more I like the
ResultSet.select approach, or even just making _get_select a public
method. They are at least very simple and don't change the current
semantics in any way.

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

Okay. I see what you meant now. Note that the example you gave doesn't actually work with Storm trunk either, so I don't think there are backward compatibility issues to worry about:

  >>> result1 = store.find(Foo)
  >>> result2 = store.find(Foo, Foo.id.is_in(result1))
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "storm/expr.py", line 496, in is_in
      others[i] = variable_factory(value=other)
    File "storm/variables.py", line 342, in parse_set
      % (type(value), value))
  TypeError: Expected int, found <class '__main__.Foo'>: <__main__.Foo object at 0x2a462d0>

The other problem I have with this branch is that if I pass a table to store.find(), the columns in the select will not be in a meaningful order. While it is fine for the store code to use class dictionary iteration order internally, I don't think we want to expose this ordering to users.

Unmerged revisions

355. By Jamu Kakar

- Change a test using a ResultSet with is_in() to request a specific
  column.

354. By Jamu Kakar

- ResultSet is now a subclass of Expr and the compiler is
  specialized to generate SQL for it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'storm/store.py'
2--- storm/store.py 2009-11-29 00:42:19 +0000
3+++ storm/store.py 2009-12-30 10:17:14 +0000
4@@ -33,7 +33,7 @@
5 from storm.expr import (
6 Expr, Select, Insert, Update, Delete, Column, Count, Max, Min,
7 Avg, Sum, Eq, And, Asc, Desc, compile_python, compare_columns, SQLRaw,
8- Union, Except, Intersect, Alias, SetExpr)
9+ Union, Except, Intersect, Alias, SetExpr, compile)
10 from storm.exceptions import (
11 WrongStoreError, NotFlushedError, OrderLoopError, UnorderedError,
12 NotOneError, FeatureError, CompileError, LostObjectError, ClassInfoError)
13@@ -892,7 +892,7 @@
14 obj_info.variables[column].checkpoint()
15
16
17-class ResultSet(object):
18+class ResultSet(Expr):
19 """The representation of the results of a query.
20
21 Note that having an instance of this class does not indicate that
22@@ -1437,6 +1437,15 @@
23 return self._set_expr(Intersect, other, all)
24
25
26+@compile.when(ResultSet)
27+def compile_result_set(compile, result, state):
28+ """Generate SQL for a L{ResultSet}."""
29+ # Reset state precendence to 0, as it would be if the compiler didn't
30+ # think this was a nested expression.
31+ state.precedence = 0
32+ return compile(result._get_select(), state)
33+
34+
35 class EmptyResultSet(object):
36 """An object that looks like a L{ResultSet} but represents no rows.
37
38
39=== modified file 'tests/store/base.py'
40--- tests/store/base.py 2009-11-30 11:35:02 +0000
41+++ tests/store/base.py 2009-12-30 10:17:14 +0000
42@@ -25,12 +25,13 @@
43 import weakref
44
45 from storm.references import Reference, ReferenceSet, Proxy
46-from storm.database import Result
47+from storm.database import Result, create_database
48 from storm.properties import Int, Float, RawStr, Unicode, Property, Pickle
49 from storm.properties import PropertyPublisherMeta, Decimal
50 from storm.variables import PickleVariable
51 from storm.expr import (
52- Asc, Desc, Select, LeftJoin, SQL, Count, Sum, Avg, And, Or, Eq, Lower)
53+ Asc, Desc, Select, LeftJoin, SQL, Count, Sum, Avg, And, Or, Eq, Lower,
54+ compile)
55 from storm.variables import Variable, UnicodeVariable, IntVariable
56 from storm.info import get_obj_info, ClassAlias
57 from storm.exceptions import *
58@@ -5340,7 +5341,7 @@
59 self.assertEquals(result3.count(), 2)
60
61 def test_is_in_empty_result_set(self):
62- result1 = self.store.find(Foo, Foo.id < 10)
63+ result1 = self.store.find(Foo.id, Foo.id < 10)
64 result2 = self.store.find(Foo, Or(Foo.id > 20, Foo.id.is_in(result1)))
65 self.assertEquals(result2.count(), 1)
66
67@@ -5709,6 +5710,37 @@
68 self.assertEquals(result_to_remove.remove(), 3)
69
70
71+class ResultSetTest(TestHelper):
72+
73+ def setUp(self):
74+ super(ResultSetTest, self).setUp()
75+ self.store = Store(create_database("sqlite:"))
76+
77+ def tearDown(self):
78+ self.store.close()
79+ super(ResultSetTest, self).tearDown()
80+
81+ def test_compile(self):
82+ """
83+ The compiler is specialized so that L{ResultSet}s can be used with the
84+ L{expression system<storm.expr>}.
85+ """
86+ result = self.store.find(Foo)
87+ self.assertEqual(u"SELECT foo.id, foo.title FROM foo",
88+ compile(result))
89+
90+ def test_compile_subselect(self):
91+ """
92+ When a L{ResultSet} is used as a subselect the compiler will generate
93+ a nested SELECT statement.
94+ """
95+ result1 = self.store.find(Foo.id)
96+ result2 = self.store.find(Foo, Foo.id.is_in(result1))
97+ self.assertEqual(u"SELECT foo.id, foo.title FROM foo "
98+ u"WHERE foo.id IN (SELECT foo.id FROM foo)",
99+ compile(result2))
100+
101+
102 class EmptyResultSetTest(object):
103
104 def setUp(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: