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    #@classmethod
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    #@staticmethod
0054    def _guess_table(classname):
0055        return style.mixed_to_underscore(classname)
0056
0057    _guess_table = staticmethod(_guess_table)
0058
0059    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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        # @@: Warn
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    #@classmethod
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    #@classmethod
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    #@classmethod
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    #@classmethod
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: # @@: KeyError is a bad error
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    # @@: Add destroySelf
0383    def destroy_self(self):
0384        self.sqlmeta.destroy_self()
0385
0386    def sync(self):
0387        self.sqlmeta.sync()
0388
0389    # @@: Add createTable, dropTable