Thread: [SQL-CVS] r134 - trunk/SQLObject
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: <sub...@co...> - 2004-06-14 12:01:20
|
Author: ahmedmo Date: 2004-06-14 03:58:45 -0400 (Mon, 14 Jun 2004) New Revision: 134 Added: trunk/SQLObject/col.py Log: Added: trunk/SQLObject/col.py =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D --- trunk/SQLObject/col.py 2004-06-14 07:52:42 UTC (rev 133) +++ trunk/SQLObject/col.py 2004-06-14 07:58:45 UTC (rev 134) @@ -0,0 +1,612 @@ +""" +Col +""" + +import sqlbuilder +import re +# Sadly the name "constraints" conflicts with many of the function +# arguments in this module, so we rename it: +import constraints as consts +from include import validators + +NoDefault =3D sqlbuilder.NoDefault +True, False =3D 1=3D=3D1, 0=3D=3D1 + =20 + +######################################## +## Columns +######################################## + +# Col is essentially a column definition, it doesn't have +# much logic to it. +class SOCol(object): + + def __init__(self, + name, + soClass, + dbName=3DNone, + default=3DNoDefault, + foreignKey=3DNone, + alternateID=3DFalse, + alternateMethodName=3DNone, + constraints=3DNone, + notNull=3DNoDefault, + notNone=3DNoDefault, + unique=3DNoDefault, + sqlType=3DNone, + columnDef=3DNone, + validator=3DNone, + immutable=3DFalse, + cascade=3DNone, + lazy=3DFalse, + noCache=3DFalse): + + # This isn't strictly true, since we *could* use backquotes or + # " or something (database-specific) around column names, but + # why would anyone *want* to use a name like that? + # @@: I suppose we could actually add backquotes to the + # dbName if we needed to... + assert sqlbuilder.sqlIdentifier(name), 'Name must be SQL-safe (l= etters, numbers, underscores): %s' \ + % repr(name) + assert name !=3D 'id', 'The column name "id" is reserved for SQL= Object use (and is implicitly created).' + assert name, "You must provide a name for all columns" + + self.columnDef =3D columnDef + + self.immutable =3D immutable + + # cascade can be one of: + # None: no constraint is generated + # True: a CASCADE constraint is generated + # False: a RESTRICT constraint is generated + self.cascade =3D cascade + + if type(constraints) not in (type([]), type(())): + constraints =3D [constraints] + self.constraints =3D self.autoConstraints() + constraints + + self.notNone =3D False + if notNull is not NoDefault: + self.notNone =3D notNull + assert notNone is NoDefault or \ + (not notNone) =3D=3D (not notNull), \ + "The notNull and notNone arguments are aliases, and m= ust not conflict. You gave notNull=3D%r, notNone=3D%r" % (notNull, notNo= ne) + elif notNone is not NoDefault: + self.notNone =3D notNone + if self.notNone: + self.constraints =3D [consts.notNull] + self.constraints + + self.name =3D name + self.soClass =3D None + self._default =3D default + self.customSQLType =3D sqlType + + self.foreignKey =3D foreignKey + if self.foreignKey: + #assert self.name.upper().endswith('ID'), "All foreign key c= olumns must end with 'ID' (%s)" % repr(self.name) + if not self.name.upper().endswith('ID'): + self.foreignName =3D self.name + self.name =3D self.name + "ID" + else: + self.foreignName =3D self.name[:-2] + else: + self.foreignName =3D None + + # if they don't give us a specific database name for + # the column, we separate the mixedCase into mixed_case + # and assume that. + if dbName is None: + self.dbName =3D soClass._style.pythonAttrToDBColumn(self.nam= e) + else: + self.dbName =3D dbName + + # alternateID means that this is a unique column that + # can be used to identify rows + self.alternateID =3D alternateID + if self.alternateID and alternateMethodName is None: + self.alternateMethodName =3D 'by' + self.name[0].capitalize(= ) + self.name[1:] + else: + self.alternateMethodName =3D alternateMethodName + + if unique is NoDefault: + self.unique =3D alternateID + else: + self.unique =3D unique + + self.validator =3D validator + self.noCache =3D noCache + self.lazy =3D lazy + + def _set_validator(self, value): + self._validator =3D value + if self._validator: + self.toPython =3D self._validator.toPython + self.fromPython =3D self._validator.fromPython + else: + self.toPython =3D None + self.fromPython =3D None + + def _get_validator(self): + return self._validator + + validator =3D property(_get_validator, _set_validator) + + def autoConstraints(self): + return [] + + def _get_default(self): + # A default can be a callback or a plain value, + # here we resolve the callback + if self._default is NoDefault: + return NoDefault + elif hasattr(self._default, '__sqlrepr__'): + return self._default + elif callable(self._default): + return self._default() + else: + return self._default + default =3D property(_get_default, None, None) + + def _get_joinName(self): + assert self.name[-2:] =3D=3D 'ID' + return self.name[:-2] + joinName =3D property(_get_joinName, None, None) + + def __repr__(self): + r =3D '<%s %s' % (self.__class__.__name__, self.name) + if self.default is not NoDefault: + r +=3D ' default=3D%s' % repr(self.default) + if self.foreignKey: + r +=3D ' connected to %s' % self.foreignKey + if self.alternateID: + r +=3D ' alternate ID' + if self.notNone: + r +=3D ' not null' + return r + '>' + + def createSQL(self): + return ' '.join([self._sqlType() + self._extraSQL()]) + + def _extraSQL(self): + result =3D [] + if self.notNone or self.alternateID: + result.append('NOT NULL') + if self.unique or self.alternateID: + result.append('UNIQUE') + return result + + def _sqlType(self): + if self.customSQLType is None: + raise ValueError, ("Col %s (%s) cannot be used for automatic= " + "schema creation (too abstract)" % + (self.name, self.__class__)) + else: + return self.customSQLType + + def _mysqlType(self): + return self._sqlType() + + def _postgresType(self): + return self._sqlType() + + def _sqliteType(self): + # SQLite is naturally typeless, so as a fallback it uses + # no type. + try: + return self._sqlType() + except ValueError: + return '' + + def _sybaseType(self): + return self._sqlType() + + def _firebirdType(self): + return self._sqlType() + =20 + def _maxdbType(self): + return self._sqlType() =20 + =20 + def mysqlCreateSQL(self): + return ' '.join([self.dbName, self._mysqlType()] + self._extraSQ= L()) + + def postgresCreateSQL(self): + return ' '.join([self.dbName, self._postgresType()] + self._extr= aSQL()) + + def sqliteCreateSQL(self): + return ' '.join([self.dbName, self._sqliteType()] + self._extraS= QL()) + + def sybaseCreateSQL(self): + return ' '.join([self.dbName, self._sybaseType()] + self._extraS= QL()) + + def firebirdCreateSQL(self): + # Ian Sparks pointed out that fb is picky about the order + # of the NOT NULL clause in a create statement. So, we handle + # them differently for Enum columns. + if not isinstance(self, SOEnumCol): + return ' '.join([self.dbName, self._firebirdType()] + self._= extraSQL()) + else: + return ' '.join([self.dbName] + [self._firebirdType()[0]] + = self._extraSQL() + [self._firebirdType()[1]]) + =20 + def maxdbCreateSQL(self): + return ' '.join([self.dbName, self._maxdbType()] + self._extraSQ= L()) + + def __get__(self, obj, type=3DNone): + if obj is None: + # class attribute, return the descriptor itself + return self + if obj.sqlmeta.obsolete: + raise '@@: figure out the exception for a delete' + if obj.sqlmeta.cacheColumns: + columns =3D obj.sqlmeta._columnCache + if columns is None: + obj.sqlmeta.loadValues() + try: + return columns[name] + except KeyError: + return obj.sqlmeta.loadColumn(self) + else: + return obj.sqlmeta.loadColumn(self) + + def __set__(self, obj, value): + if self.immutable: + raise AttributeError("The column %s.%s is immutable" % + (obj.__class__.__name__, + self.name)) + obj.sqlmeta.setColumn(self, value) + + def __delete__(self, obj): + raise AttributeError("I can't be deleted from %r" % obj) + + +class Col(object): + + baseClass =3D SOCol + + def __init__(self, name=3DNone, **kw): + kw['name'] =3D name + kw['columnDef'] =3D self + self.kw =3D kw + + def setName(self, value): + assert self.kw['name'] is None, "You cannot change a name after = it has already been set (from %s to %s)" % (self.kw['name'], value) + self.kw['name'] =3D value + + def withClass(self, soClass): + return self.baseClass(soClass=3DsoClass, **self.kw) + +class SOStringCol(SOCol): + + # 3-03 @@: What about BLOB? + + def __init__(self, **kw): + self.length =3D popKey(kw, 'length') + self.varchar =3D popKey(kw, 'varchar', 'auto') + if not self.length: + assert self.varchar =3D=3D 'auto' or not self.varchar, \ + "Without a length strings are treated as TEXT, not va= rchar" + self.varchar =3D False + elif self.varchar =3D=3D 'auto': + self.varchar =3D True + + SOCol.__init__(self, **kw) + + def autoConstraints(self): + constraints =3D [consts.isString] + if self.length is not None: + constraints +=3D [consts.MaxLength(self.length)] + return constraints + + def _sqlType(self): + if not self.length: + return 'TEXT' + elif self.varchar: + return 'VARCHAR(%i)' % self.length + else: + return 'CHAR(%i)' % self.length + + def _firebirdType(self): + if not self.length: + return 'BLOB SUB_TYPE TEXT' + else: + return self._sqlType() + =20 + def _maxdbType(self): + if not self.length: + return 'LONG ASCII' + else: + return self._sqlType() + +class StringCol(Col): + baseClass =3D SOStringCol + +class SOIntCol(SOCol): + + # 3-03 @@: support precision, maybe max and min directly + + def autoConstraints(self): + return [consts.isInt] + + def _sqlType(self): + return 'INT' + =20 + def _maxdbType(self): + return 'INT' + +class IntCol(Col): + baseClass =3D SOIntCol + +class BoolValidator(validators.Validator): + + def fromPython(self, value, state): + if value: + return sqlbuilder.TRUE + else: + return sqlbuilder.FALSE + + def toPython(self, value, state): + if not value or not int(value): + return sqlbuilder.FALSE + else: + return sqlbuilder.TRUE + +class SOBoolCol(SOCol): + + def __init__(self, **kw): + SOCol.__init__(self, **kw) + self.validator =3D validators.All.join(BoolValidator(), self.val= idator) + + def autoConstraints(self): + return [consts.isBool] + + def _postgresType(self): + return 'BOOL' + + def _mysqlType(self): + return "TINYINT" + + def _sybaseType(self): + return "BIT" + + def _firebirdType(self): + return 'INT' + =20 + def _maxdbType(self): + return "BOOLEAN" + +class BoolCol(Col): + baseClass =3D SOBoolCol + +class SOFloatCol(SOCol): + + # 3-03 @@: support precision (e.g., DECIMAL) + + def autoConstraints(self): + return [consts.isFloat] + + def _sqlType(self): + return 'FLOAT' + +class FloatCol(Col): + baseClass =3D SOFloatCol + +class SOKeyCol(SOCol): + + # 3-03 @@: this should have a simplified constructor + # Should provide foreign key information for other DBs. + + def _mysqlType(self): + return 'INT' + + def _sqliteType(self): + return 'INT' + + def _postgresType(self): + return 'INT' + + def _sybaseType(self): + return 'INT' + + def _firebirdType(self): + return 'INT' + =20 + def _maxdbType(self): + return 'INT' + +class KeyCol(Col): + + baseClass =3D SOKeyCol + +class SOForeignKey(SOKeyCol): + + def __init__(self, **kw): + foreignKey =3D kw['foreignKey'] + style =3D kw['soClass']._style + if not kw.get('name'): + kw['name'] =3D style.instanceAttrToIDAttr(style.pythonClassT= oAttr(foreignKey)) + else: + if not kw['name'].upper().endswith('ID'): + kw['name'] =3D style.instanceAttrToIDAttr(kw['name']) + SOKeyCol.__init__(self, **kw) + + def postgresCreateSQL(self): + from main import findClass + sql =3D SOKeyCol.postgresCreateSQL(self) + if self.cascade is not None: + other =3D findClass(self.foreignKey) + tName =3D other._table + idName =3D other._idName + action =3D self.cascade and 'CASCADE' or 'RESTRICT' + constraint =3D ('CONSTRAINT %(tName)s_exists ' + 'FOREIGN KEY(%(colName)s) ' + 'REFERENCES %(tName)s(%(idName)s) ' + 'ON DELETE %(action)s' % + {'tName':tName, + 'colName':self.dbName, + 'idName':idName, + 'action':action}) + sql =3D ', '.join([sql, constraint]) + return sql + + def sybaseCreateSQL(self): + from SQLObject import findClass + sql =3D SOKeyCol.sybaseCreateSQL(self) + other =3D findClass(self.foreignKey) + tName =3D other._table + idName =3D other._idName + reference =3D ('REFERENCES %(tName)s(%(idName)s) ' % + {'tName':tName, + 'idName':idName}) + sql =3D ' '.join([sql, reference]) + return sql + =20 + def maxdbCreateSQL(self): + from main import findClass + other =3D findClass(self.foreignKey) + fidName =3D self.dbName + #I assume that foreign key name is identical to the id of the referenc= e table =09 + sql =3D ' '.join([fidName, self._maxdbType()]) + tName =3D other._table + idName =3D other._idName + sql=3Dsql + ',' + '\n'=20 + sql=3Dsql + 'FOREIGN KEY (%s) REFERENCES %s(%s)'%(fidName,tName,idName= ) + return sql + +class ForeignKey(KeyCol): + + baseClass =3D SOForeignKey + + def __init__(self, foreignKey=3DNone, **kw): + KeyCol.__init__(self, foreignKey=3DforeignKey, **kw) + +class SOEnumCol(SOCol): + + def __init__(self, **kw): + self.enumValues =3D popKey(kw, 'enumValues', None) + assert self.enumValues is not None, \ + 'You must provide an enumValues keyword argument' + SOCol.__init__(self, **kw) + + def autoConstraints(self): + return [consts.isString, consts.InList(self.enumValues)] + + def _mysqlType(self): + return "ENUM(%s)" % ', '.join([sqlbuilder.sqlrepr(v, 'mysql') fo= r v in self.enumValues]) + + def _postgresType(self): + length =3D max(map(len, self.enumValues)) + enumValues =3D ', '.join([sqlbuilder.sqlrepr(v, 'postgres') for = v in self.enumValues]) + checkConstraint =3D "CHECK (%s in (%s))" % (self.dbName, enumVal= ues) + return "VARCHAR(%i) %s" % (length, checkConstraint) + + def _sqliteType(self): + return self._postgresType() + + def _sybaseType(self): + return self._postgresType() + + def _firebirdType(self): + length =3D max(map(len, self.enumValues)) + enumValues =3D ', '.join([sqlbuilder.sqlrepr(v, 'firebird') for = v in self.enumValues]) + checkConstraint =3D "CHECK (%s in (%s))" % (self.dbName, enumVal= ues) + #NB. Return a tuple, not a string here + return "VARCHAR(%i)" % (length), checkConstraint + =20 + def _maxdbType(self): + raise "Enum type is not supported" + =20 + +class EnumCol(Col): + baseClass =3D SOEnumCol + +class SODateTimeCol(SOCol): + + # 3-03 @@: provide constraints; right now we let the database + # do any parsing and checking. And DATE and TIME? + + def _mysqlType(self): + return 'DATETIME' + + def _postgresType(self): + return 'TIMESTAMP' + + def _sybaseType(self): + return 'DATETIME' + + def _sqliteType(self): + return 'TIMESTAMP' + + def _firebirdType(self): + return 'TIMESTAMP' + =20 + def _maxdbType(self): + return 'TIMESTAMP' + +class DateTimeCol(Col): + baseClass =3D SODateTimeCol + +class SODateCol(SOCol): + + # 3-03 @@: provide constraints; right now we let the database + # do any parsing and checking. And DATE and TIME? + + def _mysqlType(self): + return 'DATE' + + def _postgresType(self): + return 'DATE' + + def _sybaseType(self): + return self._postgresType() + + def _firebirdType(self): + return 'DATE' + =20 + def _maxdbType(self): + return 'DATE' + +class DateCol(Col): + baseClass =3D SODateCol + +class SODecimalCol(SOCol): + + def __init__(self, **kw): + self.size =3D popKey(kw, 'size', NoDefault) + assert self.size is not NoDefault, \ + "You must give a size argument" + self.precision =3D popKey(kw, 'precision', NoDefault) + assert self.precision is not NoDefault, \ + "You must give a precision argument" + SOCol.__init__(self, **kw) + + def _sqlType(self): + return 'DECIMAL(%i, %i)' % (self.size, self.precision) + +class DecimalCol(Col): + baseClass =3D SODecimalCol + +class SOCurrencyCol(SODecimalCol): + + def __init__(self, **kw): + pushKey(kw, 'size', 10) + pushKey(kw, 'precision', 2) + SODecimalCol.__init__(self, **kw) + +class CurrencyCol(DecimalCol): + baseClass =3D SOCurrencyCol + +def popKey(kw, name, default=3DNone): + if not kw.has_key(name): + return default + value =3D kw[name] + del kw[name] + return value + +def pushKey(kw, name, value): + if not kw.has_key(name): + kw[name] =3D value + +all =3D [] +for key, value in globals().items(): + if isinstance(value, type) and issubclass(value, Col): + all.append(key) +__all__ =3D all |