Skip to content

Commit

Permalink
More docs, bitfields and plain select queries.
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles Leifer committed Jan 29, 2018
1 parent 54c8fde commit 57c8199
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 1 deletion.
103 changes: 102 additions & 1 deletion docs/peewee/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,89 @@ have an event attached:
be formatted so they are sorted lexicographically. That is why they are
stored, by default, as ``YYYY-MM-DD HH:MM:SS``.

BitField and BigBitField
^^^^^^^^^^^^^^^^^^^^^^^^

The :py:class:`BitField` and :py:class:`BigBitField` are new as of 3.0.0. The
former provides a subclass of :py:class:`IntegerField` that is suitable for
storing feature toggles as an integer bitmask. The latter is suitable for
storing a bitmap for a large data-set, e.g. expressing membership or
bitmap-type data.

As an example of using :py:class:`BitField`, let's say we have a *Post* model
and we wish to store certain True/False flags about how the post. We could
store all these feature toggles in their own :py:class:`BooleanField` objects,
or we could use :py:class:`BitField` instead:

.. code-block:: python
class Post(Model):
content = TextField()
flags = BitField()
is_favorite = flags.flag(1)
is_sticky = flags.flag(2)
is_minimized = flags.flag(4)
is_deleted = flags.flag(8)
Using these flags is quite simple:

.. code-block:: pycon
>>> p = Post()
>>> p.is_sticky = True
>>> p.is_minimized = True
>>> print(p.flags) # Prints 4 | 2 --> "6"
6
>>> p.is_favorite
False
>>> p.is_sticky
True
We can also use the flags on the Post class to build expressions in queries:

.. code-block:: python
# Generates a WHERE clause that looks like:
# WHERE (post.flags & 1 != 0)
favorites = Post.select().where(Post.is_favorite)
# Query for sticky + favorite posts:
sticky_faves = Post.select().where(Post.is_sticky & Post.is_favorite)
Since the :py:class:`BitField` is stored in an integer, there is a maximum of
64 flags you can represent (64-bits is common size of integer column). For
storing arbitrarily large bitmaps, you can instead use :py:class:`BigBitField`,
which uses an automatically managed buffer of bytes, stored in a
:py:class:`BlobField`.

Example usage:

.. code-block:: python
class Bitmap(Model):
data = BigBitField()
bitmap = Bitmap()
# Sets the ith bit, e.g. the 1st bit, the 11th bit, the 63rd, etc.
bits_to_set = (1, 11, 63, 31, 55, 48, 100, 99)
for bit_idx in bits_to_set:
bitmap.data.set_bit(bit_idx)
# We can test whether a bit is set using "is_set":
assert bitmap.data.is_set(11)
assert not bitmap.data.is_set(12)
# We can clear a bit:
bitmap.data.clear_bit(11)
assert not bitmap.data.is_set(11)
# We can also "toggle" a bit. Recall that the 63rd bit was set earlier.
assert bitmap.data.toggle_bit(63) is False
assert bitmap.data.toggle_bit(63) is True
assert bitmap.data.is_set(63)
BareField
^^^^^^^^^

Expand All @@ -404,14 +487,32 @@ SQLite uses dynamic typing and data-types are not enforced, it can be perfectly
fine to declare fields without *any* data-type. In those cases you can use
:py:class:`BareField`. It is also common for SQLite virtual tables to use
meta-columns or untyped columns, so for those cases as well you may wish to use
an untyped field.
an untyped field (although for full-text search, you should use
:py:class:`SearchField` instead!).

:py:class:`BareField` accepts a special parameter ``coerce``. This parameter is
a function that takes a value coming from the database and converts it into the
appropriate Python type. For instance, if you have a virtual table with an
un-typed column but you know that it will return ``int`` objects, you can
specify ``coerce=int``.

Example:

.. code-block:: python
db = SqliteDatabase(':memory:')
class Junk(Model):
anything = BareField()
class Meta:
database = db
# Store multiple data-types in the Junk.anything column:
Junk.create(anything='a string')
Junk.create(anything=12345)
Junk.create(anything=3.14159)
.. _custom-fields:

Creating a custom field
Expand Down
73 changes: 73 additions & 0 deletions docs/peewee/query_builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,79 @@ would delete all notes by anyone whose last name is "Foo":
# Delete all notes by any person whose ID is in the previous query.
Note.delete().where(Note.person_id.in_(foo_people)).execute()
Query Objects
-------------

One of the fundamental limitations of the abstractions provided by Peewee 2.x
was the absence of a class that represented a structured query with no relation
to a given model class.

An example of this might be computing aggregate values over a subquery. For
example, the :py:meth:`~SelectBase.count` method, which returns the count of
rows in an arbitrary query, is implemented by wrapping the query:

.. code-block:: sql
SELECT COUNT(1) FROM (...)
To accomplish this with Peewee, the implementation is written in this way:

.. code-block:: python
def count(query):
# Select([source1, ... sourcen], [column1, ...columnn])
wrapped = Select(from_list=[query], columns=[fn.COUNT(SQL('1'))])
curs = wrapped.tuples().execute(db)
return curs[0][0] # Return first column from first row of result.
We can actually express this more concisely using the
:py:meth:`~SelectBase.scalar` method, which is suitable for returning values
from aggregate queries:

.. code-block:: python
def count(query):
wrapped = Select(from_list=[query], columns=[fn.COUNT(SQL('1'))])
return wrapped.scalar(db)
The :ref:`query_examples` document has a more complex example, in which we
write a query for a facility with the highest number of available slots booked:

The SQL we wish to express is:

.. code-block:: sql
SELECT facid, total FROM (
SELECT facid, SUM(slots) AS total,
rank() OVER (order by SUM(slots) DESC) AS rank
FROM bookings
GROUP BY facid
) AS ranked
WHERE rank = 1
We can express this fairly elegantly by using a plain :py:class:`Select` for
the outer query:

.. code-block:: python
# Store rank expression in variable for readability.
rank_expr = fn.rank().over(order_by=[fn.SUM(Booking.slots).desc()])
subq = (Booking
.select(Booking.facility, fn.SUM(Booking.slots).alias('total'),
rank_expr.alias('rank'))
.group_by(Booking.facility))
# Use a plain "Select" to create outer query.
query = (Select(columns=[subq.c.facid, subq.c.total])
.from_(subq)
.where(subq.c.rank == 1)
.tuples())
# Iterate over the resulting facility ID(s) and total(s):
for facid, total in query.execute(db):
print(facid, total)
More
----

Expand Down

0 comments on commit 57c8199

Please sign in to comment.