Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/tags/1.4.0/storage/storemysql.py

Revision 160 (checked in by fumanchu, 6 years ago)

Fix for storemysql: decompiler check now uses template conn (so that init works even if there's no db yet).

  • Property svn:eol-style set to native
Line 
1 """
2 References the MySQLdb package at:
3 http://sourceforge.net/projects/mysql-python
4
5 From the MySQL manual:
6
7 "If the server SQL mode has ANSI_QUOTES enabled, string literals can be
8 quoted only with single quotes. A string quoted with double quotes will be
9 interpreted as an identifier."
10
11 So use single quotes throughout.
12 """
13
14 # Use _mysql directly to avoid all of the DB-API overhead.
15 import _mysql
16 import warnings
17 import datetime
18 from dejavu import storage, logic
19 from dejavu.storage import db
20
21
22 class AdapterToMySQL(db.AdapterToSQL):
23    
24     escapes = [("'", "''"), ("\\", r"\\")]
25     like_escapes = [("%", r"\%"), ("_", r"\_")]
26    
27     # TRUE and FALSE only work with 4.1 or better.
28     bool_true = "1"
29     bool_false = "0"
30    
31     def coerce_str(self, value):
32         return "'" + _mysql.escape_string(value) + "'"
33    
34     def coerce_bool(self, value):
35         # TRUE and FALSE only work with 4.1 or better.
36         if value:
37             return '1'
38         return '0'
39
40
41 class AdapterFromMySQL(db.AdapterFromDB):
42    
43     def coerce_bool(self, value, coltype):
44         if isinstance(value, basestring):
45             # either '0' or '1'
46             value = (value == '1')
47         return bool(value)
48    
49     def coerce_unicode(self, value, coltype):
50         return unicode(value, "utf-8")
51
52
53 class MySQLDecompiler(db.SQLDecompiler):
54    
55     def dejavu_today(self):
56         return "CURDATE()"
57
58
59 class MySQLDecompiler411(MySQLDecompiler):
60     # Before MySQL 4.1.1, BINARY comparisons could use UPPER()
61     # or LOWER() to perform case-insensitive comparisons. Newer
62     # versions must use CONVERT() to obtain a case-sensitive
63     # encoding, like utf8.
64    
65     def dejavu_icontainedby(self, op1, op2):
66         if isinstance(op1, db.ConstWrapper):
67             # Looking for text in a field. Use Like (reverse terms).
68             return ("CONVERT("+ op2 + " USING utf8) LIKE '%" +
69                     self.adapter.escape_like(op1) + "%'")
70         else:
71             # Looking for field in (a, b, c).
72             atoms = [self.adapter.coerce(x) for x in op2.basevalue]
73             return "CONVERT(%s USING utf8) IN (%s)" % (op1, ", ".join(atoms))
74    
75     def dejavu_istartswith(self, x, y):
76         return ("CONVERT(" + x + " USING utf8) LIKE '" +
77                 self.adapter.escape_like(y) + "%'")
78    
79     def dejavu_iendswith(self, x, y):
80         return ("CONVERT(" + x + " USING utf8) LIKE '%" +
81                 self.adapter.escape_like(y) + "'")
82    
83     def dejavu_ieq(self, x, y):
84         return "CONVERT(" + x + " USING utf8) = " + y
85
86
87 class FieldTypeAdapterMySQL(db.FieldTypeAdapter):
88     """Return the SQL typename of a DB column."""
89    
90     # This was determined through experimentation. Don't change it.
91     numeric_max_precision = 253
92    
93     def coerce_str(self, cls, key):
94         prop = getattr(cls, key)
95         bytes = int(prop.hints.get(u'bytes', '0'))
96         if bytes:
97             # MySQL VARBINARY/BLOBs will do case-sensitive comparisons.
98             # They also won't truncate trailing spaces like VARCHAR does.
99             if bytes <= 255:
100                 return u"VARBINARY(%s)" % bytes
101             elif bytes < 2 ** 16:
102                 return "BLOB"
103             elif bytes < 2 ** 24:
104                 return "MEDIUMBLOB"
105         return u"LONGBLOB"
106    
107     def coerce_bool(self, cls, key):
108         # We could use BOOLEAN, but it wasn't introduced until 4.1.0.
109         return u"BOOL"
110    
111     def coerce_datetime_datetime(self, cls, key):
112         return u"DATETIME"
113
114
115 class StorageManagerMySQL(db.StorageManagerDB):
116     """StoreManager to save and retrieve Units via _mysql."""
117    
118     sql_name_max_length = 64
119     # MySQL uses case-sensitive database and table names on Unix, but
120     # not on Windows. Use all-lowercase identifiers to work around the
121     # problem. "Column names, index names, and column aliases are not
122     # case sensitive on any platform."
123     # If deployers set lower_case_table_names to 1, it would help.
124     sql_name_caseless = True
125    
126     typeAdapter = FieldTypeAdapterMySQL()
127     toAdapter = AdapterToMySQL()
128     fromAdapter = AdapterFromMySQL()
129    
130     def __init__(self, name, arena, allOptions={}):
131         db.StorageManagerDB.__init__(self, name, arena, allOptions)
132        
133         connargs = ["host", "user", "passwd", "db", "port", "unix_socket",
134                     "conv", "connect_time", "compress", "named_pipe",
135                     "init_command", "read_default_file", "read_default_group",
136                     "cursorclass", "client_flag",
137                     ]
138         self.connargs = dict([(k, v) for k, v in allOptions.iteritems()
139                               if k in connargs])
140         self.dbname = self.connargs['db']
141        
142         self.decompiler = MySQLDecompiler
143         # Try to get the version string from MySQL, to see if we need
144         # a different decompiler.
145         conn = self._template_conn()
146         data, columns = self.fetch("SELECT VERSION();", conn)
147         if data:
148             version = storage.Version(data[0][0])
149             if version > storage.Version("4.1.1"):
150                 self.decompiler = MySQLDecompiler411
151         conn.close()
152    
153     def sql_name(self, name, quoted=True):
154         name = db.StorageManagerDB.sql_name(self, name, quoted)
155         if quoted:
156             name = '`' + name.replace('`', '``') + '`'
157         return name
158    
159     def _get_conn(self):
160         try:
161             conn = _mysql.connect(**self.connargs)
162         except _mysql.OperationalError, x:
163             if x.args[0] == 1040:   # Too many connections
164                 raise db.OutOfConnectionsError
165             raise
166         return conn
167    
168     def _template_conn(self):
169         tmplconn = self.connargs.copy()
170         tmplconn['db'] = 'mysql'
171         return _mysql.connect(**tmplconn)
172    
173     def fetch(self, query, conn=None):
174         """fetch(query, conn=None) -> rowdata, columns.
175         
176         rowdata: a nested list (or tuples), column values within rows.
177         columns: a series of 2-tuples (or more). The first tuple value
178             will be the column name, the second value will be the column
179             type.
180         """
181         if conn is None:
182             conn = self.connection()
183         self.execute(query, conn)
184         # store_result uses a client-side cursor
185         res = conn.store_result()
186         return res.fetch_row(0, 0), res.describe()
187    
188     def destroy(self, unit):
189         """destroy(unit). Delete the unit."""
190         self.execute(u'DELETE FROM %s WHERE %s;' %
191                      (self.table_name(unit.__class__.__name__),
192                       self.id_clause(unit)))
193    
194     def version(self):
195         conn = self._template_conn()
196         rowdata, cols = self.fetch("SELECT version();", conn)
197         conn.close()
198         return "MySQL Version: %s" % rowdata[0][0]
199    
200     #                               Schemas                               #
201    
202     def create_database(self):
203         # _mysql has create_db and drop_db commands, but they're deprecated.
204         sql = 'CREATE DATABASE %s;' % self.sql_name(self.dbname)
205         conn = self._template_conn()
206         self.execute(sql, conn)
207         conn.close()
208    
209     def drop_database(self):
210         sql = 'DROP DATABASE %s;' % self.sql_name(self.dbname)
211         conn = self._template_conn()
212         self.execute(sql, conn)
213         conn.close()
214    
215     def create_storage(self, cls):
216         clsname = cls.__name__
217         tablename = self.table_name(clsname)
218        
219         coerce = self.typeAdapter.coerce
220         fields = []
221         for key in cls.properties():
222             fields.append(u'%s %s' % (self.column_name(clsname, key),
223                                       coerce(cls, key)))
224         self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields)))
225        
226         for index in cls.indices():
227             i = self.table_name("i" + clsname + index)
228            
229             dbtype = coerce(cls, index)
230             if dbtype.endswith('BLOB') or dbtype == 'TEXT':
231                 # MySQL won't allow indexes on a BLOB field
232                 # without a specific length.
233                 self.execute(u'CREATE INDEX %s ON %s (%s(%s));' %
234                              (i, tablename,
235                               self.column_name(clsname, index), 255))
236             else:
237                 self.execute(u'CREATE INDEX %s ON %s (%s);' %
238                              (i, tablename,
239                               self.column_name(clsname, index)))
240    
241     def rename_property(self, cls, oldname, newname):
242         clsname = cls.__name__
243         oldcolname = self.column_name(clsname, oldname)
244         newcolname = self.column_name(clsname, newname)
245         if oldcolname != newcolname:
246             self.execute("ALTER TABLE %s CHANGE %s %s %s;" %
247                          (self.table_name(clsname), oldcolname, newcolname,
248                           self.typeAdapter.coerce(cls, newname)))
249
Note: See TracBrowser for help on using the browser.