lql

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 25, 2026 License: MIT Imports: 10 Imported by: 0

README

lql

LQL (Lockd Query Language) provides a compact selector and mutation syntax for JSON documents. This module includes:

  • A Go library for parsing selectors and mutations, plus selector evaluation.
  • A CLI (cmd/lql) for selecting JSON documents, applying mutations, and formatting output via pkt.systems/prettyx.

Install

go get pkt.systems/lql@latest

Library usage

Selectors

Selectors are declarative predicates built over JSON Pointer fields.

sel, err := lql.ParseSelectorString(`
  and.eq{field=/status,value=open},
  and.range{field=/progress,gte=50}
`)
if err != nil {
  log.Fatal(err)
}

doc := map[string]any{
  "status":   "open",
  "progress": 72,
}

if lql.Matches(sel, doc) {
  fmt.Println("match")
}

If you already have selectors split into a slice, you can combine them with AND or OR without rejoining them yourself:

sel, err := lql.ParseSelectorStrings([]string{`/status="ok"`, `/msg="done"`})
sel, err := lql.ParseSelectorStringsOr([]string{`/status="ok"`, `/msg="done"`})

Shorthand forms are supported:

sel, _ := lql.ParseSelectorString(`/status="open",/progress>=50`)

If you want implicit OR semantics across a single string, use ParseSelectorStringOr:

sel, _ := lql.ParseSelectorStringOr(`/status="open",/status="queued"`)

Array element selection is supported via JSON Pointer indices:

sel, _ := lql.ParseSelectorString(`/devices/0/status="online"`)

Wildcard selection follows explicit semantics:

  • * matches any child value of an object (objects only; arrays do not match)
  • [] matches any element of an array (arrays only; objects do not match)
  • ** matches any child (object value or array element)
  • ... matches any descendant at any depth (objects or arrays)
  • Type mismatches do not match (e.g. [] on an object)
  • Bracket sugar: /items[]/sku is the same as /items/[]/sku
sel, _ := lql.ParseSelectorString(`/labels/*="production"`)
sel, _ = lql.ParseSelectorString(`/items[]/sku="ABC-123"`)
sel, _ = lql.ParseSelectorString(`/items/**/sku="ABC-123"`)
sel, _ = lql.ParseSelectorString(`/items/.../sku="ABC-123"`)
Mutations

Mutations modify JSON objects in-place.

doc := map[string]any{
  "state": map[string]any{
    "status":  "queued",
    "retries": 1,
  },
}

if err := lql.Mutate(doc,
  "/state/status=running",
  "/state/retries=+2",
  "rm:/state/legacy",
); err != nil {
  log.Fatal(err)
}

Brace shorthand applies multiple mutations under a prefix:

_ = lql.Mutate(doc, `/state{/owner="alice",/note="hi"}`)

Time-prefixed mutations normalize timestamps to RFC3339Nano:

_ = lql.MutateWithTime(doc, time.Now(), `time:/state/updated=NOW`)

Mutations support the same wildcard semantics as selectors; missing paths under wildcards are skipped.

CLI usage

usage: lql [-m mutator...] [-f field...] selector... [data.json]
   or: lql selector... < data.json
   or: cat data.json | lql selector...

By default, multiple selector arguments are combined with AND. Use -O (or --or) to combine them with OR.

Selectors determine which JSON documents to output. The matching documents are printed in full by default. If the input is a JSON array, each element is treated as a candidate document.

Mutations apply to each JSON object in the input stream (NDJSON or JSON arrays). When selectors are provided alongside -m, mutations are applied only to matching objects. With -m, selectors no longer filter output; they only control which objects are mutated (output still includes all objects, subject to -f). Use -M/--matches-only to keep selectors acting as output filters even when -m is provided. Output always contains the full (possibly mutated) object unless -f is used.

Selection examples

Select documents matching a status and region:

lql '/status="open",/region="us-west"' data.json
Full LQL examples

CLI (full LQL expressions):

lql 'and.eq{field=/status,value=open},and.range{field=/progress,gte=50}' data.json
lql 'or.eq{field=/region,value=us},or.eq{field=/region,value=eu}' data.json
lql 'not.eq{field=/state,value=disabled}' data.json
lql 'exists{/metadata/etag}' data.json
lql 'in{field=/env,any=prod|stage|dev}' data.json
lql 'and.eq{field=/items[]/sku,value=ABC-123},and.range{field=/items[]/price,lt=20}' data.json

Note: full LQL expressions use {} and should be quoted (or the braces escaped) to avoid shell brace expansion.

Note: inside {}, you can use f=/v= as aliases for field=/value=, and a= as an alias for any= (in in{...} terms).

SDK (parse full LQL expressions):

