~ubuntu-branches/debian/jessie/sqlalchemy/jessie

« back to all changes in this revision

Viewing changes to lib/sqlalchemy/ext/sqlsoup.py

  • Committer: Package Import Robot
  • Author(s): Piotr Ożarowski, Jakub Wilk, Piotr Ożarowski
  • Date: 2013-07-06 20:53:52 UTC
  • mfrom: (1.4.23) (16.1.17 experimental)
  • Revision ID: package-import@ubuntu.com-20130706205352-ryppl1eto3illd79
Tags: 0.8.2-1
[ Jakub Wilk ]
* Use canonical URIs for Vcs-* fields.

[ Piotr Ożarowski ]
* New upstream release
* Upload to unstable
* Build depend on python3-all instead of -dev, extensions are not built for
  Python 3.X 

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# ext/sqlsoup.py
2
 
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
3
 
#
4
 
# This module is part of SQLAlchemy and is released under
5
 
# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
 
 
7
 
"""
8
 
 
9
 
.. versionchanged:: 0.8
10
 
    SQLSoup is now its own project.  Documentation
11
 
    and project status are available at:
12
 
    http://pypi.python.org/pypi/sqlsoup and
13
 
    http://readthedocs.org/docs/sqlsoup\ .
14
 
    SQLSoup will no longer be included with SQLAlchemy.
15
 
 
16
 
 
17
 
Introduction
18
 
============
19
 
 
20
 
SqlSoup provides a convenient way to access existing database
21
 
tables without having to declare table or mapper classes ahead
22
 
of time. It is built on top of the SQLAlchemy ORM and provides a
23
 
super-minimalistic interface to an existing database.
24
 
 
25
 
SqlSoup effectively provides a coarse grained, alternative
26
 
interface to working with the SQLAlchemy ORM, providing a "self
27
 
configuring" interface for extremely rudimental operations. It's
28
 
somewhat akin to a "super novice mode" version of the ORM. While
29
 
SqlSoup can be very handy, users are strongly encouraged to use
30
 
the full ORM for non-trivial applications.
31
 
 
32
 
Suppose we have a database with users, books, and loans tables
33
 
(corresponding to the PyWebOff dataset, if you're curious).
34
 
 
35
 
Creating a SqlSoup gateway is just like creating an SQLAlchemy
36
 
engine::
37
 
 
38
 
    >>> from sqlalchemy.ext.sqlsoup import SqlSoup
39
 
    >>> db = SqlSoup('sqlite:///:memory:')
40
 
 
41
 
or, you can re-use an existing engine::
42
 
 
43
 
    >>> db = SqlSoup(engine)
44
 
 
45
 
You can optionally specify a schema within the database for your
46
 
SqlSoup::
47
 
 
48
 
    >>> db.schema = myschemaname
49
 
 
50
 
Loading objects
51
 
===============
52
 
 
53
 
Loading objects is as easy as this::
54
 
 
55
 
    >>> users = db.users.all()
56
 
    >>> users.sort()
57
 
    >>> users
58
 
    [
59
 
        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
60
 
                password=u'student',classname=None,admin=0),
61
 
        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
62
 
                password=u'basepair',classname=None,admin=1)
63
 
    ]
64
 
 
65
 
Of course, letting the database do the sort is better::
66
 
 
67
 
    >>> db.users.order_by(db.users.name).all()
68
 
    [
69
 
        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
70
 
            password=u'basepair',classname=None,admin=1),
71
 
        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
72
 
            password=u'student',classname=None,admin=0)
73
 
    ]
74
 
 
75
 
Field access is intuitive::
76
 
 
77
 
    >>> users[0].email
78
 
    u'student@example.edu'
79
 
 
80
 
Of course, you don't want to load all users very often. Let's
81
 
add a WHERE clause. Let's also switch the order_by to DESC while
82
 
we're at it::
83
 
 
84
 
    >>> from sqlalchemy import or_, and_, desc
85
 
    >>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
86
 
    >>> db.users.filter(where).order_by(desc(db.users.name)).all()
87
 
    [
88
 
        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
89
 
            password=u'student',classname=None,admin=0),
90
 
        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
91
 
            password=u'basepair',classname=None,admin=1)
92
 
    ]
93
 
 
94
 
You can also use .first() (to retrieve only the first object
95
 
from a query) or .one() (like .first when you expect exactly one
96
 
user -- it will raise an exception if more were returned)::
97
 
 
98
 
    >>> db.users.filter(db.users.name=='Bhargan Basepair').one()
99
 
    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
100
 
            password=u'basepair',classname=None,admin=1)
101
 
 
102
 
Since name is the primary key, this is equivalent to
103
 
 
104
 
    >>> db.users.get('Bhargan Basepair')
105
 
    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
106
 
        password=u'basepair',classname=None,admin=1)
107
 
 
108
 
This is also equivalent to
109
 
 
110
 
    >>> db.users.filter_by(name='Bhargan Basepair').one()
111
 
    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
112
 
        password=u'basepair',classname=None,admin=1)
113
 
 
114
 
filter_by is like filter, but takes kwargs instead of full
115
 
clause expressions. This makes it more concise for simple
116
 
queries like this, but you can't do complex queries like the
117
 
or\_ above or non-equality based comparisons this way.
118
 
 
119
 
Full query documentation
120
 
------------------------
121
 
 
122
 
Get, filter, filter_by, order_by, limit, and the rest of the
123
 
query methods are explained in detail in
124
 
:ref:`ormtutorial_querying`.
125
 
 
126
 
Modifying objects
127
 
=================
128
 
 
129
 
Modifying objects is intuitive::
130
 
 
131
 
    >>> user = _
132
 
    >>> user.email = 'basepair+nospam@example.edu'
133
 
    >>> db.commit()
134
 
 
135
 
(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work
136
 
code, so multiple updates to a single object will be turned into
137
 
a single ``UPDATE`` statement when you commit.)
138
 
 
139
 
To finish covering the basics, let's insert a new loan, then
140
 
delete it::
141
 
 
142
 
    >>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
143
 
    >>> db.loans.insert(book_id=book_id, user_name=user.name)
144
 
    MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
145
 
 
146
 
    >>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one()
147
 
    >>> db.delete(loan)
148
 
    >>> db.commit()
149
 
 
150
 
You can also delete rows that have not been loaded as objects.
151
 
Let's do our insert/delete cycle once more, this time using the
152
 
loans table's delete method. (For SQLAlchemy experts: note that
153
 
no flush() call is required since this delete acts at the SQL
154
 
level, not at the Mapper level.) The same where-clause
155
 
construction rules apply here as to the select methods::
156
 
 
157
 
    >>> db.loans.insert(book_id=book_id, user_name=user.name)
158
 
    MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
159
 
    >>> db.loans.delete(db.loans.book_id==2)
160
 
 
161
 
You can similarly update multiple rows at once. This will change the
162
 
book_id to 1 in all loans whose book_id is 2::
163
 
 
164
 
    >>> db.loans.filter_by(db.loans.book_id==2).update({'book_id':1})
165
 
    >>> db.loans.filter_by(book_id=1).all()
166
 
    [MappedLoans(book_id=1,user_name=u'Joe Student',
167
 
        loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
168
 
 
169
 
 
170
 
Joins
171
 
=====
172
 
 
173
 
Occasionally, you will want to pull out a lot of data from related
174
 
tables all at once.  In this situation, it is far more efficient to
175
 
have the database perform the necessary join.  (Here we do not have *a
176
 
lot of data* but hopefully the concept is still clear.)  SQLAlchemy is
177
 
smart enough to recognize that loans has a foreign key to users, and
178
 
uses that as the join condition automatically::
179
 
 
180
 
    >>> join1 = db.join(db.users, db.loans, isouter=True)
181
 
    >>> join1.filter_by(name='Joe Student').all()
182
 
    [
183
 
        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
184
 
            password=u'student',classname=None,admin=0,book_id=1,
185
 
            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))
186
 
    ]
187
 
 
188
 
If you're unfortunate enough to be using MySQL with the default MyISAM
189
 
storage engine, you'll have to specify the join condition manually,
190
 
since MyISAM does not store foreign keys.  Here's the same join again,
191
 
with the join condition explicitly specified::
192
 
 
193
 
    >>> db.join(db.users, db.loans, db.users.name==db.loans.user_name, isouter=True)
194
 
    <class 'sqlalchemy.ext.sqlsoup.MappedJoin'>
195
 
 
196
 
You can compose arbitrarily complex joins by combining Join objects
197
 
with tables or other joins.  Here we combine our first join with the
198
 
books table::
199
 
 
200
 
    >>> join2 = db.join(join1, db.books)
201
 
    >>> join2.all()
202
 
    [
203
 
        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
204
 
            password=u'student',classname=None,admin=0,book_id=1,
205
 
            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),
206
 
            id=1,title=u'Mustards I Have Known',published_year=u'1989',
207
 
            authors=u'Jones')
208
 
    ]
209
 
 
210
 
If you join tables that have an identical column name, wrap your join
211
 
with `with_labels`, to disambiguate columns with their table name
212
 
(.c is short for .columns)::
213
 
 
214
 
    >>> db.with_labels(join1).c.keys()
215
 
    [u'users_name', u'users_email', u'users_password',
216
 
        u'users_classname', u'users_admin', u'loans_book_id',
217
 
        u'loans_user_name', u'loans_loan_date']
218
 
 
219
 
You can also join directly to a labeled object::
220
 
 
221
 
    >>> labeled_loans = db.with_labels(db.loans)
222
 
    >>> db.join(db.users, labeled_loans, isouter=True).c.keys()
223
 
    [u'name', u'email', u'password', u'classname',
224
 
        u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
225
 
 
226
 
 
227
 
Relationships
228
 
=============
229
 
 
230
 
You can define relationships on SqlSoup classes:
231
 
 
232
 
    >>> db.users.relate('loans', db.loans)
233
 
 
234
 
These can then be used like a normal SA property:
235
 
 
236
 
    >>> db.users.get('Joe Student').loans
237
 
    [MappedLoans(book_id=1,user_name=u'Joe Student',
238
 
                    loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
239
 
 
240
 
    >>> db.users.filter(~db.users.loans.any()).all()
241
 
    [MappedUsers(name=u'Bhargan Basepair',
242
 
            email='basepair+nospam@example.edu',
243
 
            password=u'basepair',classname=None,admin=1)]
244
 
 
245
 
relate can take any options that the relationship function
246
 
accepts in normal mapper definition:
247
 
 
248
 
    >>> del db._cache['users']
249
 
    >>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
250
 
 
251
 
Advanced Use
252
 
============
253
 
 
254
 
Sessions, Transactions and Application Integration
255
 
---------------------------------------------------
256
 
 
257
 
.. note::
258
 
 
259
 
   Please read and understand this section thoroughly
260
 
   before using SqlSoup in any web application.
261
 
 
262
 
SqlSoup uses a ScopedSession to provide thread-local sessions.
263
 
You can get a reference to the current one like this::
264
 
 
265
 
    >>> session = db.session
266
 
 
267
 
The default session is available at the module level in SQLSoup,
268
 
via::
269
 
 
270
 
    >>> from sqlalchemy.ext.sqlsoup import Session
271
 
 
272
 
The configuration of this session is ``autoflush=True``,
273
 
``autocommit=False``. This means when you work with the SqlSoup
274
 
object, you need to call ``db.commit()`` in order to have
275
 
changes persisted. You may also call ``db.rollback()`` to roll
276
 
things back.
277
 
 
278
 
Since the SqlSoup object's Session automatically enters into a
279
 
transaction as soon as it's used, it is *essential* that you
280
 
call ``commit()`` or ``rollback()`` on it when the work within a
281
 
thread completes. This means all the guidelines for web
282
 
application integration at :ref:`session_lifespan` must be
283
 
followed.
284
 
 
285
 
The SqlSoup object can have any session or scoped session
286
 
configured onto it. This is of key importance when integrating
287
 
with existing code or frameworks such as Pylons. If your
288
 
application already has a ``Session`` configured, pass it to
289
 
your SqlSoup object::
290
 
 
291
 
    >>> from myapplication import Session
292
 
    >>> db = SqlSoup(session=Session)
293
 
 
294
 
If the ``Session`` is configured with ``autocommit=True``, use
295
 
``flush()`` instead of ``commit()`` to persist changes - in this
296
 
case, the ``Session`` closes out its transaction immediately and
297
 
no external management is needed. ``rollback()`` is also not
298
 
available. Configuring a new SQLSoup object in "autocommit" mode
299
 
looks like::
300
 
 
301
 
    >>> from sqlalchemy.orm import scoped_session, sessionmaker
302
 
    >>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
303
 
 
304
 
 
305
 
Mapping arbitrary Selectables
306
 
-----------------------------
307
 
 
308
 
SqlSoup can map any SQLAlchemy :class:`.Selectable` with the map
309
 
method. Let's map an :func:`.expression.select` object that uses an aggregate
310
 
function; we'll use the SQLAlchemy :class:`.Table` that SqlSoup
311
 
introspected as the basis. (Since we're not mapping to a simple
312
 
table or join, we need to tell SQLAlchemy how to find the
313
 
*primary key* which just needs to be unique within the select,
314
 
and not necessarily correspond to a *real* PK in the database.)::
315
 
 
316
 
    >>> from sqlalchemy import select, func
317
 
    >>> b = db.books._table
318
 
    >>> s = select([b.c.published_year, func.count('*').label('n')], from_obj=[b], group_by=[b.c.published_year])
319
 
    >>> s = s.alias('years_with_count')
320
 
    >>> years_with_count = db.map(s, primary_key=[s.c.published_year])
321
 
    >>> years_with_count.filter_by(published_year='1989').all()
322
 
    [MappedBooks(published_year=u'1989',n=1)]
323
 
 
324
 
Obviously if we just wanted to get a list of counts associated with
325
 
book years once, raw SQL is going to be less work. The advantage of
326
 
mapping a Select is reusability, both standalone and in Joins. (And if
327
 
you go to full SQLAlchemy, you can perform mappings like this directly
328
 
to your object models.)
329
 
 
330
 
An easy way to save mapped selectables like this is to just hang them on
331
 
your db object::
332
 
 
333
 
    >>> db.years_with_count = years_with_count
334
 
 
335
 
Python is flexible like that!
336
 
 
337
 
Raw SQL
338
 
-------
339
 
 
340
 
SqlSoup works fine with SQLAlchemy's text construct, described
341
 
in :ref:`sqlexpression_text`. You can also execute textual SQL
342
 
directly using the `execute()` method, which corresponds to the
343
 
`execute()` method on the underlying `Session`. Expressions here
344
 
are expressed like ``text()`` constructs, using named parameters
345
 
with colons::
346
 
 
347
 
    >>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
348
 
    >>> for name, email in rp.fetchall(): print name, email
349
 
    Bhargan Basepair basepair+nospam@example.edu
350
 
 
351
 
Or you can get at the current transaction's connection using
352
 
`connection()`. This is the raw connection object which can
353
 
accept any sort of SQL expression or raw SQL string passed to
354
 
the database::
355
 
 
356
 
    >>> conn = db.connection()
357
 
    >>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
358
 
 
359
 
Dynamic table names
360
 
-------------------
361
 
 
362
 
You can load a table whose name is specified at runtime with the
363
 
entity() method:
364
 
 
365
 
    >>> tablename = 'loans'
366
 
    >>> db.entity(tablename) == db.loans
367
 
    True
368
 
 
369
 
entity() also takes an optional schema argument. If none is
370
 
specified, the default schema is used.
371
 
 
372
 
"""
373
 
 
374
 
