Added extraction of MP3 info when saving Book models.
[wolnelektury.git] / apps / south / db / generic.py
1
2 from django.core.management.color import no_style
3 from django.db import connection, transaction, models
4 from django.db.backends.util import truncate_name
5 from django.dispatch import dispatcher
6 from django.conf import settings
7
8 class DatabaseOperations(object):
9
10     """
11     Generic SQL implementation of the DatabaseOperations.
12     Some of this code comes from Django Evolution.
13     """
14
15     def __init__(self):
16         self.debug = False
17         self.deferred_sql = []
18
19
20     def execute(self, sql, params=[]):
21         """
22         Executes the given SQL statement, with optional parameters.
23         If the instance's debug attribute is True, prints out what it executes.
24         """
25         cursor = connection.cursor()
26         if self.debug:
27             print "   = %s" % sql, params
28         cursor.execute(sql, params)
29         try:
30             return cursor.fetchall()
31         except:
32             return []
33             
34             
35     def add_deferred_sql(self, sql):
36         """
37         Add a SQL statement to the deferred list, that won't be executed until
38         this instance's execute_deferred_sql method is run.
39         """
40         self.deferred_sql.append(sql)
41         
42         
43     def execute_deferred_sql(self):
44         """
45         Executes all deferred SQL, resetting the deferred_sql list
46         """
47         for sql in self.deferred_sql:
48             self.execute(sql)
49             
50         self.deferred_sql = []
51
52
53     def create_table(self, table_name, fields):
54         """
55         Creates the table 'table_name'. 'fields' is a tuple of fields,
56         each repsented by a 2-part tuple of field name and a
57         django.db.models.fields.Field object
58         """
59         qn = connection.ops.quote_name
60         columns = [
61             self.column_sql(table_name, field_name, field)
62             for field_name, field in fields
63         ]
64         
65         self.execute('CREATE TABLE %s (%s);' % (qn(table_name), ', '.join([col for col in columns if col])))
66     
67     add_table = create_table # Alias for consistency's sake
68
69
70     def rename_table(self, old_table_name, table_name):
71         """
72         Renames the table 'old_table_name' to 'table_name'.
73         """
74         if old_table_name == table_name:
75             # No Operation
76             return
77         qn = connection.ops.quote_name
78         params = (qn(old_table_name), qn(table_name))
79         self.execute('ALTER TABLE %s RENAME TO %s;' % params)
80
81
82     def delete_table(self, table_name):
83         """
84         Deletes the table 'table_name'.
85         """
86         qn = connection.ops.quote_name
87         params = (qn(table_name), )
88         self.execute('DROP TABLE %s;' % params)
89     
90     drop_table = delete_table
91
92
93     def add_column(self, table_name, name, field):
94         """
95         Adds the column 'name' to the table 'table_name'.
96         Uses the 'field' paramater, a django.db.models.fields.Field instance,
97         to generate the necessary sql
98         
99         @param table_name: The name of the table to add the column to
100         @param name: The name of the column to add
101         @param field: The field to use
102         """
103         qn = connection.ops.quote_name
104         sql = self.column_sql(table_name, name, field)
105         if sql:
106             params = (
107                 qn(table_name),
108                 sql,
109             )
110             sql = 'ALTER TABLE %s ADD COLUMN %s;' % params
111             self.execute(sql)
112     
113     
114     alter_string_set_type = 'ALTER COLUMN %(column)s TYPE %(type)s'
115     alter_string_set_null = 'ALTER COLUMN %(column)s SET NOT NULL'
116     alter_string_drop_null = 'ALTER COLUMN %(column)s DROP NOT NULL'
117     
118     def alter_column(self, table_name, name, field):
119         """
120         Alters the given column name so it will match the given field.
121         Note that conversion between the two by the database must be possible.
122         
123         @param table_name: The name of the table to add the column to
124         @param name: The name of the column to alter
125         @param field: The new field definition to use
126         """
127         
128         # hook for the field to do any resolution prior to it's attributes being queried
129         if hasattr(field, 'south_init'):
130             field.south_init()
131         
132         qn = connection.ops.quote_name
133         
134         # First, change the type
135         params = {
136             "column": qn(name),
137             "type": field.db_type(),
138         }
139         sqls = [self.alter_string_set_type % params]
140         
141         
142         # Next, set any default
143         params = (
144             qn(name),
145         )
146         
147         if not field.null and field.has_default():
148             default = field.get_default()
149             if isinstance(default, basestring):
150                 default = "'%s'" % default
151             params += ("SET DEFAULT %s",)
152         else:
153             params += ("DROP DEFAULT",)
154         
155         sqls.append('ALTER COLUMN %s %s ' % params)
156         
157         
158         # Next, nullity
159         params = {
160             "column": qn(name),
161             "type": field.db_type(),
162         }
163         if field.null:
164             sqls.append(self.alter_string_drop_null % params)
165         else:
166             sqls.append(self.alter_string_set_null % params)
167         
168         
169         # TODO: Unique
170         
171         self.execute("ALTER TABLE %s %s;" % (qn(table_name), ", ".join(sqls)))
172
173
174     def column_sql(self, table_name, field_name, field, tablespace=''):
175         """
176         Creates the SQL snippet for a column. Used by add_column and add_table.
177         """
178         qn = connection.ops.quote_name
179         
180         field.set_attributes_from_name(field_name)
181         
182         # hook for the field to do any resolution prior to it's attributes being queried
183         if hasattr(field, 'south_init'):
184             field.south_init()
185         
186         sql = field.db_type()
187         if sql:        
188             field_output = [qn(field.column), sql]
189             field_output.append('%sNULL' % (not field.null and 'NOT ' or ''))
190             if field.primary_key:
191                 field_output.append('PRIMARY KEY')
192             elif field.unique:
193                 field_output.append('UNIQUE')
194         
195             tablespace = field.db_tablespace or tablespace
196             if tablespace and connection.features.supports_tablespaces and field.unique:
197                 # We must specify the index tablespace inline, because we
198                 # won't be generating a CREATE INDEX statement for this field.
199                 field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
200             
201             sql = ' '.join(field_output)
202             sqlparams = ()
203             # if the field is "NOT NULL" and a default value is provided, create the column with it
204             # this allows the addition of a NOT NULL field to a table with existing rows
205             if not field.null and field.has_default():
206                 default = field.get_default()
207                 if isinstance(default, basestring):
208                     default = "'%s'" % default.replace("'", "''")
209                 sql += " DEFAULT %s"
210                 sqlparams = (default)
211             
212             if field.rel:
213                 self.add_deferred_sql(
214                     self.foreign_key_sql(
215                         table_name,
216                         field.column,
217                         field.rel.to._meta.db_table,
218                         field.rel.to._meta.get_field(field.rel.field_name).column
219                     )
220                 )
221             
222             if field.db_index and not field.unique:
223                 self.add_deferred_sql(self.create_index_sql(table_name, [field.column]))
224             
225         if hasattr(field, 'post_create_sql'):
226             style = no_style()
227             for stmt in field.post_create_sql(style, table_name):
228                 self.add_deferred_sql(stmt)
229
230         if sql:
231             return sql % sqlparams
232         else:
233             return None
234         
235     def foreign_key_sql(self, from_table_name, from_column_name, to_table_name, to_column_name):
236         """
237         Generates a full SQL statement to add a foreign key constraint
238         """
239         constraint_name = '%s_refs_%s_%x' % (from_column_name, to_column_name, abs(hash((from_table_name, to_table_name))))
240         return 'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % (
241             from_table_name,
242             truncate_name(constraint_name, connection.ops.max_name_length()),
243             from_column_name,
244             to_table_name,
245             to_column_name,
246             connection.ops.deferrable_sql() # Django knows this
247         )
248         
249     def create_index_name(self, table_name, column_names):
250         """
251         Generate a unique name for the index
252         """
253         index_unique_name = ''
254         if len(column_names) > 1:
255             index_unique_name = '_%x' % abs(hash((table_name, ','.join(column_names))))
256
257         return '%s_%s%s' % (table_name, column_names[0], index_unique_name)
258
259     def create_index_sql(self, table_name, column_names, unique=False, db_tablespace=''):
260         """
261         Generates a create index statement on 'table_name' for a list of 'column_names'
262         """
263         if not column_names:
264             print "No column names supplied on which to create an index"
265             return ''
266             
267         if db_tablespace and connection.features.supports_tablespaces:
268             tablespace_sql = ' ' + connection.ops.tablespace_sql(db_tablespace)
269         else:
270             tablespace_sql = ''
271         
272         index_name = self.create_index_name(table_name, column_names)
273         qn = connection.ops.quote_name
274         return 'CREATE %sINDEX %s ON %s (%s)%s;' % (
275             unique and 'UNIQUE ' or '',
276             index_name,
277             table_name,
278             ','.join([qn(field) for field in column_names]),
279             tablespace_sql
280             )
281         
282     def create_index(self, table_name, column_names, unique=False, db_tablespace=''):
283         """ Executes a create index statement """
284         sql = self.create_index_sql(table_name, column_names, unique, db_tablespace)
285         self.execute(sql)
286
287
288     def delete_index(self, table_name, column_names, db_tablespace=''):
289         """
290         Deletes an index created with create_index.
291         This is possible using only columns due to the deterministic
292         index naming function which relies on column names.
293         """
294         name = self.create_index_name(table_name, column_names)
295         sql = "DROP INDEX %s" % name
296         self.execute(sql)
297
298
299     def delete_column(self, table_name, name):
300         """
301         Deletes the column 'column_name' from the table 'table_name'.
302         """
303         qn = connection.ops.quote_name
304         params = (qn(table_name), qn(name))
305         self.execute('ALTER TABLE %s DROP COLUMN %s CASCADE;' % params, [])
306
307
308     def rename_column(self, table_name, old, new):
309         """
310         Renames the column 'old' from the table 'table_name' to 'new'.
311         """
312         raise NotImplementedError("rename_column has no generic SQL syntax")
313
314
315     def start_transaction(self):
316         """
317         Makes sure the following commands are inside a transaction.
318         Must be followed by a (commit|rollback)_transaction call.
319         """
320         transaction.commit_unless_managed()
321         transaction.enter_transaction_management()
322         transaction.managed(True)
323
324
325     def commit_transaction(self):
326         """
327         Commits the current transaction.
328         Must be preceded by a start_transaction call.
329         """
330         transaction.commit()
331         transaction.leave_transaction_management()
332
333
334     def rollback_transaction(self):
335         """
336         Rolls back the current transaction.
337         Must be preceded by a start_transaction call.
338         """
339         transaction.rollback()
340         transaction.leave_transaction_management()
341     
342     
343     def send_create_signal(self, app_label, model_names):
344         """
345         Sends a post_syncdb signal for the model specified.
346         
347         If the model is not found (perhaps it's been deleted?),
348         no signal is sent.
349         
350         TODO: The behavior of django.contrib.* apps seems flawed in that
351         they don't respect created_models.  Rather, they blindly execute
352         over all models within the app sending the signal.  This is a
353         patch we should push Django to make  For now, this should work.
354         """
355         app = models.get_app(app_label)
356         if not app:
357             return
358             
359         created_models = []
360         for model_name in model_names:
361             model = models.get_model(app_label, model_name)
362             if model:
363                 created_models.append(model)
364                 
365         if created_models:
366             # syncdb defaults -- perhaps take these as options?
367             verbosity = 1
368             interactive = True
369             
370             if hasattr(dispatcher, "send"):
371                 dispatcher.send(signal=models.signals.post_syncdb, sender=app,
372                 app=app, created_models=created_models,
373                 verbosity=verbosity, interactive=interactive)
374             else:
375                 models.signals.post_syncdb.send(sender=app,
376                 app=app, created_models=created_models,
377                 verbosity=verbosity, interactive=interactive)
378                 
379     def mock_model(self, model_name, db_table, db_tablespace='', 
380                     pk_field_name='id', pk_field_type=models.AutoField,
381                     pk_field_kwargs={}):
382         """
383         Generates a MockModel class that provides enough information
384         to be used by a foreign key/many-to-many relationship.
385         
386         Migrations should prefer to use these rather than actual models
387         as models could get deleted over time, but these can remain in
388         migration files forever.
389         """
390         class MockOptions(object):
391             def __init__(self):
392                 self.db_table = db_table
393                 self.db_tablespace = db_tablespace or settings.DEFAULT_TABLESPACE
394                 self.object_name = model_name
395                 self.module_name = model_name.lower()
396
397                 if pk_field_type == models.AutoField:
398                     pk_field_kwargs['primary_key'] = True
399
400                 self.pk = pk_field_type(**pk_field_kwargs)
401                 self.pk.set_attributes_from_name(pk_field_name)
402                 self.abstract = False
403
404             def get_field_by_name(self, field_name):
405                 # we only care about the pk field
406                 return (self.pk, self.model, True, False)
407
408             def get_field(self, name):
409                 # we only care about the pk field
410                 return self.pk
411
412         class MockModel(object):
413             _meta = None
414
415         # We need to return an actual class object here, not an instance
416         MockModel._meta = MockOptions()
417         MockModel._meta.model = MockModel
418         return MockModel