Secure SQL Queries in Frappe That Respect Permissions

Published On: 25 March 2026.By .

Performance & security — without the tradeoff.

When making reports or fetching data in general, secure SQL queries in Frappe are often the first solution that comes to mind.

Why SQL?

1 Speed — The “C” Factor

Database engines use C/C++, which makes them highly optimized for filtering and joining data. That’s why joining and filtering is much faster in SQL than doing it in Python.

Combined with database indexing, this makes SQL an entirely different beast.

2 Intuitive

SQL is the universal language of data. Most developers already know SQL, making it simpler for existing devs to write the code and for new devs to understand it.

The Problem

In Frappe, there is a major issue in using SQL — it doesn’t know who is logged in.

SQL fetches all the data, irrespective of user/role permissions. It ignores:

User Permissions — e.g., “User A can only see Territory B”

Role Permissions — e.g., “Sales Users cannot read Purchase Receipts”

This makes it unreliable in many cases.

The Frappe Alternatives

Frappe provides tools for this — let’s look at them first.

1. get_list (docs)

Takes the name of a DocType, a list of filters, and fetches all matching entries. Works well for single-DocType fetches.

But when you need joins (e.g. customers connected to a department), it falls short — you must fetch data into Python, creating a bottleneck.

2. Query Builder

Although some sources claim it handles permissions, in practice it is often unreliable.

Query Builder Screenshot

The Solution: Secure SQL Queries

We don’t need to choose between security and efficiency. Now we can have both.

We can add user permissions to the SQL query itself as filters using the function build_match_conditions from frappe.desk.reportview.

This is the same method Frappe uses internally in get_list.

How to Implement

Import the helper

from frappe.desk.reportview \
    import build_match_conditions

Fetch the permissions

permission_sql = build_match_conditions(
    "Sales Order"
)

Example output

"(`tabSales Order`.customer = 'Cust-001' OR `tabSales Order`.owner = 'user@example.com')"

Handle aliases CRUCIAL

The system returns a string with the full table name. If your SQL uses an alias, replace it to avoid DB errors.

permission_sql = permission_sql.replace(
    "`tabSales Order`",
    "so"
)

Inject into your query

if permission_sql:
    where_clause += \
        f" AND {permission_sql}"
    # Empty = user has all perms
    # — prevents syntax error

data = frappe.db.sql(f"""
    SELECT
        so.name,
        so.grand_total
    FROM `tabSales Order` so
    WHERE
        so.docstatus = 1
        AND {permission_sql}
""", as_dict=True)

This query now only returns records the user can access.

Technical Details

⚠ Permission Error

If the user lacks access to a DocType, the function raises a PermissionError. Handle it:

try:
    permission_sql = \
        build_match_conditions(
            "Purchase Order"
        )
except frappe.PermissionError:
    # No read access
    permission_sql = "1=0"

Empty string: If no restrictions exist, the function returns an empty string. Handle this to avoid SQL syntax errors.

One DocType at a time: Call the function for each DocType individually and combine the conditions.

Reusable Helper Function

A utility that handles multiple DocTypes at once:

import frappe
from frappe.desk.reportview import \
    build_match_conditions

def get_permission_filters(
    doctype_map
):
    """
    SQL conditions for multiple
    DocTypes.

    Args:
        doctype_map (dict):
            Key = DocType
            Value = Alias
            {"Sales Order": "so"}

    Returns:
        list of SQL conditions.
        Denied -> ["1=0"]
    """
    conditions = []
    for dt, alias in \
            doctype_map.items():
        try:
            rule = \
                build_match_conditions(dt)
            if rule:
                if alias:
                    rule = rule.replace(
                        f"`tab{dt}`",
                        alias
                    )
                conditions.append(
                    f"({rule})"
                )
        except frappe.PermissionError:
            return ["1=0"]
    return conditions

Related content

Stay Close to What We’re Building

Get insights on product engineering, AI, and real-world technology decisions shaping modern businesses.

Go to Top