Skip to content

Commit 156e8dc

Browse files
committed
Initial commit
0 parents  commit 156e8dc

6 files changed

Lines changed: 423 additions & 0 deletions

File tree

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# mongodb-filter-to-postgres
2+
3+
A simple package that converts a [Mongodb Query Filter](https://www.mongodb.com/docs/compass/current/query/filter)
4+
to a Postgres WHERE clause. Aiming to be simple and safe.
5+
6+
**Project State:** Just a rough sketch and playground for Erik and Koen.
7+
8+
## Example:
9+
10+
Let's filter some lobbies in a multiplayer game:
11+
```json5
12+
{
13+
"$and": [
14+
{
15+
"$or": [ // match two maps
16+
{ "map": { "$contains": "aztec" } },
17+
{ "map": { "$contains": "nuke" } }
18+
]
19+
},
20+
{ "password": "" }, // no password set
21+
{
22+
"playerCount": { "$gte": 2, "$lt": 10 } // not empty or full
23+
}
24+
]
25+
}
26+
```
27+
Converts to:
28+
```sql
29+
(
30+
"customdata"->>"map" LIKE ?
31+
OR
32+
"customdata"->>"map" LIKE ?
33+
)
34+
AND "password" = ?
35+
AND (
36+
"playerCount" >= ?
37+
AND
38+
"playerCount" < ?
39+
)
40+
```
41+
And values:
42+
```go
43+
values := []any{"%aztec%", "%nuke%", "", 2, 10}
44+
```
45+
(given "map" is confugired to be in a jsonb column)

examples/basic_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package examples
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/poki/mongodb-filter-to-postgres/filter"
7+
)
8+
9+
func ExampleNewConverter() {
10+
converter := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))
11+
12+
mongoFilterQuery := `{
13+
"name": "John",
14+
"created_at": {
15+
"$gte": "2020-01-01T00:00:00Z"
16+
}
17+
}`
18+
conditions, values, err := converter.Convert([]byte(mongoFilterQuery))
19+
if err != nil {
20+
// handle error
21+
}
22+
23+
fmt.Println(conditions)
24+
fmt.Println(values)
25+
// Output:
26+
// "meta"->>'name' = $1 AND "created_at" >= $2
27+
// [John 2020-01-01T00:00:00Z]
28+
}

