Merge lp:~therve/storm/update-multi-tables into lp:storm

Proposed by Thomas Herve
Status: Work in progress
Proposed branch: lp:~therve/storm/update-multi-tables
Merge into: lp:storm
Diff against target: 187 lines (+92/-15)
5 files modified
storm/databases/mysql.py (+9/-3)
storm/databases/postgres.py (+11/-4)
storm/expr.py (+43/-7)
storm/store.py (+1/-1)
tests/store/base.py (+28/-0)
To merge this branch: bzr merge lp:~therve/storm/update-multi-tables
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Needs Fixing
Storm Developers Pending
Review via email: mp+40790@code.launchpad.net

Description of the change

I'm a bit fuzzy about the state.context values. The work done on the alive cache is not awesome as well.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Interesting changes, thanks Thomas. Here we go with some comments:

[1]

36 + if update.where is not Undef:
37 + state.context = EXPR
38 + where = compile(update.where, state, raw=True)
39 + if state.auto_tables:
40 + tables = set(

You were right to wonder about the context. Note that above the table set will potentially be compiled as EXPR expression, rather than TABLE. Same thing in the other version of compile.

[2]

43 + # Override the table token
44 + tokens[1] = ", ".join(tables)

This feels slightly hackish. We just went through the trouble of compiling the
tokens[1] value above. If we don't need it, the code should be organized in
such a way that tables are compiled just once, and in the same place where
the decision to compile it one way or another is taken.

Also, note that build_tables() internally is able to verify that the auto_tables
is available or not, and build it rather than a default set of tables conditionally,
so it feels like this work doesn't even have to be performed at this level, and may
instead be fully handed into build_tables().

[3]

96 + tables.remove(tokens[1])
97 + if tables:
98 + tokens.append(" FROM ")
99 + tokens.append(", ".join(tables))

Same case as above, but this time build_tables() won't be able to work alone.

[4]

It feels that the distinction between the two compilations of Update is quite small
considering the overall size. Can't we have the default (in expr.py) version which
compiles Update expressions accepting a boolean parameter to act one way or the
other, so that we can keep a single implementation and just select a flag in the
subcompiler?

[5]

118 - except CompileError:
119 + except (CompileError, KeyError):

Your gut feeling was also right on this one. This new KeyError here is quite
mysterious. I don't even understand myself where it's coming from, to be honest,
so we should find a better way to avoid this from popping up.

review: Needs Fixing
lp:~therve/storm/update-multi-tables updated
385. By Thomas Herve

Reduce duplication

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

I made a similar comment on the delete-with-using branch, but I think it'd be good to build the "FROM" clause using build_tables rather than accessing auto_tables manually. This would probably involve adding a way to tell it to exclude the primary table for the update from its output.

Unmerged revisions

385. By Thomas Herve

Reduce duplication

384. By Thomas Herve

Attemp implementing multi table update for postgres and mysql

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'storm/databases/mysql.py'
2--- storm/databases/mysql.py 2009-09-21 17:03:08 +0000
3+++ storm/databases/mysql.py 2010-12-11 13:15:10 +0000
4@@ -31,13 +31,12 @@
5 MySQLdb = dummy
6
7 from storm.expr import (
8- compile, Insert, Select, compile_select, Undef, And, Eq,
9- SQLRaw, SQLToken, is_safe_token)
10+ compile, Insert, Select, Update, compile_select, Undef, SQLToken,
11+ is_safe_token, compile_update, _MULTIPLE_UPDATE_INLINE)
12 from storm.variables import Variable
13 from storm.database import Database, Connection, Result
14 from storm.exceptions import (
15 install_exceptions, DatabaseModuleError, OperationalError)
16-from storm.variables import IntVariable
17
18
19 install_exceptions(MySQLdb)
20@@ -51,6 +50,13 @@
21 select.limit = sys.maxint
22 return compile_select(compile, select, state)
23
24+
25+@compile.when(Update)
26+def compile_mysql_update(compile, update, state):
27+ return compile_update(compile, update, state,
28+ multi_tables=_MULTIPLE_UPDATE_INLINE)
29+
30+
31 @compile.when(SQLToken)
32 def compile_sql_token_mysql(compile, expr, state):
33 """MySQL uses ` as the escape character by default."""
34
35=== modified file 'storm/databases/postgres.py'
36--- storm/databases/postgres.py 2010-10-23 21:02:22 +0000
37+++ storm/databases/postgres.py 2010-12-11 13:15:10 +0000
38@@ -39,10 +39,11 @@
39 psycopg2 = dummy
40
41 from storm.expr import (
42- Undef, Expr, SetExpr, Select, Insert, Alias, And, Eq, FuncExpr, SQLRaw,
43- Sequence, Like, SQLToken, COLUMN, COLUMN_NAME, COLUMN_PREFIX, TABLE,
44- compile, compile_select, compile_insert, compile_set_expr, compile_like,
45- compile_sql_token)
46+ Undef, Expr, SetExpr, Select, Insert, Update, Alias, And, Eq, FuncExpr,
47+ SQLRaw, Sequence, Like, SQLToken, COLUMN, COLUMN_NAME, COLUMN_PREFIX,
48+ TABLE, compile, compile_select, compile_insert, compile_set_expr,
49+ compile_like, compile_sql_token, compile_update,
50+ _MULTIPLE_UPDATE_WITH_FROM)
51 from storm.variables import Variable, ListVariable
52 from storm.database import Database, Connection, Result
53 from storm.exceptions import (
54@@ -183,6 +184,12 @@
55 return compile_insert(compile, insert, state)
56
57
58+@compile.when(Update)
59+def compile_postgres_update(compile, update, state):
60+ return compile_update(compile, update, state,
61+ multi_tables=_MULTIPLE_UPDATE_WITH_FROM)
62+
63+
64 @compile.when(Sequence)
65 def compile_sequence_postgres(compile, sequence, state):
66 return "nextval('%s')" % sequence.name
67
68=== modified file 'storm/expr.py'
69--- storm/expr.py 2010-12-07 15:00:30 +0000
70+++ storm/expr.py 2010-12-11 13:15:10 +0000
71@@ -741,21 +741,57 @@
72 self.table = table
73 self.default_table = default_table
74
75+
76+_MULTIPLE_UPDATE_UNSUPPORTED = 0
77+_MULTIPLE_UPDATE_WITH_FROM = 1
78+_MULTIPLE_UPDATE_INLINE = 2
79+
80+
81 @compile.when(Update)
82-def compile_update(compile, update, state):
83+def compile_update(compile, update, state,
84+ multi_tables=_MULTIPLE_UPDATE_UNSUPPORTED):
85+ """
86+ @param multi_tables: if multi-tables update is supported by the DB engine
87+ as a SQL extension, pass _MULTIPLE_UPDATE_WITH_FROM to use the syntax
88+ with FROM, or _MULTIPLE_UPDATE_INLINE to use the inline syntax.
89+ """
90 map = update.map
91- state.push("context", COLUMN_NAME)
92+ original_context = COLUMN_NAME
93+ if multi_tables == _MULTIPLE_UPDATE_INLINE:
94+ original_context = TABLE
95+ state.push("context", original_context)
96 sets = ["%s=%s" % (compile(col, state, token=True),
97 compile(map[col], state))
98 for col in map]
99- state.context = TABLE
100- tokens = ["UPDATE ", build_tables(compile, update.table,
101- update.default_table, state),
102- " SET ", ", ".join(sets)]
103+ where = None
104 if update.where is not Undef:
105 state.context = EXPR
106+ where = compile(update.where, state, raw=True)
107+ state.context = TABLE
108+ if multi_tables and state.auto_tables:
109+ if multi_tables == _MULTIPLE_UPDATE_WITH_FROM:
110+ first_table = build_tables(
111+ compile, update.table, update.default_table, state)
112+ other_tables = set(
113+ compile(table, state, token=True)
114+ for table in state.auto_tables)
115+ tokens = ["UPDATE ", first_table, " SET ", ", ".join(sets)]
116+ # Omit the original target
117+ other_tables.remove(first_table)
118+ if other_tables:
119+ tokens.append(" FROM ")
120+ tokens.append(", ".join(other_tables))
121+ elif multi_tables == _MULTIPLE_UPDATE_INLINE:
122+ tables = build_tables(
123+ compile, Undef, update.default_table, state)
124+ tokens = ["UPDATE ", tables, " SET ", ", ".join(sets)]
125+ else:
126+ tables = build_tables(
127+ compile, update.table, update.default_table, state)
128+ tokens = ["UPDATE ", tables, " SET ", ", ".join(sets)]
129+ if where is not None:
130 tokens.append(" WHERE ")
131- tokens.append(compile(update.where, state, raw=True))
132+ tokens.append(where)
133 state.pop()
134 return "".join(tokens)
135
136
137=== modified file 'storm/store.py'
138--- storm/store.py 2010-10-14 20:54:44 +0000
139+++ storm/store.py 2010-12-11 13:15:10 +0000
140@@ -1375,7 +1375,7 @@
141
142 try:
143 cached = self.cached()
144- except CompileError:
145+ except (CompileError, KeyError):
146 # We are iterating through all objects in memory here, so
147 # check if the object type matches to avoid trying to
148 # invalidate a column that does not exist, on an unrelated
149
150=== modified file 'tests/store/base.py'
151--- tests/store/base.py 2010-10-19 13:36:26 +0000
152+++ tests/store/base.py 2010-12-11 13:15:10 +0000
153@@ -2724,6 +2724,34 @@
154 self.assertFalse(hasattr(foo, "tainted"))
155 self.assertEquals(foo.title, "Title 40")
156
157+ def test_find_set_with_two_tables(self):
158+ """
159+ Postgres and MySQL supports updating a table using other tables as
160+ filter.
161+ """
162+ cls_name = self.__class__.__name__
163+ if cls_name.startswith("SQLite"):
164+ return
165+ result = self.store.find(
166+ Foo, Foo.id == Bar.foo_id, Bar.title == u"Title 200")
167+ result.set(title=u"Title 2a")
168+ self.assertEqual(u"Title 2a", self.store.get(Foo, 20).title)
169+
170+ def test_find_set_with_multiple_tables(self):
171+ """
172+ Update works with multiple tables in Postgres and MySQL.
173+ """
174+ cls_name = self.__class__.__name__
175+ if cls_name.startswith("SQLite"):
176+ return
177+ result = self.store.find(
178+ Foo, Foo.id == Link.foo_id, Link.bar_id == Bar.id,
179+ Bar.title == u"Title 200")
180+ # Access the result set here, to cache the objects
181+ data = list(result)
182+ result.set(title=u"Title 2a")
183+ self.assertEqual(u"Title 2a", self.store.get(Foo, 20).title)
184+
185 def test_reference(self):
186 bar = self.store.get(Bar, 100)
187 self.assertTrue(bar.foo)

Subscribers

People subscribed via source and target branches

to status/vote changes: