|
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
|