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