0001"""
0002Col -- SQLObject columns
0003
0004Note that each column object is named BlahBlahCol, and these are used
0005in class definitions.  But there's also a corresponding SOBlahBlahCol
0006object, which is used in SQLObject *classes*.
0007
0008An explanation: when a SQLObject subclass is created, the metaclass
0009looks through your class definition for any subclasses of Col.  It
0010collects them together, and indexes them to do all the database stuff
0011you like, like the magic attributes and whatnot.  It then asks the Col
0012object to create an SOCol object (usually a subclass, actually).  The
0013SOCol object contains all the interesting logic, as well as a record
0014of the attribute name you used and the class it is bound to (set by
0015the metaclass).
0016
0017So, in summary: Col objects are what you define, but SOCol objects
0018are what gets used.
0019"""
0020
0021from array import array
0022from itertools import count
0023import re, time
0024try:
0025    import cPickle as pickle
0026except ImportError:
0027    import pickle
0028# Sadly the name "constraints" conflicts with many of the function
0029# arguments in this module, so we rename it:
0030from formencode import compound, validators
0031from classregistry import findClass
0032import constraints as constrs
0033import sqlbuilder
0034from styles import capword
0035
0036NoDefault = sqlbuilder.NoDefault
0037
0038import datetime
0039datetime_available = True
0040
0041try:
0042    from mx import DateTime
0043except ImportError:
0044    try:
0045        import DateTime # old version of mxDateTime, or Zope's Version if we're running with Zope
0046    except ImportError:
0047        mxdatetime_available = False
0048    else:
0049        mxdatetime_available = True
0050else:
0051    mxdatetime_available = True
0052
0053DATETIME_IMPLEMENTATION = "datetime"
0054MXDATETIME_IMPLEMENTATION = "mxDateTime"
0055
0056if mxdatetime_available:
0057    if hasattr(DateTime, "Time"):
0058        DateTimeType = type(DateTime.now())
0059        TimeType = type(DateTime.Time())
0060    else: # Zope
0061        DateTimeType = type(DateTime.DateTime())
0062        TimeType = type(DateTime.DateTime.Time(DateTime.DateTime()))
0063
0064default_datetime_implementation = DATETIME_IMPLEMENTATION
0065
0066__all__ = ["datetime_available", "mxdatetime_available",
0067        "default_datetime_implementation", "DATETIME_IMPLEMENTATION"]
0068
0069if mxdatetime_available:
0070    __all__.append("MXDATETIME_IMPLEMENTATION")
0071
0072
0073creationOrder = count()
0074
0075########################################
0076## Columns
0077########################################
0078
0079# Col is essentially a column definition, it doesn't have
0080# much logic to it.
0081class SOCol(object):
0082
0083    def __init__(self,
0084                 name,
0085                 soClass,
0086                 creationOrder,
0087                 dbName=None,
0088                 default=NoDefault,
0089                 defaultSQL=None,
0090                 foreignKey=None,
0091                 alternateID=False,
0092                 alternateMethodName=None,
0093                 constraints=None,
0094                 notNull=NoDefault,
0095                 notNone=NoDefault,
0096                 unique=NoDefault,
0097                 sqlType=None,
0098                 columnDef=None,
0099                 validator=None,
0100                 validator2=None,
0101                 immutable=False,
0102                 cascade=None,
0103                 lazy=False,
0104                 noCache=False,
0105                 forceDBName=False,
0106                 title=None,
0107                 tags=[],
0108                 origName=None,
0109                 extra_vars=None):
0110
0111        super(SOCol, self).__init__()
0112
0113        # This isn't strictly true, since we *could* use backquotes or
0114        # " or something (database-specific) around column names, but
0115        # why would anyone *want* to use a name like that?
0116        # @@: I suppose we could actually add backquotes to the
0117        # dbName if we needed to...
0118        if not forceDBName:
0119            assert sqlbuilder.sqlIdentifier(name), 'Name must be SQL-safe (letters, numbers, underscores): %s (or use forceDBName=True)'                  % repr(name)
0121        assert name != 'id', 'The column name "id" is reserved for SQLObject use (and is implicitly created).'
0122        assert name, "You must provide a name for all columns"
0123
0124        self.columnDef = columnDef
0125        self.creationOrder = creationOrder
0126
0127        self.immutable = immutable
0128
0129        # cascade can be one of:
0130        # None: no constraint is generated
0131        # True: a CASCADE constraint is generated
0132        # False: a RESTRICT constraint is generated
0133        # 'null': a SET NULL trigger is generated
0134        if isinstance(cascade, str):
0135            assert cascade == 'null', (
0136                "The only string value allowed for cascade is 'null' (you gave: %r)" % cascade)
0137        self.cascade = cascade
0138
0139        if not isinstance(constraints, (list, tuple)):
0140            constraints = [constraints]
0141        self.constraints = self.autoConstraints() + constraints
0142
0143        self.notNone = False
0144        if notNull is not NoDefault:
0145            self.notNone = notNull
0146            assert notNone is NoDefault or                      (not notNone) == (not notNull),                      "The notNull and notNone arguments are aliases, and must not conflict.  You gave notNull=%r, notNone=%r" % (notNull, notNone)
0149        elif notNone is not NoDefault:
0150            self.notNone = notNone
0151        if self.notNone:
0152            self.constraints = [constrs.notNull] + self.constraints
0153
0154        self.name = name
0155        self.soClass = soClass
0156        self._default = default
0157        self.defaultSQL = defaultSQL
0158        self.customSQLType = sqlType
0159
0160        # deal with foreign keys
0161        self.foreignKey = foreignKey
0162        if self.foreignKey:
0163            if origName is not None:
0164                idname = soClass.sqlmeta.style.instanceAttrToIDAttr(origName)
0165            else:
0166                idname = soClass.sqlmeta.style.instanceAttrToIDAttr(name)
0167            if self.name != idname:
0168                self.foreignName = self.name
0169                self.name = idname
0170            else:
0171                self.foreignName = soClass.sqlmeta.style.instanceIDAttrToAttr(self.name)
0172        else:
0173            self.foreignName = None
0174
0175        # if they don't give us a specific database name for
0176        # the column, we separate the mixedCase into mixed_case
0177        # and assume that.
0178        if dbName is None:
0179            self.dbName = soClass.sqlmeta.style.pythonAttrToDBColumn(self.name)
0180        else:
0181            self.dbName = dbName
0182
0183        # alternateID means that this is a unique column that
0184        # can be used to identify rows
0185        self.alternateID = alternateID
0186
0187        if unique is NoDefault:
0188            self.unique = alternateID
0189        else:
0190            self.unique = unique
0191        if self.unique and alternateMethodName is None:
0192            self.alternateMethodName = 'by' + capword(self.name)
0193        else:
0194            self.alternateMethodName = alternateMethodName
0195
0196        _validators = self.createValidators()
0197        if validator: _validators.append(validator)
0198        if validator2: _validators.insert(0, validator2)
0199        _vlen = len(_validators)
0200        if _vlen == 0:
0201            self.validator = None # Set sef.{from,to}_python
0202        elif _vlen == 1:
0203            self.validator = _validators[0]
0204        elif _vlen > 1:
0205            self.validator = compound.All.join(_validators[0], *_validators[1:])
0206        self.noCache = noCache
0207        self.lazy = lazy
0208        # this is in case of ForeignKey, where we rename the column
0209        # and append an ID
0210        self.origName = origName or name
0211        self.title = title
0212        self.tags = tags
0213
0214        if extra_vars:
0215            for name, value in extra_vars.items():
0216                setattr(self, name, value)
0217
0218    def _set_validator(self, value):
0219        self._validator = value
0220        if self._validator:
0221            self.to_python = self._validator.to_python
0222            self.from_python = self._validator.from_python
0223        else:
0224            self.to_python = None
0225            self.from_python = None
0226
0227    def _get_validator(self):
0228        return self._validator
0229
0230    validator = property(_get_validator, _set_validator)
0231
0232    def createValidators(self):
0233        """Create a list of validators for the column."""
0234        return []
0235
0236    def autoConstraints(self):
0237        return []
0238
0239    def _get_default(self):
0240        # A default can be a callback or a plain value,
0241        # here we resolve the callback
0242        if self._default is NoDefault:
0243            return NoDefault
0244        elif hasattr(self._default, '__sqlrepr__'):
0245            return self._default
0246        elif callable(self._default):
0247            return self._default()
0248        else:
0249            return self._default
0250    default = property(_get_default, None, None)
0251
0252    def _get_joinName(self):
0253        return self.soClass.sqlmeta.style.instanceIDAttrToAttr(self.name)
0254    joinName = property(_get_joinName, None, None)
0255
0256    def __repr__(self):
0257        r = '<%s %s' % (self.__class__.__name__, self.name)
0258        if self.default is not NoDefault:
0259            r += ' default=%s' % repr(self.default)
0260        if self.foreignKey:
0261            r += ' connected to %s' % self.foreignKey
0262        if self.alternateID:
0263            r += ' alternate ID'
0264        if self.notNone:
0265            r += ' not null'
0266        return r + '>'
0267
0268    def createSQL(self):
0269        return ' '.join([self._sqlType()] + self._extraSQL())
0270
0271    def _extraSQL(self):
0272        result = []
0273        if self.notNone or self.alternateID:
0274            result.append('NOT NULL')
0275        if self.unique or self.alternateID:
0276            result.append('UNIQUE')
0277        if self.defaultSQL is not None:
0278            result.append("DEFAULT %s" % self.defaultSQL)
0279        return result
0280
0281    def _sqlType(self):
0282        if self.customSQLType is None:
0283            raise ValueError, ("Col %s (%s) cannot be used for automatic "
0284                               "schema creation (too abstract)" %
0285                               (self.name, self.__class__))
0286        else:
0287            return self.customSQLType
0288
0289    def _mysqlType(self):
0290        return self._sqlType()
0291
0292    def _postgresType(self):
0293        return self._sqlType()
0294
0295    def _sqliteType(self):
0296        # SQLite is naturally typeless, so as a fallback it uses
0297        # no type.
0298        try:
0299            return self._sqlType()
0300        except ValueError:
0301            return ''
0302
0303    def _sybaseType(self):
0304        return self._sqlType()
0305
0306    def _mssqlType(self):
0307        return self._sqlType()
0308
0309    def _firebirdType(self):
0310        return self._sqlType()
0311
0312    def _maxdbType(self):
0313        return self._sqlType()
0314
0315    def mysqlCreateSQL(self):
0316        return ' '.join([self.dbName, self._mysqlType()] + self._extraSQL())
0317
0318    def postgresCreateSQL(self):
0319        return ' '.join([self.dbName, self._postgresType()] + self._extraSQL())
0320
0321    def sqliteCreateSQL(self):
0322        return ' '.join([self.dbName, self._sqliteType()] + self._extraSQL())
0323
0324    def sybaseCreateSQL(self):
0325        return ' '.join([self.dbName, self._sybaseType()] + self._extraSQL())
0326
0327    def mssqlCreateSQL(self, connection=None):
0328        self.connection = connection
0329        return ' '.join([self.dbName, self._mssqlType()] + self._extraSQL())
0330
0331    def firebirdCreateSQL(self):
0332        # Ian Sparks pointed out that fb is picky about the order
0333        # of the NOT NULL clause in a create statement.  So, we handle
0334        # them differently for Enum columns.
0335        if not isinstance(self, SOEnumCol):
0336            return ' '.join([self.dbName, self._firebirdType()] + self._extraSQL())
0337        else:
0338            return ' '.join([self.dbName] + [self._firebirdType()[0]] + self._extraSQL() + [self._firebirdType()[1]])
0339
0340    def maxdbCreateSQL(self):
0341       return ' '.join([self.dbName, self._maxdbType()] + self._extraSQL())
0342
0343    def __get__(self, obj, type=None):
0344        if obj is None:
0345            # class attribute, return the descriptor itself
0346            return self
0347        if obj.sqlmeta._obsolete:
0348            raise RuntimeError('The object <%s %s> is obsolete' % (
0349                obj.__class__.__name__, obj.id))
0350        if obj.sqlmeta.cacheColumns:
0351            columns = obj.sqlmeta._columnCache
0352            if columns is None:
0353                obj.sqlmeta.loadValues()
0354            try:
0355                return columns[name]
0356            except KeyError:
0357                return obj.sqlmeta.loadColumn(self)
0358        else:
0359            return obj.sqlmeta.loadColumn(self)
0360
0361    def __set__(self, obj, value):
0362        if self.immutable:
0363            raise AttributeError("The column %s.%s is immutable" %
0364                                 (obj.__class__.__name__,
0365                                  self.name))
0366        obj.sqlmeta.setColumn(self, value)
0367
0368    def __delete__(self, obj):
0369        raise AttributeError("I can't be deleted from %r" % obj)
0370
0371
0372class Col(object):
0373
0374    baseClass = SOCol
0375
0376    def __init__(self, name=None, **kw):
0377        super(Col, self).__init__()
0378        self.__dict__['_name'] = name
0379        self.__dict__['_kw'] = kw
0380        self.__dict__['creationOrder'] = creationOrder.next()
0381        self.__dict__['_extra_vars'] = {}
0382
0383    def _set_name(self, value):
0384        assert self._name is None or self._name == value, (
0385            "You cannot change a name after it has already been set "
0386            "(from %s to %s)" % (self.name, value))
0387        self.__dict__['_name'] = value
0388
0389    def _get_name(self):
0390        return self._name
0391
0392    name = property(_get_name, _set_name)
0393
0394    def withClass(self, soClass):
0395        return self.baseClass(soClass=soClass, name=self._name,
0396                              creationOrder=self.creationOrder,
0397                              columnDef=self,
0398                              extra_vars=self._extra_vars,
0399                              **self._kw)
0400
0401    def __setattr__(self, var, value):
0402        if var == 'name':
0403            super(Col, self).__setattr__(var, value)
0404            return
0405        self._extra_vars[var] = value
0406
0407    def __repr__(self):
0408        return '<%s %s %s>' % (
0409            self.__class__.__name__, hex(abs(id(self)))[2:],
0410            self._name or '(unnamed)')
0411
0412
0413
0414class SOStringLikeCol(SOCol):
0415    """A common ancestor for SOStringCol and SOUnicodeCol"""
0416    def __init__(self, **kw):
0417        self.length = kw.pop('length', None)
0418        self.varchar = kw.pop('varchar', 'auto')
0419        self.char_binary = kw.pop('char_binary', None) # A hack for MySQL
0420        if not self.length:
0421            assert self.varchar == 'auto' or not self.varchar,                      "Without a length strings are treated as TEXT, not varchar"
0423            self.varchar = False
0424        elif self.varchar == 'auto':
0425            self.varchar = True
0426
0427        super(SOStringLikeCol, self).__init__(**kw)
0428
0429    def autoConstraints(self):
0430        constraints = [constrs.isString]
0431        if self.length is not None:
0432            constraints += [constrs.MaxLength(self.length)]
0433        return constraints
0434
0435    def _sqlType(self):
0436        if self.customSQLType is not None:
0437            return self.customSQLType
0438        if not self.length:
0439            return 'TEXT'
0440        elif self.varchar:
0441            return 'VARCHAR(%i)' % self.length
0442        else:
0443            return 'CHAR(%i)' % self.length
0444
0445    def _check_case_sensitive(self, db):
0446        if self.char_binary:
0447            raise ValueError, "%s does not support binary character columns" % db
0448
0449    def _mysqlType(self):
0450        type = self._sqlType()
0451        if self.char_binary:
0452            type += " BINARY"
0453        return type
0454
0455    def _postgresType(self):
0456        self._check_case_sensitive("PostgreSQL")
0457        return super(SOStringLikeCol, self)._postgresType()
0458
0459    def _sqliteType(self):
0460        self._check_case_sensitive("SQLite")
0461        return super(SOStringLikeCol, self)._sqliteType()
0462
0463    def _sybaseType(self):
0464        self._check_case_sensitive("SYBASE")
0465        type = self._sqlType()
0466        if not self.notNone and not self.alternateID:
0467            type += ' NULL'
0468        return type
0469
0470    def _mssqlType(self):
0471        if self.customSQLType is not None:
0472            return self.customSQLType
0473        if not self.length:
0474            if self.connection and self.connection.can_use_max_types():
0475                type = 'VARCHAR(MAX)'
0476            else:
0477                type = 'VARCHAR(4000)'
0478        elif self.varchar:
0479            type = 'VARCHAR(%i)' % self.length
0480        else:
0481            type = 'CHAR(%i)' % self.length
0482        if not self.notNone and not self.alternateID:
0483            type += ' NULL'
0484        return type
0485
0486    def _firebirdType(self):
0487        self._check_case_sensitive("FireBird")
0488        if not self.length:
0489            return 'BLOB SUB_TYPE TEXT'
0490        else:
0491            return self._sqlType()
0492
0493    def _maxdbType(self):
0494        self._check_case_sensitive("SAP DB/MaxDB")
0495        if not self.length:
0496            return 'LONG ASCII'
0497        else:
0498            return self._sqlType()
0499
0500
0501class StringValidator(validators.Validator):
0502
0503    def to_python(self, value, state):
0504        if value is None:
0505            return None
0506        try:
0507            connection = state.connection or state.soObject._connection
0508            binaryType = connection._binaryType
0509        except AttributeError:
0510            dbEncoding = "ascii"
0511            binaryType = type(None) # Just a simple workaround
0512        else:
0513            dbEncoding = getattr(connection, "dbEncoding", None) or "ascii"
0514        if isinstance(value, unicode):
0515            return value.encode(dbEncoding)
0516        if self.dataType and isinstance(value, self.dataType):
0517            return value
0518        if isinstance(value, (str, buffer, binaryType, sqlbuilder.SQLExpression)):
0519            return value
0520        if hasattr(value, '__unicode__'):
0521            return unicode(value).encode(dbEncoding)
0522        raise validators.Invalid("expected a str in the StringCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
0524
0525    from_python = to_python
0526
0527class SOStringCol(SOStringLikeCol):
0528
0529    def createValidators(self, dataType=None):
0530        return [StringValidator(name=self.name, dataType=dataType)] +               super(SOStringCol, self).createValidators()
0532
0533class StringCol(Col):
0534    baseClass = SOStringCol
0535
0536
0537class NQuoted(sqlbuilder.SQLExpression):
0538    def __init__(self, value):
0539        assert isinstance(value, unicode)
0540        self.value = value
0541    def __hash__(self):
0542        return hash(self.value)
0543    def __sqlrepr__(self, db):
0544        assert db == 'mssql'
0545        return "N" + sqlbuilder.sqlrepr(self.value, db)
0546
0547class UnicodeStringValidator(validators.Validator):
0548
0549    def getDbEncoding(self, state):
0550        try:
0551            return self.dbEncoding
0552        except AttributeError:
0553            return self.soCol.getDbEncoding(state)
0554
0555    def to_python(self, value, state):
0556        if value is None:
0557            return None
0558        if isinstance(value, (unicode, sqlbuilder.SQLExpression)):
0559            return value
0560        if isinstance(value, str):
0561            return unicode(value, self.getDbEncoding(state))
0562        if isinstance(value, array): # MySQL
0563            return unicode(value.tostring(), self.getDbEncoding(state))
0564        if hasattr(value, '__unicode__'):
0565            return unicode(value)
0566        raise validators.Invalid("expected a str or a unicode in the UnicodeCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
0568
0569    def from_python(self, value, state):
0570        if value is None:
0571            return None
0572        if isinstance(value, (str, sqlbuilder.SQLExpression)):
0573            return value
0574        if isinstance(value, unicode):
0575            try:
0576                connection = state.connection or state.soObject._connection
0577            except AttributeError:
0578                pass
0579            else:
0580                if connection.dbName == 'mssql':
0581                    return NQuoted(value)
0582            return value.encode(self.getDbEncoding(state))
0583        if hasattr(value, '__unicode__'):
0584            return unicode(value).encode(self.getDbEncoding(state))
0585        raise validators.Invalid("expected a str or a unicode in the UnicodeCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
0587
0588class SOUnicodeCol(SOStringLikeCol):
0589    def __init__(self, **kw):
0590        self.dbEncoding = kw.pop('dbEncoding', None)
0591        super(SOUnicodeCol, self).__init__(**kw)
0592
0593    def _mssqlType(self):
0594        if self.customSQLType is not None:
0595            return self.customSQLType
0596        return 'N' + super(SOUnicodeCol, self)._mssqlType()
0597
0598    def createValidators(self):
0599        return [UnicodeStringValidator(name=self.name, soCol=self)] +               super(SOUnicodeCol, self).createValidators()
0601
0602    def getDbEncoding(self, state):
0603        if self.dbEncoding:
0604            return self.dbEncoding
0605        dbEncoding = state.soObject.sqlmeta.dbEncoding
0606        if dbEncoding:
0607            return dbEncoding
0608        try:
0609            connection = state.connection or state.soObject._connection
0610        except AttributeError:
0611            dbEncoding = None
0612        else:
0613            dbEncoding = getattr(connection, "dbEncoding", None)
0614        if not dbEncoding:
0615            dbEncoding = "utf-8"
0616        return dbEncoding
0617
0618class UnicodeCol(Col):
0619    baseClass = SOUnicodeCol
0620
0621
0622class IntValidator(validators.Validator):
0623
0624    def to_python(self, value, state):
0625        if value is None:
0626            return None
0627        if isinstance(value, (int, long, sqlbuilder.SQLExpression)):
0628            return value
0629        for converter, attr_name in (int, '__int__'), (long, '__long__'):
0630            if hasattr(value, attr_name):
0631                try:
0632                    return converter(value)
0633                except:
0634                    break
0635        raise validators.Invalid("expected an int in the IntCol '%s', got %s %r instead" %                   (self.name, type(value), value), value, state)
0637
0638    from_python = to_python
0639
0640class SOIntCol(SOCol):
0641    # 3-03 @@: support precision, maybe max and min directly
0642    def __init__(self, **kw):
0643        self.length = kw.pop('length', None)
0644        self.unsigned = bool(kw.pop('unsigned', None))
0645        self.zerofill = bool(kw.pop('zerofill', None))
0646        SOCol.__init__(self, **kw)
0647
0648    def autoConstraints(self):
0649        return [constrs.isInt]
0650
0651    def createValidators(self):
0652        return [IntValidator(name=self.name)] +               super(SOIntCol, self).createValidators()
0654
0655    def addSQLAttrs(self, str):
0656        _ret = str
0657        if str is None or len(str) < 1:
0658            return None
0659
0660        if self.length >= 1:
0661            _ret = "%s(%d)" % (_ret, self.length)
0662        if self.unsigned:
0663            _ret = _ret + " UNSIGNED"
0664        if self.zerofill:
0665            _ret = _ret + " ZEROFILL"
0666        return _ret
0667
0668    def _sqlType(self):
0669        return self.addSQLAttrs("INT")
0670
0671class IntCol(Col):
0672    baseClass = SOIntCol
0673
0674class SOTinyIntCol(SOIntCol):
0675    def _sqlType(self):
0676        return self.addSQLAttrs("TINYINT")
0677
0678class TinyIntCol(Col):
0679    baseClass = SOTinyIntCol
0680
0681class SOSmallIntCol(SOIntCol):
0682    def _sqlType(self):
0683        return self.addSQLAttrs("SMALLINT")
0684
0685class SmallIntCol(Col):
0686    baseClass = SOSmallIntCol
0687
0688class SOMediumIntCol(SOIntCol):
0689    def _sqlType(self):
0690        return self.addSQLAttrs("MEDIUMINT")
0691
0692class MediumIntCol(Col):
0693    baseClass = SOMediumIntCol
0694
0695class SOBigIntCol(SOIntCol):
0696    def _sqlType(self):
0697        return self.addSQLAttrs("BIGINT")
0698
0699class BigIntCol(Col):
0700    baseClass = SOBigIntCol
0701
0702
0703class BoolValidator(validators.Validator):
0704
0705    def to_python(self, value, state):
0706        if value is None:
0707            return None
0708        if isinstance(value, (bool, sqlbuilder.SQLExpression)):
0709            return value
0710        if isinstance(value, (int, long)) or hasattr(value, '__nonzero__'):
0711            return bool(value)
0712        raise validators.Invalid("expected a bool or an int in the BoolCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
0714
0715    from_python = to_python
0716
0717class SOBoolCol(SOCol):
0718    def autoConstraints(self):
0719        return [constrs.isBool]
0720
0721    def createValidators(self):
0722        return [BoolValidator(name=self.name)] +               super(SOBoolCol, self).createValidators()
0724
0725    def _postgresType(self):
0726        return 'BOOL'
0727
0728    def _mysqlType(self):
0729        return "BOOL"
0730
0731    def _sybaseType(self):
0732        return "BIT"
0733
0734    def _mssqlType(self):
0735        return "BIT"
0736
0737    def _firebirdType(self):
0738        return 'INT'
0739
0740    def _maxdbType(self):
0741        return "BOOLEAN"
0742
0743    def _sqliteType(self):
0744        return "BOOLEAN"
0745
0746class BoolCol(Col):
0747    baseClass = SOBoolCol
0748
0749
0750class FloatValidator(validators.Validator):
0751
0752    def to_python(self, value, state):
0753        if value is None:
0754            return None
0755        if isinstance(value, (float, int, long, sqlbuilder.SQLExpression)):
0756            return value
0757        for converter, attr_name in  (float, '__float__'), (int, '__int__'), (long, '__long__'):
0758            if hasattr(value, attr_name):
0759                try:
0760                    return converter(value)
0761                except:
0762                    break
0763        raise validators.Invalid("expected a float in the FloatCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
0765
0766    from_python = to_python
0767
0768class SOFloatCol(SOCol):
0769    # 3-03 @@: support precision (e.g., DECIMAL)
0770
0771    def autoConstraints(self):
0772        return [constrs.isFloat]
0773
0774    def createValidators(self):
0775        return [FloatValidator(name=self.name)] +               super(SOFloatCol, self).createValidators()
0777
0778    def _sqlType(self):
0779        return 'FLOAT'
0780
0781    def _mysqlType(self):
0782        return "DOUBLE PRECISION"
0783
0784class FloatCol(Col):
0785    baseClass = SOFloatCol
0786
0787
0788class SOKeyCol(SOCol):
0789    key_type = {int: "INT", str: "TEXT"}
0790
0791    # 3-03 @@: this should have a simplified constructor
0792    # Should provide foreign key information for other DBs.
0793
0794    def __init__(self, **kw):
0795        self.refColumn = kw.pop('refColumn', None)
0796        super(SOKeyCol, self).__init__(**kw)
0797
0798    def _sqlType(self):
0799        return self.key_type[self.soClass.sqlmeta.idType]
0800
0801    def _sybaseType(self):
0802        key_type = {int: "NUMERIC(18,0) NULL", str: "TEXT"}
0803        return key_type[self.soClass.sqlmeta.idType]
0804
0805    def _mssqlType(self):
0806        key_type = {int: "INT NULL", str: "TEXT"}
0807        return key_type[self.soClass.sqlmeta.idType]
0808
0809class KeyCol(Col):
0810
0811    baseClass = SOKeyCol
0812
0813class SOForeignKey(SOKeyCol):
0814
0815    def __init__(self, **kw):
0816        foreignKey = kw['foreignKey']
0817        style = kw['soClass'].sqlmeta.style
0818        if kw.get('name'):
0819            kw['origName'] = kw['name']
0820            kw['name'] = style.instanceAttrToIDAttr(kw['name'])
0821        else:
0822            kw['name'] = style.instanceAttrToIDAttr(style.pythonClassToAttr(foreignKey))
0823        super(SOForeignKey, self).__init__(**kw)
0824
0825    def sqliteCreateSQL(self):
0826        sql = SOKeyCol.sqliteCreateSQL(self)
0827        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0828        tName = other.sqlmeta.table
0829        idName = self.refColumn or other.sqlmeta.idName
0830        if self.cascade is not None:
0831            if self.cascade == 'null':
0832                action = 'ON DELETE SET NULL'
0833            elif self.cascade:
0834                action = 'ON DELETE CASCADE'
0835            else:
0836                action = 'ON DELETE RESTRICT'
0837        else:
0838            action = ''
0839        constraint = ('CONSTRAINT %(colName)s_exists '
0840                      #'FOREIGN KEY(%(colName)s) '
0841                      'REFERENCES %(tName)s(%(idName)s) '
0842                      '%(action)s' %
0843                      {'tName': tName,
0844                       'colName': self.dbName,
0845                       'idName': idName,
0846                       'action': action})
0847        sql = ' '.join([sql, constraint])
0848        return sql
0849
0850    def postgresCreateSQL(self):
0851        sql = SOKeyCol.postgresCreateSQL(self)
0852        return sql
0853
0854    def postgresCreateReferenceConstraint(self):
0855        sTName = self.soClass.sqlmeta.table
0856        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0857        tName = other.sqlmeta.table
0858        idName = self.refColumn or other.sqlmeta.idName
0859        if self.cascade is not None:
0860            if self.cascade == 'null':
0861                action = 'ON DELETE SET NULL'
0862            elif self.cascade:
0863                action = 'ON DELETE CASCADE'
0864            else:
0865                action = 'ON DELETE RESTRICT'
0866        else:
0867            action = ''
0868        constraint = ('ALTER TABLE %(sTName)s ADD CONSTRAINT %(colName)s_exists '
0869                      'FOREIGN KEY (%(colName)s) '
0870                      'REFERENCES %(tName)s (%(idName)s) '
0871                      '%(action)s' %
0872                      {'tName': tName,
0873                       'colName': self.dbName,
0874                       'idName': idName,
0875                       'action': action,
0876                       'sTName': sTName})
0877        return constraint
0878
0879    def mysqlCreateReferenceConstraint(self):
0880        sTName = self.soClass.sqlmeta.table
0881        sTLocalName = sTName.split('.')[-1]
0882        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0883        tName = other.sqlmeta.table
0884        idName = self.refColumn or other.sqlmeta.idName
0885        if self.cascade is not None:
0886            if self.cascade == 'null':
0887                action = 'ON DELETE SET NULL'
0888            elif self.cascade:
0889                action = 'ON DELETE CASCADE'
0890            else:
0891                action = 'ON DELETE RESTRICT'
0892        else:
0893            action = ''
0894        constraint = ('ALTER TABLE %(sTName)s ADD CONSTRAINT %(sTLocalName)s_%(colName)s_exists '
0895                      'FOREIGN KEY (%(colName)s) '
0896                      'REFERENCES %(tName)s (%(idName)s) '
0897                      '%(action)s' %
0898                      {'tName': tName,
0899                       'colName': self.dbName,
0900                       'idName': idName,
0901                       'action': action,
0902                       'sTName': sTName,
0903                       'sTLocalName': sTLocalName})
0904        return constraint
0905
0906    def mysqlCreateSQL(self):
0907        return SOKeyCol.mysqlCreateSQL(self)
0908
0909    def sybaseCreateSQL(self):
0910        sql = SOKeyCol.sybaseCreateSQL(self)
0911        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0912        tName = other.sqlmeta.table
0913        idName = self.refColumn or other.sqlmeta.idName
0914        reference = ('REFERENCES %(tName)s(%(idName)s) ' %
0915                     {'tName':tName,
0916                      'idName':idName})
0917        sql = ' '.join([sql, reference])
0918        return sql
0919
0920    def sybaseCreateReferenceConstraint(self):
0921        # @@: Code from above should be moved here
0922        return None
0923
0924    def mssqlCreateSQL(self, connection=None):
0925        sql = SOKeyCol.mssqlCreateSQL(self, connection)
0926        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0927        tName = other.sqlmeta.table
0928        idName = self.refColumn or other.sqlmeta.idName
0929        reference = ('REFERENCES %(tName)s(%(idName)s) ' %
0930                     {'tName':tName,
0931                      'idName':idName})
0932        sql = ' '.join([sql, reference])
0933        return sql
0934
0935    def mssqlCreateReferenceConstraint(self):
0936        # @@: Code from above should be moved here
0937        return None
0938
0939    def maxdbCreateSQL(self):
0940        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0941        fidName = self.dbName
0942        #I assume that foreign key name is identical to the id of the reference table
0943        sql = ' '.join([fidName, self._maxdbType()])
0944        tName = other.sqlmeta.table
0945        idName  = self.refColumn or other.sqlmeta.idName
0946        sql=sql + ',' + '\n'
0947        sql=sql + 'FOREIGN KEY (%s) REFERENCES %s(%s)'%(fidName,tName,idName)
0948        return sql
0949
0950    def maxdbCreateReferenceConstraint(self):
0951        # @@: Code from above should be moved here
0952        return None
0953
0954class ForeignKey(KeyCol):
0955
0956    baseClass = SOForeignKey
0957
0958    def __init__(self, foreignKey=None, **kw):
0959        super(ForeignKey, self).__init__(foreignKey=foreignKey, **kw)
0960
0961
0962class EnumValidator(validators.Validator):
0963
0964    def to_python(self, value, state):
0965        if value in self.enumValues:
0966            return value
0967        elif not self.notNone and value is None:
0968            return None
0969        raise validators.Invalid("expected a member of %r in the EnumCol '%s', got %r instead" %               (self.enumValues, self.name, value), value, state)
0971
0972    from_python = to_python
0973
0974class SOEnumCol(SOCol):
0975
0976    def __init__(self, **kw):
0977        self.enumValues = kw.pop('enumValues', None)
0978        assert self.enumValues is not None,                  'You must provide an enumValues keyword argument'
0980        super(SOEnumCol, self).__init__(**kw)
0981
0982    def autoConstraints(self):
0983        return [constrs.isString, constrs.InList(self.enumValues)]
0984
0985    def createValidators(self):
0986        return [EnumValidator(name=self.name, enumValues=self.enumValues,
0987                              notNone=self.notNone)] +               super(SOEnumCol, self).createValidators()
0989
0990    def _mysqlType(self):
0991        # We need to map None in the enum expression to an appropriate
0992        # condition on NULL
0993        if None in self.enumValues:
0994            return "ENUM(%s)" % ', '.join([sqlbuilder.sqlrepr(v, 'mysql') for v in self.enumValues if v is not None])
0995        else:
0996            return "ENUM(%s) NOT NULL" % ', '.join([sqlbuilder.sqlrepr(v, 'mysql') for v in self.enumValues])
0997
0998    def _postgresType(self):
0999        length = max(map(self._getlength, self.enumValues))
1000        enumValues = ', '.join([sqlbuilder.sqlrepr(v, 'postgres') for v in self.enumValues])
1001        checkConstraint = "CHECK (%s in (%s))" % (self.dbName, enumValues)
1002        return "VARCHAR(%i) %s" % (length, checkConstraint)
1003
1004    _sqliteType = _postgresType
1005
1006    def _sybaseType(self):
1007        return self._postgresType()
1008
1009    def _mssqlType(self):
1010        return self._postgresType()
1011
1012    def _firebirdType(self):
1013        length = max(map(self._getlength, self.enumValues))
1014        enumValues = ', '.join([sqlbuilder.sqlrepr(v, 'firebird') for v in self.enumValues])
1015        checkConstraint = "CHECK (%s in (%s))" % (self.dbName, enumValues)
1016        #NB. Return a tuple, not a string here
1017        return "VARCHAR(%i)" % (length), checkConstraint
1018
1019    def _maxdbType(self):
1020        raise TypeError("Enum type is not supported on MAX DB")
1021
1022    def _getlength(self, obj):
1023        """
1024        None counts as 0; everything else uses len()
1025        """
1026        if obj is None:
1027            return 0
1028        else:
1029            return len(obj)
1030
1031class EnumCol(Col):
1032    baseClass = SOEnumCol
1033
1034
1035class SetValidator(validators.Validator):
1036    """
1037    Translates Python tuples into SQL comma-delimited SET strings.
1038    """
1039
1040    def to_python(self, value, state):
1041        if isinstance(value, str):
1042            return tuple(value.split(","))
1043        raise validators.Invalid("expected a string in the SetCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
1045
1046    def from_python(self, value, state):
1047        if isinstance(value, basestring):
1048            value = (value,)
1049        try:
1050            return ",".join(value)
1051        except:
1052            raise validators.Invalid("expected a string or a sequence of stringsin the SetCol '%s', got %s %r instead" %                   (self.name, type(value), value), value, state)
1054
1055class SOSetCol(SOCol):
1056    def __init__(self, **kw):
1057        self.setValues = kw.pop('setValues', None)
1058        assert self.setValues is not None,                   'You must provide a setValues keyword argument'
1060        super(SOSetCol, self).__init__(**kw)
1061
1062    def autoConstraints(self):
1063        return [constrs.isString, constrs.InList(self.setValues)]
1064
1065    def createValidators(self):
1066        return [SetValidator(name=self.name, setValues=self.setValues)] +               super(SOSetCol, self).createValidators()
1068
1069    def _mysqlType(self):
1070        return "SET(%s)" % ', '.join([sqlbuilder.sqlrepr(v, 'mysql') for v in self.setValues])
1071
1072class SetCol(Col):
1073    baseClass = SOSetCol
1074
1075
1076class DateTimeValidator(validators.DateValidator):
1077    def to_python(self, value, state):
1078        if value is None:
1079            return None
1080        if isinstance(value, (datetime.datetime, datetime.date, datetime.time, sqlbuilder.SQLExpression)):
1081            return value
1082        if mxdatetime_available:
1083            if isinstance(value, DateTimeType):
1084                # convert mxDateTime instance to datetime
1085                if (self.format.find("%H") >= 0) or (self.format.find("%T")) >= 0:
1086                    return datetime.datetime(value.year, value.month, value.day,
1087                        value.hour, value.minute, int(value.second))
1088                else:
1089                    return datetime.date(value.year, value.month, value.day)
1090            elif isinstance(value, TimeType):
1091                # convert mxTime instance to time
1092                if self.format.find("%d") >= 0:
1093                    return datetime.timedelta(seconds=value.seconds)
1094                else:
1095                    return datetime.time(value.hour, value.minute, int(value.second))
1096        try:
1097            stime = time.strptime(value, self.format)
1098        except:
1099            raise validators.Invalid("expected a date/time string of the '%s' format in the DateTimeCol '%s', got %s %r instead" %                   (self.format, self.name, type(value), value), value, state)
1101        return datetime.datetime(*stime[:6])
1102
1103    def from_python(self, value, state):
1104        if value is None:
1105            return None
1106        if isinstance(value, (datetime.datetime, datetime.date, datetime.time, sqlbuilder.SQLExpression)):
1107            return value
1108        if hasattr(value, "strftime"):
1109            return value.strftime(self.format)
1110        raise validators.Invalid("expected a datetime in the DateTimeCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
1112
1113if mxdatetime_available:
1114    class MXDateTimeValidator(validators.DateValidator):
1115        def to_python(self, value, state):
1116            if value is None:
1117                return None
1118            if isinstance(value, (DateTimeType, TimeType, sqlbuilder.SQLExpression)):
1119                return value
1120            if isinstance(value, datetime.datetime):
1121                return DateTime.DateTime(value.year, value.month, value.day,
1122                    value.hour, value.minute, value.second)
1123            elif isinstance(value, datetime.date):
1124                return DateTime.Date(value.year, value.month, value.day)
1125            elif isinstance(value, datetime.time):
1126                return DateTime.Time(value.hour, value.minute, value.second)
1127            try:
1128                stime = time.strptime(value, self.format)
1129            except:
1130                raise validators.Invalid("expected a date/time string of the '%s' format in the DateTimeCol '%s', got %s %r instead" %                       (self.format, self.name, type(value), value), value, state)
1132            return DateTime.mktime(stime)
1133
1134        def from_python(self, value, state):
1135            if value is None:
1136                return None
1137            if isinstance(value, (DateTimeType, TimeType, sqlbuilder.SQLExpression)):
1138                return value
1139            if hasattr(value, "strftime"):
1140                return value.strftime(self.format)
1141            raise validators.Invalid("expected a mxDateTime in the DateTimeCol '%s', got %s %r instead" %                   (self.name, type(value), value), value, state)
1143
1144class SODateTimeCol(SOCol):
1145    datetimeFormat = '%Y-%m-%d %H:%M:%S'
1146
1147    def __init__(self, **kw):
1148        datetimeFormat = kw.pop('datetimeFormat', None)
1149        if datetimeFormat:
1150            self.datetimeFormat = datetimeFormat
1151        super(SODateTimeCol, self).__init__(**kw)
1152
1153    def createValidators(self):
1154        _validators = super(SODateTimeCol, self).createValidators()
1155        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1156            validatorClass = DateTimeValidator
1157        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1158            validatorClass = MXDateTimeValidator
1159        if default_datetime_implementation:
1160            _validators.insert(0, validatorClass(name=self.name, format=self.datetimeFormat))
1161        return _validators
1162
1163    def _mysqlType(self):
1164        return 'DATETIME'
1165
1166    def _postgresType(self):
1167        return 'TIMESTAMP'
1168
1169    def _sybaseType(self):
1170        return 'DATETIME'
1171
1172    def _mssqlType(self):
1173        return 'DATETIME'
1174
1175    def _sqliteType(self):
1176        return 'TIMESTAMP'
1177
1178    def _firebirdType(self):
1179        return 'TIMESTAMP'
1180
1181    def _maxdbType(self):
1182        return 'TIMESTAMP'
1183
1184class DateTimeCol(Col):
1185    baseClass = SODateTimeCol
1186    @staticmethod
1187    def now():
1188        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1189            return datetime.datetime.now()
1190        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1191            return DateTime.now()
1192        else:
1193            assert 0, ("No datetime implementation available "
1194                       "(DATETIME_IMPLEMENTATION=%r)"
1195                       % DATETIME_IMPLEMENTATION)
1196
1197
1198class DateValidator(DateTimeValidator):
1199    def to_python(self, value, state):
1200        if isinstance(value, datetime.datetime):
1201            value = value.date()
1202        if isinstance(value, (datetime.date, sqlbuilder.SQLExpression)):
1203            return value
1204        value = super(DateValidator, self).to_python(value, state)
1205        if isinstance(value, datetime.datetime):
1206            value = value.date()
1207        return value
1208
1209    from_python = to_python
1210
1211class SODateCol(SOCol):
1212    dateFormat = '%Y-%m-%d'
1213
1214    def __init__(self, **kw):
1215        dateFormat = kw.pop('dateFormat', None)
1216        if dateFormat: self.dateFormat = dateFormat
1217        super(SODateCol, self).__init__(**kw)
1218
1219    def createValidators(self):
1220        """Create a validator for the column. Can be overriden in descendants."""
1221        _validators = super(SODateCol, self).createValidators()
1222        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1223            validatorClass = DateValidator
1224        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1225            validatorClass = MXDateTimeValidator
1226        if default_datetime_implementation:
1227            _validators.insert(0, validatorClass(name=self.name, format=self.dateFormat))
1228        return _validators
1229
1230    def _mysqlType(self):
1231        return 'DATE'
1232
1233    def _postgresType(self):
1234        return 'DATE'
1235
1236    def _sybaseType(self):
1237        return self._postgresType()
1238
1239    def _mssqlType(self):
1240        """
1241        SQL Server doesn't have  a DATE data type, to emulate we use a vc(10)
1242        """
1243        return 'VARCHAR(10)'
1244
1245    def _firebirdType(self):
1246        return 'DATE'
1247
1248    def _maxdbType(self):
1249        return  'DATE'
1250
1251    def _sqliteType(self):
1252        return 'DATE'
1253
1254class DateCol(Col):
1255    baseClass = SODateCol
1256
1257
1258class TimeValidator(DateTimeValidator):
1259    def to_python(self, value, state):
1260        if isinstance(value, (datetime.time, sqlbuilder.SQLExpression)):
1261            return value
1262        if isinstance(value, datetime.timedelta):
1263            if value.days:
1264                raise validators.Invalid(
1265                    "the value for the TimeCol '%s' must has days=0, it has days=%d" %
1266                        (self.name, value.days), value, state)
1267            return datetime.time(*time.gmtime(value.seconds)[3:6])
1268        value = super(TimeValidator, self).to_python(value, state)
1269        if isinstance(value, datetime.datetime):
1270            value = value.time()
1271        return value
1272
1273    from_python = to_python
1274
1275class SOTimeCol(SOCol):
1276    timeFormat = '%H:%M:%S'
1277
1278    def __init__(self, **kw):
1279        timeFormat = kw.pop('timeFormat', None)
1280        if timeFormat:
1281            self.timeFormat = timeFormat
1282        super(SOTimeCol, self).__init__(**kw)
1283
1284    def createValidators(self):
1285        _validators = super(SOTimeCol, self).createValidators()
1286        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1287            validatorClass = TimeValidator
1288        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1289            validatorClass = MXDateTimeValidator
1290        if default_datetime_implementation:
1291            _validators.insert(0, validatorClass(name=self.name, format=self.timeFormat))
1292        return _validators
1293
1294    def _mysqlType(self):
1295        return 'TIME'
1296
1297    def _postgresType(self):
1298        return 'TIME'
1299
1300    def _sybaseType(self):
1301        return 'TIME'
1302
1303    def _sqliteType(self):
1304        return 'TIME'
1305
1306    def _firebirdType(self):
1307        return 'TIME'
1308
1309    def _maxdbType(self):
1310        return 'TIME'
1311
1312class TimeCol(Col):
1313    baseClass = SOTimeCol
1314
1315
1316class SOTimestampCol(SODateTimeCol):
1317    """
1318    Necessary to support MySQL's use of TIMESTAMP versus DATETIME types
1319    """
1320
1321    def __init__(self, **kw):
1322        if 'default' not in kw:
1323            kw['default'] = None
1324        SOCol.__init__(self, **kw)
1325
1326    def _mysqlType(self):
1327        return 'TIMESTAMP'
1328
1329class TimestampCol(Col):
1330    baseClass = SOTimestampCol
1331
1332
1333class TimedeltaValidator(validators.Validator):
1334    def to_python(self, value, state):
1335        return value
1336
1337    from_python = to_python
1338
1339class SOTimedeltaCol(SOCol):
1340    def _postgresType(self):
1341        return 'INTERVAL'
1342
1343    def createValidators(self):
1344        return [TimedeltaValidator(name=self.name)] +               super(SOTimedeltaCol, self).createValidators()
1346
1347class TimedeltaCol(Col):
1348    baseClass = SOTimedeltaCol
1349
1350
1351from decimal import Decimal
1352
1353class DecimalValidator(validators.Validator):
1354    def to_python(self, value, state):
1355        if value is None:
1356            return None
1357        if isinstance(value, (int, long, Decimal, sqlbuilder.SQLExpression)):
1358            return value
1359        if isinstance(value, float):
1360            value = str(value)
1361        try:
1362            connection = state.connection or state.soObject._connection
1363        except AttributeError:
1364            pass
1365        else:
1366            if hasattr(connection, "decimalSeparator"):
1367                value = value.replace(connection.decimalSeparator, ".")
1368        try:
1369            return Decimal(value)
1370        except:
1371            raise validators.Invalid("expected a Decimal in the DecimalCol '%s', got %s %r instead" %                   (self.name, type(value), value), value, state)
1373
1374    def from_python(self, value, state):
1375        if value is None:
1376            return None
1377        if isinstance(value, float):
1378            value = str(value)
1379        if isinstance(value, basestring):
1380            try:
1381                connection = state.connection or state.soObject._connection
1382            except AttributeError:
1383                pass
1384            else:
1385                if hasattr(connection, "decimalSeparator"):
1386                    value = value.replace(connection.decimalSeparator, ".")
1387            try:
1388                return Decimal(value)
1389            except:
1390                raise validators.Invalid("can not parse Decimal value '%s' in the DecimalCol from '%s'" %
1391                    (value, getattr(state, 'soObject', '(unknown)')), value, state)
1392        if isinstance(value, (int, long, Decimal, sqlbuilder.SQLExpression)):
1393            return value
1394        raise validators.Invalid("expected a Decimal in the DecimalCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
1396
1397class SODecimalCol(SOCol):
1398
1399    def __init__(self, **kw):
1400        self.size = kw.pop('size', NoDefault)
1401        assert self.size is not NoDefault,                  "You must give a size argument"
1403        self.precision = kw.pop('precision', NoDefault)
1404        assert self.precision is not NoDefault,                  "You must give a precision argument"
1406        super(SODecimalCol, self).__init__(**kw)
1407
1408    def _sqlType(self):
1409        return 'DECIMAL(%i, %i)' % (self.size, self.precision)
1410
1411    def createValidators(self):
1412        return [DecimalValidator(name=self.name)] +               super(SODecimalCol, self).createValidators()
1414
1415class DecimalCol(Col):
1416    baseClass = SODecimalCol
1417
1418class SOCurrencyCol(SODecimalCol):
1419
1420    def __init__(self, **kw):
1421        pushKey(kw, 'size', 10)
1422        pushKey(kw, 'precision', 2)
1423        super(SOCurrencyCol, self).__init__(**kw)
1424
1425class CurrencyCol(DecimalCol):
1426    baseClass = SOCurrencyCol
1427
1428
1429class DecimalStringValidator(DecimalValidator):
1430    def to_python(self, value, state):
1431        value = super(DecimalStringValidator, self).to_python(value, state)
1432        if self.precision and isinstance(value, Decimal):
1433            assert value < self.max,                       "Value must be less than %s" % int(self.max)
1435            value = value.quantize(self.precision)
1436        return value
1437
1438    def from_python(self, value, state):
1439        value = super(DecimalStringValidator, self).from_python(value, state)
1440        if isinstance(value, Decimal):
1441            if self.precision:
1442                assert value < self.max,                           "Value must be less than %s" % int(self.max)
1444                value = value.quantize(self.precision)
1445            value = value.to_eng_string()
1446        elif isinstance(value, (int, long)):
1447            value = str(value)
1448        return value
1449
1450class SODecimalStringCol(SOStringCol):
1451    def __init__(self, **kw):
1452        self.size = kw.pop('size', NoDefault)
1453        assert (self.size is not NoDefault) and (self.size >= 0),               "You must give a size argument as a positive integer"
1455        self.precision = kw.pop('precision', NoDefault)
1456        assert (self.precision is not NoDefault) and (self.precision >= 0),                  "You must give a precision argument as a positive integer"
1458        kw['length'] = int(self.size) + int(self.precision)
1459        self.quantize = kw.pop('quantize', False)
1460        assert isinstance(self.quantize, bool),                   "quantize argument must be Boolean True/False"
1462        super(SODecimalStringCol, self).__init__(**kw)
1463
1464    def createValidators(self):
1465        if self.quantize:
1466            v = DecimalStringValidator(name=self.name,
1467                precision=Decimal(10) ** (-1 * int(self.precision)),
1468                max=Decimal(10) ** (int(self.size) - int(self.precision)))
1469        else:
1470            v = DecimalStringValidator(name=self.name, precision=0)
1471        return [v] +               super(SODecimalStringCol, self).createValidators(dataType=Decimal)
1473
1474class DecimalStringCol(StringCol):
1475    baseClass = SODecimalStringCol
1476
1477
1478class BinaryValidator(validators.Validator):
1479    """
1480    Validator for binary types.
1481
1482    We're assuming that the per-database modules provide some form
1483    of wrapper type for binary conversion.
1484    """
1485
1486    _cachedValue = None
1487
1488    def to_python(self, value, state):
1489        if value is None:
1490            return None
1491        try:
1492            connection = state.connection or state.soObject._connection
1493        except AttributeError:
1494            dbName = None
1495            binaryType = type(None) # Just a simple workaround
1496        else:
1497            dbName = connection.dbName
1498            binaryType = connection._binaryType
1499        if isinstance(value, str):
1500            if dbName == "sqlite":
1501                value = connection.module.decode(value)
1502            return value
1503        if isinstance(value, (buffer, binaryType)):
1504            cachedValue = self._cachedValue
1505            if cachedValue and cachedValue[1] == value:
1506                return cachedValue[0]
1507            if isinstance(value, array): # MySQL
1508                return value.tostring()
1509            return str(value) # buffer => string
1510        raise validators.Invalid("expected a string in the BLOBCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
1512
1513    def from_python(self, value, state):
1514        if value is None:
1515            return None
1516        connection = state.connection or state.soObject._connection
1517        binary = connection.createBinary(value)
1518        self._cachedValue = (value, binary)
1519        return binary
1520
1521class SOBLOBCol(SOStringCol):
1522    def __init__(self, **kw):
1523        # Change the default from 'auto' to False - this is a (mostly) binary column
1524        if 'varchar' not in kw: kw['varchar'] = False
1525        super(SOBLOBCol, self).__init__(**kw)
1526
1527    def createValidators(self):
1528        return [BinaryValidator(name=self.name)] +               super(SOBLOBCol, self).createValidators()
1530
1531    def _mysqlType(self):
1532        length = self.length
1533        varchar = self.varchar
1534        if length >= 2**24:
1535            return varchar and "LONGTEXT" or "LONGBLOB"
1536        if length >= 2**16:
1537            return varchar and "MEDIUMTEXT" or "MEDIUMBLOB"
1538        if length >= 2**8:
1539            return varchar and "TEXT" or "BLOB"
1540        return varchar and "TINYTEXT" or "TINYBLOB"
1541
1542    def _postgresType(self):
1543        return 'BYTEA'
1544
1545    def _mssqlType(self):
1546        if self.connection and self.connection.can_use_max_types():
1547            return 'VARBINARY(MAX)'
1548        else:
1549            return "IMAGE"
1550
1551class BLOBCol(StringCol):
1552    baseClass = SOBLOBCol
1553
1554
1555class PickleValidator(BinaryValidator):
1556    """
1557    Validator for pickle types.  A pickle type is simply a binary type
1558    with hidden pickling, so that we can simply store any kind of
1559    stuff in a particular column.
1560
1561    The support for this relies directly on the support for binary for
1562    your database.
1563    """
1564
1565    def to_python(self, value, state):
1566        if value is None:
1567            return None
1568        if isinstance(value, unicode):
1569            try:
1570                connection = state.connection or state.soObject._connection
1571            except AttributeError:
1572                dbEncoding = "ascii"
1573            else:
1574                dbEncoding = getattr(connection, "dbEncoding", None) or "ascii"
1575            value = value.encode(dbEncoding)
1576        if isinstance(value, str):
1577            return pickle.loads(value)
1578        raise validators.Invalid("expected a pickle string in the PickleCol '%s', got %s %r instead" %               (self.name, type(value), value), value, state)
1580
1581    def from_python(self, value, state):
1582        if value is None:
1583            return None
1584        return pickle.dumps(value, self.pickleProtocol)
1585
1586class SOPickleCol(SOBLOBCol):
1587
1588    def __init__(self, **kw):
1589        self.pickleProtocol = kw.pop('pickleProtocol', pickle.HIGHEST_PROTOCOL)
1590        super(SOPickleCol, self).__init__(**kw)
1591
1592    def createValidators(self):
1593        return [PickleValidator(name=self.name,
1594                pickleProtocol=self.pickleProtocol)] +               super(SOPickleCol, self).createValidators()
1596
1597    def _mysqlType(self):
1598        length = self.length
1599        if length >= 2**24:
1600            return "LONGBLOB"
1601        if length >= 2**16:
1602            return "MEDIUMBLOB"
1603        return "BLOB"
1604
1605class PickleCol(BLOBCol):
1606    baseClass = SOPickleCol
1607
1608
1609def pushKey(kw, name, value):
1610    if not name in kw:
1611        kw[name] = value
1612
1613all = []
1614for key, value in globals().items():
1615    if isinstance(value, type) and (issubclass(value, (Col, SOCol))):
1616        all.append(key)
1617__all__.extend(all)
1618del all