from sqlalchemy import Table, MetaData, join
375
 
from sqlalchemy import schema, sql, util
376
 
from sqlalchemy.engine.base import Engine
377
 
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
378
 
                            class_mapper, relationship, session,\
379
 
                            object_session, attributes
380
 
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
381
 
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, ArgumentError
382
 
from sqlalchemy.sql import expression
383
 
 
384
 
 
385
 
__all__ = ['PKNotFoundError', 'SqlSoup']
386
 
 
387
 
Session = scoped_session(sessionmaker(autoflush=True, autocommit=False))
388
 
 
389
 
class AutoAdd(MapperExtension):
390
 
    def __init__(self, scoped_session):
391
 
        self.scoped_session = scoped_session
392
 
 
393
 
    def instrument_class(self, mapper, class_):
394
 
        class_.__init__ = self._default__init__(mapper)
395
 
 
396
 
    def _default__init__(ext, mapper):
397
 
        def __init__(self, **kwargs):
398
 
            for key, value in kwargs.iteritems():
399
 
                setattr(self, key, value)
400
 
        return __init__
401
 
 
402
 
    def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
403
 
        session = self.scoped_session()
404
 
        state = attributes.instance_state(instance)
405
 
        session._save_impl(state)
406
 
        return EXT_CONTINUE
