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 127 (checked in by fumanchu, 7 years ago)

Initial implementation of #16 (schema changes). See modeling.html ("Managing Schema Changes") for summary. Oh, and Unit has a new remove_property method.

  • 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         data, columns = self.fetch("SELECT VERSION();")
146         if data:
147             version = storage.Version(data[0][0])
148             if version > storage.Version("4.1.1"):
149                 self.decompiler = MySQLDecompiler411
150    
151     def sql_name(self, name, quoted=True):
152         name = db.StorageManagerDB.sql_name(self, name, quoted)
153         if quoted:
154             name = '`' + name.replace('`', '``') + '`'
155         return name
156    
157     def _get_conn(self):
158         try:
159             conn = _mysql.connect(**self.connargs)
160         except _mysql.OperationalError, x:
161             if x.args[0] == 1040:   # Too many connections
162                 raise db.OutOfConnectionsError
163             elif x.args[0] == 1049 and self.CreateIfMissing:
164                 self.create_database()
165                 conn = _mysql.connect(**self.connargs)
166             else:
167                 raise
168         return conn
169    
170     def _template_conn(self):
171         tmplconn = self.connargs.copy()
172         tmplconn['db'] = 'mysql'
173         return _mysql.connect(**tmplconn)
174    
175     def fetch(self, query, conn=None):
176         """fetch(query, conn=None) -> rowdata, columns.
177         
178         rowdata: a nested list (or tuples), column values within rows.
179         columns: a series of 2-tuples (or more). The first tuple value
180             will be the column name, the second value will be the column
181             type.
182         """
183         if conn is None:
184             conn = self.connection()
185         self.execute(query, conn)
186         # store_result uses a client-side cursor
187         res = conn.store_result()
188         return res.fetch_row(0, 0), res.describe()
189    
190     def destroy(self, unit):
191         """destroy(unit). Delete the unit."""
192         self.execute(u'DELETE FROM %s WHERE %s;' %
193                      (self.table_name(unit.__class__.__name__),
194                       self.id_clause(unit)))
195    
196     def version(self):
197         conn = self._template_conn()
198         rowdata, cols = self.fetch("SELECT version();", conn)
199         conn.close()
200         return "MySQL Version: %s" % rowdata[0][0]
201    
202     #                               Schemas                               #
203    
204     def create_database(self):
205         # _mysql has create_db and drop_db commands, but they're deprecated.
206         sql = 'CREATE DATABASE %s;' % self.sql_name(self.dbname)
207         conn = self._template_conn()
208         self.execute(sql, conn)
209         conn.close()
210    
211     def drop_database(self):
212         sql = 'DROP DATABASE %s;' % self.sql_name(self.dbname)
213         conn = self._template_conn()
214         self.execute(sql, conn)
215         conn.close()
216    
217     def create_storage(self, cls):
218         clsname = cls.__name__
219         tablename = self.table_name(clsname)
220        
221         coerce = self.typeAdapter.coerce
222         fields = []
223         for key in cls.properties():
224             fields.append(u'%s %s' % (self.column_name(clsname, key),
225                                       coerce(cls, key)))
226         self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields)))
227        
228         for index in cls.indices():
229             i = self.table_name("i" + clsname + index)
230            
231             dbtype = coerce(cls, index)
232             if dbtype.endswith('BLOB') or dbtype == 'TEXT':
233                 # MySQL won't allow indexes on a BLOB field
234                 # without a specific length.
235                 self.execute(u'CREATE INDEX %s ON %s (%s(%s));' %
236                              (i, tablename,
237                               self.column_name(clsname, index), 255))
238             else:
239                 self.execute(u'CREATE INDEX %s ON %s (%s);' %
240                              (i, tablename,
241                               self.column_name(clsname, index)))
242    
243     def rename_property(self, cls, oldname, newname):
244         clsname = cls.__name__
245         oldcolname = self.column_name(clsname, oldname)
246         newcolname = self.column_name(clsname, newname)
247         if oldcolname != newcolname:
248             self.execute("ALTER TABLE %s CHANGE %s %s %s;" %
249                          (self.table_name(clsname), oldcolname, newcolname,
250                           self.typeAdapter.coerce(cls, newname)))
251
Note: See TracBrowser for help on using the browser.