Skip to main content

Query Builder API in Frappe v15

What is Query Builder in Frappe?

The Query Builder is a Python API that allows developers to build and run safe SQL queries in Frappe without writing raw SQL. It ensures:

  • Query safety using query parameterization
  • Works with MariaDB and PostgreSQL (where applicable)
  • Easy joins, ordering, and grouping
  • Support for complex WHERE clauses
  • Full chaining API

Query Builder is available under:

from frappe.query_builder import DocType

Why Use Query Builder?

Query Builder solves the limitations of raw SQL by:

  • Preventing SQL injection
  • Making queries database-agnostic
  • Allowing dynamic query composition
  • Providing Pythonic syntax
  • Integrating with Frappe ORM

It is best used when you need custom database queries beyond simple frappe.get_all or ORM operations.

Importing Query Builder in Frappe v15

You first define a DocType object representing a table:

from frappe.query_builder import DocType
Customer = DocType("Customer")

This maps Customer DocType to the underlying database table tabCustomer.

Query Builder Basic Operations

SELECT Query

from frappe.query_builder import DocType
import frappe
Customer = DocType("Customer")
query = (
frappe.qb.from_(Customer)
.select(Customer.name, Customer.customer_name, Customer.email_id)
)
data = query.run(as_dict=True)

Result Format

run(as_dict=True) returns a list of dictionaries.

WHERE Filters

Simple WHERE Condition

data = (
frappe.qb.from_(Customer)
.select("*")
.where(Customer.customer_group == "Retail")
.run(as_dict=True)
)

Multiple Conditions

Use & and | for AND/OR:

from frappe.query_builder import DocType
from pypika.terms import Criterion
Customer = DocType("Customer")
query = (
frappe.qb.from_(Customer)
.select(Customer.name)
.where(
(Customer.customer_group == "Retail") &
(Customer.territory == "India")
)
)
results = query.run(as_dict=True)

ORDER BY and LIMIT

results = (
frappe.qb.from_(Customer)
.select(Customer.name)
.orderby(Customer.creation, order="desc")
.limit(10)
.run(as_dict=True)
)

JOIN Queries

Query Builder supports inner and left joins.

INNER JOIN Example

from frappe.query_builder import DocType
import frappe
SalesOrder = DocType("Sales Order")
Customer = DocType("Customer")
results = (
frappe.qb.from_(SalesOrder)
.join(Customer)
.on(SalesOrder.customer == Customer.name)
.select(
SalesOrder.name,
Customer.customer_name,
SalesOrder.transaction_date
)
.run(as_dict=True)
)

GROUP BY and Aggregation

from frappe.query_builder.functions import Count
results = (
frappe.qb.from_(Customer)
.select(Customer.territory, Count("*").as_("total"))
.groupby(Customer.territory)
.run(as_dict=True)
)

Using Functions

Frappe Query Builder supports SQL functions via frappe.query_builder.functions.

Example: SUM, COUNT, MAX, MIN

from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
SalesInvoice = DocType("Sales Invoice")
results = (
frappe.qb.from_(SalesInvoice)
.select(Sum(SalesInvoice.grand_total).as_("revenue"))
.run(as_dict=True)
)

INSERT Query (Create Record)

While ORM (doc.insert()) is recommended, Query Builder supports insert:

query = frappe.qb.into(Customer).columns("name", "customer_name").insert(("CUST-001", "ABC Foods"))
query.run()

UPDATE Records

(
frappe.qb.update(Customer)
.set(Customer.customer_name, "Updated Name")
.where(Customer.name == "CUST-001")
).run()

DELETE Records

(
frappe.qb.from_(Customer)
.where(Customer.name == "CUST-001")
.delete()
).run()

⚠ Avoid DELETE unless absolutely required. Use soft delete or cancellation methods in DocType, if available.

Using Query Builder with frappe.qb

Frappe wraps Query Builder under frappe.qb, which ensures:

  • Automatic DB connection
  • Site context isolation
  • Query logging
  • Multi-tenant safety

Example:

query = (
frappe.qb.from_(Customer)
.select(Customer.name)
)
query.run(as_dict=True)

Advanced Query Builder Features

1. Subqueries

from frappe.query_builder.functions import Count
Order = DocType("Sales Order")
Customer = DocType("Customer")
sub = (
frappe.qb.from_(Order)
.select(Count("*"))
.where(Order.customer == Customer.name)
)
results = (
frappe.qb.from_(Customer)
.select(Customer.name, sub.as_("order_count"))
.run(as_dict=True)
)

2. Raw SQL Fallback

If you need full SQL control:

frappe.db.sql(query.get_sql(), as_dict=True)

get_sql() returns the generated SQL.

Best Practices for Query Builder

  • Prefer Frappe ORM for simple operations
  • Use Query Builder for:
    • Complex joins
    • Database-heavy operations
    • Reporting queries
    • Aggregation queries
    • Subqueries
  • Always use run(as_dict=True) for clean output
  • Avoid unsafe raw SQL
  • Use Criterion for complex filters

Troubleshooting

Query Injection Issues

Query Builder prevents injection automatically.
Do not format SQL strings manually.

No Results

Check:

  • Field names
  • Filters (where)
  • Permissions (use ignore_permissions) if required only in controlled flows

Example:

query.run(ignore_permissions=True)

Integration Patterns

Use Query Builder when building:

  • Custom reports
  • REST API fetching logic
  • Dashboard summaries
  • Advanced filters in server scripts
  • External integrations needing optimized SQL

Combine with:

  • REST API (frappe.api)
  • Background Jobs
  • Frappe ORM
  • Canned Reports

Cross-References

Related documentation:

  • Database API: frappe.db
  • Document API: frappe.get_doc
  • REST API: /api/resource/<doctype>
  • Response API
  • Full-Text Search API
Click to rate this post!
[Total: 0 Average: 0]