407
 
 
408
 
    def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
409
 
        sess = object_session(instance)
410
 
        if sess:
411
 
            sess.expunge(instance)
412
 
        return EXT_CONTINUE
413
 
 
414
 
class PKNotFoundError(SQLAlchemyError):
415
 
    pass
416
 
 
417
 
def _ddl_error(cls):
418
 
    msg = 'SQLSoup can only modify mapped Tables (found: %s)' \
419
 
          % cls._table.__class__.__name__
420
 
    raise InvalidRequestError(msg)
421
 
 
422
 
# metaclass is necessary to expose class methods with getattr, e.g.
423
 
# we want to pass db.users.select through to users._mapper.select
424
 
class SelectableClassType(type):
425
 
    def insert(cls, **kwargs):
426
 
        _ddl_error(cls)
427
 
 
428
 
    def __clause_element__(cls):
429
 
        return cls._table
430
 
 
431
 
    def __getattr__(cls, attr):
432
 
        if attr == '_query':
433
 
            # called during mapper init
434
 
            raise AttributeError()
435
 
        return getattr(cls._query, attr)
436
 
 
437
 
class TableClassType(SelectableClassType):
438
 
    def insert(cls, **kwargs):
439
 
        o = cls()
440
 
        o.__dict__.update(kwargs)
