Plain is headed towards 1.0! Subscribe for development updates →

plain.models

Model your data and store it in a database.

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


class User(models.Model):
    email = models.EmailField(unique=True)
    password = PasswordField()
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Create, update, and delete instances of your models:

from .models import User


# Create a new user
user = User.objects.create(
    email="[email protected]",
    password="password",
)

# Update a user
user.email = "[email protected]"
user.save()

# Delete a user
user.delete()

# Query for users
staff_users = User.objects.filter(is_staff=True)

Installation

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.models",
]

To connect to a database, you can provide a DATABASE_URL environment variable.

DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Or you can manually define the DATABASES setting.

# app/settings.py
DATABASES = {
    "default": {
        "ENGINE": "plain.models.backends.postgresql",
        "NAME": "dbname",
        "USER": "user",
        "PASSWORD": "password",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

Multiple backends are supported, including Postgres, MySQL, and SQLite.

Querying

Migrations

Migration docs

Fields

Field docs

Validation

Indexes and constraints

Managers

Forms

  1from contextlib import ContextDecorator, contextmanager
  2
  3from plain.models.db import (
  4    DEFAULT_DB_ALIAS,
  5    DatabaseError,
  6    Error,
  7    ProgrammingError,
  8    connections,
  9)
 10
 11
 12class TransactionManagementError(ProgrammingError):
 13    """Transaction management is used improperly."""
 14
 15    pass
 16
 17
 18def get_connection(using=None):
 19    """
 20    Get a database connection by name, or the default database connection
 21    if no name is provided. This is a private API.
 22    """
 23    if using is None:
 24        using = DEFAULT_DB_ALIAS
 25    return connections[using]
 26
 27
 28def get_autocommit(using=None):
 29    """Get the autocommit status of the connection."""
 30    return get_connection(using).get_autocommit()
 31
 32
 33def set_autocommit(autocommit, using=None):
 34    """Set the autocommit status of the connection."""
 35    return get_connection(using).set_autocommit(autocommit)
 36
 37
 38def commit(using=None):
 39    """Commit a transaction."""
 40    get_connection(using).commit()
 41
 42
 43def rollback(using=None):
 44    """Roll back a transaction."""
 45    get_connection(using).rollback()
 46
 47
 48def savepoint(using=None):
 49    """
 50    Create a savepoint (if supported and required by the backend) inside the
 51    current transaction. Return an identifier for the savepoint that will be
 52    used for the subsequent rollback or commit.
 53    """
 54    return get_connection(using).savepoint()
 55
 56
 57def savepoint_rollback(sid, using=None):
 58    """
 59    Roll back the most recent savepoint (if one exists). Do nothing if
 60    savepoints are not supported.
 61    """
 62    get_connection(using).savepoint_rollback(sid)
 63
 64
 65def savepoint_commit(sid, using=None):
 66    """
 67    Commit the most recent savepoint (if one exists). Do nothing if
 68    savepoints are not supported.
 69    """
 70    get_connection(using).savepoint_commit(sid)
 71
 72
 73def clean_savepoints(using=None):
 74    """
 75    Reset the counter used to generate unique savepoint ids in this thread.
 76    """
 77    get_connection(using).clean_savepoints()
 78
 79
 80def get_rollback(using=None):
 81    """Get the "needs rollback" flag -- for *advanced use* only."""
 82    return get_connection(using).get_rollback()
 83
 84
 85def set_rollback(rollback, using=None):
 86    """
 87    Set or unset the "needs rollback" flag -- for *advanced use* only.
 88
 89    When `rollback` is `True`, trigger a rollback when exiting the innermost
 90    enclosing atomic block that has `savepoint=True` (that's the default). Use
 91    this to force a rollback without raising an exception.
 92
 93    When `rollback` is `False`, prevent such a rollback. Use this only after
 94    rolling back to a known-good state! Otherwise, you break the atomic block
 95    and data corruption may occur.
 96    """
 97    return get_connection(using).set_rollback(rollback)
 98
 99
100@contextmanager
101def mark_for_rollback_on_error(using=None):
102    """
103    Internal low-level utility to mark a transaction as "needs rollback" when
104    an exception is raised while not enforcing the enclosed block to be in a
105    transaction. This is needed by Model.save() and friends to avoid starting a
106    transaction when in autocommit mode and a single query is executed.
107
108    It's equivalent to:
109
110        connection = get_connection(using)
111        if connection.get_autocommit():
112            yield
113        else:
114            with transaction.atomic(using=using, savepoint=False):
115                yield
116
117    but it uses low-level utilities to avoid performance overhead.
118    """
119    try:
120        yield
121    except Exception as exc:
122        connection = get_connection(using)
123        if connection.in_atomic_block:
124            connection.needs_rollback = True
125            connection.rollback_exc = exc
126        raise
127
128
129def on_commit(func, using=None, robust=False):
130    """
131    Register `func` to be called when the current transaction is committed.
132    If the current transaction is rolled back, `func` will not be called.
133    """
134    get_connection(using).on_commit(func, robust)
135
136
137#################################
138# Decorators / context managers #
139#################################
140
141
142class Atomic(ContextDecorator):
143    """
144    Guarantee the atomic execution of a given block.
145
146    An instance can be used either as a decorator or as a context manager.
147
148    When it's used as a decorator, __call__ wraps the execution of the
149    decorated function in the instance itself, used as a context manager.
150
151    When it's used as a context manager, __enter__ creates a transaction or a
152    savepoint, depending on whether a transaction is already in progress, and
153    __exit__ commits the transaction or releases the savepoint on normal exit,
154    and rolls back the transaction or to the savepoint on exceptions.
155
156    It's possible to disable the creation of savepoints if the goal is to
157    ensure that some code runs within a transaction without creating overhead.
158
159    A stack of savepoints identifiers is maintained as an attribute of the
160    connection. None denotes the absence of a savepoint.
161
162    This allows reentrancy even if the same AtomicWrapper is reused. For
163    example, it's possible to define `oa = atomic('other')` and use `@oa` or
164    `with oa:` multiple times.
165
166    Since database connections are thread-local, this is thread-safe.
167
168    An atomic block can be tagged as durable. In this case, raise a
169    RuntimeError if it's nested within another atomic block. This guarantees
170    that database changes in a durable block are committed to the database when
171    the block exists without error.
172
173    This is a private API.
174    """
175
176    def __init__(self, using, savepoint, durable):
177        self.using = using
178        self.savepoint = savepoint
179        self.durable = durable
180        self._from_testcase = False
181
182    def __enter__(self):
183        connection = get_connection(self.using)
184
185        if (
186            self.durable
187            and connection.atomic_blocks
188            and not connection.atomic_blocks[-1]._from_testcase
189        ):
190            raise RuntimeError(
191                "A durable atomic block cannot be nested within another "
192                "atomic block."
193            )
194        if not connection.in_atomic_block:
195            # Reset state when entering an outermost atomic block.
196            connection.commit_on_exit = True
197            connection.needs_rollback = False
198            if not connection.get_autocommit():
199                # Pretend we're already in an atomic block to bypass the code
200                # that disables autocommit to enter a transaction, and make a
201                # note to deal with this case in __exit__.
202                connection.in_atomic_block = True
203                connection.commit_on_exit = False
204
205        if connection.in_atomic_block:
206            # We're already in a transaction; create a savepoint, unless we
207            # were told not to or we're already waiting for a rollback. The
208            # second condition avoids creating useless savepoints and prevents
209            # overwriting needs_rollback until the rollback is performed.
210            if self.savepoint and not connection.needs_rollback:
211                sid = connection.savepoint()
212                connection.savepoint_ids.append(sid)
213            else:
214                connection.savepoint_ids.append(None)
215        else:
216            connection.set_autocommit(
217                False, force_begin_transaction_with_broken_autocommit=True
218            )
219            connection.in_atomic_block = True
220
221        if connection.in_atomic_block:
222            connection.atomic_blocks.append(self)
223
224    def __exit__(self, exc_type, exc_value, traceback):
225        connection = get_connection(self.using)
226
227        if connection.in_atomic_block:
228            connection.atomic_blocks.pop()
229
230        if connection.savepoint_ids:
231            sid = connection.savepoint_ids.pop()
232        else:
233            # Prematurely unset this flag to allow using commit or rollback.
234            connection.in_atomic_block = False
235
236        try:
237            if connection.closed_in_transaction:
238                # The database will perform a rollback by itself.
239                # Wait until we exit the outermost block.
240                pass
241
242            elif exc_type is None and not connection.needs_rollback:
243                if connection.in_atomic_block:
244                    # Release savepoint if there is one
245                    if sid is not None:
246                        try:
247                            connection.savepoint_commit(sid)
248                        except DatabaseError:
249                            try:
250                                connection.savepoint_rollback(sid)
251                                # The savepoint won't be reused. Release it to
252                                # minimize overhead for the database server.
253                                connection.savepoint_commit(sid)
254                            except Error:
255                                # If rolling back to a savepoint fails, mark for
256                                # rollback at a higher level and avoid shadowing
257                                # the original exception.
258                                connection.needs_rollback = True
259                            raise
260                else:
261                    # Commit transaction
262                    try:
263                        connection.commit()
264                    except DatabaseError:
265                        try:
266                            connection.rollback()
267                        except Error:
268                            # An error during rollback means that something
269                            # went wrong with the connection. Drop it.
270                            connection.close()
271                        raise
272            else:
273                # This flag will be set to True again if there isn't a savepoint
274                # allowing to perform the rollback at this level.
275                connection.needs_rollback = False
276                if connection.in_atomic_block:
277                    # Roll back to savepoint if there is one, mark for rollback
278                    # otherwise.
279                    if sid is None:
280                        connection.needs_rollback = True
281                    else:
282                        try:
283                            connection.savepoint_rollback(sid)
284                            # The savepoint won't be reused. Release it to
285                            # minimize overhead for the database server.
286                            connection.savepoint_commit(sid)
287                        except Error:
288                            # If rolling back to a savepoint fails, mark for
289                            # rollback at a higher level and avoid shadowing
290                            # the original exception.
291                            connection.needs_rollback = True
292                else:
293                    # Roll back transaction
294                    try:
295                        connection.rollback()
296                    except Error:
297                        # An error during rollback means that something
298                        # went wrong with the connection. Drop it.
299                        connection.close()
300
301        finally:
302            # Outermost block exit when autocommit was enabled.
303            if not connection.in_atomic_block:
304                if connection.closed_in_transaction:
305                    connection.connection = None
306                else:
307                    connection.set_autocommit(True)
308            # Outermost block exit when autocommit was disabled.
309            elif not connection.savepoint_ids and not connection.commit_on_exit:
310                if connection.closed_in_transaction:
311                    connection.connection = None
312                else:
313                    connection.in_atomic_block = False
314
315
316def atomic(using=None, savepoint=True, durable=False):
317    # Bare decorator: @atomic -- although the first argument is called
318    # `using`, it's actually the function being decorated.
319    if callable(using):
320        return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
321    # Decorator: @atomic(...) or context manager: with atomic(...): ...
322    else:
323        return Atomic(using, savepoint, durable)