Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

root/trunk/storage/storemysql.py

Revision 54 (checked in by fumanchu, 8 years ago)

Better escaping of LIKE wildcards. Postgres still broken in this regard.

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