import new from metasqlobject.declarative import Declarative, setup_attributes, \ DeclarativeMeta from metasqlobject.eventhub import EventHub from connectionhub import ConnectionHub from sqlapi import sql import events import style import col from classreg import class_registry from sresults import SelectResults __all__ = ['sqlhub', 'sqlmeta', 'SQLObject', 'SQLObjectNotFound'] sqlhub = ConnectionHub() class NoDefault: pass class SQLObjectNotFound(LookupError): pass class sqlmeta(object): connection_hub = sqlhub columns = None _column_list = None _required_to_create = None table = None lazy = False sequence = None # @@: defaultOrder default_order = None _creating = False _creating_class = False _obsolete = False def __init__(self, instance): self.instance = instance #@classmethod def _set_soclass(sqlmeta, soclass): sqlmeta.soclass = soclass sqlmeta.columns = {} sqlmeta._column_list = [] sqlmeta._required_to_create = [] if not soclass.__abstract__: if sqlmeta.table is None: sqlmeta.table = sqlmeta._guess_table(soclass.__name__) _set_soclass = classmethod(_set_soclass) #@staticmethod def _guess_table(classname): return style.mixed_to_underscore(classname) _guess_table = staticmethod(_guess_table) #@classmethod def get_connection(cls): return cls.connection_hub.get_connection() get_connection = classmethod(get_connection) def create(self): for name in self._required_to_create: if name in self._db_values: continue if name in self.columns: col = self.columns[name] try: val = self._python_values[name] = col.default except AttributeError: pass else: self._db_values[name] = col.from_python( val, self.instance) continue raise TypeError( "The required column value %s was not provided" % name) conn = self.connection auto_increment = conn.plugin.auto_increment cur = conn.cursor() if not auto_increment and 'id' not in self._db_values: if self.sequence: sequence = self.sequence else: sequence = conn.plugin.guess_sequence_name( self.table, self.columns['id'].db_name) next_id = conn.plugin.get_sequence_nextval(sequence, cur) self._db_values['id'] = self._python_values['id'] = next_id # Convert names... insert_values = {} for name, value in self._db_values.items(): insert_values[self.columns[name].db_name] = value s = sql.Insert(self.table, insert_values) cur.execute(s) if auto_increment and 'id' not in self._db_values: self._db_values['id'] = self._python_values['id'] = cur.lastrowid cur.close() del self._creating def write_updates(self): if self._creating: return s = sql.Update( self.table, [(n, self._db_values[n]) for n in self._dirty_columns], self.soclass.id == self.instance.id) self.connection.execute(s) self._dirty_columns = [] def destroy_self(self): post_funcs = [] del_id = self.instance.id self.soclass.event_hub.send( events.RowDestroySignal, instance=self.instance, post_funcs=post_funcs) s = sql.Delete( self.table, self.soclass.id == self.instance.id) self.connection.execute(s) self._obsolete = True del self._db_values del self._python_values for func in post_funcs: func(self.soclass, del_id) #@classmethod def create_sql(sqlmeta, conn=None, create_context=None): conn = conn or sqlmeta.get_connection() columns_sql = [] lazy_sql = [] for column in sqlmeta._column_list: col_sql, col_lazy = column.create_sql(conn) columns_sql.append(col_sql) lazy_sql.extend(col_lazy) create_table = sql.CreateTable( sqlmeta.table, columns_sql) create_sql = [create_table] sqlmeta.soclass.event_hub.send( events.TableSQLSignal, soclass=sqlmeta.soclass, connection=conn, create_sql=create_sql, lazy_sql=lazy_sql, create_context=create_context) return create_sql, lazy_sql create_sql = classmethod(create_sql) # @@: Some back-compat for createTable #@classmethod def drop_sql(sqlmeta, conn=None): conn = conn or sqlmeta.get_connection() return [sql.DropTable(sqlmeta.table)], [] drop_sql = classmethod(drop_sql) #@classmethod def drop_table(sqlmeta, cascade=False, conn=None): # @@: back-compat for ifExists ^ conn = conn or sqlmeta.get_connection() post_funcs = [] sqlmeta.soclass.event_hub.send( events.DropTableSignal, soclass=sqlmeta.soclass, connection=conn, cascade=cascade, post_funcs=post_funcs) sql, lazy_sql = sqlmeta.drop_sql(conn) for s in sql+lazy_sql: conn.execute(s) for func in post_funcs: func(soclass, conn) drop_table = classmethod(drop_table) #@classmethod def table_exists(sqlmeta, conn=None): conn = conn or sqlmeta.get_connection() cur = conn.cursor() result = conn.plugin.table_exists(sqlmeta.table, cur) cur.close() return result table_exists = classmethod(table_exists) #@classmethod def add_column(cls, column): cls._column_list.append(column) cls.columns[column.name] = column if not column.auto_increment: cls._required_to_create.append(column.name) if not cls._creating_class: cls._rebuild_sql_select_columns() add_column = classmethod(add_column) #@classmethod def _rebuild_sql_select_columns(cls): cls.sql_select_columns = ( [col.sqlexpr for col in cls._column_list], cls.build_from_select) _rebuild_sql_select_columns = classmethod(_rebuild_sql_select_columns) #@classmethod def build_from_select(cls, row): inst = cls.soclass._create_empty() inst.sqlmeta._sync_from_row(row) return inst def _sync_from_row(self, row): for col, value in zip(self._column_list, row): self._db_values[col.name] = value if col.to_python: python_value = col.to_python(value) else: python_value = value self._python_values[col.name] = python_value build_from_select = classmethod(build_from_select) def sync(self): inst = self.instance cur = self.connection.cursor() select = sql.Select( self.sql_select_columns[0], getattr(self.soclass, 'id') == self.instance.id) cur.execute(select) row = cur.fetchone() cur.close() self._sync_from_row(row) def as_dict(self): return self._python_values.copy() class SQLObject(object): event_hub = EventHub() __metaclass__ = DeclarativeMeta sqlmeta = sqlmeta __abstract__ = True id = col.IntCol(auto_increment=True, primary_key=True) def __classinit__(cls, new_attrs): if '__abstract__' not in new_attrs: cls.__abstract__ = False if 'event_hub' not in new_attrs: cls.event_hub = cls.event_hub.copy() if ('sqlmeta' in new_attrs and not isinstance(cls.sqlmeta, sqlmeta) and hasattr(cls.__bases__[0], 'sqlmeta')): new_sqlmeta = type('sqlmeta', (cls.__bases__[0].sqlmeta,), dict(cls.sqlmeta.__dict__)) cls.sqlmeta = new_sqlmeta elif 'sqlmeta' not in new_attrs: new_sqlmeta = type('sqlmeta', (cls.__bases__[0].sqlmeta,), {}) cls.sqlmeta = new_sqlmeta cls.__required_constructor__ = [] cls.sqlmeta._set_soclass(cls) cls.sqlmeta._creating_class = True # @@: Warn cls.q = cls setup_attributes(cls, new_attrs) cls.event_hub.send(events.ClassCreateSignal, soclass=cls) class_registry.register(cls) if isinstance(cls.id, (str, unicode, tuple)): cls.id = col.Compound(cls.id) del cls.sqlmeta._creating_class cls.sqlmeta._rebuild_sql_select_columns() def __init__(self, **kw): self._init() if '__create_empty' in kw: return self.sqlmeta._creating = True for name, value in kw.items(): if (name in self.__class__.__dict__ or hasattr(self.__class__, name)): setattr(self, name, value) else: raise TypeError( "%s() called with unknown keyword %s" % (self.__class__.__name__, name)) if not self.sqlmeta.lazy: self.sqlmeta.create() def _init(self): self.sqlmeta = self.sqlmeta(self) self.sqlmeta.connection = self.sqlmeta.get_connection() self.sqlmeta._python_values = {} self.sqlmeta._dirty_columns = [] self.sqlmeta._db_values = {} #@classmethod def select_by(cls, **kw): query = [] for name, value in kw.items(): query.append(getattr(cls, name) == value) return cls.select(sql.AND(*query)) select_by = classmethod(select_by) #@classmethod def select(cls, clause=True, connection=None, order_by=None, reversed=False, distinct=False): # @@: Backward compat: if clause is None: clause = True if order_by is None: order_by = cls.sqlmeta.default_order connection = connection or cls.sqlmeta.get_connection() return SelectResults( [cls.sqlmeta.sql_select_columns], clause, return_single=True, connection=connection, order_by=order_by, primary_class=cls, reversed=reversed, distinct=distinct) select = classmethod(select) #@classmethod def _create_empty(cls): self = cls(__create_empty=True) return self _create_empty = classmethod(_create_empty) def set(self, **kw): self.sqlmeta._creating = True for name, value in kw.items(): setattr(self, name, value) del self.sqlmeta._creating self.sqlmeta.write_updates() #@classmethod def get(cls, *args, **kw): if args: if kw: raise TypeError( "You may only provide an ID or keyword arguments " "to .get() (you gave both %r and %r)" % (args, kw)) if len(args) != 1: raise TypeError( "Only one argument (id) allowed to .get() (you " "gave %r)" % args) clause = (cls.id == args[0]) else: query = [] for name, value in kw.items(): query.append(getattr(cls, name) == value) clause = sql.AND(*query) select = list(cls.select(clause)) if not select: if args: raise SQLObjectNotFound( "No object %s by the id %r found" % (cls.__name__, args[0])) else: raise SQLObjectNotFound( "No object %s satisfying %s found" % (cls.__name__, sql.sqlrepr(clause))) if len(select) > 1: if args: raise AssertionError( "More than one object %s with ID %r!: %s" % (cls.__name__, args[0], select)) else: raise ValueError( "More than one object %s satisfies %s" % (cls.__name__, sql.sqlrepr(clause))) return select[0] get = classmethod(get) def __repr__(self): s = '<%s' % self.__class__.__name__ try: cur_id = self.id s += '.get(%r)' % cur_id except KeyError: # @@: KeyError is a bad error s += ' (no id)' s += ' '+hex(abs(id(self))) items = self.sqlmeta._python_values.items() items.sort() for name, value in items: if name == 'id': continue value = repr(value) if len(value) > 14: value = value[:9]+'...'+value[-5:] s += ' %s=%s' % (name, value) return s+'>' # @@: Add destroySelf def destroy_self(self): self.sqlmeta.destroy_self() def sync(self): self.sqlmeta.sync() # @@: Add createTable, dropTable