Skip to content

Commit d5683b5

Browse files
committed
support "sparse" structs; intended for HTTP PATCH
1 parent ce1ccbd commit d5683b5

15 files changed

+563
-288
lines changed

makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MAKEFLAGS := --silent --always-make
22
TESTFLAGS := $(if $(filter $(verb), true), -v,) -count=1
3-
TEST := test $(TESTFLAGS) -run=$(run)
3+
TEST := test $(TESTFLAGS) -timeout=1s -run=$(run)
44
BENCH := test $(TESTFLAGS) -run=- -bench=$(or $(run),.) -benchmem
55
WATCH := watchexec -r -c -d=0 -n
66

readme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* Uses data literals, not a builder API.
1818
* Supports an optional "JSON Expression Language" (JEL) for expressing SQL expressions with nested Lisp-style calls in JSON.
1919
* 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.
20+
* Supports "sparse" structs, where not all fields are "present", allowing to implement HTTP PATCH semantics without sacrificing static typing.
2021
* Decently optimized.
2122
* Small and dependency-free.
2223

@@ -106,6 +107,10 @@ func Example_composition() {
106107

107108
## Changelog
108109

110+
### `v0.2.1`
111+
112+
Added `Sparse` and `Partial` to support "sparse" structs, allowing to implement HTTP PATCH semantics more easily, efficiently, and correctly.
113+
109114
### `v0.2.0`
110115

111116
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`.

sqlb.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package sqlb
22

3+
import r "reflect"
4+
35
/*
46
Short for "expression". Defines an arbitrary SQL expression. The method appends
57
arbitrary SQL text. In both the input and output, the arguments must correspond
@@ -29,9 +31,7 @@ type ParamExpr interface {
2931
Appends a text repesentation. Sometimes allows better efficiency than
3032
`fmt.Stringer`. Implemented by all `Expr` types in this package.
3133
*/
32-
type Appender interface {
33-
Append([]byte) []byte
34-
}
34+
type Appender interface{ Append([]byte) []byte }
3535

3636
/*
3737
Dictionary of arbitrary arguments, ordinal and/or named. Used as input to
@@ -74,3 +74,28 @@ type NamedRanger interface {
7474
*/
7575
RangeNamed(func(string))
7676
}
77+
78+
/*
79+
Used by `Partial` for filtering struct fields. See `Sparse` and `Partial` for
80+
explanations.
81+
*/
82+
type Haser interface{ Has(string) bool }
83+
84+
/*
85+
Represents an arbitrary struct where not all fields are "present". Calling
86+
`.Get` returns the underlying struct value. Calling `.HasField` answers the
87+
question "is this field present?".
88+
89+
Secretly supported by struct-scanning expressions such as `StructInsert`,
90+
`StructAssign`, `StructValues`, `Cond`, and more. These types attempt to upcast
91+
the inner value to `Sparse`, falling back on using the inner value as-is. This
92+
allows to correctly implement REST PATCH semantics by using only the fields
93+
that were present in a particular HTTP request, while keeping this
94+
functionality optional.
95+
96+
Concrete implementation: `Partial`.
97+
*/
98+
type Sparse interface {
99+
Get() interface{}
100+
HasField(r.StructField) bool
101+
}

sqlb_dict.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package sqlb
22

3-
import (
4-
"reflect"
5-
)
3+
import r "reflect"
64

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

8078
// Implement part of the `ArgDict` interface.
8179
func (self StructDict) IsEmpty() bool {

sqlb_err.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ func (self ErrUnexpectedEOF) Error() string {
104104
return self.format(typeName(typeOf((*ErrUnexpectedEOF)(nil))))
105105
}
106106

107+
// Specialized type for errors reported by some functions.
108+
type ErrEmptyExpr struct{ Err }
109+
110+
// Implement the `error` interface.
111+
func (self ErrEmptyExpr) Error() string {
112+
return self.format(typeName(typeOf((*ErrEmptyExpr)(nil))))
113+
}
114+
107115
func errOrdinal(err error) error {
108116
if err == nil {
109117
return nil
@@ -181,3 +189,8 @@ func errUnknownField(while, jsonPath, typeName string) ErrUnknownField {
181189
fmt.Errorf(`no DB path corresponding to JSON path %q in type %v`, jsonPath, typeName),
182190
}}
183191
}
192+
193+
var errEmptyAssign = error(ErrEmptyExpr{Err{
194+
`building SQL assignment expression`,
195+
fmt.Errorf(`assignment must have at least one field`),
196+
}})

sqlb_expr.go

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package sqlb
22

33
import (
44
"fmt"
5-
"reflect"
5+
r "reflect"
66
"strconv"
77
"strings"
88
)
@@ -662,13 +662,11 @@ func (self Seq) Append(text []byte) []byte { return exprAppend(&self, text) }
662662
func (self Seq) String() string { return exprString(&self) }
663663

664664
func (self *Seq) any(bui *Bui, val interface{}) {
665-
rval := valueOf(val)
666-
667-
switch rval.Kind() {
668-
case reflect.Invalid:
665+
switch kindOf(val) {
666+
case r.Invalid:
669667
self.appendEmpty(bui)
670-
case reflect.Slice:
671-
self.appendSlice(bui, rval)
668+
case r.Slice:
669+
self.appendSlice(bui, val)
672670
default:
673671
panic(errExpectedSlice(`building SQL expression`, val))
674672
}
@@ -678,7 +676,9 @@ func (self *Seq) appendEmpty(bui *Bui) {
678676
bui.Str(self.Empty)
679677
}
680678

681-
func (self Seq) appendSlice(bui *Bui, val reflect.Value) {
679+
func (self Seq) appendSlice(bui *Bui, src interface{}) {
680+
val := valueOf(src)
681+
682682
if val.Len() == 0 {
683683
self.appendEmpty(bui)
684684
return
@@ -833,17 +833,14 @@ func (self Cond) Append(text []byte) []byte { return exprAppend(&self, text) }
833833
func (self Cond) String() string { return exprString(&self) }
834834

835835
func (self *Cond) any(bui *Bui, val interface{}) {
836-
rval := valueOf(val)
837-
838-
switch rval.Kind() {
839-
case reflect.Invalid:
836+
switch kindOf(val) {
837+
case r.Invalid:
840838
self.appendEmpty(bui)
841-
case reflect.Struct:
842-
self.appendStruct(bui, rval)
843-
case reflect.Slice:
844-
self.appendSlice(bui, rval)
839+
case r.Struct:
840+
self.appendStruct(bui, val)
841+
case r.Slice:
842+
self.appendSlice(bui, val)
845843
default:
846-
// panic(errExpectedStructOrSlice(`building SQL expression`, val))
847844
bui.Any(val)
848845
}
849846
}
@@ -853,30 +850,30 @@ func (self *Cond) appendEmpty(bui *Bui) {
853850
}
854851

855852
// TODO consider if we should support nested non-embedded structs.
856-
func (self *Cond) appendStruct(bui *Bui, val reflect.Value) {
857-
fields := loadStructDbFields(val.Type())
858-
if len(fields) == 0 {
859-
self.appendEmpty(bui)
860-
return
861-
}
853+
func (self *Cond) appendStruct(bui *Bui, src interface{}) {
854+
iter := makeIter(src)
862855

863-
for i, field := range fields {
864-
if i > 0 {
856+
for iter.next() {
857+
if !iter.first() {
865858
bui.Str(self.Delim)
866859
}
867860

868-
lhs := Ident(field.DbName)
869-
rhs := Eq{nil, val.FieldByIndex(field.Index).Interface()}
861+
lhs := Ident(FieldDbName(iter.field))
862+
rhs := Eq{nil, iter.value.Interface()}
870863

871864
// Equivalent to using `Eq` for the full expression, but avoids an
872865
// allocation caused by converting `Ident` to `Expr`. As a bonus, this also
873866
// avoids unnecessary parens around the ident.
874867
bui.Set(lhs.AppendExpr(bui.Get()))
875868
bui.Set(rhs.AppendRhs(bui.Get()))
876869
}
870+
871+
if iter.empty() {
872+
self.appendEmpty(bui)
873+
}
877874
}
878875

879-
func (self *Cond) appendSlice(bui *Bui, val reflect.Value) {
876+
func (self *Cond) appendSlice(bui *Bui, val interface{}) {
880877
(*Seq)(self).appendSlice(bui, val)
881878
}
882879

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

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

903904
// Implement the `fmt.Stringer` interface for debug purposes.
904905
func (self Cols) String() string {
905-
return TypeCols(reflect.TypeOf(self[0]))
906+
return TypeCols(r.TypeOf(self[0]))
906907
}
907908

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

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

931936
// Implement the `fmt.Stringer` interface for debug purposes.
932937
func (self ColsDeep) String() string {
933-
return TypeColsDeep(reflect.TypeOf(self[0]))
938+
return TypeColsDeep(r.TypeOf(self[0]))
934939
}
935940

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

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

949-
val := valueOf(self[0])
950-
if val.IsValid() {
951-
for i, field := range loadStructDbFields(val.Type()) {
952-
if i > 0 {
953-
bui.Str(`,`)
954-
}
955-
bui.SubAny(val.FieldByIndex(field.Index).Interface())
959+
// TODO consider panicking when empty.
960+
for iter.next() {
961+
if !iter.first() {
962+
bui.Str(`,`)
956963
}
964+
bui.SubAny(iter.value.Interface())
957965
}
958966

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

977989
// Implement the `Expr` interface, making this a sub-expression.
978990
func (self StructInsert) AppendExpr(text []byte, args []interface{}) ([]byte, []interface{}) {
979991
bui := Bui{text, args}
992+
iter := makeIter(self[0])
993+
994+
for iter.next() {
995+
if iter.first() {
996+
bui.Str(`(`)
997+
bui.Str(TypeCols(iter.root.Type()))
998+
bui.Str(`)`)
999+
bui.Str(`values (`)
1000+
} else {
1001+
bui.Str(`,`)
1002+
}
1003+
bui.SubAny(iter.value.Interface())
1004+
}
9801005

981-
if self[0] == nil || isStructEmpty(self[0]) {
1006+
if iter.empty() {
9821007
bui.Str(`default values`)
983-
return bui.Get()
1008+
} else {
1009+
bui.Str(`)`)
9841010
}
9851011

986-
bui.Set(Parens{Cols(self)}.AppendExpr(bui.Get()))
987-
bui.Str(`values`)
988-
bui.Set(Parens{StructValues(self)}.AppendExpr(bui.Get()))
9891012
return bui.Get()
9901013
}
9911014

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

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

1011-
val := valueOf(self[0])
1012-
if val.IsValid() {
1013-
for i, field := range loadStructDbFields(val.Type()) {
1014-
if i > 0 {
1015-
bui.Str(`,`)
1016-
}
1017-
bui.Set(Assign{
1018-
Ident(field.DbName),
1019-
val.FieldByIndex(field.Index).Interface(),
1020-
}.AppendExpr(bui.Get()))
1039+
for iter.next() {
1040+
if !iter.first() {
1041+
bui.Str(`,`)
10211042
}
1043+
bui.Set(Assign{
1044+
Ident(FieldDbName(iter.field)),
1045+
iter.value.Interface(),
1046+
}.AppendExpr(bui.Get()))
1047+
}
1048+
1049+
if iter.empty() {
1050+
panic(errEmptyAssign)
10221051
}
10231052

10241053
return bui.Get()

0 commit comments

Comments
 (0)