Skip to content

Commit

Permalink
support "sparse" structs; intended for HTTP PATCH
Browse files Browse the repository at this point in the history
  • Loading branch information
mitranim committed Nov 6, 2021
1 parent ce1ccbd commit d5683b5
Show file tree
Hide file tree
Showing 15 changed files with 563 additions and 288 deletions.
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MAKEFLAGS := --silent --always-make
TESTFLAGS := $(if $(filter $(verb), true), -v,) -count=1
TEST := test $(TESTFLAGS) -run=$(run)
TEST := test $(TESTFLAGS) -timeout=1s -run=$(run)
BENCH := test $(TESTFLAGS) -run=- -bench=$(or $(run),.) -benchmem
WATCH := watchexec -r -c -d=0 -n

Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Uses data literals, not a builder API.
* Supports an optional "JSON Expression Language" (JEL) for expressing SQL expressions with nested Lisp-style calls in JSON.
* Supports safely parsing "order by" clauses from JSON and text, for specific struct types, converting field names from `"json"` field tags to `"db"` field tags.
* Supports "sparse" structs, where not all fields are "present", allowing to implement HTTP PATCH semantics without sacrificing static typing.
* Decently optimized.
* Small and dependency-free.

Expand Down Expand Up @@ -106,6 +107,10 @@ func Example_composition() {

## Changelog

### `v0.2.1`

Added `Sparse` and `Partial` to support "sparse" structs, allowing to implement HTTP PATCH semantics more easily, efficiently, and correctly.

### `v0.2.0`

Full API revision. Added many AST/DSL-like types for common expressions. Optimized parsing and expression building. Use caching and pooling to minimize redundant work. String-based query building now uses partial parsing with caching, and should no longer be a measurable expense. Ported JEL support from `github.com/mitranim/jel`.
Expand Down
31 changes: 28 additions & 3 deletions sqlb.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package sqlb

import r "reflect"

/*
Short for "expression". Defines an arbitrary SQL expression. The method appends
arbitrary SQL text. In both the input and output, the arguments must correspond
Expand Down Expand Up @@ -29,9 +31,7 @@ type ParamExpr interface {
Appends a text repesentation. Sometimes allows better efficiency than
`fmt.Stringer`. Implemented by all `Expr` types in this package.
*/
type Appender interface {
Append([]byte) []byte
}
type Appender interface{ Append([]byte) []byte }

/*
Dictionary of arbitrary arguments, ordinal and/or named. Used as input to
Expand Down Expand Up @@ -74,3 +74,28 @@ type NamedRanger interface {
*/
RangeNamed(func(string))
}

/*
Used by `Partial` for filtering struct fields. See `Sparse` and `Partial` for
explanations.
*/
type Haser interface{ Has(string) bool }

/*
Represents an arbitrary struct where not all fields are "present". Calling
`.Get` returns the underlying struct value. Calling `.HasField` answers the
question "is this field present?".
Secretly supported by struct-scanning expressions such as `StructInsert`,
`StructAssign`, `StructValues`, `Cond`, and more. These types attempt to upcast
the inner value to `Sparse`, falling back on using the inner value as-is. This
allows to correctly implement REST PATCH semantics by using only the fields
that were present in a particular HTTP request, while keeping this
functionality optional.
Concrete implementation: `Partial`.
*/
type Sparse interface {
Get() interface{}
HasField(r.StructField) bool
}
6 changes: 2 additions & 4 deletions sqlb_dict.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package sqlb

import (
"reflect"
)
import r "reflect"

/*
Variant of `[]interface{}` conforming to the `ArgDict` interface. Supports only
Expand Down Expand Up @@ -75,7 +73,7 @@ invalid or a struct. Compared to `Dict`, a struct is way faster to construct,
but reading fields by name is way slower. Used for `StrQ`. See the `StructQ`
shortcut.
*/
type StructDict [1]reflect.Value
type StructDict [1]r.Value