441
 
        return o
442
 
 
443
 
    def relate(cls, propname, *args, **kwargs):
444
 
        class_mapper(cls)._configure_property(propname, relationship(*args, **kwargs))
445
 
 
446
 
def _is_outer_join(selectable):
447
 
    if not isinstance(selectable, sql.Join):
448
 
        return False
449
 
    if selectable.isouter:
450
 
        return True
451
 
    return _is_outer_join(selectable.left) or _is_outer_join(selectable.right)
452
 
 
453
 
def _selectable_name(selectable):
454
 
    if isinstance(selectable, sql.Alias):
455
 
        return _selectable_name(selectable.element)
456
 
    elif isinstance(selectable, sql.Select):
457
 
        return ''.join(_selectable_name(s) for s in selectable.froms)
458
 
    elif isinstance(selectable, schema.Table):
459
 
        return selectable.name.capitalize()
460
 
    else:
461
 
        x = selectable.__class__.__name__
462
 
        if x[0] == '_':
463
 
            x = x[1:]
464
 
        return x
465
 
 
466
 
def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs):
467
 
    selectable = expression._clause_element_as_expr(selectable)
468
 
    mapname = 'Mapped' + _selectable_name(selectable)
469
 
    # Py2K
470
 
    if isinstance(mapname, unicode):
