mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-10 18:49:02 -06:00
Support the @key annotation in paths (#1934)
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"github.com/attic-labs/noms/go/hash"
|
||||
)
|
||||
|
||||
var annotationRe = regexp.MustCompile("^@([a-z]+)")
|
||||
|
||||
type Path []pathPart
|
||||
|
||||
type pathPart interface {
|
||||
@@ -36,11 +39,19 @@ func (p Path) AddField(name string) Path {
|
||||
}
|
||||
|
||||
func (p Path) AddIndex(idx Value) Path {
|
||||
return p.appendPart(newIndexPart(idx))
|
||||
return p.appendPart(newIndexPart(idx, false))
|
||||
}
|
||||
|
||||
func (p Path) AddKeyIndex(idx Value) Path {
|
||||
return p.appendPart(newIndexPart(idx, true))
|
||||
}
|
||||
|
||||
func (p Path) AddHashIndex(h hash.Hash) Path {
|
||||
return p.appendPart(newHashIndexPart(h))
|
||||
return p.appendPart(newHashIndexPart(h, false))
|
||||
}
|
||||
|
||||
func (p Path) AddHashKeyIndex(h hash.Hash) Path {
|
||||
return p.appendPart(newHashIndexPart(h, true))
|
||||
}
|
||||
|
||||
func (p Path) appendPart(part pathPart) Path {
|
||||
@@ -83,10 +94,26 @@ func (p Path) addPath(str string) (Path, error) {
|
||||
return Path{}, err
|
||||
}
|
||||
|
||||
key := false
|
||||
if annParts := annotationRe.FindStringSubmatch(rem); annParts != nil {
|
||||
ann := annParts[1]
|
||||
if ann != "key" {
|
||||
return Path{}, fmt.Errorf("Unsupported annotation: @%s", ann)
|
||||
}
|
||||
key = true
|
||||
rem = rem[len(annParts[0]):]
|
||||
}
|
||||
|
||||
d.Chk.NotEqual(idx == nil, h.IsEmpty())
|
||||
if idx != nil {
|
||||
|
||||
switch {
|
||||
case idx != nil && key:
|
||||
return p.AddKeyIndex(idx).addPath(rem)
|
||||
case idx != nil:
|
||||
return p.AddIndex(idx).addPath(rem)
|
||||
} else {
|
||||
case key:
|
||||
return p.AddHashKeyIndex(h).addPath(rem)
|
||||
default:
|
||||
return p.AddHashIndex(h).addPath(rem)
|
||||
}
|
||||
|
||||
@@ -142,44 +169,58 @@ func (fp fieldPart) String() string {
|
||||
|
||||
type indexPart struct {
|
||||
idx Value
|
||||
key bool
|
||||
}
|
||||
|
||||
func newIndexPart(idx Value) indexPart {
|
||||
func newIndexPart(idx Value, key bool) indexPart {
|
||||
k := idx.Type().Kind()
|
||||
d.Chk.True(k == StringKind || k == BoolKind || k == NumberKind)
|
||||
return indexPart{idx}
|
||||
return indexPart{idx, key}
|
||||
}
|
||||
|
||||
func (ip indexPart) Resolve(v Value) Value {
|
||||
if l, ok := v.(List); ok {
|
||||
switch v := v.(type) {
|
||||
case List:
|
||||
if n, ok := ip.idx.(Number); ok {
|
||||
f := float64(n)
|
||||
if f == math.Trunc(f) && f >= 0 {
|
||||
u := uint64(f)
|
||||
if u < l.Len() {
|
||||
return l.Get(u)
|
||||
if u < v.Len() {
|
||||
if ip.key {
|
||||
return ip.idx
|
||||
}
|
||||
return v.Get(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m, ok := v.(Map); ok {
|
||||
return m.Get(ip.idx)
|
||||
case Map:
|
||||
if ip.key && v.Has(ip.idx) {
|
||||
return ip.idx
|
||||
}
|
||||
if !ip.key {
|
||||
return v.Get(ip.idx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ip indexPart) String() string {
|
||||
return fmt.Sprintf("[%s]", EncodedValue(ip.idx))
|
||||
func (ip indexPart) String() (str string) {
|
||||
ann := ""
|
||||
if ip.key {
|
||||
ann = "@key"
|
||||
}
|
||||
return fmt.Sprintf("[%s]%s", EncodedValue(ip.idx), ann)
|
||||
}
|
||||
|
||||
type hashIndexPart struct {
|
||||
h hash.Hash
|
||||
h hash.Hash
|
||||
key bool
|
||||
}
|
||||
|
||||
func newHashIndexPart(h hash.Hash) hashIndexPart {
|
||||
return hashIndexPart{h}
|
||||
func newHashIndexPart(h hash.Hash, key bool) hashIndexPart {
|
||||
return hashIndexPart{h, key}
|
||||
}
|
||||
|
||||
func (hip hashIndexPart) Resolve(v Value) (res Value) {
|
||||
@@ -188,11 +229,16 @@ func (hip hashIndexPart) Resolve(v Value) (res Value) {
|
||||
|
||||
switch v := v.(type) {
|
||||
case Set:
|
||||
// Unclear what the behavior should be if |hip.key| is true, but ignoring it for sets is arguably correct.
|
||||
seq = v.seq
|
||||
getCurrentValue = func(cur *sequenceCursor) Value { return cur.current().(Value) }
|
||||
case Map:
|
||||
seq = v.seq
|
||||
getCurrentValue = func(cur *sequenceCursor) Value { return cur.current().(mapEntry).value }
|
||||
if hip.key {
|
||||
getCurrentValue = func(cur *sequenceCursor) Value { return cur.current().(mapEntry).key }
|
||||
} else {
|
||||
getCurrentValue = func(cur *sequenceCursor) Value { return cur.current().(mapEntry).value }
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -210,7 +256,11 @@ func (hip hashIndexPart) Resolve(v Value) (res Value) {
|
||||
}
|
||||
|
||||
func (hip hashIndexPart) String() string {
|
||||
return fmt.Sprintf("[#%s]", hip.h.String())
|
||||
ann := ""
|
||||
if hip.key {
|
||||
ann = "@key"
|
||||
}
|
||||
return fmt.Sprintf("[#%s]%s", hip.h.String(), ann)
|
||||
}
|
||||
|
||||
func parsePathIndex(str string) (idx Value, h hash.Hash, rem string, err error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/attic-labs/noms/go/hash"
|
||||
"github.com/attic-labs/testify/assert"
|
||||
)
|
||||
|
||||
@@ -47,45 +48,43 @@ func TestPathStruct(t *testing.T) {
|
||||
assertPathStringResolvesTo(assert, nil, v, `.notHere`)
|
||||
}
|
||||
|
||||
func TestPathList(t *testing.T) {
|
||||
func TestPathIndex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
v := NewList(Number(1), Number(3), String("foo"), Bool(false))
|
||||
var v Value
|
||||
resolvesTo := func(exp, val Value, str string) {
|
||||
// Indices resolve to |exp|.
|
||||
assertPathResolvesTo(assert, exp, v, NewPath().AddIndex(val))
|
||||
assertPathStringResolvesTo(assert, exp, v, str)
|
||||
// Keys resolve to themselves.
|
||||
if exp != nil {
|
||||
exp = val
|
||||
}
|
||||
assertPathResolvesTo(assert, exp, v, NewPath().AddKeyIndex(val))
|
||||
assertPathStringResolvesTo(assert, exp, v, str+"@key")
|
||||
}
|
||||
|
||||
assertPathResolvesTo(assert, Number(1), v, NewPath().AddIndex(Number(0)))
|
||||
assertPathStringResolvesTo(assert, Number(1), v, `[0]`)
|
||||
assertPathResolvesTo(assert, Number(3), v, NewPath().AddIndex(Number(1)))
|
||||
assertPathStringResolvesTo(assert, Number(3), v, `[1]`)
|
||||
assertPathResolvesTo(assert, String("foo"), v, NewPath().AddIndex(Number(2)))
|
||||
assertPathStringResolvesTo(assert, String("foo"), v, `[2]`)
|
||||
assertPathResolvesTo(assert, Bool(false), v, NewPath().AddIndex(Number(3)))
|
||||
assertPathStringResolvesTo(assert, Bool(false), v, `[3]`)
|
||||
assertPathResolvesTo(assert, nil, v, NewPath().AddIndex(Number(4)))
|
||||
assertPathStringResolvesTo(assert, nil, v, `[4]`)
|
||||
assertPathResolvesTo(assert, nil, v, NewPath().AddIndex(Number(-4)))
|
||||
assertPathStringResolvesTo(assert, nil, v, `[-4]`)
|
||||
}
|
||||
v = NewList(Number(1), Number(3), String("foo"), Bool(false))
|
||||
|
||||
func TestPathMap(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
resolvesTo(Number(1), Number(0), "[0]")
|
||||
resolvesTo(Number(3), Number(1), "[1]")
|
||||
resolvesTo(String("foo"), Number(2), "[2]")
|
||||
resolvesTo(Bool(false), Number(3), "[3]")
|
||||
resolvesTo(nil, Number(4), "[4]")
|
||||
resolvesTo(nil, Number(-4), "[-4]")
|
||||
|
||||
v := NewMap(
|
||||
v = NewMap(
|
||||
Number(1), String("foo"),
|
||||
String("two"), String("bar"),
|
||||
Bool(false), Number(23),
|
||||
Number(2.3), Number(4.5),
|
||||
)
|
||||
|
||||
assertPathResolvesTo(assert, String("foo"), v, NewPath().AddIndex(Number(1)))
|
||||
assertPathStringResolvesTo(assert, String("foo"), v, `[1]`)
|
||||
assertPathResolvesTo(assert, String("bar"), v, NewPath().AddIndex(String("two")))
|
||||
assertPathStringResolvesTo(assert, String("bar"), v, `["two"]`)
|
||||
assertPathResolvesTo(assert, Number(23), v, NewPath().AddIndex(Bool(false)))
|
||||
assertPathStringResolvesTo(assert, Number(23), v, `[false]`)
|
||||
assertPathResolvesTo(assert, Number(4.5), v, NewPath().AddIndex(Number(2.3)))
|
||||
assertPathStringResolvesTo(assert, Number(4.5), v, `[2.3]`)
|
||||
assertPathResolvesTo(assert, nil, v, NewPath().AddIndex(Number(4)))
|
||||
assertPathStringResolvesTo(assert, nil, v, `[4]`)
|
||||
resolvesTo(String("foo"), Number(1), "[1]")
|
||||
resolvesTo(String("bar"), String("two"), `["two"]`)
|
||||
resolvesTo(Number(23), Bool(false), "[false]")
|
||||
resolvesTo(Number(4.5), Number(2.3), "[2.3]")
|
||||
resolvesTo(nil, Number(4), "[4]")
|
||||
}
|
||||
|
||||
func TestPathHashIndex(t *testing.T) {
|
||||
@@ -111,8 +110,15 @@ func TestPathHashIndex(t *testing.T) {
|
||||
}
|
||||
|
||||
resolvesTo := func(col, exp, val Value) {
|
||||
// Values resolve to |exp|.
|
||||
assertPathResolvesTo(assert, exp, col, NewPath().AddHashIndex(val.Hash()))
|
||||
assertPathStringResolvesTo(assert, exp, col, hashStr(val))
|
||||
// Keys resolve to themselves.
|
||||
if exp != nil {
|
||||
exp = val
|
||||
}
|
||||
assertPathResolvesTo(assert, exp, col, NewPath().AddHashKeyIndex(val.Hash()))
|
||||
assertPathStringResolvesTo(assert, exp, col, hashStr(val)+"@key")
|
||||
}
|
||||
|
||||
// Primitives are only addressable by their values.
|
||||
@@ -124,7 +130,6 @@ func TestPathHashIndex(t *testing.T) {
|
||||
resolvesTo(s, nil, str)
|
||||
|
||||
// Other values are only addressable by their hashes.
|
||||
|
||||
resolvesTo(m, i, br)
|
||||
resolvesTo(m, lr, l)
|
||||
resolvesTo(m, b, lr)
|
||||
@@ -195,23 +200,12 @@ func TestPathMulti(t *testing.T) {
|
||||
assertPathStringResolvesTo(assert, String("earth"), s, `.foo[1][false]`)
|
||||
assertPathResolvesTo(assert, String("fire"), s, NewPath().AddField("foo").AddIndex(Number(1)).AddHashIndex(m1.Hash()))
|
||||
assertPathStringResolvesTo(assert, String("fire"), s, fmt.Sprintf(`.foo[1][#%s]`, m1.Hash().String()))
|
||||
}
|
||||
|
||||
func TestPathToAndFromString(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
test := func(str string, p Path) {
|
||||
assert.Equal(str, p.String())
|
||||
p2, err := NewPath().AddPath(str)
|
||||
assert.NoError(err)
|
||||
assert.Equal(p, p2)
|
||||
}
|
||||
|
||||
test("[0]", NewPath().AddIndex(Number(0)))
|
||||
test("[\"0\"][\"1\"][\"100\"]", NewPath().AddIndex(String("0")).AddIndex(String("1")).AddIndex(String("100")))
|
||||
test(".foo[0].bar[4.5][false]", NewPath().AddField("foo").AddIndex(Number(0)).AddField("bar").AddIndex(Number(4.5)).AddIndex(Bool(false)))
|
||||
h := Number(42).Hash() // arbitrary hash
|
||||
test(fmt.Sprintf(".foo[#%s]", h.String()), NewPath().AddField("foo").AddHashIndex(h))
|
||||
assertPathResolvesTo(assert, m1, s, NewPath().AddField("foo").AddIndex(Number(1)).AddHashKeyIndex(m1.Hash()))
|
||||
assertPathStringResolvesTo(assert, m1, s, fmt.Sprintf(`.foo[1][#%s]@key`, m1.Hash().String()))
|
||||
assertPathResolvesTo(assert, String("car"), s,
|
||||
NewPath().AddField("foo").AddIndex(Number(1)).AddHashKeyIndex(m1.Hash()).AddIndex(String("c")))
|
||||
assertPathStringResolvesTo(assert, String("car"), s,
|
||||
fmt.Sprintf(`.foo[1][#%s]@key["c"]`, m1.Hash().String()))
|
||||
}
|
||||
|
||||
func TestPathImmutability(t *testing.T) {
|
||||
@@ -233,6 +227,16 @@ func TestPathParseSuccess(t *testing.T) {
|
||||
p, err := NewPath().AddPath(str)
|
||||
assert.NoError(err)
|
||||
assert.Equal(expectPath, p)
|
||||
expectStr := str
|
||||
switch expectStr { // Human readable serialization special cases.
|
||||
case "[1e4]":
|
||||
expectStr = "[10000]"
|
||||
case "[1.]":
|
||||
expectStr = "[1]"
|
||||
case "[\"line\nbreak\rreturn\"]":
|
||||
expectStr = `["line\nbreak\rreturn"]`
|
||||
}
|
||||
assert.Equal(expectStr, p.String())
|
||||
}
|
||||
|
||||
test(".foo", NewPath().AddField("foo"))
|
||||
@@ -240,26 +244,34 @@ func TestPathParseSuccess(t *testing.T) {
|
||||
test(".QQ", NewPath().AddField("QQ"))
|
||||
test("[true]", NewPath().AddIndex(Bool(true)))
|
||||
test("[false]", NewPath().AddIndex(Bool(false)))
|
||||
test("[false]@key", NewPath().AddKeyIndex(Bool(false)))
|
||||
test("[42]", NewPath().AddIndex(Number(42)))
|
||||
test("[42]@key", NewPath().AddKeyIndex(Number(42)))
|
||||
test("[1e4]", NewPath().AddIndex(Number(1e4)))
|
||||
test("[1.]", NewPath().AddIndex(Number(1.)))
|
||||
test("[1.345]", NewPath().AddIndex(Number(1.345)))
|
||||
test(`[""]`, NewPath().AddIndex(String("")))
|
||||
test(`["42"]`, NewPath().AddIndex(String("42")))
|
||||
test(`["42"]@key`, NewPath().AddKeyIndex(String("42")))
|
||||
test("[\"line\nbreak\rreturn\"]", NewPath().AddIndex(String("line\nbreak\rreturn")))
|
||||
test(`["qu\\ote\""]`, NewPath().AddIndex(String(`qu\ote"`)))
|
||||
test(`["π"]`, NewPath().AddIndex(String("π")))
|
||||
test(`["[[br][]acke]]ts"]`, NewPath().AddIndex(String("[[br][]acke]]ts")))
|
||||
test(`["xπy✌z"]`, NewPath().AddIndex(String("xπy✌z")))
|
||||
test(`["ಠ_ಠ"]`, NewPath().AddIndex(String("ಠ_ಠ")))
|
||||
test(`["ಠ_ಠ"]`, NewPath().AddIndex(String("ಠ_ಠ")))
|
||||
test("[\"0\"][\"1\"][\"100\"]", NewPath().AddIndex(String("0")).AddIndex(String("1")).AddIndex(String("100")))
|
||||
test(".foo[0].bar[4.5][false]", NewPath().AddField("foo").AddIndex(Number(0)).AddField("bar").AddIndex(Number(4.5)).AddIndex(Bool(false)))
|
||||
|
||||
h := Number(42).Hash() // arbitrary hash
|
||||
test(fmt.Sprintf(".foo[#%s]", h.String()), NewPath().AddField("foo").AddHashIndex(h))
|
||||
test(fmt.Sprintf(".bar[#%s]@key", h.String()), NewPath().AddField("bar").AddHashKeyIndex(h))
|
||||
}
|
||||
|
||||
func TestPathParseErrors(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
test := func(str, expectError string) {
|
||||
p, err := NewPath().AddPath(str)
|
||||
p, err := ParsePath(str)
|
||||
assert.Equal(Path{}, p)
|
||||
if err != nil {
|
||||
assert.Equal(expectError, err.Error())
|
||||
@@ -271,6 +283,7 @@ func TestPathParseErrors(t *testing.T) {
|
||||
test("", "Empty path")
|
||||
test(".", "Invalid field: ")
|
||||
test("[", "Path ends in [")
|
||||
test("]", "] is missing opening [")
|
||||
test(".#", "Invalid field: #")
|
||||
test(". ", "Invalid field: ")
|
||||
test(". invalid.field", "Invalid field: invalid.field")
|
||||
@@ -304,4 +317,7 @@ func TestPathParseErrors(t *testing.T) {
|
||||
test(".foo[42]bar", "Invalid operator: b")
|
||||
test("#foo", "Invalid operator: #")
|
||||
test("!foo", "Invalid operator: !")
|
||||
test("@foo", "Invalid operator: @")
|
||||
test("@key", "Invalid operator: @")
|
||||
test(fmt.Sprintf(".foo[#%s]@soup", hash.FromData([]byte{42}).String()), "Unsupported annotation: @soup")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user