~vcs-imports/sqlobject/trunk

« back to all changes in this revision

Viewing changes to sqlobject/col.py

  • Committer: ianb
  • Date: 2004-02-05 03:29:19 UTC
  • Revision ID: svn-v4:95a46c32-92d2-0310-94a5-8d71aeb3d4b3:trunk/SQLObject:1
Initial import of SQLObject and DataTest

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Col
 
3
"""
 
4
 
 
5
import SQLBuilder
 
6
import re
 
7
import Constraints
 
8
from include import Validator
 
9
 
 
10
NoDefault = SQLBuilder.NoDefault
 
11
True, False = 1==1, 0==1
 
12
    
 
13
 
 
14
########################################
 
15
## Columns
 
16
########################################
 
17
 
 
18
# Col is essentially a column definition, it doesn't have
 
19
# much logic to it.
 
20
class SOCol(object):
 
21
 
 
22
    def __init__(self,
 
23
                 name,
 
24
                 soClass,
 
25
                 dbName=None,
 
26
                 default=NoDefault,
 
27
                 foreignKey=None,
 
28
                 alternateID=False,
 
29
                 alternateMethodName=None,
 
30
                 constraints=None,
 
31
                 notNull=NoDefault,
 
32
                 notNone=NoDefault,
 
33
                 unique=NoDefault,
 
34
                 sqlType=None,
 
35
                 columnDef=None,
 
36
                 validator=None,
 
37
                 immutable=False,
 
38
                 cascade=None,
 
39
                 lazy=False,
 
40
                 noCache=False):
 
41
 
 
42
        # This isn't strictly true, since we *could* use backquotes or
 
43
        # " or something (database-specific) around column names, but
 
44
        # why would anyone *want* to use a name like that?
 
45
        # @@: I suppose we could actually add backquotes to the
 
46
        # dbName if we needed to...
 
47
        assert SQLBuilder.sqlIdentifier(name), 'Name must be SQL-safe (letters, numbers, underscores): %s' \
 
48
               % repr(name)
 
49
        assert name != 'id', 'The column name "id" is reserved for SQLObject use (and is implicitly created).'
 
50
        assert name, "You must provide a name for all columns"
 
51
 
 
52
        self.columnDef = columnDef
 
53
 
 
54
        self.immutable = immutable
 
55
 
 
56
        # cascade can be one of:
 
57
        # None: no constraint is generated
 
58
        # True: a CASCADE constraint is generated
 
59
        # False: a RESTRICT constraint is generated
 
60
        self.cascade = cascade
 
61
 
 
62
        if type(constraints) not in (type([]), type(())):
 
63
            constraints = [constraints]
 
64
        self.constraints = self.autoConstraints() + constraints
 
65
 
 
66
        self.notNone = False
 
67
        if notNull is not NoDefault:
 
68
            self.notNone = notNull
 
69
            assert notNone is NoDefault or \
 
70
                   (not notNone) == (not notNull), \
 
71
                   "The notNull and notNone arguments are aliases, and must not conflict.  You gave notNull=%r, notNone=%r" % (notNull, notNone)
 
72
        elif notNone is not NoDefault:
 
73
            self.notNone = notNone
 
74
        if self.notNone:
 
75
            self.constraints = [Constraints.notNull] + self.constraints
 
76
 
 
77
        self.name = name
 
78
        self.soClass = None
 
79
        self._default = default
 
80
        self.customSQLType = sqlType
 
81
 
 
82
        self.foreignKey = foreignKey
 
83
        if self.foreignKey:
 
84
            #assert self.name.upper().endswith('ID'), "All foreign key columns must end with 'ID' (%s)" % repr(self.name)
 
85
            if not self.name.upper().endswith('ID'):
 
86
                self.foreignName = self.name
 
87
                self.name = self.name + "ID"
 
88
            else:
 
89
                self.foreignName = self.name[:-2]
 
90
        else:
 
91
            self.foreignName = None
 
92
 
 
93
        # if they don't give us a specific database name for
 
94
        # the column, we separate the mixedCase into mixed_case
 
95
        # and assume that.
 
96
        if dbName is None:
 
97
            self.dbName = soClass.sqlmeta.style.pythonAttrToDBColumn(self.name)
 
98
        else:
 
99
            self.dbName = dbName
 
100
 
 
101
        # alternateID means that this is a unique column that
 
102
        # can be used to identify rows
 
103
        self.alternateID = alternateID
 
104
        if self.alternateID and alternateMethodName is None:
 
105
            self.alternateMethodName = 'by' + self.name[0].capitalize() + self.name[1:]
 
106
        else:
 
107
            self.alternateMethodName = alternateMethodName
 
108
 
 
109
        if unique is NoDefault:
 
110
            self.unique = alternateID
 
111
        else:
 
112
            self.unique = unique
 
113
 
 
114
        self.validator = validator
 
115
        self.noCache = noCache
 
116
        self.lazy = lazy
 
117
 
 
118
    def _set_validator(self, value):
 
119
        self._validator = value
 
120
        if self._validator:
 
121
            self.toPython = self._validator.toPython
 
122
            self.fromPython = self._validator.fromPython
 
123
        else:
 
124
            self.toPython = None
 
125
            self.fromPython = None
 
126
 
 
127
    def _get_validator(self):
 
128
        return self._validator
 
129
 
 
130
    validator = property(_get_validator, _set_validator)
 
131
 
 
132
    def autoConstraints(self):
 
133
        return []
 
134
 
 
135
    def _get_default(self):
 
136
        # A default can be a callback or a plain value,
 
137
        # here we resolve the callback
 
138
        if self._default is NoDefault:
 
139
            return NoDefault
 
140
        elif hasattr(self._default, '__sqlrepr__'):
 
141
            return self._default
 
142
        elif callable(self._default):
 
143
            return self._default()
 
144
        else:
 
145
            return self._default
 
146
    default = property(_get_default, None, None)
 
147
 
 
148
    def _get_joinName(self):
 
149
        assert self.name[-2:] == 'ID'
 
150
        return self.name[:-2]
 
151
    joinName = property(_get_joinName, None, None)
 
152
 
 
153
    def __repr__(self):
 
154
        r = '<%s %s' % (self.__class__.__name__, self.name)
 
155
        if self.default is not NoDefault:
 
156
            r += ' default=%s' % repr(self.default)
 
157
        if self.foreignKey:
 
158
            r += ' connected to %s' % self.foreignKey
 
159
        if self.alternateID:
 
160
            r += ' alternate ID'
 
161
        if self.notNone:
 
162
            r += ' not null'
 
163
        return r + '>'
 
164
 
 
165
    def createSQL(self):
 
166
        return ' '.join([self._sqlType() + self._extraSQL()])
 
167
 
 
168
    def _extraSQL(self):
 
169
        result = []
 
170
        if self.notNone or self.alternateID:
 
171
            result.append('NOT NULL')
 
172
        if self.unique or self.alternateID:
 
173
            result.append('UNIQUE')
 
174
        return result
 
175
 
 
176
    def _sqlType(self):
 
177
        if self.customSQLType is None:
 
178
            raise ValueError, ("Col %s (%s) cannot be used for automatic "
 
179
                               "schema creation (too abstract)" %
 
180
                               (self.name, self.__class__))
 
181
        else:
 
182
            return self.customSQLType
 
183
 
 
184
    def _mysqlType(self):
 
185
        return self._sqlType()
 
186
 
 
187
    def _postgresType(self):
 
188
        return self._sqlType()
 
189
 
 
190
    def _sqliteType(self):
 
191
        # SQLite is naturally typeless, so as a fallback it uses
 
192
        # no type.
 
193
        try:
 
194
            return self._sqlType()
 
195
        except ValueError:
 
196
            return ''
 
197
 
 
198
    def _sybaseType(self):
 
199
        return self._sqlType()
 
200
 
 
201
    def _firebirdType(self):
 
202
        return self._sqlType()
 
203
 
 
204
    def mysqlCreateSQL(self):
 
205
        return ' '.join([self.dbName, self._mysqlType()] + self._extraSQL())
 
206
 
 
207
    def postgresCreateSQL(self):
 
208
        return ' '.join([self.dbName, self._postgresType()] + self._extraSQL())
 
209
 
 
210
    def sqliteCreateSQL(self):
 
211
        return ' '.join([self.dbName, self._sqliteType()] + self._extraSQL())
 
212
 
 
213
    def sybaseCreateSQL(self):
 
214
        return ' '.join([self.dbName, self._sybaseType()] + self._extraSQL())
 
215
 
 
216
    def firebirdCreateSQL(self):
 
217
        # Ian Sparks pointed out that fb is picky about the order
 
218
        # of the NOT NULL clause in a create statement.  So, we handle
 
219
        # them differently for Enum columns.
 
220
        if not isinstance(self, SOEnumCol):
 
221
            return ' '.join([self.dbName, self._firebirdType()] + self._extraSQL())
 
222
        else:
 
223
            return ' '.join([self.dbName] + self._extraSQL() + [self._firebirdType()])
 
224
 
 
225
    def __get__(self, obj, type=None):
 
226
        if obj is None:
 
227
            # class attribute, return the descriptor itself
 
228
            return self
 
229
        if obj.sqlmeta.obsolete:
 
230
            raise '@@: figure out the exception for a delete'
 
231
        if obj.sqlmeta.cacheColumns:
 
232
            columns = obj.sqlmeta._columnCache
 
233
            if columns is None:
 
234
                obj.sqlmeta.loadValues()
 
235
            try:
 
236
                return columns[name]
 
237
            except KeyError:
 
238
                return obj.sqlmeta.loadColumn(self)
 
239
        else:
 
240
            return obj.sqlmeta.loadColumn(self)
 
241
 
 
242
    def __set__(self, obj, value):
 
243
        if self.immutable:
 
244
            raise AttributeError("The column %s.%s is immutable" %
 
245
                                 (obj.__class__.__name__,
 
246
                                  self.name))
 
247
        obj.sqlmeta.setColumn(self, value)
 
248
 
 
249
    def __delete__(self, obj):
 
250
        raise AttributeError("I can't be deleted from %r" % obj)
 
251
 
 
252
 
 
253
class Col(object):
 
254
 
 
255
    baseClass = SOCol
 
256
 
 
257
    def __init__(self, name=None, **kw):
 
258
        kw['name'] = name
 
259
        kw['columnDef'] = self
 
260
        self.kw = kw
 
261
 
 
262
    def setName(self, value):
 
263
        assert self.kw['name'] is None, "You cannot change a name after it has already been set (from %s to %s)" % (self.kw['name'], value)
 
264
        self.kw['name'] = value
 
265
 
 
266
    def withClass(self, soClass):
 
267
        return self.baseClass(soClass=soClass, **self.kw)
 
268
 
 
269
class SOStringCol(SOCol):
 
270
 
 
271
    # 3-03 @@: What about BLOB?
 
272
 
 
273
    def __init__(self, **kw):
 
274
        self.length = popKey(kw, 'length')
 
275
        self.varchar = popKey(kw, 'varchar', 'auto')
 
276
        if not self.length:
 
277
            assert self.varchar == 'auto' or not self.varchar, \
 
278
                   "Without a length strings are treated as TEXT, not varchar"
 
279
            self.varchar = False
 
280
        elif self.varchar == 'auto':
 
281
            self.varchar = True
 
282
 
 
283
        SOCol.__init__(self, **kw)
 
284
 
 
285
    def autoConstraints(self):
 
286
        constraints = [Constraints.isString]
 
287
        if self.length is not None:
 
288
            constraints += [Constraints.MaxLength(self.length)]
 
289
        return constraints
 
290
 
 
291
    def _sqlType(self):
 
292
        if not self.length:
 
293
            return 'TEXT'
 
294
        elif self.varchar:
 
295
            return 'VARCHAR(%i)' % self.length
 
296
        else:
 
297
            return 'CHAR(%i)' % self.length
 
298
 
 
299
    def _firebirdType(self):
 
300
        if not self.length:
 
301
            return 'BLOB SUB_TYPE TEXT'
 
302
        else:
 
303
            return self._sqlType()
 
304
 
 
305
class StringCol(Col):
 
306
    baseClass = SOStringCol
 
307
 
 
308
class SOIntCol(SOCol):
 
309
 
 
310
    # 3-03 @@: support precision, maybe max and min directly
 
311
 
 
312
    def autoConstraints(self):
 
313
        return [Constraints.isInt]
 
314
 
 
315
    def _sqlType(self):
 
316
        return 'INT'
 
317
 
 
318
class IntCol(Col):
 
319
    baseClass = SOIntCol
 
320
 
 
321
class BoolValidator(Validator.Validator):
 
322
 
 
323
    def fromPython(self, value, state):
 
324
        if value:
 
325
            return SQLBuilder.TRUE
 
326
        else:
 
327
            return SQLBuilder.FALSE
 
328
 
 
329
class SOBoolCol(SOCol):
 
330
 
 
331
    def __init__(self, **kw):
 
332
        SOCol.__init__(self, **kw)
 
333
        self.validator = Validator.All.join(BoolValidator(), self.validator)
 
334
 
 
335
    def autoConstraints(self):
 
336
        return [Constraints.isBool]
 
337
 
 
338
    def _postgresType(self):
 
339
        return 'BOOL'
 
340
 
 
341
    def _mysqlType(self):
 
342
        return "TINYINT"
 
343
 
 
344
    def _sybaseType(self):
 
345
        return "BIT"
 
346
 
 
347
class BoolCol(Col):
 
348
    baseClass = SOBoolCol
 
349
 
 
350
class SOFloatCol(SOCol):
 
351
 
 
352
    # 3-03 @@: support precision (e.g., DECIMAL)
 
353
 
 
354
    def autoConstraints(self):
 
355
        return [Constraints.isFloat]
 
356
 
 
357
    def _sqlType(self):
 
358
        return 'FLOAT'
 
359
 
 
360
class FloatCol(Col):
 
361
    baseClass = SOFloatCol
 
362
 
 
363
class SOKeyCol(SOCol):
 
364
 
 
365
    # 3-03 @@: this should have a simplified constructor
 
366
    # Should provide foreign key information for other DBs.
 
367
 
 
368
    def _mysqlType(self):
 
369
        return 'INT'
 
370
 
 
371
    def _sqliteType(self):
 
372
        return 'INT'
 
373
 
 
374
    def _postgresType(self):
 
375
        return 'INT'
 
376
 
 
377
    def _sybaseType(self):
 
378
        return 'INT'
 
379
 
 
380
    def _firebirdType(self):
 
381
        return 'INT'
 
382
 
 
383
class KeyCol(Col):
 
384
 
 
385
    baseClass = SOKeyCol
 
386
 
 
387
class SOForeignKey(SOKeyCol):
 
388
 
 
389
    def __init__(self, **kw):
 
390
        foreignKey = kw['foreignKey']
 
391
        style = kw['soClass']._style
 
392
        if not kw.get('name'):
 
393
            kw['name'] = style.instanceAttrToIDAttr(style.pythonClassToAttr(foreignKey))
 
394
        else:
 
395
            if not kw['name'].upper().endswith('ID'):
 
396
                kw['name'] = style.instanceAttrToIDAttr(kw['name'])
 
397
        SOKeyCol.__init__(self, **kw)
 
398
 
 
399
    def postgresCreateSQL(self):
 
400
        from SQLObject import findClass
 
401
        sql = SOKeyCol.postgresCreateSQL(self)
 
402
        if self.cascade is not None:
 
403
            other = findClass(self.foreignKey)
 
404
            tName = other._table
 
405
            idName = other._idName
 
406
            action = self.cascade and 'CASCADE' or 'RESTRICT'
 
407
            constraint = ('CONSTRAINT %(tName)s_exists '
 
408
                          'FOREIGN KEY(%(colName)s) '
 
409
                          'REFERENCES %(tName)s(%(idName)s) '
 
410
                          'ON DELETE %(action)s' %
 
411
                          {'tName':tName,
 
412
                           'colName':self.dbName,
 
413
                           'idName':idName,
 
414
                           'action':action})
 
415
            sql = ', '.join([sql, constraint])
 
416
        return sql
 
417
 
 
418
    def sybaseCreateSQL(self):
 
419
        from SQLObject import findClass
 
420
        sql = SOKeyCol.sybaseCreateSQL(self)
 
421
        other = findClass(self.foreignKey)
 
422
        tName = other._table
 
423
        idName = other._idName
 
424
        reference = ('REFERENCES %(tName)s(%(idName)s) ' %
 
425
                     {'tName':tName,
 
426
                      'idName':idName})
 
427
        sql = ' '.join([sql, reference])
 
428
        return sql
 
429
 
 
430
class ForeignKey(KeyCol):
 
431
 
 
432
    baseClass = SOForeignKey
 
433
 
 
434
    def __init__(self, foreignKey=None, **kw):
 
435
        KeyCol.__init__(self, foreignKey=foreignKey, **kw)
 
436
 
 
437
class SOEnumCol(SOCol):
 
438
 
 
439
    def __init__(self, **kw):
 
440
        self.enumValues = popKey(kw, 'enumValues', None)
 
441
        assert self.enumValues is not None, \
 
442
               'You must provide an enumValues keyword argument'
 
443
        SOCol.__init__(self, **kw)
 
444
 
 
445
    def autoConstraints(self):
 
446
        return [Constraints.isString, Constraints.InList(self.enumValues)]
 
447
 
 
448
    def _mysqlType(self):
 
449
        return "ENUM(%s)" % ', '.join([SQLBuilder.sqlrepr(v, 'mysql') for v in self.enumValues])
 
450
 
 
451
    def _postgresType(self):
 
452
        length = max(map(len, self.enumValues))
 
453
        enumValues = ', '.join([SQLBuilder.sqlrepr(v, 'postgres') for v in self.enumValues])
 
454
        checkConstraint = "CHECK (%s in (%s))" % (self.dbName, enumValues)
 
455
        return "VARCHAR(%i) %s" % (length, checkConstraint)
 
456
 
 
457
    def _sqliteType(self):
 
458
        return self._postgresType()
 
459
 
 
460
    def _sybaseType(self):
 
461
        return self._postgresType()
 
462
 
 
463
    def _firebirdType(self):
 
464
        return self._postgresType()
 
465
 
 
466
class EnumCol(Col):
 
467
    baseClass = SOEnumCol
 
468
 
 
469
class SODateTimeCol(SOCol):
 
470
 
 
471
    # 3-03 @@: provide constraints; right now we let the database
 
472
    # do any parsing and checking.  And DATE and TIME?
 
473
 
 
474
    def _mysqlType(self):
 
475
        return 'DATETIME'
 
476
 
 
477
    def _postgresType(self):
 
478
        return 'TIMESTAMP'
 
479
 
 
480
    def _sybaseType(self):
 
481
        return 'DATETIME'
 
482
 
 
483
class DateTimeCol(Col):
 
484
    baseClass = SODateTimeCol
 
485
 
 
486
class SODateCol(SOCol):
 
487
 
 
488
    # 3-03 @@: provide constraints; right now we let the database
 
489
    # do any parsing and checking.  And DATE and TIME?
 
490
 
 
491
    def _mysqlType(self):
 
492
        return 'DATE'
 
493
 
 
494
    def _postgresType(self):
 
495
        return 'DATE'
 
496
 
 
497
    def _sybaseType(self):
 
498
        return self._postgresType()
 
499
 
 
500
class DateCol(Col):
 
501
    baseClass = SODateCol
 
502
 
 
503
class SODecimalCol(SOCol):
 
504
 
 
505
    def __init__(self, **kw):
 
506
        self.size = popKey(kw, 'size', NoDefault)
 
507
        assert self.size is not NoDefault, \
 
508
               "You must give a size argument"
 
509
        self.precision = popKey(kw, 'precision', NoDefault)
 
510
        assert self.precision is not NoDefault, \
 
511
               "You must give a precision argument"
 
512
        SOCol.__init__(self, **kw)
 
513
 
 
514
    def _sqlType(self):
 
515
        return 'DECIMAL(%i, %i)' % (self.size, self.precision)
 
516
 
 
517
class DecimalCol(Col):
 
518
    baseClass = SODecimalCol
 
519
 
 
520
class SOCurrencyCol(SODecimalCol):
 
521
 
 
522
    def __init__(self, **kw):
 
523
        pushKey(kw, 'size', 10)
 
524
        pushKey(kw, 'precision', 2)
 
525
        SODecimalCol.__init__(self, **kw)
 
526
 
 
527
class CurrencyCol(DecimalCol):
 
528
    baseClass = SOCurrencyCol
 
529
 
 
530
def popKey(kw, name, default=None):
 
531
    if not kw.has_key(name):
 
532
        return default
 
533
    value = kw[name]
 
534
    del kw[name]
 
535
    return value
 
536
 
 
537
def pushKey(kw, name, value):
 
538
    if not kw.has_key(name):
 
539
        kw[name] = value
 
540
 
 
541
all = []
 
542
for key, value in globals().items():
 
543
    if isinstance(value, type) and issubclass(value, Col):
 
544
        all.append(key)
 
545
__all__ = all