RapidQuery: High-Performance SQL Query Builder for Python
RapidQuery is a powerful SQL query builder library designed for Python, combining the simplicity of Python with the raw speed and safety of Rust. Build complex SQL queries effortlessly and efficiently, with a library that prioritizes both performance and ease of use.
Key Features:
- 🚀 Blazing Fast Performance: Leveraging the power of Rust under the hood, RapidQuery ensures your query building process is as fast as possible.
- 🛡️ SQL Injection Protection: Built-in security measures to prevent SQL injection attacks by default.
- 📝 Intuitive Pythonic API: Write clean, readable code with an API that feels natural to Python developers.
- 🐍 Seamless Python Integration: Works perfectly with popular Python web frameworks and database drivers.
Built on Solid Foundations
RapidQuery is built with Rust and powered by the robust SeaQuery crate, bringing enterprise-grade reliability and performance to your Python applications.
Why RapidQuery Was Created
In a landscape filled with SQL libraries, we noticed a critical gap: performance was often an afterthought. That's why we built RapidQuery with speed as our primary and enduring focus.
Our Core Mission:
- Performance First: While other libraries compromise on speed, we engineered RapidQuery from the ground up for maximum performance.
- Foundation for Future ORM: RapidQuery serves as the foundational layer for building a next-generation, high-performance ORM for Python.
- Meeting Python's Needs: Python dominates backend development, particularly in web applications. Every backend deserves a fast, powerful database interaction layer — that's exactly what we're building.
- Security by Design: Unlike many alternatives, we bake security directly into our architecture with automatic SQL injection prevention.
Build your SQL queries faster, safer, and more efficiently than ever before. RapidQuery - where Python meets Rust's performance for database excellence.
To install RapidQuery, run the following command:
pip3 install rapidqueryNote
RapidQuery requires Python 3.10+. Supports CPython and PyPy.
RapidQuery supports PostgreSQL, MySQL, and SQLite databases. In RapidQuery, these are referred to as backends.
When building SQL statements, you should specify your target backend.
import rapidquery as rq
import datetime
stmt = rq.Insert().into("repositories").values(name="RapidQuery", created_at=datetime.datetime.now())
stmt.to_sql("postgres")
# INSERT INTO "repositories" ("name", "created_at") VALUES ('RapidQuery', '2025-11-14 16:18:59.188940')- Core Concepts
- Query Statements
- More About Queries
- Schema Statements
- Advanced Usage
- Performance
AdaptedValue bridges Python types, Rust types, and SQL types for seamless data conversion.
This class handles validation, adaptation, and conversion between different type systems used in the application stack.
import rapidquery as rq
# Let the system detect types automatically
rq.AdaptedValue(1) # -> INTEGER SQL type
rq.AdaptedValue(1.4) # -> DOUBLE SQL type
rq.AdaptedValue("127.0.0.1") # -> VARCHAR SQL type
rq.AdaptedValue({"key": "value"}) # -> JSON SQL type
# Explicitly specify the type
rq.AdaptedValue(1, rq.TinyUnsignedType()) # -> TINYINT UNSIGNED SQL type
rq.AdaptedValue(1.4, rq.FloatType()) # -> FLOAT SQL type
rq.AdaptedValue("127.0.0.1", rq.InetType()) # -> INET SQL type (network address)
rq.AdaptedValue([4.3, 5.6], rq.VectorType()) # -> VECTOR SQL type (for AI embeddings)
# Also you can use `AdaptedValue.to_sql()` method to convert value into SQL
val = rq.AdaptedValue([2, 3, 4], rq.ArrayType(rq.IntegerType()))
val.to_sql("postgresql") # -> ARRAY [2,3,4]As we said, AdaptedValue also validates your value:
rq.AdaptedValue(4.5, rq.CharType()) # -> TypeError: expected str, got floatTip
Important: AdaptedValue is lazy. This means it keeps your value and never converts it to Rust and then SQL until needed.
Represents a SQL expression that can be built into SQL code.
This class provides a fluent interface for constructing complex SQL expressions in a database-agnostic way. It supports arithmetic operations, comparisons, logical operations, and database-specific functions.
The class automatically handles SQL injection protection and proper quoting when building the final SQL statement.
Everything can be converted into Expr, such as built-in types, datetime, uuid, AdaptedValue, Select, etc.
Basic
import rapidquery as rp
rp.Expr(25) # -> 25 (literal value)
rp.Expr("Hello") # -> 'Hello' (literal value)
rp.Expr(rq.AdaptedValue('World')) # -> 'World' (literal value)
rp.Expr.col("id") # -> "id" (column reference)
rp.Expr.col("users.name") # -> "users"."name" (column reference)
rp.Expr(rq.ColumnRef("name", table="users")) # -> "users"."name" (column reference)Comparisons
rq.Expr.col("status") == "active" # -> "status" == 'active'
rq.Expr.col("age") > 16 # -> "age" > 16
# Note that `rq.all` is different from built-in `all`
rq.all(
rq.Expr.col("age") >= 18,
rq.Expr.col("subscription").is_null(), # same as rq.Expr.col("subscription").is_(Expr.null())
rq.Expr.col("status").in_(["pending", "approved", "active"])
) # -> "age" >= 18 AND "subscription" IS NULL AND "status" IN ('pending', 'approved', 'active')
# Note that `rq.any` is different from built-in `any`
rq.any(
rq.Expr.col("is_admin").is_(True),
rq.Expr.col("is_moderator").is_not_null(), # same as rq.Expr.col("subscription").is_not(Expr.null())
rq.Expr.col("price").between(10.00, 50.00)
) # -> "is_admin" IS TRUE OR "is_moderator" IS NOT NULL OR "price" BETWEEN 10.00 AND 50.00Best Practices
- Always use
Expr.col()for column references: This ensures proper quoting for your target database
# Column reference (properly quoted identifier)
rq.Expr.col("user_name") # → "user_name"
# String literal (value)
rq.Expr("user_name") # → 'user_name'- Use
rapidquery.all()andrapidquery.any()for logical combinations: More readable than chaining&and|operators
# Good
all(condition1, condition2, condition3)
# Less readable
condition1 & condition2 & condition3- Be careful with
Expr.custom(): It bypasses all safety checks
# Dangerous - vulnerable to SQL injection
user_input = "'; DROP TABLE users; --"
Expr.custom(f"name = '{user_input}'")
# Safe
Expr.col("name") == user_input- Use database-specific features when necessary: But understand portability trade-offs
# PostgreSQL-specific but powerful
Expr.col("tags").pg_contains(["python"])
# More portable but may be less efficient
Expr.col("tags").like("%python%")Statements are divided into 2 categories: QueryStatement, and SchemaStatement.
Some statements like Select, Update, Delete, Insert, ... are QueryStatement.
Other statements like Table, AlterTable, Index, ... are SchemaStatement.
QueryStatement class interface is:
class QueryStatement:
def build(self, backend: _Backends) -> typing.Tuple[str, typing.Tuple[AdaptedValue, ...]]:
"""
Build the SQL statement with parameter values.
"""
...
def to_sql(self, backend: _Backends) -> str:
"""
Build a SQL string representation.
**This method is unsafe and can cause SQL injection.** use `.build()` method instead.
"""
...SchemaStatement class interface is:
class SchemaStatement:
def to_sql(self, backend: _Backends) -> str:
"""
Build a SQL string representation.
"""
...Select provides a chainable API for constructing SELECT queries with support for:
- Column selection with expressions and aliases
- Table and subquery sources
- Filtering with WHERE and HAVING
- Joins (inner, left, right, full, cross, lateral)
- Grouping and aggregation
- Ordering and pagination
- Set operations (UNION, EXCEPT, INTERSECT)
- Row locking for transactions
- DISTINCT queries
Simple
query = (
rq.Select(rq.Expr.asterisk()) # Or rq.Select(rq.ASTERISK)
.from_table("users")
.where(rq.Expr.col("name").like(r"%linus%"))
)
sql, params = query.build("postgresql")
# -> SELECT * FROM "users" WHERE "name" LIKE $1
query = (
rq.Select(rq.Expr.col("product"), rq.Expr.col("price"), rq.Expr.col("category"))
.from_table("products")
.where(rq.Expr.col("price") > 50)
.order_by(rq.Expr.col("price"), "desc")
)
sql, params = query.build("postgresql")
# -> SELECT "product", "price", "category" FROM "products" WHERE "price" > $1 ORDER BY "price" DESC
query = (
rq.Select(
rq.SelectExpr(rq.FunctionCall.count(rq.ASTERISK), alias="total_customers"),
rq.SelectExpr(rq.FunctionCall.avg(rq.Expr.col("age")), alias="average_age"),
)
.from_table("customers")
)
sql, params = query.build("postgresql")
# -> SELECT COUNT(*) AS "total_customers", AVG("age") AS "average_age" FROM "customers"Complex
# This query would be easier to create by using `AliasedTable` class,
# which introduced in "Advanced" part of this page
query = (
rq.Select(
rq.Expr.col("c.customer_name"),
rq.SelectExpr(
rq.FunctionCall.count(rq.Expr.col("o.order_id")),
"total_orders"
),
rq.SelectExpr(
rq.FunctionCall.sum(rq.Expr.col("oi.quantity") * rq.Expr.col("oi.unit_price")),
"total_spent"
),
)
.from_table(rq.TableName("customers", alias="c"))
.join(
rq.TableName("orders", alias="o"),
rq.Expr.col("c.customer_id") == rq.Expr.col("o.customer_id"),
type="left"
)
.join(
rq.TableName("order_items", alias="oi"),
rq.Expr.col("o.order_id") == rq.Expr.col("oi.order_id"),
type="left"
)
.where(
rq.Expr.col("o.order_date") >= (datetime.datetime.now() - datetime.timedelta(days=360))
)
)
sql, params = query.build("postgresql")
# SELECT
# "c"."customer_name",
# COUNT("o"."order_id") AS "total_orders",
# SUM("oi"."quantity" * "oi"."unit_price") AS "total_spent"
# FROM "customers" AS "c"
# LEFT JOIN "orders" AS "o" ON "c"."customer_id" = "o"."customer_id"
# LEFT JOIN "order_items" AS "oi" ON "o"."order_id" = "oi"."order_id"
# WHERE "o"."order_date" >= $1Insert provides a chainable API for constructing INSERT queries with support for:
- Single or multiple row insertion
- Conflict resolution (UPSERT)
- RETURNING clauses
- REPLACE functionality
- Default values
query = (
rq.Insert()
.replace()
.into("glyph")
.values(aspect=5.15, image="12A")
)
sql, params = query.build("postgresql")
# REPLACE INTO "glyph" ("aspect", "image") VALUES ($1, $2)
query = (
rq.Insert()
.into("glyph")
.columns("aspect", "image")
.values(5.15, "12A")
.values(16, "14A")
.returning("id")
)
sql, params = query.build("postgresql")
# INSERT INTO "glyph" ("aspect", "image") VALUES ($1, $2), ($3, $4) RETURNING "id"
query = (
rq.Insert()
.into("users")
.values(username="awolverp", role="author")
.on_conflict(
rq.OnConflict("id")
.do_update("username")
)
)
sql, params = query.build("postgresql")
# INSERT INTO "users" ("username", "role") VALUES ($1, $2)
# ON CONFLICT ("id") DO UPDATE SET "username" = "excluded"."username"
query = (
rq.Insert()
.into("users")
.values(username="awolverp", role="author")
.on_conflict(
rq.OnConflict("id")
.do_update(role="member")
)
)
sql, params = query.build("postgresql")
# INSERT INTO "users" ("username", "role") VALUES ($1, $2)
# ON CONFLICT ("id") DO UPDATE SET "author" = $3Update provides a chainable API for constructing UPDATE queries with support for:
- Setting column values
- WHERE conditions for filtering
- LIMIT for restricting update count
- ORDER BY for determining update order
- RETURNING clauses for getting updated data
query = (
rq.Update()
.table("glyph")
.values(aspect=5.15, image="12A")
.returning_all()
.order_by(rq.Expr.col("id"), "desc")
)
sql, params = query.build("postgresql")
# UPDATE "glyph" SET "aspect" = $1, "image" = $2 ORDER BY "id" DESC RETURNING *
query = (
rq.Update()
.table("wallets")
.values(amount=rq.Expr.col("amount") + 10)
.where(rq.Expr.col("id").between(10, 30))
)
sql, params = query.build("postgresql")
# UPDATE "wallets" SET "amount" = "amount" + $1 WHERE "id" BETWEEN $2 AND $3Delete provides a chainable API for constructing DELETE queries with support for:
- WHERE conditions for filtering
- LIMIT for restricting deletion count
- ORDER BY for determining deletion order
- RETURNING clauses for getting deleted data
query = (
rq.Delete()
.from_table("users")
.where(
rq.all(
rq.Expr.col("id") > 10,
rq.Expr.col("id") < 30,
)
)
.limit(10)
)
sql, params = query.build("postgresql")
# DELETE FROM "users" WHERE "id" > $1 AND "id" < $2 LIMIT $3For working with functions in RapidQuery, you have to use FunctionCall class.
A lot of functions such as SUM, AVG, MD5, ... is ready to use. For example:
expr = rq.FunctionCall.sum(rq.Expr.col("amount"))
expr.to_sql("postgresql") # -> SUM("amount")But for functions not provided by the library, you can define custom functions.
Custom functions can be defined using the FunctionCall constructor:
unknown = rq.FunctionCall("UNKNOWN").arg(rq.ASTERISK)
expr.to_sql("postgresql") # -> UNKNOWN(*)Table represents a complete database table definition.
This class encapsulates all aspects of a table structure including:
- Column definitions with their types and constraints
- Indexes for query optimization
- Foreign key relationships for referential integrity
- Check constraints for data validation
- Table-level options like engine, collation, and character set
Used to generate CREATE TABLE SQL statements with full schema specifications.
table = rq.Table(
"users",
[
rq.Column("id", rq.BigIntegerType(), primary_key=True, auto_increment=True),
rq.Column("name", rq.StringType(64), nullable=False),
rq.Column("username", rq.StringType(64), nullable=True, default=None),
rq.Column("subscription_id", rq.BigIntegerType(), nullable=False),
rq.Column("created_at", rq.DateTimeType(), default=rq.FunctionCall.now()),
],
indexes=[
rq.Index(["created_at"], if_not_exists=True),
],
foreign_keys=[
rq.ForeignKey(
from_columns=["subscription_id"],
to_columns=["id"],
to_table="subscriptions",
),
],
if_not_exists=True,
)
table.to_sql("postgresql")
# CREATE TABLE IF NOT EXISTS "users" (
# "id" bigserial PRIMARY KEY,
# "name" varchar(64) NOT NULL,
# "username" varchar(64) NULL DEFAULT NULL,
# "subscription_id" bigint NOT NULL,
# "created_at" datetime DEFAULT NOW(),
# CONSTRAINT "fk__subscription_id_subscriptions_id" FOREIGN KEY ("subscription_id") REFERENCES "subscriptions" ("id")
# );
# CREATE INDEX IF NOT EXISTS "ix_users_created_at" ON "users" ("created_at");Tip
We will use Table in ORM-like part of this page to create query statements.
AlterTable represents an ALTER TABLE SQL statement.
Provides a flexible way to modify existing table structures by applying one or more alteration operations such as adding/dropping columns, modifying column definitions, or managing constraints.
Multiple operations can be batched together in a single ALTER TABLE statement for efficiency.
stmt = rq.AlterTable(
"users",
[
rq.AlterTableAddColumnOption(
rq.Column("updated_at", rq.TimestampWithTimeZoneType(), default=rq.FunctionCall.now())
),
rq.AlterTableAddForeignKeyOption(
rq.ForeignKey(
from_columns=["wallet_id"],
to_columns=["id"],
to_table="wallets",
on_delete="CASCADE",
)
),
rq.AlterTableDropColumnOption("deprecated"),
rq.AlterTableDropForeignKeyOption("fk__contraint_name"),
rq.AlterTableModifyColumnOption(rq.Column("created_at", rq.TimestampType())),
rq.AlterTableRenameColumnOption("oldname", "newname"),
],
)
stmt.to_sql("postgresql")
# ALTER TABLE "users" ADD COLUMN "updated_at" timestamp with time zone DEFAULT NOW(),
# ADD CONSTRAINT "fk__wallet_id_wallets_id" FOREIGN KEY ("wallet_id") REFERENCES "wallets" ("id") ON DELETE CASCADE,
# DROP COLUMN "deprecated", DROP CONSTRAINT "fk__contraint_name",
# ALTER COLUMN "created_at" TYPE timestamp,
# RENAME COLUMN "oldname" TO "newname"DropTable represents a DROP TABLE SQL statement.
Builds table deletion statements with support for:
- Conditional deletion (IF EXISTS) to avoid errors
- CASCADE to drop dependent objects
- RESTRICT to prevent deletion if dependencies exist
stmt = rq.DropTable("users", if_exists=True)
stmt.to_sql("postgresql")
# DROP TABLE IF EXISTS "users"RenameTable represents a RENAME TABLE SQL statement.
Changes the name of an existing table to a new name. Both names can be schema-qualified if needed.
stmt = rq.RenameTable("public.old_users", "archive.users")
stmt.to_sql("postgresql")
# ALTER TABLE "public"."old_users" RENAME TO "archive"."users"TruncateTable Represents a TRUNCATE TABLE SQL statement.
Quickly removes all rows from a table, typically faster than DELETE and with different transaction and trigger behavior depending on the database system.
stmt = rq.TruncateTable("temp_data")
stmt.to_sql("postgresql")
# TRUNCATE TABLE "temp_data"Index represents a database index specification.
This class defines the structure and properties of a database index, including column definitions, uniqueness constraints, index type, and partial indexing conditions.
stmt = rq.Index(
["user_id", "reseller_id"],
"ix_users_user_reseller_id",
table="users",
if_not_exists=True,
)
stmt.to_sql("postgresql")
# CREATE INDEX IF NOT EXISTS "ix_users_user_reseller_id" ON "users" ("user_id", "reseller_id")
stmt = rq.Index(
[rq.IndexColumn("name", prefix=8, order="desc")],
"ix_users_user_reseller_id",
table="users",
if_not_exists=True,
)
stmt.to_sql("postgresql")
# CREATE INDEX IF NOT EXISTS "ix_users_user_reseller_id" ON "users" ("name" (8) DESC)DropIndex represents a DROP INDEX SQL statement.
Builds index deletion statements with support for:
- Conditional deletion (IF EXISTS)
- Table-specific index dropping (for databases that require it)
- Proper error handling for non-existent indexes
stmt = rq.DropIndex("ix_users_user_reseller_id")
stmt.to_sql("postgresql")
# DROP INDEX "ix_users_user_reseller_id"Table class is not just for generating CREATE TABLE statements. It's designed to make developing
easier for you.
First you have to know some basics:
users = rq.Table(
"users",
[
rq.Column("id", rq.IntegerType()),
rq.Column("name", rq.CharType(255)),
]
)
# You can access columns easily:
users.c.id # -> <Column "id" type=<IntegerType >>
users.c.name # -> <Column "name" type=<CharType length=255>>
users.c.not_exists # -> KeyError: 'not_exists'Now you can use this structure to create Select, Update, Delete, and Insert queries:
query = (
rq.Select(users.c.name)
.from_table(users)
.where(users.c.id.to_expr() == 2)
)
sql, params = query.build("postgresql")
# SELECT "users"."name" FROM "users" WHERE "users"."id" = $1Using Table for creating queries can help you to create queries easier, but again it's hard to
have aliases (e.g. FROM users AS u) in queries. So we have AliasedTable class to make it
easy.
Imagine this table:
employees = rq.Table(
"employees",
[
rq.Column("id", rq.IntegerType()),
rq.Column("first_name", rq.CharType(255)),
rq.Column("jon_title", rq.CharType(255)),
]
)Without AliasedTable
query = (
rq.Select(
employees.c.id.to_column_ref().copy_with(table="emp"),
rq.SelectExpr(
employees.c.name.to_column_ref().copy_with(table="emp"),
"employee_name",
),
employees.c.job_title.to_column_ref().copy_with(table="emp"),
rq.SelectExpr(employees.c.id.to_column_ref().copy_with(table="mgr"), "manager_id"),
rq.SelectExpr(
employees.c.name.to_column_ref().copy_with(table="mgr"),
"employee_name",
),
rq.SelectExpr(
employees.c.job_title.to_column_ref().copy_with(table="mgr"), "manager_title"
),
)
.from_table(employees.name.copy_with(alias="emp"))
.join(
employees.name.copy_with(alias="mgr"),
(
rq.Expr(employees.c.manager_id.to_column_ref().copy_with(table="emp"))
== employees.c.id.to_column_ref().copy_with(table="mgr")
),
type="inner"
)
)
sql, params = query.build("postgresql")
# SELECT
# "emp"."id", "emp"."name" AS "employee_name",
# "emp"."job_title", "mgr"."id" AS "manager_id",
# "mgr"."name" AS "employee_name", "mgr"."job_title" AS "manager_title"
# FROM "employees" AS "emp"
# INNER JOIN "employees" AS "mgr" ON "emp"."manager_id" = "mgr"."id"It's so hard and unreadable.
With AliasedTable
emp = rq.AliasedTable(employees, "emp")
mgr = rq.AliasedTable(employees, "mgr")
query = (
rq.Select(
emp.c.id,
rq.SelectExpr(emp.c.name, "employee_name"),
emp.c.job_title,
rq.SelectExpr(emp.c.id, "manager_id"),
rq.SelectExpr(emp.c.name, "employee_name"),
rq.SelectExpr(emp.c.job_title, "manager_title"),
)
.from_table(emp)
.join(
mgr,
rq.Expr(emp.c.manager_id) == mgr.c.id,
type="inner",
)
)
sql, params = query.build("postgresql")
# SELECT
# "emp"."id", "emp"."name" AS "employee_name",
# "emp"."job_title", "mgr"."id" AS "manager_id",
# "mgr"."name" AS "employee_name", "mgr"."job_title" AS "manager_title"
# FROM "employees" AS "emp"
# INNER JOIN "employees" AS "mgr" ON "emp"."manager_id" = "mgr"."id"As you saw, it's much simpler.
Note
Benchmarks run on Linux-6.15.11-2-MANJARO-x86_64-with-glibc2.42 with CPython 3.13. Your results may vary.
Generating Select Query 100,000x times
# RapidQuery
query = rq.Select(rq.Expr.asterisk()).from_table("users").where(rq.Expr.col("name").like(r"%linus%")) \
.offset(20).limit(20)
query.to_sql('postgresql')
# PyPika
query = pypika.Query.from_("users").where(pypika.Field("name").like(r"%linus%")) \
.offset(20).limit(20).select("*")
str(query)RapidQuery: 254ms
PyPika: 3983ms
Generating Insert Query 100,000x times
# RapidQuery
query = rq.Insert().into("glyph").columns("aspect", "image") \
.values(5.15, "12A") \
.values(16, "14A") \
.returning("id")
query.to_sql('postgresql')
# PyPika
query = pypika.Query.into("glyph").columns("aspect", "image") \
.insert(5.15, "12A") \
.insert(16, "14A")
str(query)RapidQuery: 267ms
PyPika: 4299ms
Generating Update Query 100,000x times
# RapidQuery
query = rq.Update().table("wallets").values(amount=rq.Expr.col("amount") + 10).where(rq.Expr.col("id").between(10, 30))
query.to_sql('postgresql')
# PyPika
query = pypika.Query.update("wallets").set("amount", pypika.Field("amount") + 10) \
.where(pypika.Field("id").between(10, 30))
str(query)RapidQuery: 252ms
PyPika: 4412ms
Generating Delete Query 100,000x times
# RapidQuery
query = rq.Delete().from_table("users") \
.where(
rq.all(
rq.Expr.col("id") > 10,
rq.Expr.col("id") < 30,
)
) \
.limit(10)
query.to_sql('postgresql')
# PyPika
query = pypika.Query.from_("users") \
.where((pypika.Field("id") > 10) & (pypika.Field("id") < 30)) \
.limit(10).delete()
str(query)RapidQuery: 240ms
PyPika: 4556ms
- Using
ORM-likeis always slower than usingExpr.coland literalstr - "Less calls, more speed"; RapidQuery powered by Rust & SeaQuery, which made us very fast, and only thing that can effect speed, is object calls in Python.
The library may encounter errors during SQL query construction, which are correctly raised as RuntimeError exceptions. For instance, this occurs when using a function that isn't supported by your target database. While this error-raising behavior is intentional and logical, the issue is that unmanaged Rust panic information is also printed to stderr. Currently, there is no way to suppress or manage this panic output. We are working to resolve this problem as much as possible in future updates.
expr = rq.Expr.col("id").pg_contained("text")
expr.to_sql("sqlite")
thread '<unnamed>' (19535) panicked at sea-query-0.32.7/src/backend/query_builder.rs:665:22:
not implemented
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
expr.to_sql("sqlite")
~~~~~^^^^^^^^^^
RuntimeError: build failedThis repository is licensed under the GNU GPLv3 License