Generating SQL
Suppose you want to write Polars syntax and translate it to SQL. For example, what's the SQL equivalent to:
import narwhals as nw
from narwhals.typing import FrameT
def avg_monthly_price(df: FrameT) -> FrameT:
return (
df.group_by(nw.col("date").dt.truncate("1mo"))
.agg(nw.col("price").mean())
.sort("date")
)
Narwhals provides you with a narwhals.sql module to do just that!
Info
narwhals.sql currently requires DuckDB to be installed.
narwhals.sql
You can generate SQL directly from DuckDB.
import narwhals as nw
from narwhals.sql import table
prices = table("prices", {"date": nw.Date, "price": nw.Float64})
result = (
prices.group_by(nw.col("date").dt.truncate("1mo"))
.agg(nw.col("price").mean())
.sort("date")
)
print(result.to_sql())
WITH "__duckdb_s0" AS NOT MATERIALIZED (
SELECT "date", "price", date_trunc('month', "date") AS "_date_tmp" FROM "prices"
),
"__duckdb_s1" AS NOT MATERIALIZED (
SELECT "_date_tmp", mean("price") AS "price" FROM "__duckdb_s0" AS "prices" GROUP BY "_date_tmp"
),
"__duckdb_s2" AS NOT MATERIALIZED (
SELECT "_date_tmp" AS "date", "price" FROM "__duckdb_s1" AS "prices"
)
SELECT * FROM "__duckdb_s2" AS "prices" ORDER BY "date" NULLS FIRST
To make it look a bit prettier, you can pass pretty=True, but
note that this currently requires sqlparse to be installed.
print(result.to_sql(pretty=True))
WITH "__duckdb_s0" AS NOT MATERIALIZED
(SELECT "date",
"price",
date_trunc('month', "date") AS "_date_tmp"
FROM "prices"), "__duckdb_s1" AS NOT MATERIALIZED
(SELECT "_date_tmp",
mean("price") AS "price"
FROM "__duckdb_s0" AS "prices"
GROUP BY "_date_tmp"), "__duckdb_s2" AS NOT MATERIALIZED
(SELECT "_date_tmp" AS "date",
"price"
FROM "__duckdb_s1" AS "prices")
SELECT *
FROM "__duckdb_s2" AS "prices"
ORDER BY "date" NULLS FIRST
Note that the generated SQL follows DuckDB's dialect. To translate it to other dialects, you may want to look into sqlglot, or use one of the solutions below (which also use sqlglot).
Via Ibis
Ibis can generate SQL from the same dataframe-agnostic function:
import ibis
df = nw.from_native(ibis.table({"date": "date", "price": "double"}, name="prices"))
print(ibis.to_sql(avg_monthly_price(df).to_native()))
Via SQLFrame
So can SQLFrame:
from sqlframe.standalone import StandaloneSession
session = StandaloneSession.builder.getOrCreate()
session.catalog.add_table("prices", column_mapping={"date": "date", "price": "float"})
df = nw.from_native(session.read.table("prices"))
print(avg_monthly_price(df).to_native().sql(dialect="duckdb"))
WITH "t20239543" AS (
SELECT
CAST(DATE_TRUNC('MONTH', CAST("prices"."date" AS TIMESTAMPTZ)) AS TIMESTAMPTZ) AS "_date_tmp",
AVG("prices"."price") AS "price"
FROM "prices" AS "prices"
GROUP BY
CAST(DATE_TRUNC('MONTH', CAST("prices"."date" AS TIMESTAMPTZ)) AS TIMESTAMPTZ)
)
SELECT
"t11373714"."_date_tmp" AS "date",
"t11373714"."price" AS "price"
FROM "t20239543" AS "t11373714"
ORDER BY
"date" NULLS FIRST