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 _idType(self):
0884        return self.soClass.sqlmeta.idType
0885
0886    def _sqlType(self):
0887        return self.key_type[self._idType()]
0888
0889    def _sybaseType(self):
0890        key_type = {int: "NUMERIC(18,0) NULL", str: "TEXT"}
0891        return key_type[self._idType()]
0892
0893    def _mssqlType(self):
0894        key_type = {int: "INT NULL", str: "TEXT"}
0895        return key_type[self._idType()]
0896
0897
0898class KeyCol(Col):
0899
0900    baseClass = SOKeyCol
0901
0902
0903class ForeignKeyValidator(SOValidator):
0904
0905    def __init__(self, *args, **kw):
0906        super(ForeignKeyValidator, self).__init__(*args, **kw)
0907        self.fkIDType = None
0908
0909    def from_python(self, value, state):
0910        if value is None:
0911            return None
0912        # Avoid importing the main module
0913        # to get the SQLObject class for isinstance
0914        if hasattr(value, 'sqlmeta'):
0915            return value
0916        if self.fkIDType is None:
0917            otherTable = findClass(self.soCol.foreignKey,
0918                                   self.soCol.soClass.sqlmeta.registry)
0919            self.fkIDType = otherTable.sqlmeta.idType
0920        try:
0921            value = self.fkIDType(value)
0922            return value
0923        except (ValueError, TypeError):
0924            pass
0925        raise validators.Invalid("expected a %r for the ForeignKey '%s', "
0926                                 "got %s %r instead" %
0927                                 (self.fkIDType, self.name,
0928                                  type(value), value), value, state)
0929
0930
0931class SOForeignKey(SOKeyCol):
0932
0933    def __init__(self, **kw):
0934        foreignKey = kw['foreignKey']
0935        style = kw['soClass'].sqlmeta.style
0936        if kw.get('name'):
0937            kw['origName'] = kw['name']
0938            kw['name'] = style.instanceAttrToIDAttr(kw['name'])
0939        else:
0940            kw['name'] = style.instanceAttrToIDAttr(
0941                style.pythonClassToAttr(foreignKey))
0942        super(SOForeignKey, self).__init__(**kw)
0943
0944    def createValidators(self):
0945        return [ForeignKeyValidator(name=self.name)] +               super(SOForeignKey, self).createValidators()
0947
0948    def _idType(self):
0949        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0950        return other.sqlmeta.idType
0951
0952    def sqliteCreateSQL(self):
0953        sql = SOKeyCol.sqliteCreateSQL(self)
0954        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0955        tName = other.sqlmeta.table
0956        idName = self.refColumn or other.sqlmeta.idName
0957        if self.cascade is not None:
0958            if self.cascade == 'null':
0959                action = 'ON DELETE SET NULL'
0960            elif self.cascade:
0961                action = 'ON DELETE CASCADE'
0962            else:
0963                action = 'ON DELETE RESTRICT'
0964        else:
0965            action = ''
0966        constraint = ('CONSTRAINT %(colName)s_exists '
0967                      # 'FOREIGN KEY(%(colName)s) '
0968                      'REFERENCES %(tName)s(%(idName)s) '
0969                      '%(action)s' %
0970                      {'tName': tName,
0971                       'colName': self.dbName,
0972                       'idName': idName,
0973                       'action': action})
0974        sql = ' '.join([sql, constraint])
0975        return sql
0976
0977    def postgresCreateSQL(self):
0978        sql = SOKeyCol.postgresCreateSQL(self)
0979        return sql
0980
0981    def postgresCreateReferenceConstraint(self):
0982        sTName = self.soClass.sqlmeta.table
0983        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
0984        tName = other.sqlmeta.table
0985        idName = self.refColumn or other.sqlmeta.idName
0986        if self.cascade is not None:
0987            if self.cascade == 'null':
0988                action = 'ON DELETE SET NULL'
0989            elif self.cascade:
0990                action = 'ON DELETE CASCADE'
0991            else:
0992                action = 'ON DELETE RESTRICT'
0993        else:
0994            action = ''
0995        constraint = ('ALTER TABLE %(sTName)s '
0996                      'ADD CONSTRAINT %(colName)s_exists '
0997                      'FOREIGN KEY (%(colName)s) '
0998                      'REFERENCES %(tName)s (%(idName)s) '
0999                      '%(action)s' %
1000                      {'tName': tName,
1001                       'colName': self.dbName,
1002                       'idName': idName,
1003                       'action': action,
1004                       'sTName': sTName})
1005        return constraint
1006
1007    def mysqlCreateReferenceConstraint(self):
1008        sTName = self.soClass.sqlmeta.table
1009        sTLocalName = sTName.split('.')[-1]
1010        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
1011        tName = other.sqlmeta.table
1012        idName = self.refColumn or other.sqlmeta.idName
1013        if self.cascade is not None:
1014            if self.cascade == 'null':
1015                action = 'ON DELETE SET NULL'
1016            elif self.cascade:
1017                action = 'ON DELETE CASCADE'
1018            else:
1019                action = 'ON DELETE RESTRICT'
1020        else:
1021            action = ''
1022        constraint = ('ALTER TABLE %(sTName)s '
1023                      'ADD CONSTRAINT %(sTLocalName)s_%(colName)s_exists '
1024                      'FOREIGN KEY (%(colName)s) '
1025                      'REFERENCES %(tName)s (%(idName)s) '
1026                      '%(action)s' %
1027                      {'tName': tName,
1028                       'colName': self.dbName,
1029                       'idName': idName,
1030                       'action': action,
1031                       'sTName': sTName,
1032                       'sTLocalName': sTLocalName})
1033        return constraint
1034
1035    def mysqlCreateSQL(self, connection=None):
1036        return SOKeyCol.mysqlCreateSQL(self, connection)
1037
1038    def sybaseCreateSQL(self):
1039        sql = SOKeyCol.sybaseCreateSQL(self)
1040        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
1041        tName = other.sqlmeta.table
1042        idName = self.refColumn or other.sqlmeta.idName
1043        reference = ('REFERENCES %(tName)s(%(idName)s) ' %
1044                     {'tName': tName,
1045                      'idName': idName})
1046        sql = ' '.join([sql, reference])
1047        return sql
1048
1049    def sybaseCreateReferenceConstraint(self):
1050        # @@: Code from above should be moved here
1051        return None
1052
1053    def mssqlCreateSQL(self, connection=None):
1054        sql = SOKeyCol.mssqlCreateSQL(self, connection)
1055        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
1056        tName = other.sqlmeta.table
1057        idName = self.refColumn or other.sqlmeta.idName
1058        reference = ('REFERENCES %(tName)s(%(idName)s) ' %
1059                     {'tName': tName,
1060                      'idName': idName})
1061        sql = ' '.join([sql, reference])
1062        return sql
1063
1064    def mssqlCreateReferenceConstraint(self):
1065        # @@: Code from above should be moved here
1066        return None
1067
1068    def maxdbCreateSQL(self):
1069        other = findClass(self.foreignKey, self.soClass.sqlmeta.registry)
1070        fidName = self.dbName
1071        # I assume that foreign key name is identical
1072        # to the id of the reference table
1073        sql = ' '.join([fidName, self._maxdbType()])
1074        tName = other.sqlmeta.table
1075        idName = self.refColumn or other.sqlmeta.idName
1076        sql = sql + ',' + '\n'
1077        sql = sql + 'FOREIGN KEY (%s) REFERENCES %s(%s)' % (fidName, tName,
1078                                                            idName)
1079        return sql
1080
1081    def maxdbCreateReferenceConstraint(self):
1082        # @@: Code from above should be moved here
1083        return None
1084
1085
1086class ForeignKey(KeyCol):
1087
1088    baseClass = SOForeignKey
1089
1090    def __init__(self, foreignKey=None, **kw):
1091        super(ForeignKey, self).__init__(foreignKey=foreignKey, **kw)
1092
1093
1094class EnumValidator(SOValidator):
1095
1096    def to_python(self, value, state):
1097        if value in self.enumValues:
1098            # Only encode on python 2 - on python 3, the database driver
1099            # will handle this
1100            if isinstance(value, unicode_type) and PY2:
1101                dbEncoding = self.getDbEncoding(state)
1102                value = value.encode(dbEncoding)
1103            return value
1104        elif not self.notNone and value is None:
1105            return None
1106        raise validators.Invalid(
1107            "expected a member of %r in the EnumCol '%s', got %r instead" % (
1108                self.enumValues, self.name, value), value, state)
1109
1110    from_python = to_python
1111
1112
1113class SOEnumCol(SOCol):
1114
1115    def __init__(self, **kw):
1116        self.enumValues = kw.pop('enumValues', None)
1117        assert self.enumValues is not None,               'You must provide an enumValues keyword argument'
1119        super(SOEnumCol, self).__init__(**kw)
1120
1121    def autoConstraints(self):
1122        return [constrs.isString, constrs.InList(self.enumValues)]
1123
1124    def createValidators(self):
1125        return [EnumValidator(name=self.name, enumValues=self.enumValues,
1126                              notNone=self.notNone)] +               super(SOEnumCol, self).createValidators()
1128
1129    def _mysqlType(self):
1130        # We need to map None in the enum expression to an appropriate
1131        # condition on NULL
1132        if None in self.enumValues:
1133            return "ENUM(%s)" % ', '.join(
1134                [sqlbuilder.sqlrepr(v, 'mysql') for v in self.enumValues
1135                    if v is not None])
1136        else:
1137            return "ENUM(%s) NOT NULL" % ', '.join(
1138                [sqlbuilder.sqlrepr(v, 'mysql') for v in self.enumValues])
1139
1140    def _postgresType(self):
1141        length = max(map(self._getlength, self.enumValues))
1142        enumValues = ', '.join(
1143            [sqlbuilder.sqlrepr(v, 'postgres') for v in self.enumValues])
1144        checkConstraint = "CHECK (%s in (%s))" % (self.dbName, enumValues)
1145        return "VARCHAR(%i) %s" % (length, checkConstraint)
1146
1147    _sqliteType = _postgresType
1148
1149    def _sybaseType(self):
1150        return self._postgresType()
1151
1152    def _mssqlType(self):
1153        return self._postgresType()
1154
1155    def _firebirdType(self):
1156        length = max(map(self._getlength, self.enumValues))
1157        enumValues = ', '.join(
1158            [sqlbuilder.sqlrepr(v, 'firebird') for v in self.enumValues])
1159        checkConstraint = "CHECK (%s in (%s))" % (self.dbName, enumValues)
1160        # NB. Return a tuple, not a string here
1161        return "VARCHAR(%i)" % (length), checkConstraint
1162
1163    def _maxdbType(self):
1164        raise TypeError("Enum type is not supported on MAX DB")
1165
1166    def _getlength(self, obj):
1167        """
1168        None counts as 0; everything else uses len()
1169        """
1170        if obj is None:
1171            return 0
1172        else:
1173            return len(obj)
1174
1175
1176class EnumCol(Col):
1177    baseClass = SOEnumCol
1178
1179
1180class SetValidator(SOValidator):
1181    """
1182    Translates Python tuples into SQL comma-delimited SET strings.
1183    """
1184
1185    def to_python(self, value, state):
1186        if isinstance(value, str):
1187            return tuple(value.split(","))
1188        raise validators.Invalid(
1189            "expected a string in the SetCol '%s', got %s %r instead" % (
1190                self.name, type(value), value), value, state)
1191
1192    def from_python(self, value, state):
1193        if isinstance(value, string_type):
1194            value = (value,)
1195        try:
1196            return ",".join(value)
1197        except:
1198            raise validators.Invalid(
1199                "expected a string or a sequence of strings "
1200                "in the SetCol '%s', got %s %r instead" % (
1201                    self.name, type(value), value), value, state)
1202
1203
1204class SOSetCol(SOCol):
1205    def __init__(self, **kw):
1206        self.setValues = kw.pop('setValues', None)
1207        assert self.setValues is not None,               'You must provide a setValues keyword argument'
1209        super(SOSetCol, self).__init__(**kw)
1210
1211    def autoConstraints(self):
1212        return [constrs.isString, constrs.InList(self.setValues)]
1213
1214    def createValidators(self):
1215        return [SetValidator(name=self.name, setValues=self.setValues)] +               super(SOSetCol, self).createValidators()
1217
1218    def _mysqlType(self):
1219        return "SET(%s)" % ', '.join(
1220            [sqlbuilder.sqlrepr(v, 'mysql') for v in self.setValues])
1221
1222
1223class SetCol(Col):
1224    baseClass = SOSetCol
1225
1226
1227class DateTimeValidator(validators.DateValidator):
1228    def to_python(self, value, state):
1229        if value is None:
1230            return None
1231        if isinstance(value,
1232                      (datetime.datetime, datetime.date,
1233                       datetime.time, sqlbuilder.SQLExpression)):
1234            return value
1235        if mxdatetime_available:
1236            if isinstance(value, DateTimeType):
1237                # convert mxDateTime instance to datetime
1238                if (self.format.find("%H") >= 0) or                      (self.format.find("%T")) >= 0:
1240                    return datetime.datetime(value.year, value.month,
1241                                             value.day,
1242                                             value.hour, value.minute,
1243                                             int(value.second))
1244                else:
1245                    return datetime.date(value.year, value.month, value.day)
1246            elif isinstance(value, TimeType):
1247                # convert mxTime instance to time
1248                if self.format.find("%d") >= 0:
1249                    return datetime.timedelta(seconds=value.seconds)
1250                else:
1251                    return datetime.time(value.hour, value.minute,
1252                                         int(value.second))
1253        try:
1254            if self.format.find(".%f") >= 0:
1255                if '.' in value:
1256                    _value = value.split('.')
1257                    microseconds = _value[-1]
1258                    _l = len(microseconds)
1259                    if _l < 6:
1260                        _value[-1] = microseconds + '0' * (6 - _l)
1261                    elif _l > 6:
1262                        _value[-1] = microseconds[:6]
1263                    if _l != 6:
1264                        value = '.'.join(_value)
1265                else:
1266                    value += '.0'
1267            return datetime.datetime.strptime(value, self.format)
1268        except:
1269            raise validators.Invalid(
1270                "expected a date/time string of the '%s' format "
1271                "in the DateTimeCol '%s', got %s %r instead" % (
1272                    self.format, self.name, type(value), value), value, state)
1273
1274    def from_python(self, value, state):
1275        if value is None:
1276            return None
1277        if isinstance(value,
1278                      (datetime.datetime, datetime.date,
1279                       datetime.time, sqlbuilder.SQLExpression)):
1280            return value
1281        if hasattr(value, "strftime"):
1282            return value.strftime(self.format)
1283        raise validators.Invalid(
1284            "expected a datetime in the DateTimeCol '%s', "
1285            "got %s %r instead" % (
1286                self.name, type(value), value), value, state)
1287
1288if mxdatetime_available:
1289    class MXDateTimeValidator(validators.DateValidator):
1290        def to_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 isinstance(value, datetime.datetime):
1297                return DateTime.DateTime(value.year, value.month, value.day,
1298                                         value.hour, value.minute,
1299                                         value.second)
1300            elif isinstance(value, datetime.date):
1301                return DateTime.Date(value.year, value.month, value.day)
1302            elif isinstance(value, datetime.time):
1303                return DateTime.Time(value.hour, value.minute, value.second)
1304            try:
1305                if self.format.find(".%f") >= 0:
1306                    if '.' in value:
1307                        _value = value.split('.')
1308                        microseconds = _value[-1]
1309                        _l = len(microseconds)
1310                        if _l < 6:
1311                            _value[-1] = microseconds + '0' * (6 - _l)
1312                        elif _l > 6:
1313                            _value[-1] = microseconds[:6]
1314                        if _l != 6:
1315                            value = '.'.join(_value)
1316                    else:
1317                        value += '.0'
1318                value = datetime.datetime.strptime(value, self.format)
1319                return DateTime.DateTime(value.year, value.month, value.day,
1320                                         value.hour, value.minute,
1321                                         value.second)
1322            except:
1323                raise validators.Invalid(
1324                    "expected a date/time string of the '%s' format "
1325                    "in the DateTimeCol '%s', got %s %r instead" % (
1326                        self.format, self.name, type(value), value),
1327                    value, state)
1328
1329        def from_python(self, value, state):
1330            if value is None:
1331                return None
1332            if isinstance(value,
1333                          (DateTimeType, TimeType, sqlbuilder.SQLExpression)):
1334                return value
1335            if hasattr(value, "strftime"):
1336                return value.strftime(self.format)
1337            raise validators.Invalid(
1338                "expected a mxDateTime in the DateTimeCol '%s', "
1339                "got %s %r instead" % (
1340                    self.name, type(value), value), value, state)
1341
1342
1343class SODateTimeCol(SOCol):
1344    datetimeFormat = '%Y-%m-%d %H:%M:%S.%f'
1345
1346    def __init__(self, **kw):
1347        datetimeFormat = kw.pop('datetimeFormat', None)
1348        if datetimeFormat:
1349            self.datetimeFormat = datetimeFormat
1350        super(SODateTimeCol, self).__init__(**kw)
1351
1352    def createValidators(self):
1353        _validators = super(SODateTimeCol, self).createValidators()
1354        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1355            validatorClass = DateTimeValidator
1356        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1357            validatorClass = MXDateTimeValidator
1358        if default_datetime_implementation:
1359            _validators.insert(0, validatorClass(name=self.name,
1360                                                 format=self.datetimeFormat))
1361        return _validators
1362
1363    def _mysqlType(self):
1364        if self.connection and self.connection.can_use_microseconds():
1365            return 'DATETIME(6)'
1366        else:
1367            return 'DATETIME'
1368
1369    def _postgresType(self):
1370        return 'TIMESTAMP'
1371
1372    def _sybaseType(self):
1373        return 'DATETIME'
1374
1375    def _mssqlType(self):
1376        if self.connection and self.connection.can_use_microseconds():
1377            return 'DATETIME2(6)'
1378        else:
1379            return 'DATETIME'
1380
1381    def _sqliteType(self):
1382        return 'TIMESTAMP'
1383
1384    def _firebirdType(self):
1385        return 'TIMESTAMP'
1386
1387    def _maxdbType(self):
1388        return 'TIMESTAMP'
1389
1390
1391class DateTimeCol(Col):
1392    baseClass = SODateTimeCol
1393
1394    @staticmethod
1395    def now():
1396        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1397            return datetime.datetime.now()
1398        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1399            return DateTime.now()
1400        else:
1401            assert 0, ("No datetime implementation available "
1402                       "(DATETIME_IMPLEMENTATION=%r)"
1403                       % DATETIME_IMPLEMENTATION)
1404
1405
1406class DateValidator(DateTimeValidator):
1407    def to_python(self, value, state):
1408        if isinstance(value, datetime.datetime):
1409            value = value.date()
1410        if isinstance(value, (datetime.date, sqlbuilder.SQLExpression)):
1411            return value
1412        value = super(DateValidator, self).to_python(value, state)
1413        if isinstance(value, datetime.datetime):
1414            value = value.date()
1415        return value
1416
1417    from_python = to_python
1418
1419
1420class SODateCol(SOCol):
1421    dateFormat = '%Y-%m-%d'
1422
1423    def __init__(self, **kw):
1424        dateFormat = kw.pop('dateFormat', None)
1425        if dateFormat:
1426            self.dateFormat = dateFormat
1427        super(SODateCol, self).__init__(**kw)
1428
1429    def createValidators(self):
1430        """Create a validator for the column.
1431
1432        Can be overriden in descendants.
1433
1434        """
1435        _validators = super(SODateCol, self).createValidators()
1436        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1437            validatorClass = DateValidator
1438        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1439            validatorClass = MXDateTimeValidator
1440        if default_datetime_implementation:
1441            _validators.insert(0, validatorClass(name=self.name,
1442                                                 format=self.dateFormat))
1443        return _validators
1444
1445    def _mysqlType(self):
1446        return 'DATE'
1447
1448    def _postgresType(self):
1449        return 'DATE'
1450
1451    def _sybaseType(self):
1452        return self._postgresType()
1453
1454    def _mssqlType(self):
1455        """
1456        SQL Server doesn't have  a DATE data type, to emulate we use a vc(10)
1457        """
1458        return 'VARCHAR(10)'
1459
1460    def _firebirdType(self):
1461        return 'DATE'
1462
1463    def _maxdbType(self):
1464        return 'DATE'
1465
1466    def _sqliteType(self):
1467        return 'DATE'
1468
1469
1470class DateCol(Col):
1471    baseClass = SODateCol
1472
1473
1474class TimeValidator(DateTimeValidator):
1475    def to_python(self, value, state):
1476        if isinstance(value, (datetime.time, sqlbuilder.SQLExpression)):
1477            return value
1478        if isinstance(value, datetime.timedelta):
1479            if value.days:
1480                raise validators.Invalid(
1481                    "the value for the TimeCol '%s' must has days=0, "
1482                    "it has days=%d" % (self.name, value.days), value, state)
1483            return datetime.time(*time.gmtime(value.seconds)[3:6])
1484        value = super(TimeValidator, self).to_python(value, state)
1485        if isinstance(value, datetime.datetime):
1486            value = value.time()
1487        return value
1488
1489    from_python = to_python
1490
1491
1492class SOTimeCol(SOCol):
1493    timeFormat = '%H:%M:%S.%f'
1494
1495    def __init__(self, **kw):
1496        timeFormat = kw.pop('timeFormat', None)
1497        if timeFormat:
1498            self.timeFormat = timeFormat
1499        super(SOTimeCol, self).__init__(**kw)
1500
1501    def createValidators(self):
1502        _validators = super(SOTimeCol, self).createValidators()
1503        if default_datetime_implementation == DATETIME_IMPLEMENTATION:
1504            validatorClass = TimeValidator
1505        elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION:
1506            validatorClass = MXDateTimeValidator
1507        if default_datetime_implementation:
1508            _validators.insert(0, validatorClass(name=self.name,
1509                                                 format=self.timeFormat))
1510        return _validators
1511
1512    def _mysqlType(self):
1513        if self.connection and self.connection.can_use_microseconds():
1514            return 'TIME(6)'
1515        else:
1516            return 'TIME'
1517
1518    def _postgresType(self):
1519        return 'TIME'
1520
1521    def _sybaseType(self):
1522        return 'TIME'
1523
1524    def _mssqlType(self):
1525        if self.connection and self.connection.can_use_microseconds():
1526            return 'TIME(6)'
1527        else:
1528            return 'TIME'
1529
1530    def _sqliteType(self):
1531        return 'TIME'
1532
1533    def _firebirdType(self):
1534        return 'TIME'
1535
1536    def _maxdbType(self):
1537        return 'TIME'
1538
1539
1540class TimeCol(Col):
1541    baseClass = SOTimeCol
1542
1543
1544class SOTimestampCol(SODateTimeCol):
1545    """
1546    Necessary to support MySQL's use of TIMESTAMP versus DATETIME types
1547    """
1548
1549    def __init__(self, **kw):
1550        if 'default' not in kw:
1551            kw['default'] = None
1552        SOCol.__init__(self, **kw)
1553
1554    def _mysqlType(self):
1555        if self.connection and self.connection.can_use_microseconds():
1556            return 'TIMESTAMP(6)'
1557        else:
1558            return 'TIMESTAMP'
1559
1560
1561class TimestampCol(Col):
1562    baseClass = SOTimestampCol
1563
1564
1565class TimedeltaValidator(SOValidator):
1566    def to_python(self, value, state):
1567        return value
1568
1569    from_python = to_python
1570
1571
1572class SOTimedeltaCol(SOCol):
1573    def _postgresType(self):
1574        return 'INTERVAL'
1575
1576    def createValidators(self):
1577        return [TimedeltaValidator(name=self.name)] +               super(SOTimedeltaCol, self).createValidators()
1579
1580
1581class TimedeltaCol(Col):
1582    baseClass = SOTimedeltaCol
1583
1584
1585class DecimalValidator(SOValidator):
1586    def to_python(self, value, state):
1587        if value is None:
1588            return None
1589        if isinstance(value, (int, long, Decimal, sqlbuilder.SQLExpression)):
1590            return value
1591        if isinstance(value, float):
1592            value = str(value)
1593        try:
1594            connection = state.connection or state.soObject._connection
1595        except AttributeError:
1596            pass
1597        else:
1598            if hasattr(connection, "decimalSeparator"):
1599                value = value.replace(connection.decimalSeparator, ".")
1600        try:
1601            return Decimal(value)
1602        except:
1603            raise validators.Invalid(
1604                "expected a Decimal in the DecimalCol '%s', "
1605                "got %s %r instead" % (
1606                    self.name, type(value), value), value, state)
1607
1608    def from_python(self, value, state):
1609        if value is None:
1610            return None
1611        if isinstance(value, float):
1612            value = str(value)
1613        if isinstance(value, string_type):
1614            try:
1615                connection = state.connection or state.soObject._connection
1616            except AttributeError:
1617                pass
1618            else:
1619                if hasattr(connection, "decimalSeparator"):
1620                    value = value.replace(connection.decimalSeparator, ".")
1621            try:
1622                return Decimal(value)
1623            except:
1624                raise validators.Invalid(
1625                    "can not parse Decimal value '%s' "
1626                    "in the DecimalCol from '%s'" % (
1627                        value, getattr(state, 'soObject', '(unknown)')),
1628                    value, state)
1629        if isinstance(value, (int, long, Decimal, sqlbuilder.SQLExpression)):
1630            return value
1631        raise validators.Invalid(
1632            "expected a Decimal in the DecimalCol '%s', got %s %r instead" % (
1633                self.name, type(value), value), value, state)
1634
1635
1636class SODecimalCol(SOCol):
1637
1638    def __init__(self, **kw):
1639        self.size = kw.pop('size', NoDefault)
1640        assert self.size is not NoDefault,               "You must give a size argument"
1642        self.precision = kw.pop('precision', NoDefault)
1643        assert self.precision is not NoDefault,               "You must give a precision argument"
1645        super(SODecimalCol, self).__init__(**kw)
1646
1647    def _sqlType(self):
1648        return 'DECIMAL(%i, %i)' % (self.size, self.precision)
1649
1650    def createValidators(self):
1651        return [DecimalValidator(name=self.name)] +               super(SODecimalCol, self).createValidators()
1653
1654
1655class DecimalCol(Col):
1656    baseClass = SODecimalCol
1657
1658
1659class SOCurrencyCol(SODecimalCol):
1660
1661    def __init__(self, **kw):
1662        pushKey(kw, 'size', 10)
1663        pushKey(kw, 'precision', 2)
1664        super(SOCurrencyCol, self).__init__(**kw)
1665
1666
1667class CurrencyCol(DecimalCol):
1668    baseClass = SOCurrencyCol
1669
1670
1671class DecimalStringValidator(DecimalValidator):
1672    def to_python(self, value, state):
1673        value = super(DecimalStringValidator, self).to_python(value, state)
1674        if self.precision and isinstance(value, Decimal):
1675            assert value < self.max,                   "Value must be less than %s" % int(self.max)
1677            value = value.quantize(self.precision)
1678        return value
1679
1680    def from_python(self, value, state):
1681        value = super(DecimalStringValidator, self).from_python(value, state)
1682        if isinstance(value, Decimal):
1683            if self.precision:
1684                assert value < self.max,                       "Value must be less than %s" % int(self.max)
1686                value = value.quantize(self.precision)
1687            value = value.to_eng_string()
1688        elif isinstance(value, (int, long)):
1689            value = str(value)
1690        return value
1691
1692
1693class SODecimalStringCol(SOStringCol):
1694    def __init__(self, **kw):
1695        self.size = kw.pop('size', NoDefault)
1696        assert (self.size is not NoDefault) and (self.size >= 0),               "You must give a size argument as a positive integer"
1698        self.precision = kw.pop('precision', NoDefault)
1699        assert (self.precision is not NoDefault) and (self.precision >= 0),               "You must give a precision argument as a positive integer"
1701        kw['length'] = int(self.size) + int(self.precision)
1702        self.quantize = kw.pop('quantize', False)
1703        assert isinstance(self.quantize, bool),               "quantize argument must be Boolean True/False"
1705        super(SODecimalStringCol, self).__init__(**kw)
1706
1707    def createValidators(self):
1708        if self.quantize:
1709            v = DecimalStringValidator(
1710                name=self.name,
1711                precision=Decimal(10) ** (-1 * int(self.precision)),
1712                max=Decimal(10) ** (int(self.size) - int(self.precision)))
1713        else:
1714            v = DecimalStringValidator(name=self.name, precision=0)
1715        return [v] +               super(SODecimalStringCol, self).createValidators(dataType=Decimal)
1717
1718
1719class DecimalStringCol(StringCol):
1720    baseClass = SODecimalStringCol
1721
1722
1723class BinaryValidator(SOValidator):
1724    """
1725    Validator for binary types.
1726
1727    We're assuming that the per-database modules provide some form
1728    of wrapper type for binary conversion.
1729    """
1730
1731    _cachedValue = None
1732
1733    def to_python(self, value, state):
1734        if value is None:
1735            return None
1736        try:
1737            connection = state.connection or state.soObject._connection
1738        except AttributeError:
1739            dbName = None
1740            binaryType = type(None)  # Just a simple workaround
1741        else:
1742            dbName = connection.dbName
1743            binaryType = connection._binaryType
1744        if isinstance(value, str):
1745            if dbName == "sqlite":
1746                if not PY2:
1747                    value = bytes(value, 'ascii')
1748                value = connection.module.decode(value)
1749            if dbName == "mysql" and not PY2:
1750                value = value.encode('ascii', errors='surrogateescape')
1751            return value
1752        if isinstance(value, (buffer_type, binaryType)):
1753            cachedValue = self._cachedValue
1754            if cachedValue and cachedValue[1] == value:
1755                return cachedValue[0]
1756            if isinstance(value, array):  # MySQL
1757                return value.tostring()
1758            if not PY2 and isinstance(value, memoryview):
1759                return value.tobytes()
1760            return str(value)  # buffer => string
1761        raise validators.Invalid(
1762            "expected a string in the BLOBCol '%s', got %s %r instead" % (
1763                self.name, type(value), value), value, state)
1764
1765    def from_python(self, value, state):
1766        if value is None:
1767            return None
1768        connection = state.connection or state.soObject._connection
1769        binary = connection.createBinary(value)
1770        if not PY2 and isinstance(binary, memoryview):
1771            binary = str(binary.tobytes(), 'ascii')
1772        self._cachedValue = (value, binary)
1773        return binary
1774
1775
1776class SOBLOBCol(SOStringCol):
1777    def __init__(self, **kw):
1778        # Change the default from 'auto' to False -
1779        # this is a (mostly) binary column
1780        if 'varchar' not in kw:
1781            kw['varchar'] = False
1782        super(SOBLOBCol, self).__init__(**kw)
1783
1784    def createValidators(self):
1785        return [BinaryValidator(name=self.name)] +               super(SOBLOBCol, self).createValidators()
1787
1788    def _mysqlType(self):
1789        length = self.length
1790        varchar = self.varchar
1791        if length:
1792            if length >= 2 ** 24:
1793                return varchar and "LONGTEXT" or "LONGBLOB"
1794            if length >= 2 ** 16:
1795                return varchar and "MEDIUMTEXT" or "MEDIUMBLOB"
1796            if length >= 2 ** 8:
1797                return varchar and "TEXT" or "BLOB"
1798        return varchar and "TINYTEXT" or "TINYBLOB"
1799
1800    def _postgresType(self):
1801        return 'BYTEA'
1802
1803    def _mssqlType(self):
1804        if self.connection and self.connection.can_use_max_types():
1805            return 'VARBINARY(MAX)'
1806        else:
1807            return "IMAGE"
1808
1809
1810class BLOBCol(StringCol):
1811    baseClass = SOBLOBCol
1812
1813
1814class PickleValidator(BinaryValidator):
1815    """
1816    Validator for pickle types.  A pickle type is simply a binary type
1817    with hidden pickling, so that we can simply store any kind of
1818    stuff in a particular column.
1819
1820    The support for this relies directly on the support for binary for
1821    your database.
1822    """
1823
1824    def to_python(self, value, state):
1825        if value is None:
1826            return None
1827        if isinstance(value, unicode_type):
1828            dbEncoding = self.getDbEncoding(state, default='ascii')
1829            value = value.encode(dbEncoding)
1830        if isinstance(value, bytes):
1831            return pickle.loads(value)
1832        raise validators.Invalid(
1833            "expected a pickle string in the PickleCol '%s', "
1834            "got %s %r instead" % (
1835                self.name, type(value), value), value, state)
1836
1837    def from_python(self, value, state):
1838        if value is None:
1839            return None
1840        return pickle.dumps(value, self.pickleProtocol)
1841
1842
1843class SOPickleCol(SOBLOBCol):
1844
1845    def __init__(self, **kw):
1846        self.pickleProtocol = kw.pop('pickleProtocol', pickle.HIGHEST_PROTOCOL)
1847        super(SOPickleCol, self).__init__(**kw)
1848
1849    def createValidators(self):
1850        return [PickleValidator(name=self.name,
1851                pickleProtocol=self.pickleProtocol)] +               super(SOPickleCol, self).createValidators()
1853
1854    def _mysqlType(self):
1855        length = self.length
1856        if length:
1857            if length >= 2 ** 24:
1858                return "LONGBLOB"
1859            if length >= 2 ** 16:
1860                return "MEDIUMBLOB"
1861        return "BLOB"
1862
1863
1864class PickleCol(BLOBCol):
1865    baseClass = SOPickleCol
1866
1867
1868def pushKey(kw, name, value):
1869    if name not in kw:
1870        kw[name] = value
1871
1872all = []
1873# Use copy() to avoid 'dictionary changed' issues on python 3
1874for key, value in globals().copy().items():
1875    if isinstance(value, type) and (issubclass(value, (Col, SOCol))):
1876        all.append(key)
1877__all__.extend(all)
1878del all