phel-lang / phel-sql
Data-driven SQL DSL for Phel Lang. HoneySQL-style. Pure data in, [sql params] out.
Fund package maintenance!
Requires
- php: >=8.5
- phel-lang/phel-lang: ^0.54
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.2.1
- v0.2.0
- v0.1.0
- dev-feat/47-operator-expressions
- dev-feat/43-clearer-errors
- dev-docs/44-cookbook
- dev-feat/42-skip-nil-clauses
- dev-feat/49-rollup-cube-grouping-sets
- dev-feat/50-window-clause
- dev-feat/45-insert-select
- dev-feat/46-set-op-clauses
- dev-test/48-sqlite-in-memory
- dev-ci/41-format-check-smoke-test
- dev-ref/faster-format
- dev-docs/refresh-readme-and-guides
- dev-ref/use-thrown-with-msg
- dev-ref/tidy-validation-code
- dev-fix/35-validation-regressions
- dev-feat/19-nil-equality
- dev-feat/18-validate-keyword-identifiers
- dev-docs/17-readme-link
- dev-docs/17-raw-string-positions
- dev-fix/16-extra-elements
- dev-fix/15-readable-error-values
- dev-fix/14-untagged-vector-operand
- dev-fix/13-update-without-set
- dev-fix/12-empty-clauses
- dev-fix/11-where-arity
- dev-fix/10-orphan-upsert-keys
- dev-fix/9-insert-row-shape
- dev-fix/8-columns-map-rows
- dev-chore/agnostic-ai-setup
- dev-fix/2-compiled-load-path
This package is auto-updated.
Last update: 2026-10-31 07:17:32 UTC
README
Data-driven SQL for Phel. You pass a map and get back [sql params]. It never connects to a database: pass the result to PDO or any other driver.
Inspired by HoneySQL.
(sql/format
{:select [:id :name]
:from [:users]
:where [:= :status "active"]})
;; => ["SELECT id, name FROM users WHERE status = ?" ["active"]]
Install
composer require phel-lang/phel-sql
Requires PHP 8.5+ and phel-lang/phel-lang 0.54+.
Use it
(ns my-app.queries
(:require phel.sql :as sql))
(sql/format
{:with [[:active {:select [:id] :from [:users] :where [:= :status "active"]}]]
:select [:u/id
[[:over [:fn :row_number]
{:partition-by [:u/dept]
:order-by [[:u/salary :desc]]}]
:rk]
[[:case [:> :u/salary 100000] "high" :else "low"] :tier]]
:from [[:users :u]]
:join [[:active :a] [:using :id]]
:where [:>= :u/age 18]
:order-by [[:u/dept :asc :nulls-last]]
:limit 10})
Run the result with any PDO-like driver:
$pdo->prepare($sql)->execute($params);
Safety
- Values are always bound. Every value becomes a
?and lands in the params vector. Only:limitand:offsetare written into the SQL, and only as non-negative integers. - Keyword identifiers are checked.
:users/idand:t.colpass. A keyword with spaces, quotes or semicolons throws, so(keyword user-input)cannot inject SQL. - Strings and
[:raw "..."]are not checked. A string in an identifier position, and every raw fragment, is written as is. Never build one from user input. See Parameters for the full list of positions. - Bad input throws. Unknown clauses, wrong argument counts, empty lists and malformed rows raise
InvalidArgumentExceptionwith the offending value in the message.
What it covers
| Statement | Clauses |
|---|---|
SELECT |
with, with-recursive, select / select-distinct / select-distinct-on, from, joins (inner / left / right / full / cross / LATERAL), USING / ON, where, group-by, having, order-by (asc / desc / nulls-first / nulls-last), limit, offset, for (lock), returning |
INSERT |
with, insert-into, columns, values (vector rows or map rows), on-conflict + do-nothing / do-update-set / on-conflict-where / do-update-where (Postgres), on-duplicate-key-update (MySQL), returning |
UPDATE |
with, update, set, from, where, order-by, limit, returning |
DELETE |
with, delete-from, where, order-by, limit, returning |
| Set ops | union, union-all, intersect, except (top-level over a vector of queries) |
| VALUES | top-level {:values [...]} as a standalone expression |
WHERE operators: =, !=, <, >, <=, >=, like, not-like, ilike, not-ilike, and, or, not, in, not-in, between, not-between, is-null, is-not-null, exists, not-exists.
Tagged expression forms usable anywhere an identifier or operand is accepted:
| Form | Renders |
|---|---|
[:raw "SQL"] |
raw fragment |
[:fn name & args] |
name(args...) |
[:cast expr type] |
CAST(expr AS TYPE) |
[:case test then ... :else d] |
CASE WHEN ... THEN ... ELSE ... END |
[:over expr spec] |
window function with :partition-by, :order-by, :frame |
[:filter expr where] |
aggregate FILTER (WHERE ...) |
[:lateral subquery] |
LATERAL (...) |
A map works as a subquery anywhere an identifier or value is accepted. Use [:raw "SQL"] for anything the DSL does not model yet.
Docs
- Quickstart: three queries, end to end.
- Cookbook: recipes, running queries with PDO, FAQ.
- Clauses: every clause and operator.
- Expressions: function calls, CAST, CASE, window, FILTER, LATERAL.
- Parameters: identifiers, values, subqueries, raw fragments.
- Contributing: repo layout, tests, adding a clause.
- Changelog
Development
composer install composer test # every deftest under tests/ composer format # format the Phel source
The library is pure Phel, so there is no PHP source and no PHPUnit. The contributing guide covers adding clauses and operators.
License
MIT. See LICENSE.