Merge lp:~kamstrup/zeitgeist/zeitgeist-querymancer into lp:zeitgeist/0.1
- zeitgeist-querymancer
- Merge into 0.8-python
Proposed by
Seif Lotfy
Status: | Merged |
---|---|
Merge reported by: | Mikkel Kamstrup Erlandsen |
Merged at revision: | not available |
Proposed branch: | lp:~kamstrup/zeitgeist/zeitgeist-querymancer |
Merge into: | lp:zeitgeist/0.1 |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~kamstrup/zeitgeist/zeitgeist-querymancer |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mikkel Kamstrup Erlandsen | Approve | ||
Review via email: mp+9417@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 999. By Mikkel Kamstrup Erlandsen
-
Implement all comments in RainCTs review
Revision history for this message
Mikkel Kamstrup Erlandsen (kamstrup) : | # |
review:
Approve
Revision history for this message
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'zeitgeist/engine/querymancer.py' |
2 | --- zeitgeist/engine/querymancer.py 1970-01-01 00:00:00 +0000 |
3 | +++ zeitgeist/engine/querymancer.py 2009-07-05 22:45:10 +0000 |
4 | @@ -0,0 +1,379 @@ |
5 | +# -.- encoding: utf-8 -.- |
6 | + |
7 | +# Querymancer - Super simple lightweight ORM inspired by Storm |
8 | +# |
9 | +# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com> |
10 | +# |
11 | +# This program is free software: you can redistribute it and/or modify |
12 | +# it under the terms of the GNU Lesser General Public License as published by |
13 | +# the Free Software Foundation, either version 3 of the License, or |
14 | +# (at your option) any later version. |
15 | +# |
16 | +# This program is distributed in the hope that it will be useful, |
17 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | +# GNU Lesser General Public License for more details. |
20 | +# |
21 | +# You should have received a copy of the GNU Lesser General Public License |
22 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
23 | + |
24 | +""" |
25 | +This module contains a very simple and light query builder for SQL databases, |
26 | +primarily SQLite. It is written to be mostly compatible with the nice syntax of |
27 | +the full featured Storm ORM. |
28 | + |
29 | +The premise of Querymancer is to only provide syntactic sugar and explicitly |
30 | +not do any caching or hidden tricks. That is left for the consumer. Another |
31 | +high priority goal is to keep it light. This means that we try to avoid object |
32 | +allocations where ever possible, and that some nice validations (like type |
33 | +safety) are omitted. |
34 | + |
35 | +The primary class for utilizing Querymancer is the L{Table} class - which |
36 | +also provides a good point to start learning about Querymancer. |
37 | +""" |
38 | + |
39 | +# |
40 | +# TODO: |
41 | +# |
42 | +# * Use qmark substitution in query building to enabled prepared statements |
43 | +# * Fix SQL injection (will be fixed implicitly by the point above) |
44 | +# |
45 | + |
46 | +class ColumnType: |
47 | + """ |
48 | + Base class for all data types held in a L{Table}. This class is an abstract |
49 | + class and can not be instantiated directly. |
50 | + """ |
51 | + __eq_template__ = "%s = %s" |
52 | + __ne_template__ = "%s != %s" |
53 | + __gt_template__ = "%s > %s" |
54 | + __ge_template__ = "%s >= %s" |
55 | + __lt_template__ = "%s < %s" |
56 | + __le_template__ = "%s <= %s" |
57 | + __like_template__ = "%s LIKE %s" |
58 | + |
59 | + def __init__ (self): |
60 | + if self.__class__ == ColumnType: |
61 | + raise TypeError("ColumnType is an abstract class and can not be " |
62 | + "instantiated directly") |
63 | + |
64 | + self._table = None |
65 | + self._name = None |
66 | + |
67 | + def __eq__(self, other): |
68 | + return self.__class__.__eq_template__ % (self, |
69 | + self.__class__.format(other)) |
70 | + |
71 | + def __ne__(self, other): |
72 | + return self.__class__.__ne_template__ % (self, |
73 | + self.__class__.format(other)) |
74 | + |
75 | + def __gt__(self, other): |
76 | + return self.__class__.__gt_template__ % (self, |
77 | + self.__class__.format(other)) |
78 | + |
79 | + def __ge__(self, other): |
80 | + return self.__class__.__ge_template__ % (self, |
81 | + self.__class__.format(other)) |
82 | + |
83 | + def __lt__(self, other): |
84 | + return self.__class__.__lt_template__ % (self, |
85 | + self.__class__.format(other)) |
86 | + |
87 | + def __le__(self, other): |
88 | + return self.__class__.__le_template__ % (self, |
89 | + self.__class__.format(other)) |
90 | + |
91 | + def _set_table (self, table): |
92 | + self._table = table |
93 | + |
94 | + def _set_colname (self, name): |
95 | + self._name = name |
96 | + |
97 | + def __str__ (self): |
98 | + return "%s.%s" % (self._table, self._name) |
99 | + |
100 | + def like (self, other): |
101 | + return self.__class__.__like_template__ % (self, |
102 | + self.__class__.format(other)) |
103 | + |
104 | + @classmethod |
105 | + def format (klass, value): |
106 | + """ |
107 | + Format a data type of this class for inclusion in a query. For strings |
108 | + this means adding quotes around it, integers needs conversion to strings |
109 | + etc. |
110 | + """ |
111 | + return str(value) |
112 | + |
113 | +class Integer(ColumnType): |
114 | + """ |
115 | + Basic data type for an integer |
116 | + """ |
117 | + |
118 | +class String(ColumnType): |
119 | + """ |
120 | + Basic data type for a string |
121 | + """ |
122 | + @classmethod |
123 | + def format(klass, value): |
124 | + return "'%s'" % value |
125 | + |
126 | +class EchoCursor: |
127 | + """ |
128 | + Dummy cursor used when no cursor has been installed on a L{Table} via |
129 | + L{Table.set_cursor}. |
130 | + """ |
131 | + def execute(self, stmt): |
132 | + print "EchoCursor:", stmt |
133 | + |
134 | +class Table: |
135 | + """ |
136 | + Primary class which everything revolves about. You declare a |
137 | + table with a name and the columns that you want to use. Assume you have |
138 | + earlier created a table 'customers' like: |
139 | + |
140 | + CREATE TABLE custormers (name VARCHAR, debt INT) |
141 | + |
142 | + To create a C{Table} instance for this table use: |
143 | + |
144 | + customers = Table("customers", name = String(), debt = Integer()) |
145 | + |
146 | + Before you can start using the C{custormers} instance you need to install |
147 | + a cursor for it using L{set_cursor}. It is an explicit goal of Querymancer |
148 | + not to do this automagically. |
149 | + |
150 | + You can now query or update the C{custormers} table via the methods |
151 | + L{find}, L{update}, and {add}. |
152 | + """ |
153 | + def __init__ (self, name, **colspecs): |
154 | + """ |
155 | + Create a new C{Table} instance for the table given by C{name}. |
156 | + The columns to use on this table are given by C{colname = DataType()} |
157 | + pairs, like: |
158 | + |
159 | + Table("customers", name = String(), debt = Integer()) |
160 | + |
161 | + Before you can use a C{Table} you need to install a C{Cursor} for it |
162 | + by calling L{set_cursor}. |
163 | + |
164 | + The registered columns can be accessed like attributes on the table |
165 | + instance like: |
166 | + |
167 | + custormers.name |
168 | + custormers.debt |
169 | + |
170 | + @param name: The table name as given in the SQL |
171 | + @param colspecs: A list of keyword arguments setting each column name |
172 | + to the relevant L{ColumnType} |
173 | + """ |
174 | + self._cursor = None |
175 | + self._cols = {} |
176 | + self._cursor = EchoCursor() |
177 | + |
178 | + if not isinstance(name,str): |
179 | + raise ValueError("Table name must be a string, got %s" % type(name)) |
180 | + self._name = name |
181 | + |
182 | + for colname, coltype in colspecs.items(): |
183 | + coltype._set_table(self) |
184 | + coltype._set_colname(colname) |
185 | + self._cols[colname] = coltype |
186 | + |
187 | + def set_cursor (self, cursor): |
188 | + """ |
189 | + Install a cursor for use by C{Table} instance. The cursor may be changed |
190 | + and used externally at a ny later point in time. |
191 | + |
192 | + @param cursor: The cursor to use |
193 | + """ |
194 | + self._cursor = cursor |
195 | + |
196 | + def get_cursor(self): |
197 | + """ |
198 | + Return the cursor currently in use by this table |
199 | + |
200 | + @return: The cursor currently in use by this table |
201 | + """ |
202 | + if isinstance(self._cursor, EchoCursor): |
203 | + return None |
204 | + |
205 | + return self._cursor |
206 | + |
207 | + def find(self, resultspec, *where): |
208 | + """ |
209 | + Execute a SELECT statement on the table. The C{resultspec} argument |
210 | + signifies the returned columns and may be a free form string, a |
211 | + column from the table instance (eg. C{custormers.debt}), a C{Table} |
212 | + instance, or a list or tuple of any of these. Where ever a C{Table} |
213 | + instance is given all columns defined for that table will be returned. |
214 | + |
215 | + To find full data for all custormers who owns more than 100 bucks: |
216 | + |
217 | + custormers.find(custormers, custormers.debt > 100) |
218 | + |
219 | + The same, but only those name is "Bob": |
220 | + |
221 | + custormers.find(custormers, |
222 | + custormers.debt > 100, |
223 | + custormers.name == "Bob") |
224 | + |
225 | + Or doing an implicit join on the C{employees} table, to find only the |
226 | + name of the customers who has a name similar to an employee: |
227 | + |
228 | + custormers.find(customers.name, |
229 | + customers.name == employees.name) |
230 | + |
231 | + - or the same query returning the full data for both the custormer and |
232 | + the employee: |
233 | + |
234 | + custormers.find((customers, employees), |
235 | + customers.name == employees.name) |
236 | + |
237 | + @param where: A list of boolean comparisons between table columns and |
238 | + target values |
239 | + @return: A result set directly from the installed cursor |
240 | + """ |
241 | + return self._cursor.execute(self.SELECT(resultspec, *where)) |
242 | + |
243 | + def add(self, **rowspec): |
244 | + """ |
245 | + Execute an INSERT statement on the table. |
246 | + |
247 | + To insert a new custormer with a zero debt: |
248 | + |
249 | + custormers.add(name="John Doe", debt=0) |
250 | + |
251 | + @param rowspec: A list of keyword arguments C{column=value} |
252 | + @return: A result set directly from the installed cursor |
253 | + """ |
254 | + return self._cursor.execute(self.INSERT(**rowspec)) |
255 | + |
256 | + def update(self, *where, **rowspec): |
257 | + """ |
258 | + Execute an UPDATE statement on the table. |
259 | + |
260 | + To update the custormer "Bob"s debt to 200, issue: |
261 | + |
262 | + custormers.update(custormers.name == "Bob", debt=200) |
263 | + |
264 | + @param rowspec: A list of keyword arguments C{column=value} |
265 | + @return: A result set directly from the installed cursor |
266 | + """ |
267 | + return self._cursor.execute(self.UPDATE(*where, **rowspec)) |
268 | + |
269 | + def SELECT(self, resultspec, *where): |
270 | + """ |
271 | + Create an SQL statement as defined in L{find} and return it as a string. |
272 | + |
273 | + This method will not touch the database in any way. |
274 | + """ |
275 | + stmt = "SELECT %s FROM %s" |
276 | + |
277 | + # Calc result columns |
278 | + result_part = self._expand_result_spec(resultspec) |
279 | + |
280 | + # Calc where clause |
281 | + if where: |
282 | + stmt += " " + self.WHERE(*where) |
283 | + |
284 | + return stmt % (result_part, self) |
285 | + |
286 | + def INSERT(self, **rowspec): |
287 | + """ |
288 | + Create an SQL statement as defined in L{add} and return it as a string. |
289 | + |
290 | + This method will not touch the database in any way. |
291 | + """ |
292 | + if not rowspec: |
293 | + raise ValueError("Expected non-empty col=value sequence, got %s" |
294 | + % rowspec) |
295 | + stmt = "INSERT INTO %s (%s) VALUES (%s)" |
296 | + cols = None |
297 | + vals = None |
298 | + |
299 | + for name, value in rowspec.iteritems(): |
300 | + if not name in self._cols: |
301 | + raise AttributeError("No such row in table '%s': '%s'" \ |
302 | + % (self,name)) |
303 | + coltype = self._cols[name] |
304 | + if cols: |
305 | + cols += ", " + coltype.__class__.format(name) |
306 | + vals += ", " + value |
307 | + else: |
308 | + cols = name |
309 | + vals = value |
310 | + |
311 | + return stmt % (self, cols, vals) |
312 | + |
313 | + def UPDATE(self, *where, **rowspec): |
314 | + """ |
315 | + Create an SQL statement as defined in L{update} and return it as a |
316 | + string. |
317 | + |
318 | + This method will not touch the database in any way. |
319 | + """ |
320 | + if not rowspec: |
321 | + raise ValueError("Expected non-empty col=value sequence, got %s" |
322 | + % rowspec) |
323 | + |
324 | + stmt = "UPDATE %s SET %s " + self.WHERE(*where) |
325 | + values = None |
326 | + |
327 | + for col, value in rowspec.iteritems(): |
328 | + if not col in self._cols: |
329 | + raise AttributeError("No such row in table '%s': '%s'" \ |
330 | + % (self,col)) |
331 | + |
332 | + coltype = self._cols[col] |
333 | + if values: |
334 | + values += ", %s=%s" % (col, coltype.__class__.format(value)) |
335 | + else: |
336 | + values = "%s=%s" % (col, coltype.__class__.format(value)) |
337 | + |
338 | + return stmt % (self, values) |
339 | + |
340 | + def WHERE(self, *where): |
341 | + """ |
342 | + Create an SQL WHERE clause and return it as a string. Used internally |
343 | + by methods such as L{SELECT} and L{UPDATE}. |
344 | + |
345 | + This method will not touch the database in any way. |
346 | + """ |
347 | + return "WHERE " + " AND ".join(where) |
348 | + |
349 | + def __str__ (self): |
350 | + return self._name |
351 | + |
352 | + def get_name(self): |
353 | + """ |
354 | + Return the SQL table name for this instance |
355 | + """ |
356 | + return self._name |
357 | + |
358 | + def columns(self): |
359 | + """ |
360 | + Return an iterator over the columns defined when creating this table |
361 | + """ |
362 | + return self._cols.iterkeys() |
363 | + |
364 | + def __getattr__ (self, name): |
365 | + if name in self._cols: |
366 | + return self._cols[name] |
367 | + else: |
368 | + raise AttributeError("No such row '%s'" % name) |
369 | + |
370 | + def _expand_result_spec (self, resultspec): |
371 | + if isinstance(resultspec, (str, unicode)): |
372 | + return resultspec |
373 | + elif isinstance(resultspec, ColumnType): |
374 | + return str(resultspec) |
375 | + elif isinstance(resultspec, Table): |
376 | + return ", ".join((str(col) |
377 | + for col in resultspec._cols.itervalues())) |
378 | + elif isinstance(resultspec, (list,tuple)): |
379 | + return ", ".join((self._expand_result_spec(sub) |
380 | + for sub in resultspec)) |
381 | + else: |
382 | + raise ValueError("Malformed result spec: %s" % resultspec) |
383 | + |
Marked this very old branch as approved to make LP happy