Merge lp:~kamstrup/zeitgeist/zeitgeist-querymancer into lp:zeitgeist/0.1

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
Reviewer Review Type Date Requested Status
Mikkel Kamstrup Erlandsen Approve
Review via email: mp+9417@code.launchpad.net
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 :

Marked this very old branch as approved to make LP happy

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+