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