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.
- 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 query builder or SQL drivers directly
- Struct matching with dumbqlstruct tag
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"))
}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
}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]
}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 for more examples.
This section is a non-formal description of DumbQL syntax. For strict description see grammar file.
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).
<field_name> <operator> <value>
for example
period_months < 4
| 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 | 
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")
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"]
If number does not have digits after . it's treated as integer and stored as int64. And it's float64 otherwise.
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.