sel, err := lql.ParseSelectorString(
  "and.eq{field=/status,value=open},and.range{field=/progress,gte=50}",
)
sel, err := lql.ParseSelectorString(
  "or.eq{field=/region,value=us},or.eq{field=/region,value=eu}",
)
sel, err := lql.ParseSelectorString("not.eq{field=/state,value=disabled}")
sel, err := lql.ParseSelectorString("exists{/metadata/etag}")
sel, err := lql.ParseSelectorString("in{field=/env,any=prod|stage|dev}")
sel, err := lql.ParseSelectorString(
  "and.eq{field=/items[]/sku,value=ABC-123},and.range{field=/items[]/price,lt=20}",
)

Select only a few fields from matching documents:

lql '/status="open"' -f /id -f /status -f /region data.json

Filter array input (each element is evaluated):

cat devices.json | lql '/telemetry/battery_mv<3600'

Match on array elements inside each document:

lql '/devices/0/status="online"' data.json

Match on any array element using wildcards:

lql '/items[]/sku="ABC-123"' data.json

Match on any descendant using recursive descent:

lql '/items/.../sku="ABC-123"' data.json
Mutation examples

Apply mutations conditionally:

lql -m '/state/retries++' -m '/state/status=running' '/state/status="queued"' state.json

Apply mutations and emit only selected fields:

lql -m '/state/retries=+3' -f /state/retries -f /state/status state.json

Apply mutations across array elements:

lql -m '/items[]/status=ready' data.json

Apply mutations using recursive descent:

lql -m '/groups/.../status=ready' data.json

Note: mutations apply to a JSON object root, but paths may traverse arrays using wildcards or numeric indices.

Write mutations inline:

lql -m '/state/status=done' -i state.json
Output formatting

By default, output is pretty-printed using prettyx. Use -c for compact one-line JSON documents and -t to select a prettyx theme (see lql -h).

Selector grammar overview

  • Logical: and, or, not
  • Clauses: eq, prefix, range, in, exists
  • JSON Pointer fields: /path/to/field
  • Shorthand: /field=value, /field!=value, /field>=10, /field<5
  • Arrays: /items/0/sku="ABC-123"
  • Wildcards: * (object values), [] (array elements), ** (any child), ... (recursive descent)

Mutation grammar overview

  • Set: /path=value
  • Increment: /path++, /path--, /path=+3, /path=-2
  • Remove: rm:/path, delete:/path
  • Time: time:/path=NOW or RFC3339 timestamp
  • Brace: /path{/a=1,/b=2}
  • Wildcards: *, [], **, ... in path segments

Documentation

Overview

Package lql implements the Lockd Query Language (LQL) selector and mutation syntax for JSON documents.

Selectors

LQL selectors are declarative predicates over JSON documents. They compile to a Selector AST and can be evaluated with Matches.

Basic forms:

eq{field=/status,value=open}
prefix{field=/owner,value=team-}
range{field=/progress,gt=10}
in{field=/state,any=["queued","running"]}
exists{/metadata/etag}

Logical composition:

and.eq{field=/status,value=open},and.range{field=/progress,gte=50}
or.eq{field=/region,value=us},or.eq{field=/region,value=eu}
not.eq{field=/state,value=disabled}

Shorthand:

/status="open"
/status!=closed
/progress>=50

Arrays:

/devices/0/status="online"
/items/2/sku="ABC-123"

Wildcards (selector paths):

  • any child value of an object (objects only; arrays do not match) [] any element of an array (arrays only; objects do not match) ** any child (object value or array element) ... recursive descent (any depth, objects or arrays)

Type mismatches do not match (e.g. [] on an object). Bracket sugar expands "/items[]/sku" to "/items/[]/sku".

/labels/*="production"
/items[]/sku="ABC-123"
/items/**/sku="ABC-123"
/items/.../sku="ABC-123"

Example:

sel, _ := lql.ParseSelectorString(`/status="open",/progress>=50`)
ok := lql.Matches(sel, map[string]any{
  "status": "open",
  "progress": 72,
})

Mutations

Mutations mutate JSON objects in-place using JSON Pointer paths.

/state/status=running        # set
/state/retries++             # increment by 1
/state/retries=+3            # add 3
rm:/state/legacy             # delete
time:/state/updated=NOW      # RFC3339 timestamp

Mutations support the same wildcard semantics as selectors. When a wildcard path segment is used, missing paths under matched nodes are skipped.

Brace shorthand applies a set of nested mutations under a prefix:

/state{/owner="alice",/note="hi"}

Example:

doc := map[string]any{"state": map[string]any{"retries": 1}}
_ = lql.Mutate(doc, "/state/retries=+2", "/state/status=running")

The package is intentionally small and dependency-free so it can be embedded in CLIs and services that need LQL parsing or evaluation.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyMutations

func ApplyMutations(doc map[string]any, muts []Mutation) error

ApplyMutations mutates the provided JSON object in-place according to muts.

func Matches

func Matches(sel Selector, doc map[string]any) bool

Matches reports whether doc satisfies sel. A nil document never matches.

func MatchesValue

func MatchesValue(sel Selector, value any) bool

MatchesValue reports whether value satisfies sel. Only JSON objects match.

func Mutate

func Mutate(doc map[string]any, exprs ...string) error

Mutate applies the provided mutation expressions to doc using time.Now().

