mox/vendor/github.com/mjl-/bstore/nonzero.go
Mechiel Lukkien cb229cb6cf
mox!
2023-01-30 14:27:06 +01:00

218 lines
5.2 KiB
Go

package bstore
import (
"fmt"
"reflect"
)
// isZero returns whether v is the zero value for the fields that we store.
// reflect.IsZero cannot be used on structs because it checks private fields as well.
func (ft fieldType) isZero(v reflect.Value) bool {
if !v.IsValid() {
return true
}
if ft.Ptr {
return v.IsNil()
}
switch ft.Kind {
case kindStruct:
for _, f := range ft.Fields {
if !f.Type.isZero(v.FieldByIndex(f.structField.Index)) {
return false
}
}
return true
}
// Use standard IsZero otherwise, also for kindBinaryMarshal.
return v.IsZero()
}
// checkNonzero compare ofields and nfields (from previous type schema vs newly
// created type schema) for nonzero struct tag. If an existing field got a
// nonzero struct tag added, we verify that there are indeed no nonzero values
// in the database. If there are, we return ErrZero.
func (tx *Tx) checkNonzero(st storeType, tv *typeVersion, ofields, nfields []field) error {
// First we gather paths that we need to check, so we can later simply
// execute those steps on all data we need to read.
paths := &follows{}
next:
for _, f := range nfields {
for _, of := range ofields {
if f.Name == of.Name {
err := f.checkNonzeroGather(&of, paths)
if err != nil {
return err
}
continue next
}
}
if err := f.checkNonzeroGather(nil, paths); err != nil {
return err
}
}
if len(paths.paths) == 0 {
// Common case, not reading all data.
return nil
}
// Finally actually do the checks.
// todo: if there are only top-level fields to check, and we have an index, we can use the index check this without reading all data.
return tx.checkNonzeroPaths(st, tv, paths.paths)
}
type follow struct {
mapKey, mapValue bool
field field
}
type follows struct {
current []follow
paths [][]follow
}
func (f *follows) push(ff follow) {
f.current = append(f.current, ff)
}
func (f *follows) pop() {
f.current = f.current[:len(f.current)-1]
}
func (f *follows) add() {
f.paths = append(f.paths, append([]follow{}, f.current...))
}
func (f field) checkNonzeroGather(of *field, paths *follows) error {
paths.push(follow{field: f})
defer paths.pop()
if f.Nonzero && (of == nil || !of.Nonzero) {
paths.add()
}
if of != nil {
return f.Type.checkNonzeroGather(of.Type, paths)
}
return nil
}
func (ft fieldType) checkNonzeroGather(oft fieldType, paths *follows) error {
switch ft.Kind {
case kindMap:
paths.push(follow{mapKey: true})
if err := ft.MapKey.checkNonzeroGather(*oft.MapKey, paths); err != nil {
return err
}
paths.pop()
paths.push(follow{mapValue: true})
if err := ft.MapValue.checkNonzeroGather(*oft.MapValue, paths); err != nil {
return err
}
paths.pop()
case kindSlice:
err := ft.List.checkNonzeroGather(*oft.List, paths)
if err != nil {
return err
}
case kindStruct:
next:
for _, ff := range ft.Fields {
for _, off := range oft.Fields {
if ff.Name == off.Name {
err := ff.checkNonzeroGather(&off, paths)
if err != nil {
return err
}
continue next
}
}
err := ff.checkNonzeroGather(nil, paths)
if err != nil {
return err
}
}
}
return nil
}
// checkNonzero reads through all records of a type, and checks that the fields
// indicated by paths are nonzero. If not, ErrZero is returned.
func (tx *Tx) checkNonzeroPaths(st storeType, tv *typeVersion, paths [][]follow) error {
rb, err := tx.recordsBucket(st.Current.name, st.Current.fillPercent)
if err != nil {
return err
}
return rb.ForEach(func(bk, bv []byte) error {
tx.stats.Records.Cursor++
rv, err := st.parseNew(bk, bv)
if err != nil {
return err
}
// todo optimization: instead of parsing the full record, use the fieldmap to see if the value is nonzero.
for _, path := range paths {
frv := rv.FieldByIndex(path[0].field.structField.Index)
if err := path[0].field.checkNonzero(frv, path[1:]); err != nil {
return err
}
}
return nil
})
}
func (f field) checkNonzero(rv reflect.Value, path []follow) error {
if len(path) == 0 {
if !f.Nonzero {
return fmt.Errorf("internal error: checkNonzero: expected field to have Nonzero set")
}
if f.Type.isZero(rv) {
return fmt.Errorf("%w: field %q", ErrZero, f.Name)
}
return nil
}
return f.Type.checkNonzero(rv, path)
}
func (ft fieldType) checkNonzero(rv reflect.Value, path []follow) error {
switch ft.Kind {
case kindMap:
follow := path[0]
path = path[1:]
key := follow.mapKey
if !key && !follow.mapValue {
return fmt.Errorf("internal error: following map, expected mapKey or mapValue, got %#v", follow)
}
iter := rv.MapRange()
for iter.Next() {
var err error
if key {
err = ft.MapKey.checkNonzero(iter.Key(), path)
} else {
err = ft.MapValue.checkNonzero(iter.Value(), path)
}
if err != nil {
return err
}
}
case kindSlice:
n := rv.Len()
for i := 0; i < n; i++ {
if err := ft.List.checkNonzero(rv.Index(i), path); err != nil {
return err
}
}
case kindStruct:
follow := path[0]
path = path[1:]
frv := rv.FieldByIndex(follow.field.structField.Index)
if err := follow.field.checkNonzero(frv, path); err != nil {
return err
}
default:
return fmt.Errorf("internal error: checkNonzero with non-empty path, but kind %v", ft.Kind)
}
return nil
}