0001import new
0002from metasqlobject.declarative import Declarative, setup_attributes, DeclarativeMeta
0004from metasqlobject.eventhub import EventHub
0005from connectionhub import ConnectionHub
0006from sqlapi import sql
0007import events
0008import style
0009import col
0010from classreg import class_registry
0011from sresults import SelectResults
0012
0013__all__ = ['sqlhub', 'sqlmeta', 'SQLObject', 'SQLObjectNotFound']
0014
0015sqlhub = ConnectionHub()
0016
0017class NoDefault:
0018 pass
0019
0020class SQLObjectNotFound(LookupError):
0021 pass
0022
0023class sqlmeta(object):
0024
0025 connection_hub = sqlhub
0026
0027 columns = None
0028 _column_list = None
0029 _required_to_create = None
0030 table = None
0031 lazy = False
0032 sequence = None
0033
0034 _creating = False
0035 _creating_class = False
0036 _obsolete = False
0037
0038 def __init__(self, instance):
0039 self.instance = instance
0040
0041
0042 def _set_soclass(sqlmeta, soclass):
0043 sqlmeta.soclass = soclass
0044 sqlmeta.columns = {}
0045 sqlmeta._column_list = []
0046 sqlmeta._required_to_create = []
0047 if not soclass.__abstract__:
0048 if sqlmeta.table is None:
0049 sqlmeta.table = sqlmeta._guess_table(soclass.__name__)
0050
0051 _set_soclass = classmethod(_set_soclass)
0052
0053
0054 def _guess_table(classname):
0055 return style.mixed_to_underscore(classname)
0056
0057 _guess_table = staticmethod(_guess_table)
0058
0059
0060 def get_connection(cls):
0061 return cls.connection_hub.get_connection()
0062
0063 get_connection = classmethod(get_connection)
0064
0065 def create(self):
0066 for name in self._required_to_create:
0067 if name in self._db_values:
0068 continue
0069 if name in self.columns:
0070 col = self.columns[name]
0071 try:
0072 val = self._python_values[name] = col.default
0073 except AttributeError:
0074 pass
0075 else:
0076 self._db_values[name] = col.from_python(val)
0077 continue
0078 raise TypeError(
0079 "The required column value %s was not provided"
0080 % name)
0081 conn = self.connection
0082 auto_increment = conn.plugin.auto_increment
0083 cur = conn.cursor()
0084 if not auto_increment and 'id' not in self._db_values:
0085 if self.sequence:
0086 sequence = self.sequence
0087 else:
0088 sequence = conn.plugin.guess_sequence_name(
0089 self.table, self.columns['id'].db_name)
0090 next_id = conn.plugin.get_sequence_nextval(sequence, cur)
0091 self._db_values['id'] = self._python_values['id'] = next_id
0092 s = sql.Insert(self.table, self._db_values)
0093 cur.execute(s)
0094 if auto_increment and 'id' not in self._db_values:
0095 self._db_values['id'] = self._python_values['id'] = cur.lastrowid
0096 cur.close()
0097 del self._creating
0098
0099 def write_updates(self):
0100 if self._creating:
0101 return
0102 s = sql.Update(
0103 self.table,
0104 [(n, self._db_values[n]) for n in self._dirty_columns],
0105 self.soclass.id == self.instance.id)
0106 self.connection.execute(s)
0107 self._dirty_columns = []
0108
0109 def destroy_self(self):
0110 post_funcs = []
0111 del_id = self.instance.id
0112 self.soclass.event_hub.send(
0113 events.RowDestroySignal,
0114 instance=self.instance,
0115 post_funcs=post_funcs)
0116 s = sql.Delete(
0117 self.table,
0118 self.soclass.id == self.instance.id)
0119 self.connection.execute(s)
0120 self._obsolete = True
0121 del self._db_values
0122 del self._python_values
0123 for func in post_funcs:
0124 func(self.soclass, del_id)
0125
0126
0127 def create_sql(sqlmeta, conn=None):
0128 conn = conn or sqlmeta.get_connection()
0129 columns_sql = []
0130 lazy_sql = []
0131 for column in sqlmeta._column_list:
0132 col_sql, col_lazy = column.create_sql(conn)
0133 columns_sql.append(col_sql)
0134 lazy_sql.extend(col_lazy)
0135 create_table = sql.CreateTable(
0136 sqlmeta.table, columns_sql)
0137 return [create_table], lazy_sql
0138
0139 create_sql = classmethod(create_sql)
0140
0141
0142 def drop_sql(sqlmeta, conn=None):
0143 conn = conn or sqlmeta.get_connection()
0144 return [sql.DropTable(sqlmeta.table)], []
0145
0146 drop_sql = classmethod(drop_sql)
0147
0148
0149 def drop_table(sqlmeta, cascade=False, conn=None):
0150 conn = conn or sqlmeta.get_connection()
0151 post_funcs = []
0152 sqlmeta.soclass.event_hub.send(
0153 events.DropTableSignal,
0154 soclass=sqlmeta.soclass,
0155 connection=conn,
0156 cascade=cascade,
0157 post_funcs=post_funcs)
0158 sql, lazy_sql = sqlmeta.drop_sql(conn)
0159 for s in sql+lazy_sql:
0160 conn.execute(s)
0161 for func in post_funcs:
0162 func(soclass, conn)
0163
0164 drop_table = classmethod(drop_table)
0165
0166
0167 def table_exists(sqlmeta, conn=None):
0168 conn = conn or sqlmeta.get_connection()
0169 cur = conn.cursor()
0170 result = conn.plugin.table_exists(sqlmeta.table, cur)
0171 cur.close()
0172 return result
0173
0174 table_exists = classmethod(table_exists)
0175
0176
0177 def add_column(cls, column):
0178 cls._column_list.append(column)
0179 cls.columns[column.name] = column
0180 if not column.auto_increment:
0181 cls._required_to_create.append(column.name)
0182 if not cls._creating_class:
0183 cls._rebuild_sql_select_columns()
0184
0185 add_column = classmethod(add_column)
0186
0187
0188 def _rebuild_sql_select_columns(cls):
0189 cls.sql_select_columns = (
0190 [col.sqlexpr for col in cls._column_list],
0191 cls.build_from_select)
0192
0193 _rebuild_sql_select_columns = classmethod(_rebuild_sql_select_columns)
0194
0195
0196 def build_from_select(cls, row):
0197 inst = cls.soclass._create_empty()
0198 inst.sqlmeta._sync_from_row(row)
0199 return inst
0200
0201 def _sync_from_row(self, row):
0202 for col, value in zip(self._column_list, row):
0203 self._db_values[col.name] = value
0204 if col.to_python:
0205 python_value = col.to_python(value)
0206 else:
0207 python_value = value
0208 self._python_values[col.name] = python_value
0209
0210 build_from_select = classmethod(build_from_select)
0211
0212 def sync(self):
0213 inst = self.instance
0214 cur = self.connection.cursor()
0215 select = sql.Select(
0216 self.sql_select_columns[0],
0217 getattr(self.soclass, 'id') == self.instance.id)
0218 cur.execute(select)
0219 row = cur.fetchone()
0220 cur.close()
0221 self._sync_from_row(row)
0222
0223 def as_dict(self):
0224 return self._python_values.copy()
0225
0226class SQLObject(object):
0227
0228 event_hub = EventHub()
0229 __metaclass__ = DeclarativeMeta
0230
0231 sqlmeta = sqlmeta
0232 __abstract__ = True
0233
0234 id = col.IntCol(auto_increment=True, primary_key=True)
0235
0236 def __classinit__(cls, new_attrs):
0237 if '__abstract__' not in new_attrs:
0238 cls.__abstract__ = False
0239 if 'event_hub' not in new_attrs:
0240 cls.event_hub = cls.event_hub.copy()
0241 if ('sqlmeta' in new_attrs
0242 and not isinstance(cls.sqlmeta, sqlmeta)
0243 and hasattr(cls.__bases__[0], 'sqlmeta')):
0244 new_sqlmeta = type('sqlmeta', (cls.__bases__[0].sqlmeta,),
0245 cls.sqlmeta.__dict__)
0246 cls.sqlmeta = new_sqlmeta
0247 elif 'sqlmeta' not in new_attrs:
0248 new_sqlmeta = type('sqlmeta', (cls.__bases__[0].sqlmeta,),
0249 {})
0250 cls.sqlmeta = new_sqlmeta
0251 cls.__required_constructor__ = []
0252 cls.sqlmeta._set_soclass(cls)
0253 cls.sqlmeta._creating_class = True
0254
0255 cls.q = cls
0256 setup_attributes(cls, new_attrs)
0257 cls.event_hub.send(events.ClassCreateSignal, soclass=cls)
0258 class_registry.register(cls)
0259 if isinstance(cls.id, (str, unicode, tuple)):
0260 cls.id = col.Compound(cls.id)
0261 del cls.sqlmeta._creating_class
0262 cls.sqlmeta._rebuild_sql_select_columns()
0263
0264 def __init__(self, **kw):
0265 self._init()
0266 if '__create_empty' in kw:
0267 return
0268 self.sqlmeta._creating = True
0269 for name, value in kw.items():
0270 if (name in self.__class__.__dict__
0271 or hasattr(self.__class__, name)):
0272 setattr(self, name, value)
0273 else:
0274 raise TypeError(
0275 "%s() called with unknown keyword %s"
0276 % (self.__class__.__name__, name))
0277
0278 if not self.sqlmeta.lazy:
0279 self.sqlmeta.create()
0280
0281 def _init(self):
0282 self.sqlmeta = self.sqlmeta(self)
0283 self.sqlmeta.connection = self.sqlmeta.get_connection()
0284 self.sqlmeta._python_values = {}
0285 self.sqlmeta._dirty_columns = []
0286 self.sqlmeta._db_values = {}
0287
0288
0289 def select_by(cls, **kw):
0290 query = []
0291 for name, value in kw.items():
0292 query.append(getattr(cls, name) == value)
0293 return cls.select(sql.AND(*query))
0294
0295 select_by = classmethod(select_by)
0296
0297
0298 def select(cls, clause=True, connection=None):
0299 connection = connection or cls.sqlmeta.get_connection()
0300 return SelectResults(
0301 [cls.sqlmeta.sql_select_columns],
0302 clause,
0303 return_single=True,
0304 connection=connection)
0305
0306 select = classmethod(select)
0307
0308
0309 def _create_empty(cls):
0310 self = cls(__create_empty=True)
0311 return self
0312
0313 _create_empty = classmethod(_create_empty)
0314
0315 def set(self, **kw):
0316 self.sqlmeta._creating = True
0317 for name, value in kw.items():
0318 setattr(self, name, value)
0319 del self.sqlmeta._creating
0320 self.sqlmeta.write_updates()
0321
0322
0323 def get(cls, *args, **kw):
0324 if args:
0325 if kw:
0326 raise TypeError(
0327 "You may only provide an ID or keyword arguments "
0328 "to .get() (you gave both %r and %r)"
0329 % (args, kw))
0330 if len(args) != 1:
0331 raise TypeError(
0332 "Only one argument (id) allowed to .get() (you "
0333 "gave %r)" % args)
0334 clause = (cls.id == args[0])
0335 else:
0336 query = []
0337 for name, value in kw.items():
0338 query.append(getattr(cls, name) == value)
0339 clause = sql.AND(*query)
0340 select = list(cls.select(clause))
0341 if not select:
0342 if args:
0343 raise SQLObjectNotFound(
0344 "No object %s by the id %r found"
0345 % (cls.__name__, args[0]))
0346 else:
0347 raise SQLObjectNotFound(
0348 "No object %s satisfying %s found"
0349 % (cls.__name__, sql.sqlrepr(clause)))
0350 if len(select) > 1:
0351 if args:
0352 raise AssertionError(
0353 "More than one object %s with ID %r!: %s"
0354 % (cls.__name__, args[0], select))
0355 else:
0356 raise ValueError(
0357 "More than one object %s satisfies %s"
0358 % (cls.__name__, sql.sqlrepr(clause)))
0359 return select[0]
0360
0361 get = classmethod(get)
0362
0363 def __repr__(self):
0364 s = '<%s' % self.__class__.__name__
0365 try:
0366 cur_id = self.id
0367 s += '.get(%r)' % cur_id
0368 except KeyError:
0369 s += ' (no id)'
0370 s += ' '+hex(abs(id(self)))
0371 items = self.sqlmeta._python_values.items()
0372 items.sort()
0373 for name, value in items:
0374 if name == 'id':
0375 continue
0376 value = repr(value)
0377 if len(value) > 14:
0378 value = value[:9]+'...'+value[-5:]
0379 s += ' %s=%s' % (name, value)
0380 return s+'>'
0381
0382
0383 def destroy_self(self):
0384 self.sqlmeta.destroy_self()
0385
0386 def sync(self):
0387 self.sqlmeta.sync()
0388
0389