func MutateWithTime

func MutateWithTime(doc map[string]any, now time.Time, exprs ...string) error

MutateWithTime applies mutation expressions using the supplied time.

Types

type InTerm

type InTerm struct {
	Field string   `json:"field"`
	Any   []string `json:"any"`
}

InTerm represents a small set membership filter.

type Mutation

type Mutation struct {
	Path  []string
	Kind  MutationKind
	Value any
	Delta float64
}

Mutation describes a parsed mutation expression.

func Mutations

func Mutations(now time.Time, exprs ...string) ([]Mutation, error)

Mutations parses variadic mutation expressions (each of which may contain comma/newline separated clauses) using the provided timestamp for time: operands.

func ParseMutations

func ParseMutations(exprs []string, now time.Time) ([]Mutation, error)

ParseMutations parses CLI-style mutation expressions into Mutation structs. Paths follow JSON Pointer semantics (`/foo/bar`), so literal dots or spaces in keys require no extra quoting. Brace shorthand (`/foo{/bar=1,/baz=2}`), rm:/time: prefixes, and ++/--/+=/-= increment forms are supported.

func ParseMutationsString

func ParseMutationsString(expr string, now time.Time) ([]Mutation, error)

ParseMutationsString parses a comma/newline separated LQL mutation string.

type MutationKind

type MutationKind int

MutationKind identifies the operation applied to a JSON path.

const (
	// MutationSet assigns a value to the target path.
	MutationSet MutationKind = iota
	// MutationIncrement adds/subtracts a numeric delta to the target path.
	MutationIncrement
	// MutationRemove deletes the target path.
	MutationRemove
)

type RangeTerm

type RangeTerm struct {
	Field string   `json:"field"`
	GTE   *float64 `json:"gte,omitempty"`
	GT    *float64 `json:"gt,omitempty"`
	LTE   *float64 `json:"lte,omitempty"`
	LT    *float64 `json:"lt,omitempty"`
}

RangeTerm captures numeric/timestamp bounds.

type Selector

type Selector struct {
	And    []Selector `json:"and,omitempty"`
	Or     []Selector `json:"or,omitempty"`
	Not    *Selector  `json:"not,omitempty"`
	Eq     *Term      `json:"eq,omitempty"`
	Prefix *Term      `json:"prefix,omitempty"`
	Range  *RangeTerm `json:"range,omitempty"`
	In     *InTerm    `json:"in,omitempty"`
	Exists string     `json:"exists,omitempty"`
}

Selector represents the recursive selector AST.

func ParseSelectorString

func ParseSelectorString(expr string) (Selector, error)

ParseSelectorString parses a comma/newline separated selector expression string (LQL). When multiple expressions are provided, non-explicit clauses are combined with AND.

func ParseSelectorStringOr added in v0.4.0

func ParseSelectorStringOr(expr string) (Selector, error)

ParseSelectorStringOr parses a comma/newline separated selector expression string (LQL). When multiple expressions are provided, non-explicit clauses are combined with OR.

Example
sel, err := ParseSelectorStringOr(`/status="ok",/msg="done"`)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(!sel.IsEmpty(), len(sel.Or))
Output:

true 2

func ParseSelectorStrings added in v0.3.0

func ParseSelectorStrings(exprs []string) (Selector, error)

ParseSelectorStrings parses each selector expression and combines them with AND.

Example
sel, err := ParseSelectorStrings([]string{`/status="ok"`, `/msg="done"`})
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(!sel.IsEmpty(), len(sel.And))
Output:

true 2

func ParseSelectorStringsOr added in v0.3.0

func ParseSelectorStringsOr(exprs []string) (Selector, error)

ParseSelectorStringsOr parses each selector expression and combines them with OR.

Example
sel, err := ParseSelectorStringsOr([]string{`/status="ok"`, `/msg="done"`})
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(!sel.IsEmpty(), len(sel.Or))
Output:

true 2

func ParseSelectorValues

func ParseSelectorValues(values url.Values) (Selector, error)

ParseSelectorValues scans URL parameters for selector expressions and returns the AST.

func (Selector) IsEmpty

func (s Selector) IsEmpty() bool

IsEmpty reports whether the selector contains any clauses.

func (*Selector) UnmarshalJSON

func (s *Selector) UnmarshalJSON(data []byte) error

UnmarshalJSON ensures empty selectors stay zeroed without nil pointers.

type Term

type Term struct {
	Field string `json:"field"`
	Value string `json:"value"`
}

Term represents a simple field/value predicate.

func (*Term) UnmarshalJSON

func (t *Term) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts string/bool/number for value and converts to string.

Directories

Path Synopsis
cmd
lql command
Command lql evaluates LQL selectors and applies LQL mutations over JSON input.
Command lql evaluates LQL selectors and applies LQL mutations over JSON input.
Package jsonpointer implements RFC 6901 JSON Pointer utilities.
Package jsonpointer implements RFC 6901 JSON Pointer utilities.

Jump to

Keyboard shortcuts

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