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