filter/converter.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package filter
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
)
9+
10+
var OperatorMap = map[string]string{
11+
"$gt": ">",
12+
"$gte": ">=",
13+
}
14+
15+
type Converter struct {
16+
nestedColumn string
17+
nestedExemptions []string
18+
}
19+
20+
// NewConverter creates a new Converter with optional nested JSONB field mapping.
21+
func NewConverter(options ...Option) *Converter {
22+
converter := &Converter{}
23+
for _, option := range options {
24+
if option != nil {
25+
option(converter)
26+
}
27+
}
28+
return converter
29+
}
30+
31+
// Convert converts a MongoDB filter query into SQL conditions and values.
32+
func (c *Converter) Convert(query []byte) (string, []any, error) {
33+
var mongoFilter map[string]any
34+
err := json.Unmarshal(query, &mongoFilter)
35+
if err != nil {
36+
return "", nil, err
37+
}
38+
39+
conditions, values, err := c.convertFilter(mongoFilter)
40+
if err != nil {
41+
return "", nil, err
42+
}
43+
44+
return conditions, values, nil
45+
}
46+
47+
func (c *Converter) convertFilter(filter map[string]any) (string, []any, error) {
48+
var conditions []string
49+
var values []any
50+
51+
keys := []string{}
52+
for key := range filter {
53+
keys = append(keys, key)
54+
}
55+
sort.Strings(keys)
56+
57+
for _, key := range keys {
58+
value := filter[key]
59+
switch v := value.(type) {
60+
case map[string]any:
61+
inner := []string{}
62+
operators := []string{}
63+
for operator := range v {
64+
operators = append(operators, operator)
65+
}
66+
sort.Strings(operators)
67+
for _, operator := range operators {
68+
value := v[operator]
69+
op, ok := OperatorMap[operator]
70+
if !ok {
71+
return "", nil, fmt.Errorf("unknown operator: %s", operator)
72+
}
73+
inner = append(inner, fmt.Sprintf("(%s %s $%d)", c.columnName(key), op, len(values)+1))
74+
values = append(values, value)
75+
}
76+
innerResult := strings.Join(inner, " AND ")
77+
if len(inner) > 1 {
78+
innerResult = "(" + innerResult + ")"
79+
}
80+
conditions = append(conditions, innerResult)
81+
default:
82+
conditions = append(conditions, fmt.Sprintf("(%s = $%d)", c.columnName(key), len(values)+1))
83+
values = append(values, value)
84+
}
85+
}
86+
87+
result := strings.Join(conditions, " AND ")
88+
if len(conditions) > 1 {
89+
result = "(" + result + ")"
90+
}
91+
return result, values, nil
92+
}
93+
94+
func (c *Converter) columnName(column string) string {
95+
if c.nestedColumn == "" {
96+
return fmt.Sprintf("%q", column)
97+
}
98+
for _, exemption := range c.nestedExemptions {
99+
if exemption == column {
100+
return fmt.Sprintf("%q", column)
101+
}
102+
}
103+
return fmt.Sprintf(`%q->>'%s'`, c.nestedColumn, column)
104+
}
105+
106+
/*
107+
type Converter struct {
108+
nestedColumn string
109+
nestedExemptions []string
110+
}
111+
112+
func NewConverter(options ...Option) *Converter {
113+
converter := &Converter{}
114+
for _, option := range options {
115+
option(converter)
116+
}
117+
return converter
118+
}
119+
120+
func (c *Converter) Convert(filter []byte) (conditions string, values []any, err error) {
121+
expr, err := c.parse(filter)
122+
if err != nil {
123+
return "", nil, fmt.Errorf("failed to parse filter: %w", err)
124+
}
125+
conditions, values, err = expr.ToPostgresWhereClause()
126+
if err != nil {
127+
return "", nil, fmt.Errorf("failed to convert expression to where clause: %w", err)
128+
}
129+
return
130+
}
131+
132+
type expression interface {
133+
ToPostgresWhereClause() (string, []any, error)
134+
}
135+
136+
type compoundExpression struct {
137+
expressions []expression
138+
operator string
139+
}
140+
141+
func (e compoundExpression) ToPostgresWhereClause() (string, []any, error) {
142+
values := []any{}
143+
conditions := []string{}
144+
for _, expr := range e.expressions {
145+
condition, value, err := expr.ToPostgresWhereClause()
146+
if err != nil {
147+
return "", nil, fmt.Errorf("failed to convert expression to where clause: %w", err)
148+
}
149+
conditions = append(conditions, condition)
150+
values = append(values, value...)
151+
}
152+
return "(" + strings.Join(conditions, " AND ") + ")", values, nil
153+
}
154+
155+
type scalarExpression struct {
156+
column string
157+
operator string
158+
value string
159+
}
160+
161+
func (e scalarExpression) ToPostgresWhereClause() (string, []any, error) {
162+
return fmt.Sprintf(`"%s" %s ?`, e.column, e.operator), []any{e.value}, nil
163+
}
164+
165+
func (c *Converter) parse(input []byte) (expression, error) {
166+
raw := map[string]any{}
167+
err := json.Unmarshal(input, &raw)
168+
if err != nil {
169+
return nil, fmt.Errorf("failed to unmarshal input: %w", err)
170+
}
171+
root := compoundExpression{
172+
expressions: []expression{},
173+
operator: "AND",
174+
}
175+
for key, value := range raw {
176+
expr := convertToExpression(key, value, key)
177+
if expr == nil {
178+
return nil, fmt.Errorf("failed to convert expression")
179+
}
180+
root.expressions = append(root.expressions, expr)
181+
}
182+
183+
if root.operator != "AND" {
184+
return nil, fmt.Errorf("root operator must be AND")
185+
}
186+
return root, nil
187+
}
188+
189+
func convertToExpression(key string, value any, currentColumn string) expression {
190+
switch value := value.(type) {
191+
case int:
192+
case int64:
193+
case float64:
194+
case string:
195+
switch key {
196+
case "$gt":
197+
return &scalarExpression{
198+
column: currentColumn,
199+
operator: ">",
200+
value: value,
201+
}
202+
case "$gte":
203+
return &scalarExpression{
204+
column: currentColumn,
205+
operator: ">=",
206+
value: value,
207+
}
208+
case "$lt":
209+
return &scalarExpression{
210+
column: currentColumn,
211+
operator: "<",
212+
value: value,
213+
}
214+
case "$lte":
215+
return &scalarExpression{
216+
column: currentColumn,
217+
operator: "<=",
218+
value: value,
219+
}
220+
case "$eq":
221+
fallthrough
222+
default:
223+
return &scalarExpression{
224+
column: currentColumn,
225+
operator: "=",
226+
value: value,
227+
}
228+
}
229+
case map[string]any:
230+
return convertToExpression(key, value, key)
231+
}
232+
return nil
233+
}
234+
*/

0 commit comments

Comments
 (0)