Skip to content
This repository was archived by the owner on Feb 11, 2025. It is now read-only.

defer-panic/dumbql

Repository files navigation

dumbql GitHub go.mod Go version GitHub License GitHub Tag Go Report Card CI Coverage Status Go Reference

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 query builder or SQL drivers directly
  • Struct matching with dumbql struct tag

Examples

Simple parse

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

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

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

Match against structs

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.

Query syntax

This section is a non-formal description of DumbQL syntax. For strict description see grammar file.

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).

<field_name> <operator> <value>

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.

About

Simple query language

Topics

Resources

License

Stars

Watchers

Forks

Languages