// Implement part of the `ArgDict` interface.
func (self StructDict) IsEmpty() bool {
Expand Down
13 changes: 13 additions & 0 deletions sqlb_err.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ func (self ErrUnexpectedEOF) Error() string {
return self.format(typeName(typeOf((*ErrUnexpectedEOF)(nil))))
}

// Specialized type for errors reported by some functions.
type ErrEmptyExpr struct{ Err }

// Implement the `error` interface.
func (self ErrEmptyExpr) Error() string {
return self.format(typeName(typeOf((*ErrEmptyExpr)(nil))))
}

func errOrdinal(err error) error {
if err == nil {
return nil
Expand Down Expand Up @@ -181,3 +189,8 @@ func errUnknownField(while, jsonPath, typeName string) ErrUnknownField {
fmt.Errorf(`no DB path corresponding to JSON path %q in type %v`, jsonPath, typeName),
}}
}

var errEmptyAssign = error(ErrEmptyExpr{Err{
`building SQL assignment expression`,
fmt.Errorf(`assignment must have at least one field`),
}})
133 changes: 81 additions & 52 deletions sqlb_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package sqlb

import (
"fmt"
"reflect"
r "reflect"
"strconv"
"strings"
)
Expand Down Expand Up @@ -662,13 +662,11 @@ func (self Seq) Append(text []byte) []byte { return exprAppend(&self, text) }
func (self Seq) String() string { return exprString(&self) }

func (self *Seq) any(bui *Bui, val interface{}) {
rval := valueOf(val)

switch rval.Kind() {
case reflect.Invalid:
switch kindOf(val) {
case r.Invalid:
self.appendEmpty(bui)
case reflect.Slice:
self.appendSlice(bui, rval)
case r.Slice:
self.appendSlice(bui, val)
default:
panic(errExpectedSlice(`building SQL expression`, val))
}
Expand All @@ -678,7 +676,9 @@ func (self *Seq) appendEmpty(bui *Bui) {
bui.Str(self.Empty)
}

func (self Seq) appendSlice(bui *Bui, val reflect.Value) {
func (self Seq) appendSlice(bui *Bui, src interface{}) {
val := valueOf(src)

if val.Len() == 0 {
self.appendEmpty(bui)
return
Expand Down Expand Up @@ -833,17 +833,14 @@ func (self Cond) Append(text []byte) []byte { return exprAppend(&self, text) }
func (self Cond) String() string { return exprString(&self) }

func (self *Cond) any(bui *Bui, val interface{}) {
rval := valueOf(val)

switch rval.Kind() {
case reflect.Invalid:
switch kindOf(val) {
case r.Invalid:
self.appendEmpty(bui)
case reflect.Struct:
self.appendStruct(bui, rval)
case reflect.Slice:
self.appendSlice(bui, rval)
case r.Struct:
self.appendStruct(bui, val)
case r.Slice:
self.appendSlice(bui, val)
default:
// panic(errExpectedStructOrSlice(`building SQL expression`, val))
bui.Any(val)
}
}
Expand All @@ -853,30 +850,30 @@ func (self *Cond) appendEmpty(bui *Bui) {
}

// TODO consider if we should support nested non-embedded structs.
func (self *Cond) appendStruct(bui *Bui, val reflect.Value) {
fields := loadStructDbFields(val.Type())
if len(fields) == 0 {
self.appendEmpty(bui)
return
}
func (self *Cond) appendStruct(bui *Bui, src interface{}) {
iter := makeIter(src)

for i, field := range fields {
if i > 0 {
for iter.next() {
if !iter.first() {
bui.Str(self.Delim)
}

lhs := Ident(field.DbName)
rhs := Eq{nil, val.FieldByIndex(field.Index).Interface()}
lhs := Ident(FieldDbName(iter.field))
rhs := Eq{nil, iter.value.Interface()}

// Equivalent to using `Eq` for the full expression, but avoids an
// allocation caused by converting `Ident` to `Expr`. As a bonus, this also
// avoids unnecessary parens around the ident.
bui.Set(lhs.AppendExpr(bui.Get()))
bui.Set(rhs.AppendRhs(bui.Get()))
}

if iter.empty() {
self.appendEmpty(bui)
}
}

func (self *Cond) appendSlice(bui *Bui, val reflect.Value) {
func (self *Cond) appendSlice(bui *Bui, val interface{}) {
(*Seq)(self).appendSlice(bui, val)
}

Expand All @@ -886,6 +883,10 @@ any type, and is used as a type carrier; its actual value is ignored. If the
inner value is a struct or struct slice, the resulting expression is a list of
column names corresponding to its fields, using a "db" tag. Otherwise the
expression is `*`.
Unlike many other struct-scanning expressions, this doesn't support filtering
via `Sparse`. It operates at the level of a struct type, not an individual
struct value.
*/
type Cols [1]interface{}

Expand All @@ -902,7 +903,7 @@ func (self Cols) Append(text []byte) []byte {

// Implement the `fmt.Stringer` interface for debug purposes.
func (self Cols) String() string {
return TypeCols(reflect.TypeOf(self[0]))
return TypeCols(r.TypeOf(self[0]))
}

/*
Expand All @@ -914,6 +915,10 @@ expression is `*`.
Unlike `Cols`, this has special support for nested structs and nested column
paths. See the examples.
Unlike many other struct-scanning expressions, this doesn't support filtering
via `Sparse`. It operates at the level of a struct type, not an individual
struct value.
*/
type ColsDeep [1]interface{}

Expand All @@ -930,7 +935,7 @@ func (self ColsDeep) Append(text []byte) []byte {

// Implement the `fmt.Stringer` interface for debug purposes.
func (self ColsDeep) String() string {
return TypeColsDeep(reflect.TypeOf(self[0]))
return TypeColsDeep(r.TypeOf(self[0]))
}

/*
Expand All @@ -939,21 +944,24 @@ struct. Field/column names are ignored. Values may be arbitrary sub-expressions
or arguments. The value passed to `StructValues` may be nil, which is
equivalent to an empty struct. It may also be an arbitrarily-nested struct
pointer, which is automatically dereferenced.
Supports filtering. If the inner value implements `Sparse`, then not all fields
are considered to be "present", which is useful for PATCH semantics. See the
docs on `Sparse` and `Part`.
*/
type StructValues [1]interface{}

// Implement the `Expr` interface, making this a sub-expression.
func (self StructValues) AppendExpr(text []byte, args []interface{}) ([]byte, []interface{}) {
bui := Bui{text, args}
iter := makeIter(self[0])

val := valueOf(self[0])
if val.IsValid() {
for i, field := range loadStructDbFields(val.Type()) {
if i > 0 {
bui.Str(`,`)
}
bui.SubAny(val.FieldByIndex(field.Index).Interface())
// TODO consider panicking when empty.
for iter.next() {
if !iter.first() {
bui.Str(`,`)
}
bui.SubAny(iter.value.Interface())
}

return bui.Get()
Expand All @@ -971,21 +979,36 @@ Represents a names-and-values clause suitable for insertion. The inner value
must be nil or a struct. Nil or empty struct generates a "default values"
clause. Otherwise the resulting expression has SQL column names and values
generated by scanning the input struct. See the examples.
Supports filtering. If the inner value implements `Sparse`, then not all fields
are considered to be "present", which is useful for PATCH semantics. See the
docs on `Sparse` and `Part`.
*/
type StructInsert [1]interface{}

// Implement the `Expr` interface, making this a sub-expression.
func (self StructInsert) AppendExpr(text []byte, args []interface{}) ([]byte, []interface{}) {
bui := Bui{text, args}
iter := makeIter(self[0])

for iter.next() {
if iter.first() {
bui.Str(`(`)
bui.Str(TypeCols(iter.root.Type()))
bui.Str(`)`)
bui.Str(`values (`)
} else {
bui.Str(`,`)
}
bui.SubAny(iter.value.Interface())
}

if self[0] == nil || isStructEmpty(self[0]) {
if iter.empty() {
bui.Str(`default values`)
return bui.Get()
} else {
bui.Str(`)`)
}

bui.Set(Parens{Cols(self)}.AppendExpr(bui.Get()))
bui.Str(`values`)
bui.Set(Parens{StructValues(self)}.AppendExpr(bui.Get()))
return bui.Get()
}

Expand All @@ -1001,24 +1024,30 @@ Represents an SQL assignment clause suitable for "update set" operations. The
inner value must be a struct. The resulting expression consists of
comma-separated assignments with column names and values derived from the
provided struct. See the example.
Supports filtering. If the inner value implements `Sparse`, then not all fields
are considered to be "present", which is useful for PATCH semantics. See the
docs on `Sparse` and `Part`.
*/
type StructAssign [1]interface{}

// Implement the `Expr` interface, making this a sub-expression.
func (self StructAssign) AppendExpr(text []byte, args []interface{}) ([]byte, []interface{}) {
bui := Bui{text, args}
iter := makeIter(self[0])

val := valueOf(self[0])
if val.IsValid() {
for i, field := range loadStructDbFields(val.Type()) {
if i > 0 {
bui.Str(`,`)
}
bui.Set(Assign{
Ident(field.DbName),
val.FieldByIndex(field.Index).Interface(),
}.AppendExpr(bui.Get()))
for iter.next() {
if !iter.first() {
bui.Str(`,`)
}
bui.Set(Assign{
Ident(FieldDbName(iter.field)),
iter.value.Interface(),
}.AppendExpr(bui.Get()))
}

if iter.empty() {
panic(errEmptyAssign)
}

return bui.Get()
Expand Down
Loading

0 comments on commit d5683b5

Please sign in to comment.