dumbql

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 11, 2025 License: MIT Imports: 2 Imported by: 0

README

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

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.

Documentation

Overview

Package dumbql provides simple (dumb) query language and it's parser.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Query

type Query struct {
	query.Expr
}

func Parse

func Parse(q string, opts ...query.Option) (*Query, error)

Parse parses the input query string q, returning a Query reference or an error in case of invalid input.

Example
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"))

func (*Query) ToSql

func (q *Query) ToSql() (string, []any, error)

ToSql converts the Query into an SQL string, returning the SQL string, arguments slice, and any potential error encountered.

Example
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]

func (*Query) Validate

func (q *Query) Validate(s schema.Schema) (query.Expr, error)

Validate checks the query against the provided schema, returning a validated expression or an error if any rule is violated. Even when error returned Validate can return query AST with invalided nodes dropped.

Example
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.Println(err)
}
Output:

(and (= status "pending") (= title "hello world"))
field "period_months": value must be equal or less than 3, got 4; field "name" not found in schema

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL