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