gophrql is a Go implementation of PRQL (Pipelined Relational Query Language) — a modern, composable query language that compiles to SQL.
Pipelined Relational Query Language, pronounced "Prequel".
PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. Like SQL, it's readable, explicit and declarative. Unlike SQL, it forms a logical pipeline of transformations, and supports abstractions such as variables and functions.
PRQL queries are pipelines of transformations, where each line transforms the result of the previous line:
from employees # Start with a table
filter department == "Sales" # Filter rows
derive { # Add computed columns
monthly_salary = salary / 12,
annual_bonus = salary * 0.1
}
select { # Choose columns
first_name,
last_name,
monthly_salary,
annual_bonus
}
sort {-monthly_salary} # Sort descending by monthly_salary
take 20 # Limit results
- Pipelines:
|chains transformations (optional, newlines also work) - Variables: Define reusable expressions with
let - Functions: Create custom transformations
- Dates: First-class date support with
@2024-01-01syntax - F-strings: String interpolation with
f"{first_name} {last_name}" - S-strings: SQL escape hatch with
s"UPPER(name)" - Comments:
#for single-line comments
For the complete language reference, visit PRQL Book.
- ✅ Full PRQL Syntax Support - Implements the PRQL language spec
- ✅ Multi-Dialect SQL Generation - Postgres, MySQL, SQLite, MSSQL, DuckDB, BigQuery, Snowflake, ClickHouse
- ✅ Composable Pipelines - Transform data with intuitive, chained operations
- ✅ Type-Safe - Catch errors at compile time, not runtime
- ✅ Extensible - Access the AST directly to build custom backends (MongoDB, ElasticSearch, etc.)
go get github.com/maxpert/gophrqlpackage main
import (
"fmt"
"github.com/maxpert/gophrql"
)
func main() {
prql := `
from employees
filter department == "Engineering"
select {first_name, last_name, salary}
sort {-salary}
take 10
`
sql, err := gophrql.Compile(prql)
if err != nil {
panic(err)
}
fmt.Println(sql)
// Output:
// SELECT
// first_name,
// last_name,
// salary
// FROM
// employees
// WHERE
// department = 'Engineering'
// ORDER BY
// salary DESC
// LIMIT 10
}// PostgreSQL
sql, err := gophrql.Compile(prql, gophrql.WithTarget("sql.postgres"))
// MySQL
sql, err := gophrql.Compile(prql, gophrql.WithTarget("sql.mysql"))
// Microsoft SQL Server
sql, err := gophrql.Compile(prql, gophrql.WithTarget("sql.mssql"))
// DuckDB
sql, err := gophrql.Compile(prql, gophrql.WithTarget("sql.duckdb"))prql := `
from orders
group {customer_id} (
aggregate {
total_orders = count this,
total_revenue = sum amount,
avg_order_value = average amount
}
)
filter total_revenue > 1000
sort {-total_revenue}
`
sql, _ := gophrql.Compile(prql)prql := `
from employees
join departments (==department_id)
select {
employees.first_name,
employees.last_name,
departments.name
}
`
sql, _ := gophrql.Compile(prql)prql := `
from sales
derive {
gross_revenue = quantity * price,
discount_amount = gross_revenue * discount_rate,
net_revenue = gross_revenue - discount_amount
}
filter net_revenue > 0
group {product_id, year} (
aggregate {
total_quantity = sum quantity,
total_revenue = sum net_revenue,
avg_price = average price
}
)
`
sql, _ := gophrql.Compile(prql)One of gophrql's unique features is exposing the parse tree, allowing you to build custom backends for non-SQL databases. Here's a basic example converting PRQL syntax to a MongoDB aggregation pipeline:
Here's a real-world time series analytics query transpiled to DuckDB, based on actual user workflows from the data community. This example analyzes cryptocurrency OHLCV data with moving averages and rolling statistics:
package main
import (
"fmt"
"github.com/maxpert/gophrql"
)
func main() {
prql := `
# Time series analysis with rolling windows and aggregations
from ohlcv_data
filter s"date_part(['year', 'month'], time) = {year: 2021, month: 2}"
# Calculate moving averages and rolling statistics
window rolling:28 (
derive {
ma_28d = average close,
volatility_28d = stddev close
}
)
# Calculate expanding cumulative average
window rows:..0 (
derive {
expanding_avg = average close,
cumulative_volume = sum volume
}
)
# Combine rolling aggregations for Bollinger Bands
window rows:-15..14 (
derive {
rolling_mean = average close,
rolling_std = stddev close,
upper_band = average close + 2 * stddev close,
lower_band = average close - 2 * stddev close
}
)
# Final selection with technical indicators
select {
time,
close,
ma_28d,
expanding_avg,
volatility_28d,
rolling_mean,
upper_band,
lower_band,
volume,
cumulative_volume
}
sort time
take 10
`
sql, err := gophrql.Compile(prql, gophrql.WithTarget("sql.duckdb"))
if err != nil {
panic(err)
}
fmt.Println(sql)
// Output: Optimized DuckDB query with window functions,
// perfect for financial analysis and time series workloads
}This demonstrates gophrql's ability to handle:
- Time series filtering with DuckDB's date functions
- Window functions for moving averages and rolling statistics
- Multiple window frames (rolling, expanding, centered)
- Technical indicators like Bollinger Bands and volatility
- Complex analytics common in financial data analysis
Based on real user workflows from eitsupi/querying-with-prql, this example shows how PRQL simplifies complex time series analytics that would be verbose in raw SQL.
package main
import (
"fmt"
"strings"
"github.com/maxpert/gophrql"
"github.com/maxpert/gophrql/ast"
)
func main() {
prql := `
from users
filter age > 21
filter country == "US"
select { name, email, age }
sort { -age }
take 10
`
// Parse PRQL to an AST
query, err := gophrql.Parse(prql)
if err != nil {
panic(err)
}
// Convert AST to MongoDB aggregation pipeline string
mongo := convertToMongo(query)
fmt.Println(mongo)
// db.users.aggregate([
// { $match: { age: { $gt: 21 }, country: "US" } },
// { $project: { name: 1, email: 1, age: 1, _id: 0 } },
// { $sort: { age: -1 } },
// { $limit: 10 }
// ])
}
func convertToMongo(q *ast.Query) string {
var stages []string
// Combine all filters into a single $match
filters := []string{}
for _, step := range q.Steps {
if f, ok := step.(*ast.FilterStep); ok {
if cond := toMongoCondition(f.Expr); cond != "" {
filters = append(filters, cond)
}
}
}
if len(filters) > 0 {
stages = append(stages, fmt.Sprintf("{ $match: { %s } }", strings.Join(filters, ", ")))
}
for _, step := range q.Steps {
switch s := step.(type) {
case *ast.SelectStep:
fields := []string{}
for _, item := range s.Items {
name := item.As
if name == "" {
name = exprToField(item.Expr)
}
fields = append(fields, fmt.Sprintf("%s: 1", name))
}
// Exclude _id for clarity
fields = append(fields, "_id: 0")
stages = append(stages, fmt.Sprintf("{ $project: { %s } }", strings.Join(fields, ", ")))
case *ast.SortStep:
sorts := []string{}
for _, item := range s.Items {
dir := 1
if item.Desc {
dir = -1
}
sorts = append(sorts, fmt.Sprintf("%s: %d", exprToField(item.Expr), dir))
}
if len(sorts) > 0 {
stages = append(stages, fmt.Sprintf("{ $sort: { %s } }", strings.Join(sorts, ", ")))
}
case *ast.TakeStep:
if s.Limit > 0 {
stages = append(stages, fmt.Sprintf("{ $limit: %d }", s.Limit))
}
}
}
return fmt.Sprintf("db.%s.aggregate([%s])", q.From.Table, strings.Join(stages, ", "))
}
func toMongoCondition(e ast.Expr) string {
b, ok := e.(*ast.Binary)
if !ok {
return ""
}
field := exprToField(b.Left)
value := exprToValue(b.Right)
switch b.Op {
case "==":
return fmt.Sprintf("%s: %s", field, value)
case ">":
return fmt.Sprintf("%s: { $gt: %s }", field, value)
case "<":
return fmt.Sprintf("%s: { $lt: %s }", field, value)
default:
return ""
}
}
func exprToField(e ast.Expr) string {
if id, ok := e.(*ast.Ident); ok && len(id.Parts) > 0 {
return strings.Join(id.Parts, ".")
}
return e.String()
}
func exprToValue(e ast.Expr) string {
switch v := e.(type) {
case *ast.Number:
return v.Value
case *ast.StringLit:
return fmt.Sprintf("\"%s\"", v.Value)
default:
return "null"
}
}See examples/mongo/main.go for the full example with more operators and safer parsing.
PRQL queries are pipelines of transformations, where each line transforms the result of the previous line:
from employees # Start with a table
filter department == "Sales" # Filter rows
derive { # Add computed columns
monthly_salary = salary / 12,
annual_bonus = salary * 0.1
}
select { # Choose columns
first_name,
last_name,
monthly_salary,
annual_bonus
}
sort {-monthly_salary} # Sort descending by monthly_salary
take 20 # Limit results
- Pipelines:
|chains transformations (optional, newlines also work) - Variables: Define reusable expressions with
let - Functions: Create custom transformations
- Dates: First-class date support with
@2024-01-01syntax - F-strings: String interpolation with
f"{first_name} {last_name}" - S-strings: SQL escape hatch with
s"UPPER(name)" - Comments:
#for single-line comments
For the complete language reference, visit PRQL Book.
| Dialect | Status | Notes |
|---|---|---|
| Generic | ✅ | Postgres-compatible fallback |
| PostgreSQL | ✅ | Full support |
| MySQL | ✅ | Backtick identifiers, LIMIT syntax |
| SQLite | ✅ | Standard SQL subset |
| DuckDB | ✅ | Advanced analytics functions |
| MS SQL Server | ✅ | TOP clause, T-SQL functions |
| BigQuery | ✅ | Google BigQuery syntax |
| Snowflake | ✅ | Snowflake-specific features |
| ClickHouse | ✅ | ClickHouse syntax |
- Go 1.21+
go build ./...go test ./...go run examples/basic/main.go
go run examples/mongo/main.gogophrql/
├── ast/ # Public AST types
├── internal/
│ ├── parser/ # PRQL parser
│ └── sqlgen/ # SQL generation + dialects
├── examples/ # Usage examples
├── docs/ # Documentation
└── gophrql.go # Public API
Contributions are welcome! Please see CODE_OF_CONDUCT.md for community guidelines.
- Keep changes focused - One feature/fix per PR
- Add tests - Ensure coverage for new features
- Follow conventions - Use
gofmtand follow existing patterns - Update docs - Keep README and examples current
This project is inspired by and implements the PRQL language specification. Special thanks to the PRQL community and the upstream prql project.
Apache License 2.0 - see LICENSE for details.