471
 
        engine_encoding = engine.dialect.encoding
472
 
        mapname = mapname.encode(engine_encoding)
473
 
    # end Py2K
474
 
 
475
 
    if isinstance(selectable, Table):
476
 
        klass = TableClassType(mapname, (base_cls,), {})
477
 
    else:
478
 
        klass = SelectableClassType(mapname, (base_cls,), {})
479
 
 
480
 
    def _compare(self, o):
481
 
        L = list(self.__class__.c.keys())
482
 
        L.sort()
483
 
        t1 = [getattr(self, k) for k in L]
484
 
        try:
485
 
            t2 = [getattr(o, k) for k in L]
486
 
        except AttributeError:
487
 
            raise TypeError('unable to compare with %s' % o.__class__)
488
 
        return t1, t2
489
 
 
490
 
    # python2/python3 compatible system of
491
 
    # __cmp__ - __lt__ + __eq__
492
 
 
493
 
    def __lt__(self, o):
494
 
        t1, t2 = _compare(self, o)
495
 
        return t1 < t2
496
 
 
497
 
    def __eq__(self, o):
498
 
        t1, t2 = _compare(self, o)
499
 
        return t1 == t2
500
 
 
501
 
    def __repr__(self):
502
 
        L = ["%s=%r" % (key, getattr(self, key, ''))
503
 
             for key in self.__class__.c.keys()]
504
 
        return '%s(%s)' % (self.__class__.__name__, ','.join(L))
505
 
 
506
 
    for m in ['__eq__', '__repr__', '__lt__']:
507
 
        setattr(klass, m, eval(m))
508
 
    klass._table = selectable
509
 
    klass.c = expression.ColumnCollection()
510
 
    mappr = mapper(klass,
511
 
                   selectable,
512
 
                   extension=AutoAdd(session),
513
 
                   **mapper_kwargs)
514
 
 
515
 
    for k in mappr.iterate_properties:
516
 
        klass.c[k.key] = k.columns[0]
517
 
 
518
 
    klass._query = session.query_property()
519
 
    return klass
520
 
 
521
 
class SqlSoup(object):
522
 
    """Represent an ORM-wrapped database resource."""
523
 
 
524
 
    def __init__(self, engine_or_metadata, base=object, session=None):
525
 
        """Initialize a new :class:`.SqlSoup`.
526
 
 
527
 
        :param engine_or_metadata: a string database URL, :class:`.Engine`
528
 
          or :class:`.MetaData` object to associate with. If the
529
 
          argument is a :class:`.MetaData`, it should be *bound*
530
 
          to an :class:`.Engine`.
531
 
        :param base: a class which will serve as the default class for
532
 
          returned mapped classes.  Defaults to ``object``.
533
 
        :param session: a :class:`.ScopedSession` or :class:`.Session` with
534
 
          which to associate ORM operations for this :class:`.SqlSoup` instance.
535
 
          If ``None``, a :class:`.ScopedSession` that's local to this
536
 
          module is used.
537
 
 
538
 
        """
539
 
 
540
 
        self.session = session or Session
541
 
        self.base=base
542
 
 
543
 
        if isinstance(engine_or_metadata, MetaData):
544
 
            self._metadata = engine_or_metadata
545
 
        elif isinstance(engine_or_metadata, (basestring, Engine)):
546
 
            self._metadata = MetaData(engine_or_metadata)
547
 
        else:
548
 
            raise ArgumentError("invalid engine or metadata argument %r" %
549
 
                                engine_or_metadata)
550
 
 
551
 
        self._cache = {}
552
 
        self.schema = None
553
 
 
554
 
    @property
555
 
    def bind(self):
556
 
        """The :class:`.Engine` associated with this :class:`.SqlSoup`."""
557
 
        return self._metadata.bind
558
 
 
559
 
    engine = bind
560
 
 
561
 
    def delete(self, instance):
562
 
        """Mark an instance as deleted."""
563
 
 
564
 
        self.session.delete(instance)
565
 
 
566
 
    def execute(self, stmt, **params):
567
 
        """Execute a SQL statement.
568
 
 
569
 
        The statement may be a string SQL string,
570
 
        an :func:`.expression.select` construct, or an :func:`.expression.text`
571
 
        construct.
572
 
 
573
 
        """
574
 
        return self.session.execute(sql.text(stmt, bind=self.bind), **params)
575
 
 
576
 
    @property
577
 
    def _underlying_session(self):
578
 
        if isinstance(self.session, session.Session):
579
 
            return self.session
580
 
        else:
581
 
            return self.session()
582
 
 
583
 
    def connection(self):
584
 
        """Return the current :class:`.Connection` in use by the current transaction."""
585
 
 
586
 
        return self._underlying_session._connection_for_bind(self.bind)
587
 
 
588
 
    def flush(self):
589
 
        """Flush pending changes to the database.
590
 
 
591
 
        See :meth:`.Session.flush`.
592
 
 
593
 
        """
594
 
        self.session.flush()
595
 
 
596
 
    def rollback(self):
597
 
        """Rollback the current transaction.
598
 
 
599
 
        See :meth:`.Session.rollback`.
600
 
 
601
 
        """
602
 
        self.session.rollback()
