An open API service indexing awesome lists of open source software.

https://github.com/defer-panic/dumbql

Simple query language
https://github.com/defer-panic/dumbql

parser query-language

Last synced: about 1 year ago
JSON representation

Simple query language

Awesome Lists containing this project

README

          

# dumbql ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/defer-panic/dumbql) ![GitHub License](https://img.shields.io/github/license/defer-panic/dumbql) ![GitHub Tag](https://img.shields.io/github/v/tag/defer-panic/dumbql) [![Go Report Card](https://goreportcard.com/badge/github.com/defer-panic/dumbql)](https://goreportcard.com/report/github.com/defer-panic/dumbql) [![CI](https://github.com/defer-panic/dumbql/actions/workflows/main.yml/badge.svg)](https://github.com/defer-panic/dumbql/actions/workflows/main.yml) [![Coverage Status](https://coveralls.io/repos/github/defer-panic/dumbql/badge.svg?branch=main)](https://coveralls.io/github/defer-panic/dumbql?branch=main) [![Go Reference](https://pkg.go.dev/badge/github.com/defer-panic/dumbql.svg)](https://pkg.go.dev/github.com/defer-panic/dumbql)

> [!WARNING]
> This version of DumbQL is archived. Further development will take place here: https://github.com/tomakado/dumbql

Simple (dumb) query language and parser for Go.

## Features

- Field expressions (`age >= 18`, `field.name:"field value"`, etc.)
- Boolean expressions (`age >= 18 and city = Barcelona`, `occupation = designer or occupation = "ux analyst"`)
- One-of/In expressions (`occupation = [designer, "ux analyst"]`)
- Schema validation
- Drop-in usage with [squirrel](https://github.com/Masterminds/squirrel) query builder or SQL drivers directly
- Struct matching with `dumbql` struct tag

## Examples

### Simple parse

```go
package main

import (
"fmt"

"github.com/defer-panic/dumbql"
)

func main() {
const q = `profile.age >= 18 and profile.city = Barcelona`
ast, err := dumbql.Parse(q)
if err != nil {
panic(err)
}

fmt.Println(ast)
// Output: (and (>= profile.age 18) (= profile.city "Barcelona"))
}
```

### Validation against schema

```go
package main

import (
"fmt"

"github.com/defer-panic/dumbql"
"github.com/defer-panic/dumbql/schema"
)

func main() {
schm := schema.Schema{
"status": schema.All(
schema.Is[string](),
schema.EqualsOneOf("pending", "approved", "rejected"),
),
"period_months": schema.Max(int64(3)),
"title": schema.LenInRange(1, 100),
}

// The following query is invalid against the schema:
// - period_months == 4, but max allowed value is 3
// - field `name` is not described in the schema
//
// Invalid parts of the query are dropped.
const q = `status:pending and period_months:4 and (title:"hello world" or name:"John Doe")`
expr, err := dumbql.Parse(q)
if err != nil {
panic(err)
}

validated, err := expr.Validate(schm)
fmt.Println(validated)
fmt.Printf("validation error: %v\n", err)
// Output:
// (and (= status "pending") (= title "hello world"))
// validation error: field "period_months": value must be equal or less than 3, got 4; field "name" not found in schema
}
```

### Convert to SQL

```go
package main

import (
"fmt"

sq "github.com/Masterminds/squirrel"
"github.com/defer-panic/dumbql"
)

func main() {
const q = `status:pending and period_months < 4 and (title:"hello world" or name:"John Doe")`
expr, err := dumbql.Parse(q)
if err != nil {
panic(err)
}

sql, args, err := sq.Select("*").
From("users").
Where(expr).
ToSql()
if err != nil {
panic(err)
}

fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT * FROM users WHERE ((status = ? AND period_months < ?) AND (title = ? OR name = ?))
// [pending 4 hello world John Doe]
}
```

See [dumbql_example_test.go](dumbql_example_test.go)

### Match against structs

```go
package main

import (
"fmt"

"github.com/defer-panic/dumbql"
"github.com/defer-panic/dumbql/match"
"github.com/defer-panic/dumbql/query"
)

type User struct {
ID int64 `dumbql:"id"`
Name string `dumbql:"name"`
Age int64 `dumbql:"age"`
Score float64 `dumbql:"score"`
Location string `dumbql:"location"`
Role string `dumbql:"role"`
}

func main() {
users := []User{
{
ID: 1,
Name: "John Doe",
Age: 30,
Score: 4.5,
Location: "New York",
Role: "admin",
},
{
ID: 2,
Name: "Jane Smith",
Age: 25,
Score: 3.8,
Location: "Los Angeles",
Role: "user",
},
{
ID: 3,
Name: "Bob Johnson",
Age: 35,
Score: 4.2,
Location: "Chicago",
Role: "user",
},
// This one will be dropped:
{
ID: 4,
Name: "Alice Smith",
Age: 25,
Score: 3.8,
Location: "Los Angeles",
Role: "admin",
},
}

q := `(age >= 30 and score > 4.0) or (location:"Los Angeles" and role:"user")`
ast, _ := query.Parse("test", []byte(q))
expr := ast.(query.Expr)

matcher := &match.StructMatcher{}

filtered := make([]User, 0, len(users))

for _, user := range users {
if expr.Match(&user, matcher) {
filtered = append(filtered, user)
}
}

fmt.Println(filtered)
// [{1 John Doe 30 4.5 New York admin} {2 Jane Smith 25 3.8 Los Angeles user} {3 Bob Johnson 35 4.2 Chicago user}]
}
```

See [match_example_test.go](match_example_test.go) for more examples.

## Query syntax

This section is a non-formal description of DumbQL syntax. For strict description see [grammar file](query/grammar.peg).

### Field expression

Field name & value pair divided by operator. Field name is any alphanumeric identifier (with underscore), value can be string, int64 or floa64.
One-of expression is also supported (see below).

```

```

for example

```
period_months < 4
```

### Field expression operators

| Operator | Meaning | Supported types |
|----------------------|-------------------------------|------------------------------|
| `:` or `=` | Equal, one of | `int64`, `float64`, `string` |
| `!=` or `!:` | Not equal | `int64`, `float64`, `string` |
| `~` | “Like” or “contains” operator | `string` |
| `>`, `>=`, `<`, `<=` | Comparison | `int64`, `float64` |

### Boolean operators

Multiple field expression can be combined into boolean expressions with `and` (`AND`) or `or` (`OR`) operators:

```
status:pending and period_months < 4 and (title:"hello world" or name:"John Doe")
```

### “One of” expression

Sometimes instead of multiple `and`/`or` clauses against the same field:

```
occupation = designer or occupation = "ux analyst"
```

it's more convenient to use equivalent “one of” expressions:

```
occupation: [designer, "ux analyst"]
```

### Numbers

If number does not have digits after `.` it's treated as integer and stored as `int64`. And it's `float64` otherwise.

### Strings

String is a sequence on Unicode characters surrounded by double quotes (`"`). In some cases like single word it's possible to write string value without double quotes.