Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'unstable/2.0'

Conflicts:
	peewee.py
	tests.py
  • Loading branch information...
commit 755c899e545f0f34427e69c0a4dd8c869908fd0c 2 parents b76f80e + 7af7076
@coleifer authored
View
5 TODO.rst
@@ -1,5 +1,6 @@
todo
====
-* investigate fetching the optimal number of results from the cursor
- as opposed to pulling them off one at a time
+* backwards compat, esp places where existing api allows strings
+* stronger input validation?
+* docs
View
8 bench/peewee_bench/bench.py
@@ -24,26 +24,26 @@ def create_entry(blog, title, content, pub_date=None):
def list_users(ordered=False):
if ordered:
- sq = User.select().order_by('username')
+ sq = User.select().order_by(User.username.asc())
else:
sq = User.select()
return list(sq)
def list_blogs_select_related():
- qs = Blog.select({Blog: ['*'], User: ['*']}).join(User)
+ qs = Blog.select(Blog, User).join(User)
return list(qs)
def list_blogs_for_user(user):
return list(user.blog_set)
def list_entries_by_user(user):
- return list(Entry.select().join(Blog).where(user=user))
+ return list(Entry.select().join(Blog).where(Blog.user == user))
def get_user_count():
return User.select().count()
def list_entries_subquery(user):
- return list(Entry.select().where(blog__in=Blog.select().where(user=user)))
+ return list(Entry.select().where(Entry.blog << Blog.select().where(Blog.user == user)))
def get_user(username):
return User.get(username=username)
View
2  bench/peewee_bench/models.py
@@ -1,7 +1,7 @@
import peewee
-test_db = peewee.Database(peewee.SqliteAdapter(), 'test_pw.db')
+test_db = peewee.SqliteDatabase('test_pw.db')
class User(peewee.Model):
username = peewee.CharField()
View
4 docs/conf.py
@@ -48,9 +48,9 @@
# built documents.
#
# The short X.Y version.
-version = '1.0.0'
+version = '2.0.0'
# The full version, including alpha/beta/rc tags.
-release = '1.0.0'
+release = '2.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
View
5 docs/index.rst
@@ -10,11 +10,13 @@ peewee
* written in python
* provides a lightweight querying interface over sql
* uses sql concepts when querying, like joins and where clauses
-* support for special extensions like `hstore <http://peewee.readthedocs.org/en/latest/peewee/playhouse.html#postgresql-extensions-hstore-ltree>`_ and `full-text search <http://peewee.readthedocs.org/en/latest/peewee/playhouse.html#full-text-search>`_
+* support for some extensions, like hstore
For flask integration, including an admin interface and RESTful API, check
out `flask-peewee <https://github.com/coleifer/flask-peewee/>`_.
+See notes on :ref:`notes on upgrading and changes from 1.0 <upgrading>`
+
Contents:
---------
@@ -24,6 +26,7 @@ Contents:
peewee/overview
peewee/installation
+ peewee/upgrading
peewee/cookbook
peewee/example
peewee/models
View
465 docs/peewee/cookbook.rst
@@ -10,19 +10,16 @@ Examples will use the following models:
.. code-block:: python
- import peewee
-
- class Blog(peewee.Model):
- creator = peewee.CharField()
- name = peewee.CharField()
+ from peewee import *
+ class User(Model):
+ username = CharField()
- class Entry(peewee.Model):
- blog = peewee.ForeignKeyField(Blog)
- title = peewee.CharField()
- body = peewee.TextField()
- pub_date = peewee.DateTimeField()
- published = peewee.BooleanField(default=True)
+ class Tweet(Model):
+ user = ForeignKeyField(User, related_name='tweets')
+ message = TextField()
+ created_date = DateTimeField(default=datetime.datetime.now)
+ is_published = BooleanField(default=True)
Database and Connection Recipes
@@ -58,10 +55,10 @@ models from each):
.. code-block:: python
- >>> custom_db = peewee.SqliteDatabase('custom.db')
+ >>> custom_db = SqliteDatabase('custom.db')
- >>> class CustomModel(peewee.Model):
- ... whatev = peewee.CharField()
+ >>> class CustomModel(Model):
+ ... whatev = CharField()
...
... class Meta:
... database = custom_db
@@ -76,17 +73,16 @@ you wish to use, and then all your models will extend it:
.. code-block:: python
- custom_db = peewee.SqliteDatabase('custom.db')
+ custom_db = SqliteDatabase('custom.db')
- class CustomModel(peewee.Model):
+ class CustomModel(Model):
class Meta:
database = custom_db
- class Blog(CustomModel):
- creator = peewee.CharField()
- name = peewee.TextField()
+ class User(CustomModel):
+ username = CharField()
- class Entry(CustomModel):
+ class Tweet(CustomModel):
# etc, etc
.. note:: Remember to specify a database in a model class (or its parent class),
@@ -100,16 +96,16 @@ Point models at an instance of :py:class:`PostgresqlDatabase`.
.. code-block:: python
- psql_db = peewee.PostgresqlDatabase('my_database', user='code')
+ psql_db = PostgresqlDatabase('my_database', user='code')
- class PostgresqlModel(peewee.Model):
+ class PostgresqlModel(Model):
"""A base model that will use our Postgresql database"""
class Meta:
database = psql_db
- class Blog(PostgresqlModel):
- creator = peewee.CharField()
+ class User(PostgresqlModel):
+ username = CharField()
# etc, etc
@@ -120,16 +116,16 @@ Point models at an instance of :py:class:`MySQLDatabase`.
.. code-block:: python
- mysql_db = peewee.MySQLDatabase('my_database', user='code')
+ mysql_db = MySQLDatabase('my_database', user='code')
- class MySQLModel(peewee.Model):
+ class MySQLModel(Model):
"""A base model that will use our MySQL database"""
class Meta:
database = mysql_db
- class Blog(MySQLModel):
- creator = peewee.CharField()
+ class User(MySQLModel):
+ username = CharField()
# etc, etc
@@ -168,9 +164,9 @@ of the database by passing in ``None`` as the database_name.
.. code-block:: python
- deferred_db = peewee.SqliteDatabase(None)
+ deferred_db = SqliteDatabase(None)
- class SomeModel(peewee.Model):
+ class SomeModel(Model):
class Meta:
database = deferred_db
@@ -199,10 +195,10 @@ Creating a new record
You can use the :py:meth:`Model.create` method on the model:
-.. code-block:: python
+.. code-block:: pycon
- >>> Blog.create(creator='Charlie', name='My Blog')
- <__main__.Blog object at 0x2529350>
+ >>> User.create(username='Charlie')
+ <__main__.User object at 0x2529350>
This will ``INSERT`` a new row into the database. The primary key will automatically
be retrieved and stored on the model instance.
@@ -210,14 +206,13 @@ be retrieved and stored on the model instance.
Alternatively, you can build up a model instance programmatically and then
save it:
-.. code-block:: python
+.. code-block:: pycon
- >>> blog = Blog()
- >>> blog.creator = 'Chuck'
- >>> blog.name = 'Another blog'
- >>> blog.save()
- >>> blog.id
- 2
+ >>> user = User()
+ >>> user.username = 'Charlie'
+ >>> user.save()
+ >>> user.id
+ 1
See also :py:meth:`Model.save`, :py:meth:`Model.insert` and :py:class:`InsertQuery`
@@ -228,22 +223,22 @@ Updating existing records
Once a model instance has a primary key, any attempt to re-save it will result
in an ``UPDATE`` rather than another ``INSERT``:
-.. code-block:: python
+.. code-block:: pycon
- >>> blog.save()
- >>> blog.id
- 2
- >>> blog.save()
- >>> blog.id
- 2
+ >>> user.save()
+ >>> user.id
+ 1
+ >>> user.save()
+ >>> user.id
+ 1
If you want to update multiple records, issue an ``UPDATE`` query. The following
example will update all ``Entry`` objects, marking them as "published", if their
pub_date is less than today's date.
-.. code-block:: python
+.. code-block:: pycon
- >>> update_query = Entry.update(published=True).where(pub_date__lt=datetime.today())
+ >>> update_query = Tweet.update(is_published=True).where(Tweet.creation_date < datetime.today())
>>> update_query.execute()
4 # <--- number of rows updated
@@ -256,21 +251,25 @@ Deleting a record
To delete a single model instance, you can use the :py:meth:`Model.delete_instance`
shortcut:
- >>> blog = Blog.get(id=1)
- >>> blog.delete_instance()
+.. code-block:: pycon
+
+ >>> user = User.get(User.id == 1)
+ >>> user.delete_instance()
1 # <--- number of rows deleted
- >>> Blog.get(id=1)
- BlogDoesNotExist: instance matching query does not exist:
- SQL: SELECT "id", "creator", "name" FROM "blog" WHERE "id" = ? LIMIT 1
+ >>> User.get(User.id == 1)
+ UserDoesNotExist: instance matching query does not exist:
+ SQL: SELECT t1."id", t1."username" FROM "user" AS t1 WHERE t1."id" = ?
PARAMS: [1]
To delete an arbitrary group of records, you can issue a ``DELETE`` query. The
-following will delete all ``Entry`` objects that are a year old.
+following will delete all ``Tweet`` objects that are a year old.
- >>> delete_query = Entry.delete().where(pub_date__lt=one_year_ago)
+.. code-block:: pycon
+
+ >>> delete_query = Tweet.delete().where(Tweet.pub_date < one_year_ago)
>>> delete_query.execute()
- 7 # <--- number of entries deleted
+ 7 # <--- number of rows deleted
For more information, see the documentation on :py:class:`DeleteQuery`.
@@ -279,27 +278,27 @@ Selecting a single record
^^^^^^^^^^^^^^^^^^^^^^^^^
You can use the :py:meth:`Model.get` method to retrieve a single instance matching
-the given query (passed in as a mix of :py:class:`Q` objects and keyword arguments).
+the given query.
This method is a shortcut that calls :py:meth:`Model.select` with the given query,
but limits the result set to 1. Additionally, if no model matches the given query,
a ``DoesNotExist`` exception will be raised.
-.. code-block:: python
+.. code-block:: pycon
- >>> Blog.get(id=1)
+ >>> User.get(User.id == 1)
<__main__.Blog object at 0x25294d0>
- >>> Blog.get(id=1).name
- u'My Blog'
+ >>> User.get(User.id == 1).username
+ u'Charlie'
- >>> Blog.get(creator='Chuck')
+ >>> User.get(User.username == 'Charlie')
<__main__.Blog object at 0x2529410>
- >>> Blog.get(id=1000)
- BlogDoesNotExist: instance matching query does not exist:
- SQL: SELECT "id", "creator", "name" FROM "blog" WHERE "id" = ? LIMIT 1
- PARAMS: [1000]
+ >>> User.get(User.username == 'nobody')
+ UserDoesNotExist: instance matching query does not exist:
+ SQL: SELECT t1."id", t1."username" FROM "user" AS t1 WHERE t1."username" = ?
+ PARAMS: ['nobody']
For more information see notes on :py:class:`SelectQuery` and :ref:`querying` in general.
@@ -309,13 +308,13 @@ Selecting multiple records
To simply get all instances in a table, call the :py:meth:`Model.select` method:
-.. code-block:: python
+.. code-block:: pycon
- >>> for blog in Blog.select():
- ... print blog.name
+ >>> for user in User.select():
+ ... print user.username
...
- My Blog
- Another blog
+ Charlie
+ Peewee Herman
When you iterate over a :py:class:`SelectQuery`, it will automatically execute
it and start returning results from the database cursor. Subsequent iterations
@@ -323,124 +322,148 @@ of the same query will not hit the database as the results are cached.
Another useful note is that you can retrieve instances related by :py:class:`ForeignKeyField`
by iterating. To get all the related instances for an object, you can query the related name.
-Looking at the example models, we have Blogs and Entries. Entry has a foreign key to Blog,
-meaning that any given blog may have 0..n entries. A blog's related entries are exposed
+Looking at the example models, we have Users and Tweets. Tweet has a foreign key to User,
+meaning that any given user may have 0..n tweets. A user's related tweets are exposed
using a :py:class:`SelectQuery`, and can be iterated the same as any other SelectQuery:
-.. code-block:: python
+.. code-block:: pycon
- >>> for entry in blog.entry_set:
- ... print entry.title
+ >>> for tweet in user.tweets:
+ ... print tweet.message
...
- entry 1
- entry 2
- entry 3
- entry 4
+ hello world
+ this is fun
+ look at this picture of my food
-The ``entry_set`` attribute is just another select query and any methods available
+The ``tweets`` attribute is just another select query and any methods available
to :py:class:`SelectQuery` are available:
- >>> for entry in blog.entry_set.order_by(('pub_date', 'desc')):
- ... print entry.title
+.. code-block:: pycon
+
+ >>> for tweet in user.tweets.order_by(Tweet.created_date.desc()):
+ ... print tweet.message
...
- entry 4
- entry 3
- entry 2
- entry 1
+ look at this picture of my food
+ this is fun
+ hello world
Filtering records
^^^^^^^^^^^^^^^^^
-.. code-block:: python
+You can filter for particular records using normal python operators.
- >>> for entry in Entry.select().where(blog=blog, published=True):
- ... print '%s: %s (%s)' % (entry.blog.name, entry.title, entry.published)
+.. code-block:: pycon
+
+ >>> user = User.get(User.username == 'Charlie')
+ >>> for tweet in Tweet.select().where(Tweet.user == user, Tweet.is_published == True):
+ ... print '%s: %s (%s)' % (tweet.user.username, tweet.message)
...
- My Blog: Some Entry (True)
- My Blog: Another Entry (True)
+ Charlie: hello world
+ Charlie: this is fun
- >>> for entry in Entry.select().where(pub_date__lt=datetime.datetime(2011, 1, 1)):
- ... print entry.title, entry.pub_date
+ >>> for tweet in Tweet.select().where(Tweet.created_date < datetime.datetime(2011, 1, 1)):
+ ... print tweet.message, tweet.created_date
...
- Old entry 2010-01-01 00:00:00
+ Really old tweet 2010-01-01 00:00:00
You can also filter across joins:
-.. code-block:: python
+.. code-block:: pycon
- >>> for entry in Entry.select().join(Blog).where(name='My Blog'):
- ... print entry.title
- Old entry
- Some Entry
- Another Entry
+ >>> for tweet in Tweet.select().join(User).where(User.username == 'Charlie'):
+ ... print tweet.message
+ hello world
+ this is fun
+ look at this picture of my food
-If you are already familiar with Django's ORM, you can use the "double underscore"
-syntax:
+If you want to express a complex query, use parentheses and python's "or" and "and"
+operators:
-.. code-block:: python
+.. code-block:: pycon
- >>> for entry in Entry.filter(blog__name='My Blog'):
- ... print entry.title
- Old entry
- Some Entry
- Another Entry
+ >>> Tweet.select().join(User).where(
+ ... (User.username == 'Charlie') |
+ ... (User.username == 'Peewee Herman')
+ ... )
-If you prefer, you can use python operators to query:
+Check out :ref:`the table of query operations <column-lookups>` to see what types of
+queries are possible.
-.. code-block:: python
+.. note::
- >>> for entry in Entry.select().join(Blog).where(Blog.name=='My Blog')
- ... print entry.title
+ A lot of fun things can go in the where clause of a query, such as:
-To perform OR lookups, use the special :py:class:`Q` object. These work in
-both calls to ``filter()`` and ``where()``:
+ * a field expression, e.g. ``User.username == 'Charlie'``
+ * a function expression, e.g. ``fn.Lower(fn.Substr(User.username, 1, 1)) == 'a'``
+ * a comparison of one column to another, e.g. ``Employee.salary < (Employee.tenure * 1000) + 40000``
-.. code-block:: python
+ You can also nest queries, for example tweets by users whose username starts with "a":
- >>> User.filter(Q(staff=True) | Q(superuser=True)) # get staff or superusers
+ .. code-block:: python
-To perform lookups against *another column* in a given row, use the :py:class:`F` object:
+ # the "<<" operator signifies an "IN" query
+ Tweet.select().where(
+ Tweet.user << User.select().where(fn.Lower(fn.Substr(User.username, 1, 1)) == 'a')
+ )
-.. code-block:: python
+.. note::
+ If you are already familiar with Django's ORM, you can use the "double underscore"
+ syntax using the :py:meth:`SelectQuery.filter` method:
- >>> Employee.filter(salary__lt=F('desired_salary'))
+ .. code-block:: python
+ >>> for tweet in Tweet.filter(user__username='Charlie'):
+ ... print tweet.message
+ hello world
+ this is fun
+ look at this picture of my food
-Check :ref:`the docs <query_compare>` for more examples of querying.
+ To perform OR lookups, use the special :py:class:`DQ` object:
+
+ .. code-block:: python
+
+ >>> User.filter(DQ(username='Charlie') | DQ(username='Peewee Herman'))
+
+.. warning::
+ The *Zen of Python* says "There should be one-- and preferably only one --obvious way to do it."
+ The django-style filtering is supported for backwards compatibility with 1.0, so if you can, its
+ probably best not to use it.
+
+Check :ref:`the docs <query_compare>` for some more example queries.
Sorting records
^^^^^^^^^^^^^^^
-.. code-block:: python
+.. code-block:: pycon
- >>> for e in Entry.select().order_by('pub_date'):
- ... print e.pub_date
+ >>> for t in Tweet.select().order_by(Tweet.created_date):
+ ... print t.pub_date
...
2010-01-01 00:00:00
2011-06-07 14:08:48
2011-06-07 14:12:57
- >>> for e in Entry.select().order_by(peewee.desc('pub_date')):
- ... print e.pub_date
+ >>> for t in Tweet.select().order_by(Tweet.created_date.desc()):
+ ... print t.pub_date
...
2011-06-07 14:12:57
2011-06-07 14:08:48
2010-01-01 00:00:00
You can also order across joins. Assuming you want
-to order entries by the name of the blog, then by pubdate desc:
+to order tweets by the username of the author, then by created_date:
-.. code-block:: python
+.. code-block:: pycon
- >>> qry = Entry.select().join(Blog).order_by(
- ... (Blog, 'name'),
- ... (Entry, 'pub_date', 'DESC'),
- ... )
+ >>> qry = Tweet.select().join(User).order_by(User.username, Tweet.created_date.desc())
- >>> qry.sql()
- ('SELECT t1.* FROM entry AS t1 INNER JOIN blog AS t2 ON t1.blog_id = t2.id ORDER BY t2.name ASC, t1.pub_date DESC', [])
+.. code-block:: sql
+ -- generates --
+ SELECT t1."id", t1."user_id", t1."message", t1."is_published", t1."created_date"
+ FROM "tweet" AS t1 INNER JOIN "user" AS t2 ON t1."user_id" = t2."id"
+ ORDER BY t2."username", t1."created_date" DESC
Paginating records
@@ -449,21 +472,21 @@ Paginating records
The paginate method makes it easy to grab a "page" or records -- it takes two
parameters, `page_number`, and `items_per_page`:
-.. code-block:: python
+.. code-block:: pycon
- >>> for entry in Entry.select().order_by('id').paginate(2, 10):
- ... print entry.title
+ >>> for tweet in Tweet.select().order_by(Tweet.id).paginate(2, 10):
+ ... print tweet.message
...
- entry 10
- entry 11
- entry 12
- entry 13
- entry 14
- entry 15
- entry 16
- entry 17
- entry 18
- entry 19
+ tweet 10
+ tweet 11
+ tweet 12
+ tweet 13
+ tweet 14
+ tweet 15
+ tweet 16
+ tweet 17
+ tweet 18
+ tweet 19
Counting records
@@ -473,9 +496,9 @@ You can count the number of rows in any select query:
.. code-block:: python
- >>> Entry.select().count()
+ >>> Tweet.select().count()
100
- >>> Entry.select().where(id__gt=50).count()
+ >>> Tweet.select().where(Tweet.id > 50).count()
50
@@ -515,47 +538,45 @@ query method. See the documentation for details on this optimization.
Performing atomic updates
^^^^^^^^^^^^^^^^^^^^^^^^^
-Use the special :py:class:`F` object to perform an atomic update:
-
.. code-block:: python
- >>> MessageCount.update(count=F('count') + 1).where(user=some_user)
+ >>> Stat.update(counter=Stat.counter + 1).where(Stat.url == request.url)
Aggregating records
^^^^^^^^^^^^^^^^^^^
-Suppose you have some blogs and want to get a list of them along with the count
-of entries in each. First I will show you the shortcut:
+Suppose you have some users and want to get a list of them along with the count
+of tweets in each. First I will show you the shortcut:
.. code-block:: python
- query = Blog.select().annotate(Entry)
+ query = User.select().annotate(Tweet)
This is equivalent to the following:
.. code-block:: python
- query = Blog.select({
- Blog: ['*'],
- Entry: [Count('id')],
- }).group_by(Blog).join(Entry)
+ query = User.select(
+ User, fn.Count(Tweet.id).alias('count')
+ ).join(Tweet).group_by(User)
-The resulting query will return Blog objects with all their normal attributes
-plus an additional attribute 'count' which will contain the number of entries.
+
+The resulting query will return User objects with all their normal attributes
+plus an additional attribute 'count' which will contain the number of tweets.
By default it uses an inner join if the foreign key is not nullable, which means
blogs without entries won't appear in the list. To remedy this, manually specify
-the type of join to include blogs with 0 entries:
+the type of join to include users with 0 tweets:
.. code-block:: python
- query = Blog.select().join(Entry, 'left outer').annotate(Entry)
+ query = User.select().join(Tweet, JOIN_LEFT_OUTER).annotate(Tweet)
You can also specify a custom aggregator:
.. code-block:: python
- query = Blog.select().annotate(Entry, peewee.Max('pub_date', 'max_pub_date'))
+ query = User.select().annotate(Tweet, fn.Max(Tweet.created_date).alias('latest'))
Let's assume you have a tagging application and want to find tags that have a
certain number of related objects. For this example we'll use some different
@@ -577,36 +598,26 @@ Now say we want to find tags that have at least 5 photos associated with them:
.. code-block:: python
- >>> Tag.select().join(PhotoTag).join(Photo).group_by(Tag).having('count(*) > 5').sql()
+ >>> Tag.select().join(PhotoTag).join(Photo).group_by(Tag).having(fn.Count(Photo.id) > 5)
+
+Yields the following:
+
+.. code-block:: sql
SELECT t1."id", t1."name"
FROM "tag" AS t1
- INNER JOIN "phototag" AS t2
- ON t1."id" = t2."tag_id"
- INNER JOIN "photo" AS t3
- ON t2."photo_id" = t3."id"
- GROUP BY
- t1."id", t1."name"
- HAVING count(*) > 5
+ INNER JOIN "phototag" AS t2 ON t1."id" = t2."tag_id"
+ INNER JOIN "photo" AS t3 ON t2."photo_id" = t3."id"
+ GROUP BY t1."id", t1."name"
+ HAVING Count(t3."id") > 5
Suppose we want to grab the associated count and store it on the tag:
.. code-block:: python
- >>> Tag.select({
- ... Tag: ['*'],
- ... Photo: [Count('id', 'count')]
- ... }).join(PhotoTag).join(Photo).group_by(Tag).having('count(*) > 5').sql()
-
- SELECT t1."id", t1."name", COUNT(t3."id") AS count
- FROM "tag" AS t1
- INNER JOIN "phototag" AS t2
- ON t1."id" = t2."tag_id"
- INNER JOIN "photo" AS t3
- ON t2."photo_id" = t3."id"
- GROUP BY
- t1."id", t1."name"
- HAVING count(*) > 5
+ >>> Tag.select(
+ ... Tag, fn.Count(Photo.id).alias('count')
+ ... ).join(PhotoTag).join(Photo).group_by(Tag).having(fn.Count(Photo.id) > 5)
SQL Functions, Subqueries and "Raw expressions"
@@ -614,49 +625,33 @@ SQL Functions, Subqueries and "Raw expressions"
Suppose you need to want to get a list of all users whose username begins with "a".
There are a couple ways to do this, but one method might be to use some SQL functions
-like ``LOWER`` and ``SUBSTR``. To use arbitrary SQL functions, use the special :py:class:`R`
-object to construct queries:
+like ``LOWER`` and ``SUBSTR``. To use arbitrary SQL functions, use the special :py:func:`fn`
+function to construct queries:
.. code-block:: python
# select the users' id, username and the first letter of their username, lower-cased
- query = User.select(['id', 'username', R('LOWER(SUBSTR(username, 1, 1))', 'first_letter')])
+ query = User.select(User, fn.Lower(fn.Substr(User.username, 1, 1)).alias('first_letter'))
- # now filter this list to include only users whose username begins with "a"
- a_users = query.where(R('first_letter=%s', 'a'))
+ # alternatively we could select only users whose username begins with 'a'
+ a_users = User.select().where(fn.Lower(fn.Substr(User.username, 1, 1)) == 'a')
>>> for user in a_users:
- ... print user.first_letter, user.username
-
-This same functionality could be easily exposed as part of the where clause, the
-only difference being that the first letter is not selected and therefore not an
-attribute of the model instance:
-
-.. code-block:: python
-
- a_users = User.filter(R('LOWER(SUBSTR(username, 1, 1)) = %s', 'a'))
+ ... print user.username
-We can write subqueries as part of a :py:class:`SelectQuery`, for example counting
-the number of entries on a blog:
+There are times when you may want to simply pass in some arbitrary sql. You can do
+this using the special :py:class:`R` class. One use-case is when referencing an
+alias:
.. code-block:: python
- entry_query = R('(SELECT COUNT(*) FROM entry WHERE entry.blog_id=blog.id)', 'entry_count')
- blogs = Blog.select(['id', 'name', entry_query]).order_by(('entry_count', 'desc'))
+ # we'll query the user table and annotate it with a count of tweets for
+ # the given user
+ query = User.select(User, fn.Count(Tweet.id).alias('ct')).join(Tweet).group_by(User)
- for blog in blogs:
- print blog.title, blog.entry_count
+ # now we will order by the count, which was aliased to "ct"
+ query = query.order_by(R('ct'))
-It is also possible to use subqueries as part of a where clause, for example finding
-blogs that have no entries:
-
-.. code-block:: python
-
- no_entry_query = R('NOT EXISTS (SELECT * FROM entry WHERE entry.blog_id=blog.id)')
- blogs = Blog.filter(no_entry_query)
-
- for blog in blogs:
- print blog.name, ' has no entries'
.. _working_with_transactions:
@@ -674,7 +669,7 @@ which will issue a commit if all goes well, or a rollback if an exception is rai
db = SqliteDatabase(':memory:')
with db.transaction():
- blog.delete_instance(recursive=True) # delete blog and associated entries
+ user.delete_instance(recursive=True) # delete user and associated tweets
Decorator
@@ -688,8 +683,8 @@ decorator:
db = SqliteDatabase(':memory:')
@db.commit_on_success
- def delete_blog(blog):
- blog.delete_instance(recursive=True)
+ def delete_user(user):
+ user.delete_instance(recursive=True)
Changing autocommit behavior
@@ -703,7 +698,7 @@ context manager and decorator:
db.set_autocommit(False)
try:
- blog.delete_instance(recursive=True)
+ user.delete_instance(recursive=True)
except:
db.rollback()
raise
@@ -720,7 +715,7 @@ off when instantiating your database:
db = SqliteDatabase(':memory:', autocommit=False)
- Blog.create(name='foo blog')
+ User.create(username='somebody')
db.commit()
@@ -737,11 +732,10 @@ you can override the default ``column_class`` of the :py:class:`PrimaryKeyField`
.. code-block:: python
- from peewee import Model, PrimaryKeyField, VarCharColumn
+ from peewee import *
class UUIDModel(Model):
- # explicitly declare a primary key field, and specify the class to use
- id = PrimaryKeyField(column_class=VarCharColumn)
+ id = CharField(primary_key=True)
inst = UUIDModel(id=str(uuid.uuid4()))
@@ -754,7 +748,7 @@ you can override the default ``column_class`` of the :py:class:`PrimaryKeyField`
.. note::
Any foreign keys to a model with a non-integer primary key will have the
- ``ForeignKeyField`` use the same underlying column type as the primary key
+ ``ForeignKeyField`` use the same underlying storage type as the primary key
they are related to.
See full documentation on :ref:`non-integer primary keys <non_int_pks>`.
@@ -775,24 +769,24 @@ import:
User._meta.auto_increment = False # turn off auto incrementing IDs
with db.transaction():
for row in data:
- u = User(id=row[0], username=row[1], email=row[2])
+ u = User(id=row[0], username=row[1])
u.save(force_insert=True) # <-- force peewee to insert row
User._meta.auto_increment = True
-If you *always* want to have control over the primary key, you can use a different
-``column_class`` with the :py:class:`PrimaryKeyField`:
+If you *always* want to have control over the primary key, simply do not use
+the ``PrimaryKeyField`` type:
.. code-block:: python
class User(BaseModel):
- id = PrimaryKeyField(column_class=IntegerColumn)
+ id = IntegerField(primary_key=True)
username = CharField()
>>> u = User.create(id=999, username='somebody')
>>> u.id
999
- >>> User.get(username='somebody').id
+ >>> User.get(User.username == 'somebody').id
999
@@ -826,11 +820,4 @@ The generated code is written to stdout.
Schema migrations
-----------------
-Currently peewee does not have support for automatic schema migrations. Peewee
-does, however, come with a few helper functions:
-
-* :py:meth:`Database.add_column_sql`
-* :py:meth:`Database.rename_column_sql`
-* :py:meth:`Database.drop_column_sql`
-
-Honestly, your best bet is to script any migrations and use plain ol' SQL.
+Currently peewee does not have support for automatic schema migrations.
View
307 docs/peewee/database.rst
@@ -5,14 +5,14 @@ Databases
Below the :py:class:`Model` level, peewee uses an abstraction for representing the database. The
:py:class:`Database` is responsible for establishing and closing connections, making queries,
-and gathering information from the database.
+and gathering information from the database. The :py:class:`Database` encapsulates functionality
+specific to a given db driver. For example difference in column types across database engines,
+or support for certain features like sequences. The database is responsible for smoothing out
+the quirks of each backend driver to provide a consistent interface.
-The :py:class:`Database` in turn uses another abstraction called an :py:class:`BaseAdapter`, which
-is backend-specific and encapsulates functionality specific to a given db driver. Since there
-is some difference in column types across database engines, this information also resides
-in the adapter. The adapter is responsible for smoothing out the quirks of each database
-driver to provide a consistent interface, for example sqlite uses the question-mark "?" character
-for parameter interpolation, while all the other backends use "%s".
+The :py:class:`Database` also uses a subclass of :py:class:`QueryCompiler` to generate
+valid SQL. The QueryCompiler maps the internal data structures used by peewee to
+SQL statements.
For a high-level overview of working with transactions, check out the :ref:`transactions cookbook <working_with_transactions>`.
@@ -20,7 +20,7 @@ For notes on deferring instantiation of database, for example if loading configu
at run-time, see the notes on :ref:`deferring initialization <deferring_initialization>`.
.. note::
- The internals of the :py:class:`Database` and :py:class:`BaseAdapter` will be
+ The internals of the :py:class:`Database` and :py:class:`QueryCompiler` will be
of interest to anyone interested in adding support for another database driver.
@@ -41,101 +41,37 @@ sqlite3 driver, psycopg2 or the like. Peewee currently relies on a handful of p
* `Connection.rollback`
* `Cursor.description`
* `Cursor.fetchone`
-* `Cursor.fetchmany`
These methods are generally wrapped up in higher-level abstractions and exposed
-by the :py:class:`Database` and :py:class:`BaseAdapter`, so even if your driver doesn't
+by the :py:class:`Database`, so even if your driver doesn't
do these exactly you can still get a lot of mileage out of peewee. An example
-is the `apsw sqlite driver <http://code.google.com/p/apsw/>`_ which I'm tinkering with
-adding support for.
+is the `apsw sqlite driver <http://code.google.com/p/apsw/>`_ in the "playhouse"
+module.
-.. note:: In later versions of peewee, the db-api 2.0 methods may be further abstracted
- out to add support for drivers that don't conform to the spec.
-Getting down to it, writing some classes
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Starting out
+^^^^^^^^^^^^
-There are two classes you will want to implement at the very least:
-
-* :py:class:`BaseAdapter` - handles low-level functionality like opening and closing
- connections to the database, as well as describing the features provided by
- the database engine
-* :py:class:`Database` - higher-level interface that executes queries, manage
- transactions, and can introspect the underlying db.
-
-Let's say we want to add support for a fictitious "FooDB" which has an open-source
-python driver that uses the DB-API 2.0.
-
-The Adapter
-^^^^^^^^^^^
-
-The adapter provides a bridge between the driver and peewee's higher-level database
-class which is responsible for executing queries.
+The first thing is to provide a subclass of :py:class:`Database` that will open
+a connection.
.. code-block:: python
- from peewee import BaseAdapter
+ from peewee import Database
import foodb # our fictional driver
- class FooAdapter(BaseAdapter):
- def connect(self, database, **kwargs):
- return foodb.connect(database, **kwargs)
-
-
-Now we want to create a mapping that exposes the operations our database engine
-supports. These are the operations that a user perform when building out the
-``WHERE`` clause of a given query.
-
-.. code-block:: python
-
- class FooAdapter(BaseAdapter):
- operations = {
- 'lt': '< %s',
- 'lte': '<= %s',
- 'gt': '> %s',
- 'gte': '>= %s',
- 'eq': '= %s',
- 'ne': '!= %s',
- 'in': 'IN (%s)',
- 'is': 'IS %s',
- 'isnull': 'IS NULL',
- 'between': 'BETWEEN %s AND %s',
- 'icontains': 'ILIKE %s',
- 'contains': 'LIKE %s',
- 'istartswith': 'ILIKE %s',
- 'startswith': 'LIKE %s',
- }
-
- def connect(self, database, **kwargs):
+ class FooDatabase(Database):
+ def _connect(self, database, **kwargs):
return foodb.connect(database, **kwargs)
-Other things the adapter handles that are not covered here include:
-
-* last insert id and number of rows modified
-* specifying characters used for string interpolation and quoting identifiers,
- for instance, sqlite uses "?" for interpolation and MySQL uses a backtick for quoting
-* modifying user input for various lookup types, for instance a "LIKE" query will
- surround the incoming phrase with "%" characters.
-The database class
-^^^^^^^^^^^^^^^^^^
+Essential methods to override
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :py:class:`Database` provides a higher-level API and is responsible for executing queries,
-creating tables and indexes, and introspecting the database to get lists of tables.
-Each database must specify a :py:class:`BaseAdapter` subclass, so our database will
-need to specify the ``FooAdapter`` we just defined:
-
-.. code-block:: python
-
- from peewee import Database
-
- class FooDatabase(Database):
- def __init__(self, database, **connect_kwargs):
- super(FooDatabase, self).__init__(FooAdapter(), database, **connect_kwargs)
-
-
-This is the absolute minimum needed, though some features will not work -- for best
+creating tables and indexes, and introspecting the database to get lists of tables. The above
+implementation is the absolute minimum needed, though some features will not work -- for best
results you will want to additionally add a method for extracting a list of tables
and indexes for a table from the database. We'll pretend that ``FooDB`` is a lot like
MySQL and has special "SHOW" statements:
@@ -143,8 +79,8 @@ MySQL and has special "SHOW" statements:
.. code-block:: python
class FooDatabase(Database):
- def __init__(self, database, **connect_kwargs):
- super(FooDatabase, self).__init__(FooAdapter(), database, **connect_kwargs)
+ def _connect(self, database, **kwargs):
+ return foodb.connect(database, **kwargs)
def get_tables(self):
res = self.execute('SHOW TABLES;')
@@ -155,8 +91,15 @@ MySQL and has special "SHOW" statements:
rows = sorted([(r[2], r[1] == 0) for r in res.fetchall()])
return rows
-There is a good deal of functionality provided by the Database class that is not
-covered here. Refer to the documentation below or the `source code <https://github.com/coleifer/peewee/blob/master/peewee.py>`_. for details.
+
+Other things the database handles that are not covered here include:
+
+* last insert id and number of rows modified
+* specifying characters used for string interpolation and quoting identifiers,
+ for instance, sqlite uses "?" for interpolation and MySQL uses a backtick for quoting
+* mapping operations such as "LIKE/ILIKE" to their database equivalent
+
+Refer to the documentation below or the `source code <https://github.com/coleifer/peewee/blob/master/peewee.py>`_. for details.
.. note:: If your driver conforms to the db-api 2.0 spec, there shouldn't be
much work needed to get up and running.
@@ -196,9 +139,50 @@ Database and its subclasses
- execution of SQL queries
- creating and dropping tables and indexes
- .. py:method:: __init__(adapter, database[, threadlocals=False[, autocommit=True[, **connect_kwargs]]])
+ .. py:attribute:: compiler_class = QueryCompiler
+
+ A class suitable for compiling queries
+
+ .. py:attribute:: expr_overrides = {}
+
+ A mapping of expression codes to string operators
+
+ .. py:attribute:: field_overrides = {}
+
+ A mapping of field types to database column types, e.g. ``{'primary_key': 'SERIAL'}``
+
+ .. py:attribute:: for_update = False
+
+ Whether the given backend supports selecting rows for update
+
+ .. py:attribute:: interpolation = '%s'
+
+ The string used by the driver to interpolate query parameters
+
+ .. py:attribute:: op_overrides = {}
+
+ A mapping of operation codes to string operations, e.g. ``{OP_LIKE: 'LIKE BINARY'}``
+
+ .. py:attribute:: quote_char = '"'
+
+ The string used by the driver to quote names
+
+ .. py:attribute:: reserved_tables = []
+
+ Table names that are reserved by the backend -- if encountered in the
+ application a warning will be issued.
+
+ .. py:attribute:: sequences = False
+
+ Whether the given backend supports sequences
+
+ .. py:attribute:: subquery_delete_same_table = True
+
+ Whether the given backend supports deleting rows using a subquery
+ that selects from the same table
+
+ .. py:method:: __init__(database[, threadlocals=False[, autocommit=True[, **connect_kwargs]]])
- :param adapter: an instance of a :py:class:`BaseAdapter` subclass
:param database: the name of the database (or filename if using sqlite)
:param threadlocals: whether to store connections in a threadlocal
:param autocommit: automatically commit every query executed by calling :py:meth:`~Database.execute`
@@ -245,6 +229,10 @@ Database and its subclasses
:rtype: a cursor for executing queries
+ .. py:method:: get_compiler()
+
+ :rtype: an instance of :py:class:`QueryCompiler`
+
.. py:method:: set_autocommit(autocommit)
:param autocommit: a boolean value indicating whether to turn on/off autocommit
@@ -254,7 +242,12 @@ Database and its subclasses
:rtype: a boolean value indicating whether autocommit is on **for the current connection**
- .. py:method:: execute(sql[, params=None])
+ .. py:method:: execute(query)
+
+ :param: a query instance, such as a :py:class:`SelectQuery`
+ :rtype: the resulting cursor
+
+ .. py:method:: execute_sql(sql[, params=None[, require_commit=True]])
:param sql: a string sql query
:param params: a list or tuple of parameters to interpolate
@@ -311,15 +304,14 @@ Database and its subclasses
:rtype: number of rows affected by the last query
- .. py:method:: create_table(model_class[, safe=False])
+ .. py:method:: create_table(model_class)
:param model_class: :py:class:`Model` class to create table for
- :param safe: if ``True``, query will add a ``IF NOT EXISTS`` clause
- .. py:method:: create_index(model_class, field_names[, unique=False])
+ .. py:method:: create_index(model_class, fields[, unique=False])
:param model_class: :py:class:`Model` table on which to create index
- :param field_name: name of field(s) to create index on (a string or list)
+ :param fields: field(s) to create index on (either field instances or field names)
:param unique: whether the index should enforce uniqueness
.. py:method:: create_foreign_key(model_class, field)
@@ -337,37 +329,6 @@ Database and its subclasses
exists that prevents a table being dropped, you will need to handle
that in application logic.
- .. py:method:: add_column_sql(model_class, field_name)
-
- :param model_class: :py:class:`Model` which we are adding a column to
- :param string field_name: the name of the field we are adding
-
- :rtype: SQL suitable for adding the column
-
- .. note::
- Adding a non-null column to a table with rows may cause an IntegrityError.
-
- .. py:method:: rename_column_sql(model_class, field_name, new_name)
-
- :param model_class: :py:class:`Model` instance
- :param string field_name: the current name of the field
- :param string new_name: new name for the field
-
- :rtype: SQL suitable for renaming the column
-
- .. note::
- There must be a field instance named ``field_name`` at the time this SQL
- is generated.
-
- .. note:: SQLite does not support renaming columns
-
- .. py:method:: drop_column_sql(model_class, field_name)
-
- :param model_class: :py:class:`Model` instance
- :param string field_name: the name of the field to drop
-
- .. note:: SQLite does not support dropping columns
-
.. py:method:: create_sequence(sequence_name)
:param sequence_name: name of sequence to create
@@ -411,93 +372,3 @@ Database and its subclasses
.. py:class:: PostgresqlDatabase(Database)
:py:class:`Database` subclass that communicates to the "psycopg2" driver
-
-
-BaseAdapter and its subclasses
-------------------------------
-
-.. py:class:: BaseAdapter
-
- The various subclasses of `BaseAdapter` provide a bridge between the high-
- level :py:class:`Database` abstraction and the underlying python libraries like
- psycopg2. It also provides a way to unify the pythonic field types with
- the underlying column types used by the database engine.
-
- The `BaseAdapter` provides two types of mappings:
- - mapping between filter operations and their database equivalents
- - mapping between basic field types and their database column types
-
- The `BaseAdapter` also is the mechanism used by the :py:class:`Database` class to:
- - handle connections with the database
- - extract information from the database cursor
-
- .. py:attribute:: operations = {'eq': '= %s'}
-
- A mapping of query operation to SQL
-
- .. py:attribute:: interpolation = '%s'
-
- The string used by the driver to interpolate query parameters
-
- .. py:attribute:: sequence_support = False
-
- Whether the given backend supports sequences
-
- .. py:attribute:: reserved_tables = []
-
- Table names that are reserved by the backend -- if encountered in the
- application a warning will be issued.
-
- .. py:method:: get_field_types()
-
- :rtype: a dictionary mapping "user-friendly field type" to specific column type,
- e.g. ``{'string': 'VARCHAR', 'float': 'REAL', ... }``
-
- .. py:method:: get_field_type_overrides()
-
- :rtype: a dictionary similar to that returned by ``get_field_types()``.
-
- Provides a mechanism to override any number of field types without having
- to override all of them.
-
- .. py:method:: connect(database, **kwargs)
-
- :param database: string representing database name (or filename if using sqlite)
- :param kwargs: any keyword arguments to pass along to the database driver when connecting
- :rtype: a database connection
-
- .. py:method:: close(conn)
-
- :param conn: a database connection
-
- Close the given database connection
-
- .. py:method:: lookup_cast(lookup, value)
-
- :param lookup: a string representing the lookup type
- :param value: a python value that will be passed in to the lookup
- :rtype: a converted value appropriate for the given lookup
-
- Used as a hook when a specific lookup requires altering the given value,
- like for example when performing a LIKE query you may need to insert wildcards.
-
- .. py:method:: last_insert_id(cursor, model)
-
- :rtype: most recently inserted primary key
-
- .. py:method:: rows_affected(cursor)
-
- :rtype: number of rows affected by most recent query
-
-
-.. py:class:: SqliteAdapter(BaseAdapter)
-
- Subclass of :py:class:`BaseAdapter` that works with the "sqlite3" driver
-
-.. py:class:: MySQLAdapter(BaseAdapter)
-
- Subclass of :py:class:`BaseAdapter` that works with the "MySQLdb" driver
-
-.. py:class:: PostgresqlAdapter(BaseAdapter)
-
- Subclass of :py:class:`BaseAdapter` that works with the "psycopg2" driver
View
136 docs/peewee/example.rst
@@ -5,7 +5,7 @@ Example app
.. image:: tweepee.jpg
-peewee ships with an example web app that runs on the
+peewee ships with an example web app that runs on the
`Flask <http://flask.pocoo.org/>`_ microframework. If you already have flask
and its dependencies installed you should be good to go, otherwise install from
the included requirements file.
@@ -33,14 +33,14 @@ Diving into the code
Models
^^^^^^
-In the spirit of the ur-python framework, django, peewee uses declarative model
+In the spirit of the ur-python framework, django, peewee uses declarative model
definitions. If you're not familiar with django, the idea is that you declare
-a class with some members which map directly to the database schema. For the
+a class with some members which map directly to the database schema. For the
twitter clone, there are just three models:
``User``:
represents a user account and stores the username and password, an email
- address for generating avatars using *gravatar*, and a datetime field
+ address for generating avatars using *gravatar*, and a datetime field
indicating when that account was created
``Relationship``:
@@ -60,33 +60,45 @@ Here is what the code looks like:
.. code-block:: python
+ # create a peewee database instance -- our models will use this database to
+ # persist information
database = SqliteDatabase(DATABASE)
- # model definitions
+ # model definitions -- the standard "pattern" is to define a base model class
+ # that specifies which database to use. then, any subclasses will automatically
+ # use the correct storage. for more information, see:
+ # http://charlesleifer.com/docs/peewee/peewee/models.html#model-api-smells-like-django
class BaseModel(Model):
class Meta:
database = database
-
+
+ # the user model specifies its fields (or columns) declaratively, like django
class User(BaseModel):
username = CharField()
password = CharField()
email = CharField()
join_date = DateTimeField()
+ class Meta:
+ order_by = ('username',)
+
+ # it often makes sense to put convenience methods on model instances, for
+ # example, "give me all the users this user is following":
def following(self):
+ # query other users through the "relationship" table
return User.select().join(
- Relationship, on='to_user_id'
- ).where(from_user=self).order_by('username')
+ Relationship, on=Relationship.to_user,
+ ).where(Relationship.from_user == self)
def followers(self):
return User.select().join(
- Relationship
- ).where(to_user=self).order_by('username')
+ Relationship, on=Relationship.from_user,
+ ).where(Relationship.to_user == self)
def is_following(self, user):
return Relationship.select().where(
- from_user=self,
- to_user=user
+ (Relationship.from_user == self) &
+ (Relationship.to_user == user)
).count() > 0
def gravatar_url(self, size=80):
@@ -94,16 +106,26 @@ Here is what the code looks like:
(md5(self.email.strip().lower().encode('utf-8')).hexdigest(), size)
+ # this model contains two foreign keys to user -- it essentially allows us to
+ # model a "many-to-many" relationship between users. by querying and joining
+ # on different columns we can expose who a user is "related to" and who is
+ # "related to" a given user
class Relationship(BaseModel):
from_user = ForeignKeyField(User, related_name='relationships')
to_user = ForeignKeyField(User, related_name='related_to')
+ # a dead simple one-to-many relationship: one user has 0..n messages, exposed by
+ # the foreign key. because we didn't specify, a users messages will be accessible
+ # as a special attribute, User.message_set
class Message(BaseModel):
user = ForeignKeyField(User)
content = TextField()
pub_date = DateTimeField()
+ class Meta:
+ order_by = ('-pub_date',)
+
peewee supports a handful of field types which map to different column types in
sqlite. Conversion between python and the database is handled transparently,
@@ -168,8 +190,8 @@ database and point your models at it. This is a peewee idiom:
database = SqliteDatabase(DATABASE) # tell our models to use "tweepee.db"
-Because sqlite likes to have a separate connection per-thread, we will tell
-flask that during the request/response cycle we need to create a connection to
+Because sqlite likes to have a separate connection per-thread, we will tell
+flask that during the request/response cycle we need to create a connection to
the database. Flask provides some handy decorators to make this a snap:
.. code-block:: python
@@ -185,7 +207,7 @@ the database. Flask provides some handy decorators to make this a snap:
return response
.. note::
- We're storing the db on the magical variable ``g`` - that's a
+ We're storing the db on the magical variable ``g`` - that's a
flask-ism and can be ignored as an implementation detail. The meat of this code
is in the idea that we connect to our db every request and close that connection
every response. Django does the `exact same thing <http://code.djangoproject.com/browser/django/tags/releases/1.2.3/django/db/__init__.py#L80>`_.
@@ -194,68 +216,68 @@ the database. Flask provides some handy decorators to make this a snap:
Doing queries
^^^^^^^^^^^^^
-In the ``User`` model there are a few instance methods that encapsulate some
+In the ``User`` model there are a few instance methods that encapsulate some
user-specific functionality, i.e.
* ``following()``: who is this user following?
* ``followers()``: who is following this user?
-These methods are rather similar in their implementation but with one key
+These methods are rather similar in their implementation but with one key
difference:
.. code-block:: python
def following(self):
+ # query other users through the "relationship" table
return User.select().join(
- Relationship, on='to_user_id'
- ).where(from_user=self).order_by('username')
+ Relationship, on=Relationship.to_user,
+ ).where(Relationship.from_user == self)
def followers(self):
return User.select().join(
- Relationship
- ).where(to_user=self).order_by('username')
+ Relationship, on=Relationship.from_user,
+ ).where(Relationship.to_user == self)
.. note:
- The ``following()`` method specifies an extra bit of metadata,
- ``on='to_user_id'``. Because there are two foreign keys to ``User``, peewee
+ ``on=Relationship.to_user``. Because there are two foreign keys to ``User``, peewee
will automatically assume the first one, which happens to be ``from_user``.
-Specifying the foreign key manually instructs peewee to join on the ``to_user_id`` field.
The queries end up looking like:
.. code-block:: sql
# following:
- SELECT t1.*
- FROM user AS t1
- INNER JOIN relationship AS t2
- ON t1.id = t2.to_user_id # <-- joining on to_user_id
- WHERE t2.from_user_id = ?
- ORDER BY username ASC
-
+ SELECT t1."id", t1."username", t1."password", t1."email", t1."join_date"
+ FROM "user" AS t1
+ INNER JOIN "relationship" AS t2
+ ON t1."id" = t2."to_user_id" # <-- joining on to_user_id
+ WHERE t2."from_user_id" = ?
+ ORDER BY t1."username" ASC
+
# followers
- SELECT t1.*
- FROM user AS t1
- INNER JOIN relationship AS t2
- ON t1.id = t2.from_user_id # <-- joining on from_user_id
- WHERE t2.to_user_id = ?
- ORDER BY username ASC
+ SELECT t1."id", t1."username", t1."password", t1."email", t1."join_date"
+ FROM user AS t1
+ INNER JOIN relationship AS t2
+ ON t1."id" = t2."from_user_id" # <-- joining on from_user_id
+ WHERE t2."to_user_id" = ?
+ ORDER BY t1."username" ASC
Creating new objects
^^^^^^^^^^^^^^^^^^^^
-So what happens when a new user wants to join the site? Looking at the
+So what happens when a new user wants to join the site? Looking at the
business end of the ``join()`` view, we can that it does a quick check to see
if the username is taken, and if not executes a :py:meth:`~Model.create`.
.. code-block:: python
-
try:
+ # use the .get() method to quickly see if a user with that name exists
user = User.get(username=request.form['username'])
flash('That username is already taken')
except User.DoesNotExist:
+ # if not, create the user and store the form data on the new model
user = User.create(
username=request.form['username'],
password=md5(request.form['password']).hexdigest(),
@@ -263,6 +285,10 @@ if the username is taken, and if not executes a :py:meth:`~Model.create`.
join_date=datetime.datetime.now()
)
+ # mark the user as being 'authenticated' by setting the session vars
+ auth_user(user)
+ return redirect(url_for('homepage'))
+
Much like the :py:meth:`~Model.create` method, all models come with a built-in method called
:py:meth:`~Model.get_or_create` which is used when one user follows another:
@@ -277,28 +303,30 @@ Much like the :py:meth:`~Model.create` method, all models come with a built-in m
Doing subqueries
^^^^^^^^^^^^^^^^
-If you are logged-in and visit the twitter homepage, you will see tweets from
+If you are logged-in and visit the twitter homepage, you will see tweets from
the users that you follow. In order to implement this, it is necessary to do
a subquery:
.. code-block:: python
# python code
- qr = Message.select().where(user__in=some_user.following())
+ messages = Message.select().where(
+ Message.user << user.following()
+ )
Results in the following SQL query:
.. code-block:: sql
- SELECT *
- FROM message
- WHERE user_id IN (
- SELECT t1.id
- FROM user AS t1
- INNER JOIN relationship AS t2
- ON t1.id = t2.to_user_id
- WHERE t2.from_user_id = ?
- ORDER BY username ASC
+ SELECT t1."id", t1."user_id", t1."content", t1."pub_date"
+ FROM "message" AS t1
+ WHERE t1."user_id" IN (
+ SELECT t2."id"
+ FROM "user" AS t2
+ INNER JOIN "relationship" AS t3
+ ON t2."id" = t3."to_user_id"
+ WHERE t3."from_user_id" = ?
+ ORDER BY t1."username" ASC
)
peewee supports doing subqueries on any :py:class:`ForeignKeyField` or :py:class:`PrimaryKeyField`.
@@ -314,7 +342,7 @@ mentioning briefly.
the views that return lists of objects.
.. code-block:: python
-
+
def object_list(template_name, qr, var_name='object_list', **kwargs):
kwargs.update(
page=int(request.args.get('page', 1)),
@@ -330,7 +358,7 @@ mentioning briefly.
login page.
.. code-block:: python
-
+
def auth_user(user):
session['logged_in'] = True
session['user'] = user
@@ -347,9 +375,9 @@ mentioning briefly.
* Return a 404 response instead of throwing exceptions when an object is not
found in the database.
-
+
.. code-block:: python
-
+
def get_object_or_404(model, **kwargs):
try:
return model.get(**kwargs)
View
104 docs/peewee/fields.rst
@@ -50,13 +50,15 @@ Field types table
Parameters accepted by all field types and their default values:
* ``null = False`` -- boolean indicating whether null values are allowed to be stored
-* ``db_index = False`` -- boolean indicating whether to create an index on this column
+* ``index = False`` -- boolean indicating whether to create an index on this column
* ``unique = False`` -- boolean indicating whether to create a unique index on this column
* ``verbose_name = None`` -- string representing the "user-friendly" name of this field
* ``help_text = None`` -- string representing any helpful text for this field
* ``db_column = None`` -- string representing the underlying column to use if different, useful for legacy databases
* ``default = None`` -- any value to use as a default for uninitialized models
* ``choices = None`` -- an optional iterable containing 2-tuples of ``value``, ``display``
+* ``primary_key = False`` -- whether this field is the primary key for the table
+* ``sequence = None`` -- sequence to populate field (if backend supports it)
=================== ================= ================= =================
@@ -94,9 +96,7 @@ Some fields take special parameters...
| :py:class:`DecimalField` | ``max_digits``, ``decimal_places``, |
| | ``auto_round``, ``rounding`` |
+-------------------------------+----------------------------------------------+
-| :py:class:`PrimaryKeyField` | ``column_class`` |
-+-------------------------------+----------------------------------------------+
-| :py:class:`ForeignKeyField` | ``to``, ``related_name``, |
+| :py:class:`ForeignKeyField` | ``rel_model``, ``related_name``, |
| | ``cascade``, ``extra`` |
+-------------------------------+----------------------------------------------+
@@ -148,25 +148,14 @@ To query, let's say we want to find students who are enrolled in math class:
.. code-block:: python
- for student in Student.select().join(StudentCourse).join(Course).where(name='math'):
- print student.name
-
-You could also express this as:
-
-.. code-block:: python
-
- for student in Student.filter(studentcourse_set__course__name='math'):
+ for student in Student.select().join(StudentCourse).join(Course).where(Course.name == 'math'):
print student.name
To query what classes a given student is enrolled in:
.. code-block:: python
- for course in Course.select().join(StudentCourse).join(Student).where(name='da vinci'):
- print course.name
-
- # or, similarly
- for course in Course.filter(studentcourse_set__student__name='da vinci'):
+ for course in Course.select().join(StudentCourse).join(Student).where(Student.name == 'da vinci'):
print course.name
To efficiently iterate over a many-to-many relation, i.e., list all students
@@ -175,11 +164,9 @@ and "precompute" the Student and Course:
.. code-block:: python
- query = StudentCourse.select({
- Student: ['*'],
- StudentCourse: ['*'],
- Course: ['*'],
- }).join(Course).switch(StudentCourse).join(Student).order_by('name')
+ query = StudentCourse.select(
+ StudentCourse, Student, Course)
+ ).join(Course).switch(StudentCourse).join(Student)
To print a list of students and their courses you might do the following:
@@ -214,13 +201,13 @@ use non-integer pks in peewee.
class UUIDModel(Model):
# explicitly declare a primary key field, and specify the class to use
- id = PrimaryKeyField(column_class=VarCharColumn)
+ id = CharField(primary_key=True)
Auto-increment IDs are, as their name says, automatically generated for you when
you insert a new row into the database. The way peewee determines whether to
do an ``INSERT`` versus an ``UPDATE`` comes down to checking whether the primary
-key field is ``None``. If ``None``, it will do an insert, otherwise it does an
+key value is ``None``. If ``None``, it will do an insert, otherwise it does an
update on the existing value. Since, with our uuid example, the database driver
won't generate a new ID, we need to specify it manually. When we call save()
for the first time, pass in ``force_insert = True``:
@@ -237,7 +224,7 @@ for the first time, pass in ``force_insert = True``:
.. note::
Any foreign keys to a model with a non-integer primary key will have the
- ``ForeignKeyField`` use the same underlying column type as the primary key
+ ``ForeignKeyField`` use the same underlying storage type as the primary key
they are related to.
@@ -248,16 +235,26 @@ Field class API
The base class from which all other field types extend.
- .. py:method:: __init__(null=False, db_index=False, unique=False, verbose_name=None, help_text=None, db_column=None, default=None, choices=None, *args, **kwargs)
+ .. py:attribute:: db_field = '<some field type>'
+
+ Attribute used to map this field to a column type, e.g. "string" or "datetime"
+
+ .. py:attribute:: template = '%(column_type)s'
+
+ A template for generating the SQL for this field
+
+ .. py:method:: __init__(null=False, index=False, unique=False, verbose_name=None, help_text=None, db_column=None, default=None, choices=None, *args, **kwargs)
:param null: this column can accept ``None`` or ``NULL`` values
- :param db_index: create an index for this column when creating the table
+ :param index: create an index for this column when creating the table
:param unique: create a unique index for this column when creating the table
:param verbose_name: specify a "verbose name" for this field, useful for metadata purposes
:param help_text: specify some instruction text for the usage/meaning of this field
:param db_column: column class to use for underlying storage
:param default: a value to use as an uninitialized default
:param choices: an iterable of 2-tuples mapping ``value`` to ``display``
+ :param boolean primary_key: whether to use this as the primary key for the table
+ :param sequence: name of sequence (if backend supports it)
.. py:method:: db_value(value)
@@ -269,11 +266,20 @@ Field class API
:param value: data coming from the backend storage
:rtype: python data type
- .. py:method:: lookup_value(lookup_type, value)
+ .. py:method:: coerce(value)
+
+ This method is a shorthand that is used, by default, by both ``db_value`` and
+ ``python_value``. You can usually get away with just implementing this.
+
+ :param value: arbitrary data from app or backend
+ :rtype: python data type
- :param lookup_type: a peewee lookup type, such as 'eq' or 'contains'
- :param value: a python data type
- :rtype: data type converted for use when querying
+ .. py:method:: field_attributes()
+
+ This method is responsible for return a dictionary containing the default
+ field attributes for the column, e.g. ``{'max_length': 255}``
+
+ :rtype: a python dictionary
.. py:method:: class_prepared()
@@ -356,14 +362,7 @@ Field class API
.. py:class:: PrimaryKeyField
- Stores: auto-incrementing integer fields suitable for use as primary key by
- default, though other types of data can be stored by specifying a column_class.
- See :ref:`notes on non-integer primary keys <non_int_pks>`.
-
- .. py:method:: __init__(column_class[, ...])
-
- :param column_class: a reference to a subclass of ``Column`` to use for
- the underlying storage, defaults to ``PrimaryKeyColumn``.
+ Stores: auto-incrementing integer fields suitable for use as primary key.
.. py:class:: ForeignKeyField
@@ -371,27 +370,26 @@ Field class API
.. py:method:: __init__(to[, related_name=None[, ...]])
- :param to: related :py:class:`Model` class or the string 'self' if declaring
+ :param rel_model: related :py:class:`Model` class or the string 'self' if declaring
a self-referential foreign key
:param related_name: attribute to expose on related model
.. code-block:: python
- class Blog(Model):
+ class User(Model):
name = CharField()
- class Entry(Model):
- blog = ForeignKeyField(Blog, related_name='entries')
- title = CharField()
+ class Tweet(Model):
+ user = ForeignKeyField(User, related_name='tweets')
content = TextField()
- # "blog" attribute
- >>> some_entry.blog
- <Blog: My Awesome Blog>
+ # "user" attribute
+ >>> some_tweet.user
+ <User: charlie>
- # "entries" related name attribute
- >>> for entry in my_awesome_blog.entries:
- ... print entry.title
- Some entry
- Another entry
- Yet another entry
+ # "tweets" related name attribute
+ >>> for tweet in charlie.tweets:
+ ... print tweet.content
+ Some tweet
+ Another tweet
+ Yet another tweet
View
277 docs/peewee/models.rst
@@ -3,7 +3,7 @@
Model API (smells like django)
==============================
-Models and their fields map directly to database tables and columns. Consider
+Models and their fields map directly to database tables and columns. Consider
the following:
.. _blog-models:
@@ -11,24 +11,22 @@ the following:
.. code-block:: python
from peewee import *
-
+
db = SqliteDatabase('test.db')
-
+
# create a base model class that our application's models will extend
class BaseModel(Model):
class Meta:
database = db
-
- class Blog(BaseModel):
- name = CharField() # <-- VARCHAR
-
-
- class Entry(BaseModel):
- headline = CharField()
- content = TextField() # <-- TEXT
- pub_date = DateTimeField() # <-- DATETIME
- blog = ForeignKeyField(Blog) # <-- INTEGER referencing the Blog table
+ class User(BaseModel):
+ username = CharField()
+
+ class Tweet(BaseModel):
+ user = ForeignKeyField(User, related_name='tweets')
+ message = TextField()
+ created_date = DateTimeField(default=datetime.datetime.now)
+ is_published = BooleanField(default=True)
This is a typical example of how to specify models with peewee. There are several
@@ -62,13 +60,13 @@ things going on:
.. code-block:: python
- class Blog(BaseModel):
- name = CharField()
+ class User(BaseModel):
+ username = CharField()
Model definition is pretty similar to django or sqlalchemy -- you basically define
a class which represents a single table in the database, then its attributes (which
are subclasses of :py:class:`Field`) represent columns.
-
+
Models provide methods for creating/reading/updating/deleting rows in the
database.
@@ -82,11 +80,11 @@ database and create the tables first:
.. code-block:: python
# connect to our database
- db.connect()
-
+ db.connect()
+
# create the tables
- Blog.create_table()
- Entry.create_table()
+ User.create_table()
+ Tweet.create_table()
.. note::
Strictly speaking, the explicit call to :py:meth:`~Database.connect` is not
@@ -97,7 +95,7 @@ database and create the tables first:
Model instances
---------------
-Assuming you've created the tables and connected to the database, you are now
+Assuming you've created the tables and connected to the database, you are now
free to create models and execute queries.
Creating models in the interactive interpreter is a snap.
@@ -106,54 +104,50 @@ Creating models in the interactive interpreter is a snap.
.. code-block:: python
- >>> blog = Blog.create(name='Funny pictures of animals blog')
- >>> entry = Entry.create(
- ... headline='maru the kitty',
- ... content='http://www.youtube.com/watch?v=xdhLQCYQ-nQ',
- ... pub_date=datetime.datetime.now(),
- ... blog=blog
+ >>> user = User.create(username='charlie')
+ >>> tweet = Tweet.create(
+ ... message='http://www.youtube.com/watch?v=xdhLQCYQ-nQ',
+ ... user=user
... )
-
- >>> entry.blog.name
- 'Funny pictures of animals blog'
+
+ >>> tweet.user.username
+ 'charlie'
2. Build up the instance programmatically:
.. code-block:: python
-
- >>> blog = Blog()
- >>> blog.name = 'Another sweet blog'
- >>> blog.save()
+
+ >>> user = User()
+ >>> user.username = 'charlie'
+ >>> user.save()
Traversing foriegn keys
^^^^^^^^^^^^^^^^^^^^^^^
-As you can see from above, the foreign key from ``Entry`` to ``Blog`` can be
+As you can see from above, the foreign key from ``Tweet`` to ``User`` can be
traversed automatically:
.. code-block:: python
- >>> entry.blog.name
- 'Funny pictures of animals blog'
+ >>> tweet.user.username
+ 'charlie'
-The reverse is also true, we can iterate a ``Blog`` objects associated ``Entries``:
+The reverse is also true, we can iterate a ``User`` objects associated ``Tweets``:
.. code-block:: python
- >>> for entry in blog.entry_set:
- ... print entry.headline
- ...
- maru the kitty
+ >>> for tweet in user.tweets:
+ ... print tweet.message
+ ...
+ http://www.youtube.com/watch?v=xdhLQCYQ-nQ
-Under the hood, the ``entry_set`` attribute is just a :py:class:`SelectQuery`:
+Under the hood, the ``tweets`` attribute is just a :py:class:`SelectQuery` with
+the where clause prepopulated to point at the right ``User`` instance:
.. code-block:: python
- >>> blog.entry_set
+ >>> user.tweets
<peewee.SelectQuery object at 0x151f510>
-
- >>> blog.entry_set.sql()
- ('SELECT * FROM entry WHERE blog_id = ?', [1])
Model options
@@ -166,9 +160,9 @@ the django framework:
.. code-block:: python
from peewee import *
-
+
custom_db = SqliteDatabase('custom.db')
-
+
class CustomModel(Model):
class Meta:
database = custom_db
@@ -187,9 +181,7 @@ There are several options you can specify as ``Meta`` attributes:
* database: specifies a :py:class:`Database` instance to use with this model
* db_table: the name of the database table this model maps to
* indexes: a list of fields to index
-* ordering: a sequence of columns to use as the default ordering for this model
-* pk_sequence: name of sequence to create for the primary key (peewee will autogenerate one
- if not provided and the backend supports sequences).
+* order_by: a sequence of columns to use as the default ordering for this model
Specifying indexes:
@@ -215,31 +207,30 @@ Example of ordering:
.. code-block:: python
- class Entry(Model):
- title = CharField()
- body = TextField()
+ class Tweet(Model):
+ message = TextField()
created = DateTimeField()
class Meta:
- # order by created date descending, then title ascending
- ordering = (('created', 'desc'), 'title')
+ # order by created date descending
+ ordering = ('-created',)
-.. note::
+.. note::
These options are "inheritable", which means that you can define a database
adapter on one model, then subclass that model and the child models will use
that database.
-
+
.. code-block:: python
-
+
my_db = PostgresqlDatabase('my_db')
-
+
class BaseModel(Model):
class Meta:
database = my_db
-
+
class SomeModel(BaseModel):
field1 = CharField()
-
+
class Meta:
ordering = ('field1',)
# no need to define database again since it will be inherited from
@@ -256,11 +247,11 @@ Model methods
Save the given instance, creating or updating depending on whether it has a
primary key. If ``force_insert=True`` an ``INSERT`` will be issued regardless
of whether or not the primary key exists.
-
+
example:
-
+
.. code-block:: python
-
+
>>> some_obj.title = 'new title' # <-- does not touch the database
>>> some_obj.save() # <-- change is persisted to the db
@@ -269,69 +260,61 @@ Model methods
:param attributes: key/value pairs of model attributes
Create an instance of the ``Model`` with the given attributes set.
-
+
example:
-
+
.. code-block:: python
-
+
>>> user = User.create(username='admin', password='test')
- .. py:method:: delete_instance([recursive=False])
+ .. py:method:: delete_instance([recursive=False[, delete_nullable=False]])
+
+ :param recursive: Delete this instance and anything that depends on it,
+ optionally updating those that have nullable dependencies
+ :param delete_nullable: If doing a recursive delete, delete all dependent
+ objects regardless of whether it could be updated to NULL
Delete the given instance. Any foreign keys set to cascade on
delete will be deleted automatically. For more programmatic control,
you can call with recursive=True, which will delete any non-nullable
- related models (those that *are* nullable will be set to NULL).
+ related models (those that *are* nullable will be set to NULL). If you
+ wish to delete all dependencies regardless of whether they are nullable,
+ set ``delete_nullable=True``.
example:
.. code-block:: python
-
- >>> some_obj.delete_instance() # <-- it is gone forever
- .. py:classmethod:: filter(*args, **kwargs)
-
- :param args: a list of :py:class:`Q` or :py:class:`Node` objects
- :param kwargs: a mapping of column + lookup to value, e.g. "age__gt=55"
- :rtype: :py:class:`SelectQuery` with appropriate ``WHERE`` clauses
-
- Provides a django-like syntax for building a query. The key difference
- between :py:meth:`~Model.filter` and :py:meth:`SelectQuery.where`
- is that :py:meth:`~Model.filter` supports traversing joins using
- django's "double-underscore" syntax:
-
- .. code-block:: python
-
- >>> sq = Entry.filter(blog__title='Some Blog')
-
- This method is chainable::
-
- >>> base_q = User.filter(active=True)
- >>> some_user = base_q.filter(username='charlie')
+ >>> some_obj.delete_instance() # <-- it is gone forever
.. py:classmethod:: get(*args, **kwargs)
- :param args: a list of :py:class:`Q` or :py:class:`Node` objects
+ :param args: a list of query expressions, e.g. ``Usre.username == 'foo'``
:param kwargs: a mapping of column + lookup to value, e.g. "age__gt=55"
:rtype: :py:class:`Model` instance or raises ``DoesNotExist`` exception
Get a single row from the database that matches the given query. Raises a
``<model-class>.DoesNotExist`` if no rows are returned:
-
+
.. code-block:: python
-
- >>> user = User.get(username=username, password=password)
-
- This method is also expose via the :py:class:`SelectQuery`:
-
+
+ >>> user = User.get(User.username == username, User.password == password)
+
+ This method is also expose via the :py:class:`SelectQuery`, though it takes
+ no parameters:
+
.. code-block:: python
-
- >>> active = User.select().where(active=True)
+
+ >>> active = User.select().where(User.active == True)
>>> try:
- ... user = active.get(username=username, password=password)
+ ... users = active.where(User.username == username, User.password == password)
+ ... user = users.get()
... except User.DoesNotExist:
... user = None
+ .. note:: the "kwargs" style syntax is provided for compatibility with
+ version 1.0. The expression-style syntax is preferable.
+
.. py:classmethod:: get_or_create(**attributes)
:param attributes: key/value pairs of model attributes
@@ -339,49 +322,47 @@ Model methods
Get the instance with the given attributes set. If the instance
does not exist it will be created.
-
+
example:
-
+
.. code-block:: python
-
+
>>> CachedObj.get_or_create(key=key, val=some_val)
- .. py:classmethod:: select(query=None)
-
+ .. py:classmethod:: select(*selection)
+
+ :param selection: a list of model classes, field instances, functions or expressions
:rtype: a :py:class:`SelectQuery` for the given ``Model``
-
+
example:
-
+
.. code-block:: python
-
- >>> User.select().where(active=True).order_by('username')
+
+ >>> User.select().where(User.active == True).order_by(User.username)
+ >>> Tweet.select(Tweet, User).join(User).order_by(Tweet.created_date.desc())
.. py:classmethod:: update(**query)
:rtype: an :py:class:`UpdateQuery` for the given ``Model``
-
+
example:
-
+
.. code-block:: python
-
- >>> q = User.update(active=False).where(registration_expired=True)
- >>> q.sql()
- ('UPDATE user SET active=? WHERE registration_expired = ?', [0, 1])
+
+ >>> q = User.update(active=False).where(User.registration_expired == True)
>>> q.execute() # <-- execute it
- .. py:classmethod:: delete(**query)
+ .. py:classmethod:: delete()
:rtype: a :py:class:`DeleteQuery` for the given ``Model``
-
+
example:
-
+
.. code-block:: python
-
- >>> q = User.delete().where(active=False)
- >>> q.sql()
- ('DELETE FROM user WHERE active = ?', [0])
+
+ >>> q = User.delete().where(User.active == False)
>>> q.execute() # <-- execute it
-
+
.. warning::
Assume you have a model instance -- calling ``model_instance.delete()``
does **not** delete it.
@@ -389,14 +370,12 @@ Model methods
.. py:classmethod:: insert(**query)
:rtype: an :py:class:`InsertQuery` for the given ``Model``
-
+
example:
-
+
.. code-block:: python
-
+
>>> q = User.insert(username='admin', active=True, registration_expired=False)
- >>> q.sql()
- ('INSERT INTO user (username,active,registration_expired) VALUES (?,?,?)', ['admin', 1, 0])
>>> q.execute()
1
@@ -412,31 +391,53 @@ Model methods
>>> for user in q:
... print user.id, user.username
+ .. py:classmethod:: filter(*args, **kwargs)
+
+ :param args: a list of :py:class:`DQ` or :py:class:`Node` objects
+ :param kwargs: a mapping of column + lookup to value, e.g. "age__gt=55"
+ :rtype: :py:class:`SelectQuery` with appropriate ``WHERE`` clauses
+
+ Provides a django-like syntax for building a query. The key difference
+ between :py:meth:`~Model.filter` and :py:meth:`SelectQuery.where`
+ is that :py:meth:`~Model.filter` supports traversing joins using
+ django's "double-underscore" syntax:
+
+ .. code-block:: python
+
+ >>> sq = Entry.filter(blog__title='Some Blog')
+
+ This method is chainable::
+
+ >>> base_q = User.filter(active=True)
+ >>> some_user = base_q.filter(username='charlie')
+