summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES29
-rw-r--r--lib/sqlalchemy/ext/assignmapper.py3
-rw-r--r--lib/sqlalchemy/ext/sqlsoup.py254
-rw-r--r--lib/sqlalchemy/mods/threadlocal.py7
-rw-r--r--setup.py2
5 files changed, 217 insertions, 78 deletions
diff --git a/CHANGES b/CHANGES
index dc07abd1d..f91a1eb8f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -50,6 +50,35 @@ unit tests
runner to insure its properly working. docs generally overhauled to
deal with new code patterns
+0.1.7
+- some fixes to topological sort algorithm
+- added DISTINCT ON support to Postgres (just supply distinct=[col1,col2..])
+- added __mod__ (% operator) to sql expressions
+- "order_by" mapper property inherited from inheriting mapper
+- fix to column type used when mapper UPDATES/DELETEs
+- with convert_unicode=True, reflection was failing, has been fixed
+- types types types! still werent working....have to use TypeDecorator again :(
+- mysql binary type converts array output to buffer, fixes PickleType
+- fixed the attributes.py memory leak once and for all
+- unittests are qualified based on the databases that support each one
+- fixed bug where column defaults would clobber VALUES clause of insert objects
+- fixed bug where table def w/ schema name would force engine connection
+- fix for parenthesis to work correctly with subqueries in INSERT/UPDATE
+- HistoryArraySet gets extend() method
+- fixed lazyload support for other comparison operators besides =
+- lazyload fix where two comparisons in the join condition point to the
+samem column
+- added "construct_new" flag to mapper, will use __new__ to create instances
+instead of __init__ (standard in 0.2)
+- added selectresults.py to SVN, missed it last time
+- tweak to allow a many-to-many relationship from a table to itself via
+an association table
+- small fix to "translate_row" function used by polymorphic example
+- create_engine uses cgi.parse_qsl to read query string (out the window in 0.2)
+- tweaks to CAST operator
+- fixed function names LOCAL_TIME/LOCAL_TIMESTAMP -> LOCALTIME/LOCALTIMESTAMP
+- fixed order of ORDER BY/HAVING in compile
+
0.1.6
- support for MS-SQL added courtesy Rick Morrison, Runar Petursson
- the latest SQLSoup from J. Ellis
diff --git a/lib/sqlalchemy/ext/assignmapper.py b/lib/sqlalchemy/ext/assignmapper.py
index b8a676b75..07ba95a69 100644
--- a/lib/sqlalchemy/ext/assignmapper.py
+++ b/lib/sqlalchemy/ext/assignmapper.py
@@ -10,6 +10,9 @@ def monkeypatch_query_method(ctx, class_, name):
def monkeypatch_objectstore_method(ctx, class_, name):
def do(self, *args, **kwargs):
session = ctx.current
+ if name == "flush":
+ # flush expects a list of objects
+ self = [self]
return getattr(session, name)(self, *args, **kwargs)
setattr(class_, name, do)
diff --git a/lib/sqlalchemy/ext/sqlsoup.py b/lib/sqlalchemy/ext/sqlsoup.py
index 043abc38b..b1fb0b889 100644
--- a/lib/sqlalchemy/ext/sqlsoup.py
+++ b/lib/sqlalchemy/ext/sqlsoup.py
@@ -1,72 +1,182 @@
-from sqlalchemy import *
-
-class NoSuchTableError(SQLAlchemyError): pass
-
-# metaclass is necessary to expose class methods with getattr, e.g.
-# we want to pass db.users.select through to users._mapper.select
-class TableClassType(type):
- def insert(cls, **kwargs):
- o = cls()
- o.__dict__.update(kwargs)
- return o
- def __getattr__(cls, attr):
- if attr == '_mapper':
- # called during mapper init
- raise AttributeError()
- return getattr(cls._mapper, attr)
-
-def class_for_table(table):
- klass = TableClassType('Class_' + table.name.capitalize(), (object,), {})
- def __repr__(self):
- import locale
- encoding = locale.getdefaultlocale()[1]
- L = []
- for k in self.__class__.c.keys():
- value = getattr(self, k, '')
- if isinstance(value, unicode):
- value = value.encode(encoding)
- L.append("%s=%r" % (k, value))
- return '%s(%s)' % (self.__class__.__name__, ','.join(L))
- klass.__repr__ = __repr__
- klass._mapper = mapper(klass, table)
- return klass
-
-class SqlSoup:
- def __init__(self, *args, **kwargs):
- """
- args may either be an SQLEngine or a set of arguments suitable
- for passing to create_engine
- """
- from sqlalchemy.sql import Engine
- # meh, sometimes having method overloading instead of kwargs would be easier
- if isinstance(args[0], Engine):
- engine = args.pop(0)
- if args or kwargs:
- raise ArgumentError('Extra arguments not allowed when engine is given')
- else:
- engine = create_engine(*args, **kwargs)
- self._engine = engine
- self._cache = {}
- def delete(self, *args, **kwargs):
- objectstore.delete(*args, **kwargs)
- def commit(self):
- objectstore.get_session().commit()
- def rollback(self):
- objectstore.clear()
- def _reset(self):
- # for debugging
- self._cache = {}
- self.rollback()
- def __getattr__(self, attr):
- try:
- t = self._cache[attr]
- except KeyError:
- table = Table(attr, self._engine, autoload=True)
- if table.columns:
- t = class_for_table(table)
- else:
- t = None
- self._cache[attr] = t
- if not t:
- raise NoSuchTableError('%s does not exist' % attr)
- return t
+from sqlalchemy import *
+
+"""
+SqlSoup provides a convenient way to access database tables without having
+to declare table or mapper classes ahead of time.
+
+Suppose we have a database with users, books, and loans tables
+(corresponding to the PyWebOff dataset, if you're curious).
+For testing purposes, we can create this db as follows:
+
+>>> from sqlalchemy import create_engine
+>>> e = create_engine('sqlite://filename=:memory:')
+>>> for sql in _testsql: e.execute(sql)
+...
+
+Creating a SqlSoup gateway is just like creating an SqlAlchemy engine:
+>>> from sqlalchemy.ext.sqlsoup import SqlSoup
+>>> soup = SqlSoup('sqlite://filename=:memory:')
+
+or, you can re-use an existing engine:
+>>> soup = SqlSoup(e)
+
+Loading objects is as easy as this:
+>>> users = soup.users.select()
+>>> users.sort()
+>>> users
+[Class_Users(name='Bhargan Basepair',email='basepair@example.edu',password='basepair',classname=None,admin=1), Class_Users(name='Joe Student',email='student@example.edu',password='student',classname=None,admin=0)]
+
+Of course, letting the database do the sort is better (".c" is short for ".columns"):
+>>> soup.users.select(order_by=[soup.users.c.name])
+[Class_Users(name='Bhargan Basepair',email='basepair@example.edu',password='basepair',classname=None,admin=1),
+ Class_Users(name='Joe Student',email='student@example.edu',password='student',classname=None,admin=0)]
+
+Field access is intuitive:
+>>> users[0].email
+u'basepair@example.edu'
+
+Of course, you don't want to load all users very often. The common case is to
+select by a key or other field:
+>>> soup.users.selectone_by(name='Bhargan Basepair')
+Class_Users(name='Bhargan Basepair',email='basepair@example.edu',password='basepair',classname=None,admin=1)
+
+All the SqlAlchemy mapper select variants (select, select_by, selectone, selectone_by, selectfirst, selectfirst_by)
+are available. See the SqlAlchemy documentation for details:
+http://www.sqlalchemy.org/docs/sqlconstruction.myt
+
+Modifying objects is intuitive:
+>>> user = _
+>>> user.email = 'basepair+nospam@example.edu'
+>>> soup.commit()
+
+(SqlSoup leverages the sophisticated SqlAlchemy unit-of-work code, so
+multiple updates to a single object will be turned into a single UPDATE
+statement when you commit.)
+
+Finally, insert and delete. Let's insert a new loan, then delete it:
+>>> soup.loans.insert(book_id=soup.books.selectfirst().id, user_name=user.name)
+Class_Loans(book_id=1,user_name='Bhargan Basepair',loan_date=None)
+>>> soup.commit()
+
+>>> loan = soup.loans.selectone_by(book_id=1, user_name='Bhargan Basepair')
+>>> soup.delete(loan)
+>>> soup.commit()
+"""
+
+_testsql = """
+CREATE TABLE books (
+ id integer PRIMARY KEY, -- auto-SERIAL in sqlite
+ title text NOT NULL,
+ published_year char(4) NOT NULL,
+ authors text NOT NULL
+);
+
+CREATE TABLE users (
+ name varchar(32) PRIMARY KEY,
+ email varchar(128) NOT NULL,
+ password varchar(128) NOT NULL,
+ classname text,
+ admin int NOT NULL -- 0 = false
+);
+
+CREATE TABLE loans (
+ book_id int PRIMARY KEY REFERENCES books(id),
+ user_name varchar(32) references users(name)
+ ON DELETE SET NULL ON UPDATE CASCADE,
+ loan_date date NOT NULL DEFAULT current_timestamp
+);
+
+insert into users(name, email, password, admin)
+values('Bhargan Basepair', 'basepair@example.edu', 'basepair', 1);
+insert into users(name, email, password, admin)
+values('Joe Student', 'student@example.edu', 'student', 0);
+
+insert into books(title, published_year, authors)
+values('Mustards I Have Known', '1989', 'Jones');
+insert into books(title, published_year, authors)
+values('Regional Variation in Moss', '1971', 'Flim and Flam');
+
+insert into loans(book_id, user_name)
+values (
+ (select min(id) from books),
+ (select name from users where name like 'Joe%'))
+;
+""".split(';')
+
+__all__ = ['NoSuchTableError', 'SqlSoup']
+
+class NoSuchTableError(SQLAlchemyError): pass
+
+# metaclass is necessary to expose class methods with getattr, e.g.
+# we want to pass db.users.select through to users._mapper.select
+class TableClassType(type):
+ def insert(cls, **kwargs):
+ o = cls()
+ o.__dict__.update(kwargs)
+ return o
+ def __getattr__(cls, attr):
+ if attr == '_mapper':
+ # called during mapper init
+ raise AttributeError()
+ return getattr(cls._mapper, attr)
+
+def class_for_table(table):
+ klass = TableClassType('Class_' + table.name.capitalize(), (object,), {})
+ def __repr__(self):
+ import locale
+ encoding = locale.getdefaultlocale()[1]
+ L = []
+ for k in self.__class__.c.keys():
+ value = getattr(self, k, '')
+ if isinstance(value, unicode):
+ value = value.encode(encoding)
+ L.append("%s=%r" % (k, value))
+ return '%s(%s)' % (self.__class__.__name__, ','.join(L))
+ klass.__repr__ = __repr__
+ klass._mapper = mapper(klass, table)
+ return klass
+
+class SqlSoup:
+ def __init__(self, *args, **kwargs):
+ """
+ args may either be an SQLEngine or a set of arguments suitable
+ for passing to create_engine
+ """
+ from sqlalchemy.engine import SQLEngine
+ # meh, sometimes having method overloading instead of kwargs would be easier
+ if isinstance(args[0], SQLEngine):
+ args = list(args)
+ engine = args.pop(0)
+ if args or kwargs:
+ raise ArgumentError('Extra arguments not allowed when engine is given')
+ else:
+ engine = create_engine(*args, **kwargs)
+ self._engine = engine
+ self._cache = {}
+ def delete(self, *args, **kwargs):
+ objectstore.delete(*args, **kwargs)
+ def commit(self):
+ objectstore.get_session().commit()
+ def rollback(self):
+ objectstore.clear()
+ def _reset(self):
+ # for debugging
+ self._cache = {}
+ self.rollback()
+ def __getattr__(self, attr):
+ try:
+ t = self._cache[attr]
+ except KeyError:
+ table = Table(attr, self._engine, autoload=True)
+ if table.columns:
+ t = class_for_table(table)
+ else:
+ t = None
+ self._cache[attr] = t
+ if not t:
+ raise NoSuchTableError('%s does not exist' % attr)
+ return t
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/lib/sqlalchemy/mods/threadlocal.py b/lib/sqlalchemy/mods/threadlocal.py
index b67329612..6f122a409 100644
--- a/lib/sqlalchemy/mods/threadlocal.py
+++ b/lib/sqlalchemy/mods/threadlocal.py
@@ -29,18 +29,15 @@ class Objectstore(SessionContext):
def assign_mapper(class_, *args, **kwargs):
assignmapper.assign_mapper(objectstore, class_, *args, **kwargs)
-def _mapper_extension():
- return SessionContext._get_mapper_extension(objectstore)
-
objectstore = Objectstore(Session)
def install_plugin():
sqlalchemy.objectstore = objectstore
- global_extensions.append(_mapper_extension)
+ global_extensions.append(objectstore.mapper_extension)
engine.default_strategy = 'threadlocal'
sqlalchemy.assign_mapper = assign_mapper
def uninstall_plugin():
engine.default_strategy = 'plain'
- global_extensions.remove(_mapper_extension)
+ global_extensions.remove(objectstore.mapper_extension)
install_plugin()
diff --git a/setup.py b/setup.py
index 690c945e4..1797dd3df 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ use_setuptools()
from setuptools import setup, find_packages
setup(name = "SQLAlchemy",
- version = "0.1.6",
+ version = "0.2.0alpha",
description = "Database Abstraction Library",
author = "Mike Bayer",
author_email = "mike_mp@zzzcomputing.com",