pyDAL Documentation

repository·master·Indexed 19 days ago

https://github.com/web2py/pydal

A pure-Python Database Abstraction Layer that generates SQL or NoSQL queries in real time. pyDAL provides a consistent API across approximately 15 different database backends, including SQLite, PostgreSQL, MySQL, MSSQL, Oracle, and MongoDB. It features a Query DSL for constructing queries, support for migrations via table redefinition, built-in validators, and capabilities for building subqueries and Common Table Expressions (CTEs).

Tokens
34.4K
Snippets
114
Records
139
Agent score
68%

What's inside pyDAL

  1. Use computed, virtual, and filtered fields

    master

    pyDAL allows you to extend table behavior using computed fields, virtual fields, and common filters.

    Computed vs Virtual Fields

    • Computed Field: Calculated during insert or update and physically stored in the database. Defined via the compute argument in Field().
    • Virtual Field: Calculated on-the-fly every time the field is accessed from a result set. It is not stored in the database and cannot be used in queries. Defined using Field.Virtual().

    Common Filters

    You can attach a query to a table using _common_filter. Every Set (query) performed against that table will automatically include this filter. This is useful for implementing soft-delete or multi-tenant isolation.

    • To bypass the common filter, use db(query, ignore_common_filters=True).

    Callbacks

    You can hook into the lifecycle of a record using callbacks:

    • _before_insert
    • _after_update
    • _after_delete

    Note: If a _before_* callback returns a truthy value, the operation is cancelled.

    # Computed Field (Stored)
    db.define_table("person",
        Field("first"),
        Field("last"),
        Field("full", compute=lambda row: f"{row['first']} {row['last']}"),
    )
    
    # Virtual Field (Not stored, computed on access)
    class PersonMethods:
        def full(row):
            return row.first + " " + row.last
    
    db.person.full = Field.Virtual("full", lambda row: row.first + " " + row.last)
    
    # Common Filter (e.g., for soft-delete)
    db.thing._common_filter = lambda q: db.thing.deleted == False
    
    # Callbacks
    db.thing._before_insert.append(lambda fields: ...)
    db.thing._after_update.append(lambda set, fields: ...)
    db.thing._after_delete.append(lambda set: ...)
  2. Understand the pyDAL backend architecture

    master

    pyDAL's backend logic is split into two primary modules: pydal.backend_base (the framework) and pydal.backends.<name> (specific database implementations).

    Core Abstractions

    The backend relies on four collaborating layers:

    1. Adapter (SQLAdapter / NoSQLAdapter): Manages the session-level connection.
    2. Dialect (SQLDialect / NoSQLDialect): Responsible for converting Abstract Syntax Tree (AST) nodes into database-specific strings (e.g., SQL).
    3. Representer (SQLRepresenter / NoSQLRepresenter / JSONRepresenter): Converts Python values into database literals.
    4. Parser (BasicParser + mixins): Converts driver row values back into Python values.

    How Connection URIs Work

    When you instantiate DAL("protocol://..."), pyDAL uses the URI prefix to select a registered adapter from the adapters registry. The selected adapter then automatically selects the appropriate dialect, representer, and parser through its Method Resolution Order (MRO).

  3. Build subqueries and CTEs

    master

    pyDAL provides three ways to construct subqueries, with AST-native forms being recommended for better parameter handling.

    Subquery Methods

    1. AST-native (Recommended): db(query).subselect(field).
    2. Legacy nested_select: db(query).nested_select(field). This can also be used as a join source by calling .with_alias(name) on the result.
    3. Raw SQL (Inline only): db(query)._select(field).

    Common Table Expressions (CTEs)

    • Standard CTE: Created using set.cte(name, *fields).
    • Recursive CTE: Use .union(lambda self: ...) to add the recursive step to a CTE.

    Using a SELECT as a Join Source

    You can use a nested_select as a table in a join by providing an alias.

    # 1. Recommended AST-native subquery
    sub = db(db.thing.color == "red").subselect(db.thing.owner_id)
    db(db.person.id.belongs(sub)).select()
    
    # 2. Using nested_select as a join source
    sub = db(db.thing.color == "red").nested_select(
        db.thing.owner_id, db.thing.name
    ).with_alias("red_things")
    
    db(db.person).select(
        db.person.name, sub.name,
        join=sub.on(sub.owner_id == db.person.id),
    )
    
    # 3. Standard CTE
    recent = db(db.event.created > "2026-01-01").cte(
        "recent", db.event.id, db.event.user_id
    )
    db(db.user.id.belongs(recent.user_id)).select()
    
    # 4. Recursive CTE
    descendants = (
        db(db.org.id == root_id).cte(
            "descendants",
            db.org.id, db.org.name, db.org.parent_id,
        )
        .union(lambda descendants: 
            db(db.org.parent_id == descendants.id).nested_select(
                db.org.id, db.org.name, db.org.parent_id,
            )
        )
    )
  4. Quickstart with pyDAL

    master

    This example demonstrates the basic workflow: connecting to a database, defining a table with a field, inserting data, and querying the results using a WHERE clause. Note that db.commit() is used to persist changes.

    from pydal import DAL, Field
    
    db = DAL("sqlite://storage.db")
    db.define_table("thing", Field("name"))
    
    db.thing.insert(name="Chair")
    db.thing.insert(name="Table")
    
    for row in db(db.thing.name.startswith("C")).select():
        print(row.id, row.name)
    # 1 Chair
    
    db.commit()
  5. How to add a new database backend to pyDAL

    master

    To implement a new database backend, follow these steps:

    1. Create a new module at pydal/backends/<name>.py.
    2. Implement a subclass of the appropriate Adapter (e.g., SQLAdapter).
    3. Override the dialect, parser, or representer as needed to handle specific database requirements.
    4. Register the new backend by adding the import to pydal/backends/__init__.py.
  6. Create custom validators

    master

    Any callable with the signature f(value) -> (cleaned, error_or_None) works as a validator. For more advanced features like translation or record_id-aware uniqueness checks, subclass Validator and implement the validate method.

    Validators also accept an error_message= argument in their constructor to override default messages. Messages can be passed through Validator.translator for i18n support.

    from pydal.validators import Validator, ValidationError
    
    class IS_EVEN(Validator):
        def __init__(self, error_message="Must be even"):
            self.error_message = error_message
    
        def validate(self, value, record_id=None):
            if int(value) % 2 != 0:
                raise ValidationError(self.translator(self.error_message))
            return int(value)
    
    Field("n", "integer", requires=IS_EVEN())
  7. Install pyDAL via pip

    master

    Install the pyDAL package using pip. The only hard dependency is Python ≥ 3.7. While SQLite support is built into Python, other databases require their respective Python drivers (e.g., psycopg2 for PostgreSQL, pymysql for MySQL) to be installed separately. pyDAL will automatically detect and use the installed drivers.

    pip install pyDAL
  8. Generate SQL without a database connection

    master

    You can use pyDAL as a pure SQL generator without requiring a running database or specific drivers. This is useful for inspecting generated SQL or cross-dialect comparisons.

    Process:

    1. Connect to an in-memory SQLite database: DAL("sqlite:memory", migrate=False).
    2. Define your tables and fields.
    3. Swap the adapter's dialect to your target backend (e.g., PostgresDialect or MySQLDialect).
    4. Disable parameterization if you want human-readable SQL with inline values: db._adapter.compiler.parameterize = False.
    5. Use the underscore-prefixed methods to return SQL strings instead of executing them:
      • db(query)._select(...)
      • db(query)._delete()
      • db(query)._update(...)
      • db.table._insert(...)
      • db(query)._count()
    from pydal import DAL, Field
    from pydal.backends.postgres import PostgresDialect
    
    # Scratch connection
    db = DAL("sqlite:memory", migrate=False)
    
    # Retarget to PostgreSQL and disable parameterization for inspection
    db._adapter.dialect = PostgresDialect(db._adapter)
    db._adapter.compiler.parameterize = False
    
    db.define_table("person", Field("name"), Field("age", "integer"))
    
    q = (db.person.age >= 18) & (db.person.name.like("A%"))
    
    # Returns SQL string without execution
    print(db(q)._select(db.person.id, db.person.name))
    # SELECT "person"."id", "person"."name" FROM "person" 
    # WHERE (("person"."age" >= 18) AND ("person"."name" LIKE 'A%' ESCAPE '\'));
    
    print(db.person._insert(name="Alice", age=30))
    # INSERT INTO "person"("name","age") VALUES ('Alice',30);
  9. How DAL customization hooks work via MetaDAL

    master

    The DAL class uses a metaclass MetaDAL to allow customization via constructor arguments. Passing certain keyword arguments to DAL(...) is equivalent to subclassing DAL and setting those attributes at the class level. This allows you to globally configure behavior for all tables and rows created by that DAL instance.

    Intercepted customization hooks:

    • logger: Custom logger instance.
    • representers: Custom field representers.
    • serializers: Custom serializers.
    • uuid: Custom UUID generation method.
    • validators: Custom validators.
    • validators_method: Custom method for handling validators.
    • Table: Custom Table class.
    • Row: Custom Row class.
  10. Configure connection pooling

    master

    Connection pooling is managed via the ConnectionPool.POOLS dictionary, which maps a connection URI to a list of reusable connections.

    To utilize pooling, ensure the adapter has a pool_size > 0. When get_connection(use_pool=True) is called, pyDAL will attempt to retrieve an existing connection from the pool for that URI and validate it using test_connection().

  11. Implement a custom Representer for data storage

    master

    Representers convert Python objects into the format required by the database driver for storage.

    SQLRepresenter

    Used for SQL backends. It typically involves:

    • Quoting strings via adapter.adapt.
    • Base64 encoding blob types.
    • Using pipe-delimited encoding for list:* types.
    • Handling SQLCustomType via an encoder callback.
    • Mapping None to NULL.

    NoSQLRepresenter

    Used for NoSQL backends. It stores native Python values directly. The adapt method is a no-op because NoSQL drivers handle parameter encoding themselves.

  12. Use ExecutionHandlers to intercept SQL execution

    master

    SQLAdapter supports ExecutionHandler instances that are called around every cursor.execute. You can define custom handlers to log, profile, or modify statements.

    Example: DebugHandler logs every executed statement at the DEBUG level. If db._debug is enabled, DebugHandler is automatically inserted into the execution handler chain.

    class DebugHandler(ExecutionHandler):
        """ExecutionHandler that logs every executed statement at DEBUG level."""
        def before_execute(self, command):
            """Log the SQL before sending it to the cursor."""
            self.adapter.db.logger.debug("SQL: %s" % command)