603
 
 
604
 
    def commit(self):
605
 
        """Commit the current transaction.
606
 
 
607
 
        See :meth:`.Session.commit`.
608
 
 
609
 
        """
610
 
        self.session.commit()
611
 
 
612
 
    def clear(self):
613
 
        """Synonym for :meth:`.SqlSoup.expunge_all`."""
614
 
 
615
 
        self.session.expunge_all()
616
 
 
617
 
    def expunge(self, instance):
618
 
        """Remove an instance from the :class:`.Session`.
619
 
 
620
 
        See :meth:`.Session.expunge`.
621
 
 
622
 
        """
623
 
        self.session.expunge(instance)
624
 
 
625
 
    def expunge_all(self):
626
 
        """Clear all objects from the current :class:`.Session`.
627
 
 
628
 
        See :meth:`.Session.expunge_all`.
629
 
 
630
 
        """
631
 
        self.session.expunge_all()
632
 
 
633
 
    def map_to(self, attrname, tablename=None, selectable=None,
634
 
                    schema=None, base=None, mapper_args=util.immutabledict()):
635
 
        """Configure a mapping to the given attrname.
636
 
 
637
 
        This is the "master" method that can be used to create any
638
 
        configuration.
639
 
 
640
 
        .. versionadded:: 0.6.6
641
 
 
642
 
        :param attrname: String attribute name which will be
643
 
          established as an attribute on this :class:.`.SqlSoup`
644
 
          instance.
645
 
        :param base: a Python class which will be used as the
646
 
          base for the mapped class. If ``None``, the "base"
647
 
          argument specified by this :class:`.SqlSoup`
648
 
          instance's constructor will be used, which defaults to
649
 
          ``object``.
650
 
        :param mapper_args: Dictionary of arguments which will
651
 
          be passed directly to :func:`.orm.mapper`.
652
 
        :param tablename: String name of a :class:`.Table` to be
653
 
          reflected. If a :class:`.Table` is already available,
654
 
          use the ``selectable`` argument. This argument is
655
 
          mutually exclusive versus the ``selectable`` argument.
656
 
        :param selectable: a :class:`.Table`, :class:`.Join`, or
657
 
          :class:`.Select` object which will be mapped. This
658
 
          argument is mutually exclusive versus the ``tablename``
659
 
          argument.
660
 
        :param schema: String schema name to use if the
661
 
          ``tablename`` argument is present.
662
 
 
663
 
 
664
 
        """
665
 
        if attrname in self._cache:
666
 
            raise InvalidRequestError(
667
 
                "Attribute '%s' is already mapped to '%s'" % (
668
 
                attrname,
669
 
                class_mapper(self._cache[attrname]).mapped_table
670
 
            ))
671
 
 
672
 
        if tablename is not None:
673
 
            if not isinstance(tablename, basestring):
674
 
                raise ArgumentError("'tablename' argument must be a string."
675
 
                                    )
676
 
            if selectable is not None:
677
 
                raise ArgumentError("'tablename' and 'selectable' "
678
 
                                    "arguments are mutually exclusive")
679
 
 
680
 
            selectable = Table(tablename,
681
 
                                        self._metadata,
682
 
                                        autoload=True,
683
 
                                        autoload_with=self.bind,
684
 
                                        schema=schema or self.schema)
685
 
        elif schema:
686
 
            raise ArgumentError("'tablename' argument is required when "
687
 
                                "using 'schema'.")
688
 
        elif selectable is not None:
689
 
            if not isinstance(selectable, expression.FromClause):
690
 
                raise ArgumentError("'selectable' argument must be a "
691
 
                                    "table, select, join, or other "
692
 
                                    "selectable construct.")
693
 
        else:
694
 
            raise ArgumentError("'tablename' or 'selectable' argument is "
695
 
                                    "required.")
696
 
 
697
 
        if not selectable.primary_key.columns:
698
 
            if tablename:
699
 
                raise PKNotFoundError(
700
 
                            "table '%s' does not have a primary "
701
 
                            "key defined" % tablename)
702
 
            else:
703
 
                raise PKNotFoundError(
704
 
                            "selectable '%s' does not have a primary "
705
 
                            "key defined" % selectable)
