Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

268 lines
8.1 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tree
import (
"context"
"fmt"
"io"
)
// JsonCursor wraps a cursor to an IndexedJsonDocument stored in a prolly tree. This allows seeking for a specific
// location in the stored JSON document.
type JsonCursor struct {
cur *cursor
jsonScanner JsonScanner
}
// getPreviousKey computes the key of a cursor's predecessor node. This is important for scanning JSON because that
// key represents the location within the document where the current node begins, and can be used to compute the location
// of every value within the node.
func getPreviousKey(ctx context.Context, cur *cursor) ([]byte, error) {
cur2 := cur.clone()
err := cur2.retreat(ctx)
if err != nil {
return nil, err
}
// If we're at the start of the tree, return nil.
if !cur2.Valid() {
return nil, nil
}
key := cur2.CurrentKey()
if len(key) == 0 {
key = cur2.parent.CurrentKey()
}
err = errorIfNotSupportedLocation(key)
if err != nil {
return nil, err
}
return key, nil
}
// newJsonCursor takes the root node of a prolly tree representing a JSON document, and creates a new JsonCursor for reading
// JSON, starting at the specified location in the document. Returns a boolean indicating whether the location already exists
// in the document. If the location does not exist in the document, the resulting JsonCursor
// will be at the location where the value would be if it was inserted.
func newJsonCursor(ctx context.Context, ns NodeStore, root *Node, startKey jsonLocation, forRemoval bool) (jCur *JsonCursor, found bool, err error) {
jcur, err := newJsonCursorAtStartOfChunk(ctx, ns, root, startKey.key)
if err != nil {
return nil, false, err
}
found, err = jcur.AdvanceToLocation(ctx, startKey, forRemoval)
return jcur, found, err
}
func newJsonCursorAtStartOfChunk(ctx context.Context, ns NodeStore, root *Node, startKey jsonLocationKey) (jCur *JsonCursor, err error) {
ordering := jsonLocationOrdering{}
cur, err := newCursorAtKey(ctx, ns, root, startKey, &ordering)
if err != nil {
return nil, err
}
if ordering.err != nil {
return nil, err
}
return newJsonCursorFromCursor(ctx, cur)
}
func newJsonCursorFromCursor(ctx context.Context, cur *cursor) (*JsonCursor, error) {
previousKey, err := getPreviousKey(ctx, cur)
if err != nil {
return nil, err
}
if !cur.isLeaf() {
nd, err := fetchChild(ctx, cur.nrw, cur.currentRef())
if err != nil {
return nil, err
}
return newJsonCursorFromCursor(ctx, &cursor{nd: nd, parent: cur, nrw: cur.nrw})
}
jsonBytes := cur.currentValue()
jsonDecoder := ScanJsonFromMiddleWithKey(jsonBytes, previousKey)
jcur := JsonCursor{cur: cur, jsonScanner: jsonDecoder}
return &jcur, nil
}
func (j JsonCursor) Valid() bool {
return j.cur.Valid()
}
// NextValue reads and consumes an entire value from the JSON document, returning its bytes.
// Precondition: The scanner is currently at the start of a value.
func (j *JsonCursor) NextValue(ctx context.Context) (result []byte, err error) {
if !j.jsonScanner.atStartOfValue() {
return nil, fmt.Errorf("JSON cursor in unexpected state. This is likely a bug")
}
path := j.jsonScanner.currentPath.Clone()
path.setScannerState(endOfValue)
jsonBuffer := j.jsonScanner.jsonBuffer
startPos := j.jsonScanner.valueOffset
parseChunk := func() {
var crossedBoundary bool
crossedBoundary, err = j.AdvanceToNextLocation(ctx)
if err != nil {
return
}
if crossedBoundary {
result = append(result, jsonBuffer[startPos:]...)
jsonBuffer = j.jsonScanner.jsonBuffer
startPos = 0
}
}
parseChunk()
if err != nil {
return
}
for {
var cmp int
cmp, err = compareJsonLocations(j.jsonScanner.currentPath, path)
if err != nil {
return
}
if cmp >= 0 {
break
}
parseChunk()
if err != nil {
return
}
}
result = append(result, jsonBuffer[startPos:j.jsonScanner.valueOffset]...)
return
}
func (j *JsonCursor) isKeyInChunk(path jsonLocation) (bool, error) {
if j.cur.parent == nil {
// This is the only chunk, so the path must refer to this chunk.
return true, nil
}
nodeEndPosition := jsonPathFromKey(j.cur.parent.CurrentKey())
cmp, err := compareJsonLocations(path, nodeEndPosition)
return cmp <= 0, err
}
// AdvanceToLocation causes the cursor to advance to the specified position. This function returns a boolean indicating
// whether the position already exists. If it doesn't, the cursor stops at the location where the value would be if it
// were inserted.
// The `forRemoval` parameter changes the behavior when advancing to the start of an object key. When this parameter is true,
// the cursor advances to the end of the previous value, prior to the object key. This allows the key to be removed along
// with the value.
func (j *JsonCursor) AdvanceToLocation(ctx context.Context, path jsonLocation, forRemoval bool) (found bool, err error) {
isInChunk, err := j.isKeyInChunk(path)
if err != nil {
return false, err
}
if !isInChunk {
// Our destination is in another chunk, load it.
ordering := jsonLocationOrdering{}
err := Seek(ctx, j.cur.parent, path.key, &ordering)
if err != nil {
return false, err
}
if ordering.err != nil {
return false, err
}
j.cur.nd, err = fetchChild(ctx, j.cur.nrw, j.cur.parent.currentRef())
if err != nil {
return false, err
}
previousKey, err := getPreviousKey(ctx, j.cur)
if err != nil {
return false, err
}
j.jsonScanner = ScanJsonFromMiddleWithKey(j.cur.currentValue(), previousKey)
}
previousScanner := j.jsonScanner
cmp, err := compareJsonLocations(j.jsonScanner.currentPath, path)
if err != nil {
return false, err
}
for cmp < 0 {
previousScanner = j.jsonScanner.Clone()
err := j.jsonScanner.AdvanceToNextLocation()
if err == io.EOF {
// We reached the end of the document without finding the path. This shouldn't be possible, because
// there is no path greater than the end-of-document path, which is always the last key.
panic("Reached the end of the JSON document while advancing. This should not be possible. Is the document corrupt?")
} else if err != nil {
return false, err
}
cmp, err = compareJsonLocations(j.jsonScanner.currentPath, path)
if err != nil {
return false, err
}
}
// If the supplied path doesn't exist in the document, then we want to stop the cursor at the start of the point
// were it would appear. This may mean that we've gone too far and need to rewind one location.
if cmp > 0 {
j.jsonScanner = previousScanner
return false, nil
}
// If the element is going to be removed, rewind the scanner one location, to before the key of the removed object.
if forRemoval {
j.jsonScanner = previousScanner
}
return true, nil
}
func (j *JsonCursor) advanceCursor(ctx context.Context) error {
err := j.cur.advance(ctx)
if err != nil {
return err
}
if !j.cur.Valid() {
// We hit the end of the tree. This shouldn't happen.
return io.EOF
}
j.jsonScanner = ScanJsonFromMiddle(j.cur.currentValue(), j.jsonScanner.currentPath)
return nil
}
func (j *JsonCursor) AdvanceToNextLocation(ctx context.Context) (crossedBoundary bool, err error) {
err = j.jsonScanner.AdvanceToNextLocation()
if err == io.EOF {
crossedBoundary = true
// We hit the end of the chunk, load the next one
err = j.advanceCursor(ctx)
if err != nil {
return false, err
}
return true, j.jsonScanner.AdvanceToNextLocation()
} else if err != nil {
return
}
return
}
func (j *JsonCursor) GetCurrentPath() jsonLocation {
return j.jsonScanner.currentPath
}
func (j *JsonCursor) nextCharacter(ctx context.Context) (byte, error) {
if j.jsonScanner.atEndOfChunk() {
err := j.advanceCursor(ctx)
if err != nil {
return 255, err
}
}
return j.jsonScanner.jsonBuffer[j.jsonScanner.valueOffset], nil
}