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()
    password = PasswordField()
    is_admin = 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
admin_users = User.objects.filter(is_admin=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 atomic block."
192            )
193        if not connection.in_atomic_block:
194            # Reset state when entering an outermost atomic block.
195            connection.commit_on_exit = True
196            connection.needs_rollback = False
197            if not connection.get_autocommit():
198                # Pretend we're already in an atomic block to bypass the code
199                # that disables autocommit to enter a transaction, and make a
200                # note to deal with this case in __exit__.
201                connection.in_atomic_block = True
202                connection.commit_on_exit = False
203
204        if connection.in_atomic_block:
205            # We're already in a transaction; create a savepoint, unless we
206            # were told not to or we're already waiting for a rollback. The
207            # second condition avoids creating useless savepoints and prevents
208            # overwriting needs_rollback until the rollback is performed.
209            if self.savepoint and not connection.needs_rollback:
210                sid = connection.savepoint()
211                connection.savepoint_ids.append(sid)
212            else:
213                connection.savepoint_ids.append(None)
214        else:
215            connection.set_autocommit(
216                False, force_begin_transaction_with_broken_autocommit=True
217            )
218            connection.in_atomic_block = True
219
220        if connection.in_atomic_block:
221            connection.atomic_blocks.append(self)
222
223    def __exit__(self, exc_type, exc_value, traceback):
224        connection = get_connection(self.using)
225
226        if connection.in_atomic_block:
227            connection.atomic_blocks.pop()
228
229        if connection.savepoint_ids:
230            sid = connection.savepoint_ids.pop()
231        else:
232            # Prematurely unset this flag to allow using commit or rollback.
233            connection.in_atomic_block = False
234
235        try:
236            if connection.closed_in_transaction:
237                # The database will perform a rollback by itself.
238                # Wait until we exit the outermost block.
239                pass
240
241            elif exc_type is None and not connection.needs_rollback:
242                if connection.in_atomic_block:
243                    # Release savepoint if there is one
244                    if sid is not None:
245                        try:
246                            connection.savepoint_commit(sid)
247                        except DatabaseError:
248                            try:
249                                connection.savepoint_rollback(sid)
250                                # The savepoint won't be reused. Release it to
251                                # minimize overhead for the database server.
252                                connection.savepoint_commit(sid)
253                            except Error:
254                                # If rolling back to a savepoint fails, mark for
255                                # rollback at a higher level and avoid shadowing
256                                # the original exception.
257                                connection.needs_rollback = True
258                            raise
259                else:
260                    # Commit transaction
261                    try:
262                        connection.commit()
263                    except DatabaseError:
264                        try:
265                            connection.rollback()
266                        except Error:
267                            # An error during rollback means that something
268                            # went wrong with the connection. Drop it.
269                            connection.close()
270                        raise
271            else:
272                # This flag will be set to True again if there isn't a savepoint
273                # allowing to perform the rollback at this level.
274                connection.needs_rollback = False
275                if connection.in_atomic_block:
276                    # Roll back to savepoint if there is one, mark for rollback
277                    # otherwise.
278                    if sid is None:
279                        connection.needs_rollback = True
280                    else:
281                        try:
282                            connection.savepoint_rollback(sid)
283                            # The savepoint won't be reused. Release it to
284                            # minimize overhead for the database server.
285                            connection.savepoint_commit(sid)
286                        except Error:
287                            # If rolling back to a savepoint fails, mark for
288                            # rollback at a higher level and avoid shadowing
289                            # the original exception.
290                            connection.needs_rollback = True
291                else:
292                    # Roll back transaction
293                    try:
294                        connection.rollback()
295                    except Error:
296                        # An error during rollback means that something
297                        # went wrong with the connection. Drop it.
298                        connection.close()
299
300        finally:
301            # Outermost block exit when autocommit was enabled.
302            if not connection.in_atomic_block:
303                if connection.closed_in_transaction:
304                    connection.connection = None
305                else:
306                    connection.set_autocommit(True)
307            # Outermost block exit when autocommit was disabled.
308            elif not connection.savepoint_ids and not connection.commit_on_exit:
309                if connection.closed_in_transaction:
310                    connection.connection = None
311                else:
312                    connection.in_atomic_block = False
313
314
315def atomic(using=None, savepoint=True, durable=False):
316    # Bare decorator: @atomic -- although the first argument is called
317    # `using`, it's actually the function being decorated.
318    if callable(using):
319        return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
320    # Decorator: @atomic(...) or context manager: with atomic(...): ...
321    else:
322        return Atomic(using, savepoint, durable)