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