https://github.com/pgx-contrib/pgxcel
CEL → PostgreSQL WHERE clause transpiler for pgx. Fail-closed column allow-list, parameterized $N placeholders.
https://github.com/pgx-contrib/pgxcel
cel cel-go filtering go golang google-cel pgx postgres postgresql sql transpiler where-clause
Last synced: 8 days ago
JSON representation
CEL → PostgreSQL WHERE clause transpiler for pgx. Fail-closed column allow-list, parameterized $N placeholders.
- Host: GitHub
- URL: https://github.com/pgx-contrib/pgxcel
- Owner: pgx-contrib
- License: mit
- Created: 2026-04-26T02:32:57.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-08T02:40:50.000Z (13 days ago)
- Last Synced: 2026-06-08T04:21:41.699Z (13 days ago)
- Topics: cel, cel-go, filtering, go, golang, google-cel, pgx, postgres, postgresql, sql, transpiler, where-clause
- Language: Go
- Homepage: https://pkg.go.dev/github.com/pgx-contrib/pgxcel
- Size: 46.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pgxcel
[](https://pkg.go.dev/github.com/pgx-contrib/pgxcel)
[](LICENSE)
[](https://go.dev)
`pgxcel` converts a checked [CEL](https://github.com/google/cel-go) AST into
a Postgres `WHERE` fragment with positional bind placeholders. It is
deliberately small: one walker over the standard CEL expression
protobuf, a fail-closed identifier allow-list, and `time.Time` /
`time.Duration` bindings for the `timestamp(...)` / `duration(...)`
literals.
The package accepts any `*cel.Ast` regardless of how it was produced.
That includes ASTs translated back from an [AIP-160](https://google.aip.dev/160)
filter via `cel.CheckedExprToAst`, so the same transpiler powers both
direct CEL and AIP filtering on top of Postgres.
## Installation
```bash
go get github.com/pgx-contrib/pgxcel
```
## Usage
```go
env, _ := cel.NewEnv(
cel.Variable("name", cel.StringType),
cel.Variable("age", cel.IntType),
)
ast, iss := env.Compile(`name == "Alice" && age > 30`)
if iss.Err() != nil {
return iss.Err()
}
columns := map[string]string{
"name": "users.name",
"age": "users.age",
}
where, args, err := pgxcel.Transpile(ast, pgxcel.WithColumns(columns))
// where: ("users"."name" = $1 AND "users"."age" > $2)
// args: []any{"Alice", int64(30)}
```
### Options
- `pgxcel.WithColumns(map[string]string)` — the path → DB-column
allow-list. Lookup is **fail-closed**: any identifier the AST
references that is not in the map causes `Transpile` to return an
error. When omitted, every ident in the AST errors. **Never feed
user input as a column name**; the value of each map entry is
emitted into the SQL after only identifier quoting.
- `pgxcel.WithFunctions(map[string]string)` — alias → canonical
function-name map applied before dispatch. Use it to feed in ASTs
produced by parsers other than cel-go (for example einride/aip-go
emits `"="` / `"AND"` / `"NOT"` instead of the cel-go operator
names). Unknown aliases pass through unchanged.
- `pgxcel.WithParamOffset(int)` — the first placeholder number.
Defaults to `1`. Use a higher value when splicing the fragment into
a query that already has bound values.
A nil ast returns `("", nil, nil)`. An unchecked ast
(`ast.IsChecked() == false`) returns an error.
## Operator coverage
| CEL | Postgres fragment |
| ------------------------------------ | --------------------------------------- |
| `==`, `!=`, `<`, `<=`, `>`, `>=` | `col op $N` (or `col op col`) |
| `&&`, `\|\|` | `(lhs AND rhs)` / `(lhs OR rhs)` |
| `!` | `(NOT expr)` |
| `x in [a, b, c]` | `x IN ($1, $2, $3)` (empty → `FALSE`) |
| `s.contains(x)` | `s LIKE '%' \|\| $N \|\| '%'` |
| `s.startsWith(x)` | `s LIKE $N \|\| '%'` |
| `s.endsWith(x)` | `s LIKE '%' \|\| $N` |
| `s.matches(re)` | `s ~ $N` (POSIX regex) |
| `timestamp("2025-01-02T03:04:05Z")` | `$N` bound as `time.Time` |
| `duration("1h30m")` | `$N` bound as `time.Duration` |
| unary `-` | bound as signed numeric literal |
## Development
```bash
go test ./...
go vet ./...
```
## License
[MIT](LICENSE)