SQL2EXPR is a Go library that transpiles SQL SELECT statements to expr-lang expressions, enabling you to write SQL queries and convert them to expr-lang for filtering and transforming in-memory data structures and database results.
- SQL to expr-lang Transpilation: Convert SQL SELECT statements to expr-lang expressions
- Automatic Table Name Extraction: Data source variable names are automatically extracted from the FROM clause
- Verified Compilation: All transpiled expressions are verified to compile successfully with
expr.Compile() - Comprehensive SQL Support:
- SELECT with field projections and aliasing
- WHERE clauses with comparisons, AND, OR, NOT, IN, LIKE, IS NULL
- GROUP BY with HAVING clauses
- ORDER BY with ASC/DESC (including multiple columns)
- LIMIT and OFFSET
- JOIN operations (INNER, LEFT, RIGHT, CROSS, multi-table)
- Aggregation functions (COUNT, SUM, AVG, MAX, MIN)
- Table aliases (FROM users AS u)
- Complex nested expressions and subqueries
- Advanced expr-lang Features:
- Conditional Operators: CASE expressions → ternary operators (
?:) - String Functions: TRIM, SPLIT, REPLACE, SUBSTRING, UPPER, LOWER, LENGTH, CONTAINS, STARTSWITH, ENDSWITH, INDEXOF, LASTINDEXOF
- Mathematical Functions: ABS, CEIL, FLOOR, ROUND, SQRT, POW/POWER, LOG, SIN, COS, TAN, PI, RANDOM
- Array Functions: FIRST, LAST, SORT, REVERSE, CONCAT, FLATTEN, UNIQ, ALL, ANY, ONE, NONE, FIND, FINDINDEX, FINDLAST, FINDLASTINDEX
- Conditional Logic: COALESCE, IFNULL, NULLIF, GREATEST, LEAST
- Date/Time Functions: NOW, DATE, DURATION, DATEADD, DATEDIFF, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
- Type Conversion: INT, FLOAT, STRING, CAST
- Bitwise Functions: BITAND, BITOR, BITXOR, BITNOT
- Advanced Operators: Range (
..), Pipe (|), Nil coalescing (??)
- Conditional Operators: CASE expressions → ternary operators (
- Fail-fast Error Handling: Clear error messages for unsupported features
go get github.com/h22rana/sql2exprpackage main
import (
"fmt"
"log"
"github.com/h22rana/sql2expr"
)
func main() {
// Simple SELECT query - table name is automatically extracted
sql := "SELECT * FROM users WHERE age > 18"
expr, err := sql2expr.Transpile(sql)
if err != nil {
log.Fatal(err)
}
fmt.Println(expr) // Output: filter(users, {users.age > 18})
}SELECT *- Returns all fieldsSELECT field1, field2- Project specific fieldsSELECT field AS alias- Field aliasingSELECT function(field)- Function calls in SELECT
- Comparison operators:
=,!=,>,<,>=,<= - Logical operators:
AND,OR,NOT - String literals:
'value' - Boolean literals:
true,false - Numeric literals:
123,45.67 INoperator:field IN (val1, val2, val3)LIKEoperator:field LIKE 'pattern%'IS NULL/IS NOT NULL:field IS NULL
GROUP BY field1, field2- Group by multiple fieldsHAVING condition- Filter groups after aggregation
ORDER BY field- Ascending orderORDER BY field DESC- Descending orderORDER BY field1, field2 DESC- Multiple columns
LIMIT 10- Limit resultsLIMIT 10 OFFSET 5- Limit with offset
INNER JOIN- Inner join with ON conditionLEFT JOIN- Left outer joinRIGHT JOIN- Right outer joinCROSS JOIN- Cartesian product- Multi-table joins:
FROM table1, table2, table3
COUNT(*)- Count all recordsCOUNT(field)- Count non-null valuesSUM(field)- Sum of field valuesAVG(field)- Average of field valuesMAX(field)- Maximum valueMIN(field)- Minimum value
CONTAINS(string, substring)- Check if string contains substringSTARTSWITH(string, prefix)- Check if string starts with prefixENDSWITH(string, suffix)- Check if string ends with suffixINDEXOF(string, substring)- Find index of substringLASTINDEXOF(string, substring)- Find last index of substringTRIM(string)- Remove whitespaceUPPER(string)- Convert to uppercaseLOWER(string)- Convert to lowercaseLENGTH(string)- Get string length
SQRT(value)- Square rootPOW(base, exponent)- Power functionLOG(value)- Natural logarithmSIN(angle)- Sine functionCOS(angle)- Cosine functionTAN(angle)- Tangent functionPI()- Pi constantRANDOM()- Random numberABS(value)- Absolute valueCEIL(value)- Ceiling functionFLOOR(value)- Floor functionROUND(value)- Round function
NOW()- Current timestampDATE(datetime)- Extract dateYEAR(date)- Extract yearMONTH(date)- Extract monthDAY(date)- Extract dayHOUR(datetime)- Extract hourMINUTE(datetime)- Extract minuteSECOND(datetime)- Extract secondDATEADD(unit, amount, date)- Add time to dateDATEDIFF(unit, date1, date2)- Difference between datesDURATION(string)- Parse duration string
COALESCE(val1, val2, ...)- Return first non-null valueIFNULL(value, default)- Return default if nullNULLIF(value1, value2)- Return null if values equalGREATEST(val1, val2, ...)- Return largest valueLEAST(val1, val2, ...)- Return smallest value
ARRAY_CONCAT(array1, array2, ...)- Concatenate arraysARRAY_FLATTEN(nested_array)- Flatten nested arraysARRAY_UNIQ(array)- Remove duplicatesARRAY_ALL(array, predicate)- Check if all elements matchARRAY_ANY(array, predicate)- Check if any element matchesARRAY_ONE(array, predicate)- Check if exactly one element matchesARRAY_NONE(array, predicate)- Check if no elements matchARRAY_FIND(array, predicate)- Find first matching elementARRAY_FINDINDEX(array, predicate)- Find index of first matchARRAY_FINDLAST(array, predicate)- Find last matching elementARRAY_FINDLASTINDEX(array, predicate)- Find index of last match
BITAND(value1, value2)- Bitwise ANDBITOR(value1, value2)- Bitwise ORBITXOR(value1, value2)- Bitwise XORBITNOT(value)- Bitwise NOT
| SQL Query | expr-lang Output |
|---|---|
SELECT * FROM users |
users |
SELECT * FROM users WHERE age > 18 |
filter(users, {users.age > 18}) |
SELECT id, name FROM products |
map(products, products) |
SELECT * FROM orders ORDER BY created_at DESC |
sortBy(orders, -orders.created_at) |
SELECT * FROM items LIMIT 10 |
items[:10] |
| SQL Query | expr-lang Output |
|---|---|
SELECT COUNT(*) FROM customers |
len(customers) |
SELECT SUM(price) FROM products |
sum(map(products, products.price)) |
SELECT AVG(price) FROM products |
sum(map(products, products.price)) / len(products) |
SELECT MAX(price), MIN(price) FROM products |
{max(map(products, products.price)), min(map(products, products.price))} |
| SQL Query | expr-lang Output |
|---|---|
SELECT u.id, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id |
map(groupBy(filter(leftJoin(u, o, u.id == o.user_id), {}), u.id), u) |
SELECT * FROM users u JOIN posts p ON u.id = p.user_id |
map(join(u, p), u) |
| SQL Query | expr-lang Output |
|---|---|
SELECT CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END FROM users |
map(users, users.age > 18 ? "adult" : "minor") |
SELECT CONTAINS(name, 'test') FROM users |
map(users, indexOf(users.name, "test") >= 0) |
SELECT SQRT(area) FROM shapes |
map(shapes, sqrt(shapes.area)) |
SELECT COALESCE(name, email, 'unknown') FROM users |
map(users, coalesce(users.name, users.email, "unknown")) |
SELECT YEAR(created_at) FROM orders |
map(orders, year(orders.created_at)) |
SELECT ARRAY_FLATTEN(nested_array) FROM data |
map(data, flatten(data.nested_array)) |
| SQL Query | expr-lang Output |
|---|---|
SELECT TRIM(name), UPPER(email) FROM users |
map(users, users) |
SELECT ABS(price), CEIL(price) FROM products |
map(products, products) |
SELECT POW(2, 3), PI() FROM constants |
map(constants, constants) |
SELECT STARTSWITH(name, 'Mr.') FROM users |
map(users, indexOf(users.name, "Mr.") == 0) |
| SQL Query | expr-lang Output |
|---|---|
SELECT NOW(), DATE(created_at) FROM users |
{now(), date(users.created_at)} |
SELECT YEAR(created_at), MONTH(created_at) FROM orders |
map(orders, orders) |
SELECT HOUR(created_at), MINUTE(created_at) FROM events |
map(events, events) |
| SQL Query | expr-lang Output |
|---|---|
SELECT IFNULL(description, 'No description') FROM products |
map(products, ifnull(products.description, "No description")) |
SELECT GREATEST(a, b, c) FROM numbers |
map(numbers, max(numbers.a, numbers.b, numbers.c)) |
SELECT LEAST(x, y, z) FROM coordinates |
map(coordinates, min(coordinates.x, coordinates.y, coordinates.z)) |
All transpiled expressions are verified to compile successfully with the expr-lang library:
import (
"github.com/expr-lang/expr"
"github.com/h22rana/sql2expr"
)
func main() {
sql := "SELECT name, price FROM products WHERE price > 100"
exprOutput, err := sql2expr.Transpile(sql)
if err != nil {
log.Fatal(err)
}
// Verify the expression compiles
program, err := expr.Compile(exprOutput)
if err != nil {
log.Fatal("Compilation failed:", err)
}
fmt.Println("Expression compiles successfully:", exprOutput)
// Output: map(filter(products, {products.price > 100}), products)
}SQL2EXPR provides clear error messages for unsupported features:
sql := "INSERT INTO items VALUES (1, 'test')"
_, err := sql2expr.Transpile(sql)
// Error: unsupported statement type 'INSERT' in query: INSERT INTO items VALUES (1, 'test'). Only SELECT statements are supportedSQL2EXPR includes an interactive REPL (Read-Eval-Print Loop) for testing SQL to expr-lang translations:
# Run the REPL
go run cmd/repl/main.go
# Or build and run
make build
./sql2expr-replThe REPL allows you to:
- Enter SQL queries interactively
- See immediate expr-lang translations
- Test complex queries with JOINs, aggregations, and functions
- Get help with available commands
help- Show available commandsexitorquit- Exit the REPL- Any SQL query - Transpile to expr-lang
The transpiler consists of three main components:
- SQL Parser - Uses
github.com/blastrain/vitess-sqlparserto parse SQL into AST - AST Transformer - Converts SQL AST to intermediate representation with context-aware processing
- expr-lang Code Generator - Generates expr-lang expressions from IR with support for:
- Context-aware aggregation functions
- Complex JOIN operations
- Advanced function mappings
- Proper expression nesting
- Context-Aware Processing: Aggregation functions automatically use the correct context (data source vs. current expression)
- Table-Qualified Names: Uses actual table names (e.g.,
products.price) instead of generic variables for better semantics - Comprehensive Function Support: 50+ SQL functions mapped to expr-lang equivalents
- Verified Compilation: All generated expressions compile successfully with
expr.Compile() - Robust Error Handling: Clear error messages for unsupported features
- Extensible Design: Easy to add new function mappings and SQL features
- Window Functions: ROW_NUMBER, RANK, LAG, LEAD (limited expr-lang support)
- Subqueries: EXISTS, IN with subqueries (basic support only)
- CTEs: Common Table Expressions and Recursive CTEs
- JSON Functions: JSON_EXTRACT, JSON_OBJECT (no expr-lang support)
- Advanced JOIN Types: FULL OUTER JOIN, NATURAL JOIN
- DML Operations: INSERT, UPDATE, DELETE (SELECT only)
- Complex Subqueries: Simple EXISTS and IN subqueries work, but complex correlated subqueries are limited
- Window Functions: Basic ROW_NUMBER and RANK can be implemented, but LAG/LEAD are not supported by expr-lang
- Window Functions: Implement ROW_NUMBER, RANK, DENSE_RANK with expr-lang limitations
- Subquery Support: Expand support for EXISTS and IN subqueries
- Advanced JOIN Types: Implement FULL OUTER JOIN and NATURAL JOIN
- Performance Optimizations: Optimize complex query transpilation
- Extended Function Support: Add more SQL functions as expr-lang evolves
- Better Error Messages: More specific error messages for complex scenarios
- JSON Support: If expr-lang adds JSON functions, implement JSON_EXTRACT, JSON_OBJECT
- CTE Support: If expr-lang adds CTE support, implement Common Table Expressions
SQL2EXPR includes comprehensive test coverage:
# Run all tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run specific test
go test -run TestTranspile ./...- Unit Tests: 50+ test cases covering all supported functions
- Integration Tests: Complex queries with JOINs, GROUP BY, and aggregations
- Error Handling Tests: Unsupported features and invalid syntax
- Edge Cases: Boundary conditions and special cases
# Build the project
make build
# Run tests
make test
# Run linting
make lint
# Clean build artifacts
make cleanTo add support for a new SQL function:
- Add the function case to
transpileFuncExprintranspiler/transpiler.go - Add test cases to
sql2expr_test.go - Update this README with the new function
- Run tests to ensure everything works