706
 
 
707
 
        mapped_cls = _class_for_table(
708
 
            self.session,
709
 
            self.engine,
710
 
            selectable,
711
 
            base or self.base,
712
 
            mapper_args
713
 
        )
714
 
        self._cache[attrname] = mapped_cls
715
 
        return mapped_cls
716
 
 
717
 
 
718
 
    def map(self, selectable, base=None, **mapper_args):
719
 
        """Map a selectable directly.
720
 
 
721
 
        .. versionchanged:: 0.6.6
722
 
            The class and its mapping are not cached and will
723
 
            be discarded once dereferenced.
724
 
 
725
 
        :param selectable: an :func:`.expression.select` construct.
726
 
        :param base: a Python class which will be used as the
727
 
          base for the mapped class. If ``None``, the "base"
728
 
          argument specified by this :class:`.SqlSoup`
729
 
          instance's constructor will be used, which defaults to
730
 
          ``object``.
731
 
        :param mapper_args: Dictionary of arguments which will
732
 
          be passed directly to :func:`.orm.mapper`.
733
 
 
734
 
        """
735
 
 
736
 
        return _class_for_table(
737
 
            self.session,
738
 
            self.engine,
739
 
            selectable,
740
 
            base or self.base,
741
 
            mapper_args
742
 
        )
743
 
 
744
 
    def with_labels(self, selectable, base=None, **mapper_args):
745
 
        """Map a selectable directly, wrapping the
746
 
        selectable in a subquery with labels.
747
 
 
748
 
        .. versionchanged:: 0.6.6
749
 
            The class and its mapping are not cached and will
750
 
            be discarded once dereferenced.
751
 
 
752
 
        :param selectable: an :func:`.expression.select` construct.
753
 
        :param base: a Python class which will be used as the
754
 
          base for the mapped class. If ``None``, the "base"
755
 
          argument specified by this :class:`.SqlSoup`
756
 
          instance's constructor will be used, which defaults to
757
 
          ``object``.
758
 
        :param mapper_args: Dictionary of arguments which will
759
 
          be passed directly to :func:`.orm.mapper`.
760
 
 
761
 
        """
762
 
 
763
 
        # TODO give meaningful aliases
764
 
        return self.map(
765
 
                    expression._clause_element_as_expr(selectable).
766
 
                            select(use_labels=True).
767
 
                            alias('foo'), base=base, **mapper_args)
768
 
 
769
 
    def join(self, left, right, onclause=None, isouter=False,
770
 
                base=None, **mapper_args):
771
 
        """Create an :func:`.expression.join` and map to it.
772
 
 
773
 
        .. versionchanged:: 0.6.6
774
 
            The class and its mapping are not cached and will
775
 
            be discarded once dereferenced.
776
 
 
777
 
        :param left: a mapped class or table object.
778
 
        :param right: a mapped class or table object.
779
 
        :param onclause: optional "ON" clause construct..
780
 
        :param isouter: if True, the join will be an OUTER join.
781
 
        :param base: a Python class which will be used as the
782
 
          base for the mapped class. If ``None``, the "base"
783
 
          argument specified by this :class:`.SqlSoup`
784
 
          instance's constructor will be used, which defaults to
785
 
          ``object``.
786
 
        :param mapper_args: Dictionary of arguments which will
787
 
          be passed directly to :func:`.orm.mapper`.
788
 
 
789
 
        """
790
 
 
791
 
        j = join(left, right, onclause=onclause, isouter=isouter)
792
 
        return self.map(j, base=base, **mapper_args)
793
 
 
794
 
    def entity(self, attr, schema=None):
795
 
        """Return the named entity from this :class:`.SqlSoup`, or
796
 
        create if not present.
797
 
 
798
 
        For more generalized mapping, see :meth:`.map_to`.
799
 
 
800
 
        """
801
 
        try:
802
 
            return self._cache[attr]
803
 
        except KeyError, ke:
804
 
            return self.map_to(attr, tablename=attr, schema=schema)
805
 
 
806
 
    def __getattr__(self, attr):
807
 
        return self.entity(attr)
808
 
 
809
 
    def __repr__(self):
810
 
        return 'SqlSoup(%r)' % self._metadata
811