Skip to content

Commit 99f3365

Browse files
committed
Decode only fields observed by projections
1 parent de4495e commit 99f3365

8 files changed

Lines changed: 187 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66

77
### Fixed 🐛
88

9+
- **Inclusion projections no longer decode fields the query cannot observe.**
10+
SQLite still parses and filters the unchanged stored SJSON document, but its
11+
iterator now recursively decodes only projected top-level fields plus fields
12+
required by filters and sorts. Small projections such as `_id` no longer
13+
decode large member arrays from every matching document during collection
14+
scans. Exclusion projections retain full decoding. Unit tests cover nested
15+
query-field collection, missing fields, stored field order and skipped nested
16+
values by @xet7. Thanks to xet7.
17+
918
- **Tailable OpLog cursors wake on writes instead of polling SQLite.** The
1019
OpLog decorator now broadcasts each successful append to every `awaitData`
1120
waiter. Cursors retain that notification generation and wait before their

internal/backends/collection.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ type QueryParams struct {
7878
Filter *types.Document
7979
Sort *types.Document
8080
Limit int64
81+
// DecodeFields lets document backends avoid recursively decoding fields that
82+
// the handler's inclusion projection, filter and sort cannot observe. Other
83+
// backends may ignore it and return complete documents.
84+
DecodeFields []string
8185

8286
OnlyRecordIDs bool
8387
Comment string

internal/backends/sqlite/collection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (c *collection) Query(ctx context.Context, params *backends.QueryParams) (*
9494
return nil, lazyerrors.Error(err)
9595
}
9696

97-
iter := newQueryIterator(ctx, rows, params.OnlyRecordIDs)
97+
iter := newQueryIterator(ctx, rows, params.OnlyRecordIDs, params.DecodeFields)
9898
if os.Getenv("DEBUGSPEED") == "true" {
9999
iter = newSpeedQueryIterator(iter, &querySpeed{
100100
logger: c.r.Logger(),

internal/backends/sqlite/query_iterator.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type queryIterator struct {
4141
token *resource.Token
4242
m sync.Mutex
4343
onlyRecordIDs bool
44+
decodeFields []string
4445
speed *querySpeed
4546
}
4647

@@ -80,13 +81,16 @@ func newSpeedQueryIterator(iter *queryIterator, speed *querySpeed) *queryIterato
8081
//
8182
// Nil rows are possible and return already done iterator.
8283
// It still should be Close'd.
83-
func newQueryIterator(ctx context.Context, rows *fsql.Rows, onlyRecordIDs bool) *queryIterator {
84+
func newQueryIterator(ctx context.Context, rows *fsql.Rows, onlyRecordIDs bool, decodeFields ...[]string) *queryIterator {
8485
iter := &queryIterator{
8586
ctx: ctx,
8687
rows: rows,
8788
onlyRecordIDs: onlyRecordIDs,
8889
token: resource.NewToken(),
8990
}
91+
if len(decodeFields) != 0 {
92+
iter.decodeFields = decodeFields[0]
93+
}
9094
resource.Track(iter, iter.token)
9195

9296
return iter
@@ -154,7 +158,12 @@ func (iter *queryIterator) Next() (struct{}, *types.Document, error) {
154158

155159
if !iter.onlyRecordIDs {
156160
decodeStarted := time.Now()
157-
if doc, err = sjson.Unmarshal(b); err != nil {
161+
if len(iter.decodeFields) == 0 {
162+
doc, err = sjson.Unmarshal(b)
163+
} else {
164+
doc, err = sjson.UnmarshalFields(b, iter.decodeFields)
165+
}
166+
if err != nil {
158167
iter.close()
159168
return unused, nil, lazyerrors.Error(err)
160169
}

internal/handler/msg_find.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"log/slog"
2222
"math"
23+
"slices"
2324
"strings"
2425
"time"
2526

@@ -244,6 +245,18 @@ func (h *Handler) makeFindQueryParams(ctx context.Context, params *common.FindPa
244245
Comment: params.Comment,
245246
}
246247

248+
if _, inclusion, projectionErr := common.ValidateProjection(params.Projection); projectionErr == nil && inclusion {
249+
fields := make(map[string]struct{})
250+
collectDecodeFields(params.Projection, fields)
251+
collectDecodeFields(params.Filter, fields)
252+
collectDecodeFields(params.Sort, fields)
253+
qp.DecodeFields = make([]string, 0, len(fields))
254+
for field := range fields {
255+
qp.DecodeFields = append(qp.DecodeFields, field)
256+
}
257+
slices.Sort(qp.DecodeFields)
258+
}
259+
247260
var err error
248261
if params.Filter != nil {
249262
if qp.Comment, err = common.GetOptionalParam(params.Filter, "$comment", qp.Comment); err != nil {
@@ -316,6 +329,34 @@ func (h *Handler) makeFindQueryParams(ctx context.Context, params *common.FindPa
316329
return qp, nil
317330
}
318331

332+
// collectDecodeFields records top-level document fields observable by a query.
333+
// Operator documents are traversed; dotted paths require decoding their root.
334+
func collectDecodeFields(doc *types.Document, fields map[string]struct{}) {
335+
if doc == nil {
336+
return
337+
}
338+
for _, key := range doc.Keys() {
339+
value := must.NotFail(doc.Get(key))
340+
if strings.HasPrefix(key, "$") {
341+
switch value := value.(type) {
342+
case *types.Document:
343+
collectDecodeFields(value, fields)
344+
case *types.Array:
345+
for i := 0; i < value.Len(); i++ {
346+
if branch, ok := must.NotFail(value.Get(i)).(*types.Document); ok {
347+
collectDecodeFields(branch, fields)
348+
}
349+
}
350+
}
351+
continue
352+
}
353+
root, _, _ := strings.Cut(strings.TrimSuffix(key, ".$"), ".")
354+
if root != "" {
355+
fields[root] = struct{}{}
356+
}
357+
}
358+
}
359+
319360
// makeFindIter creates an iterator chain for the find command.
320361
//
321362
// Iter is passed from the backend's query.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2021 FerretDB Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package handler
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
22+
"github.com/FerretDB/FerretDB/internal/types"
23+
"github.com/FerretDB/FerretDB/internal/util/must"
24+
)
25+
26+
func TestCollectDecodeFields(t *testing.T) {
27+
t.Parallel()
28+
29+
fields := make(map[string]struct{})
30+
collectDecodeFields(must.NotFail(types.NewDocument(
31+
"_id", int64(1),
32+
"profile.name", int64(1),
33+
"services.resume.tokens.$", int64(1),
34+
"$and", must.NotFail(types.NewArray(
35+
must.NotFail(types.NewDocument("archived", false)),
36+
must.NotFail(types.NewDocument("members.userId", "u1")),
37+
)),
38+
)), fields)
39+
40+
assert.Equal(t, map[string]struct{}{
41+
"_id": {}, "profile": {}, "services": {}, "archived": {}, "members": {},
42+
}, fields)
43+
}

internal/handler/sjson/sjson.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,58 @@ func Unmarshal(data []byte) (*types.Document, error) {
237237
return d, nil
238238
}
239239

240+
// UnmarshalFields decodes only selected top-level fields. It still parses the
241+
// complete outer JSON object, but avoids recursively decoding unobservable
242+
// values and their often much larger schemas (for example member arrays when a
243+
// client requested only _id). Missing selected fields remain missing.
244+
func UnmarshalFields(data []byte, fields []string) (*types.Document, error) {
245+
var values map[string]json.RawMessage
246+
if err := json.Unmarshal(data, &values); err != nil {
247+
return nil, lazyerrors.Error(err)
248+
}
249+
250+
jsch, ok := values["$s"]
251+
if !ok {
252+
return nil, lazyerrors.Errorf("schema is not set")
253+
}
254+
var sch struct {
255+
Properties map[string]json.RawMessage `json:"p"`
256+
Keys []string `json:"$k"`
257+
}
258+
if err := json.Unmarshal(jsch, &sch); err != nil {
259+
return nil, lazyerrors.Error(err)
260+
}
261+
262+
wanted := make(map[string]struct{}, len(fields))
263+
for _, field := range fields {
264+
wanted[field] = struct{}{}
265+
}
266+
d := must.NotFail(types.NewDocument())
267+
for _, key := range sch.Keys {
268+
if _, ok := wanted[key]; !ok {
269+
continue
270+
}
271+
value, exists := values[key]
272+
if !exists {
273+
return nil, lazyerrors.Errorf("sjson.UnmarshalFields: missing key %q", key)
274+
}
275+
rawElem, exists := sch.Properties[key]
276+
if !exists {
277+
return nil, lazyerrors.Errorf("sjson.UnmarshalFields: missing schema for key %q", key)
278+
}
279+
var fieldSchema elem
280+
if err := json.Unmarshal(rawElem, &fieldSchema); err != nil {
281+
return nil, lazyerrors.Error(err)
282+
}
283+
decoded, err := unmarshalSingleValue(value, &fieldSchema)
284+
if err != nil {
285+
return nil, lazyerrors.Error(err)
286+
}
287+
d.Set(key, decoded)
288+
}
289+
return d, nil
290+
}
291+
240292
// unmarshalSingleValue decodes the given sjson-encoded data element by the given schema.
241293
func unmarshalSingleValue(data json.RawMessage, sch *elem) (any, error) {
242294
if bytes.Equal(data, []byte("null")) {

internal/handler/sjson/sjson_marshal_unmarshal_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ func TestMarshalUnmarshal(t *testing.T) {
6666
}
6767
}
6868

69+
func TestUnmarshalFields(t *testing.T) {
70+
t.Parallel()
71+
72+
nested := must.NotFail(types.NewDocument(
73+
"members", must.NotFail(types.NewArray(
74+
must.NotFail(types.NewDocument("userId", "u1", "active", true)),
75+
)),
76+
))
77+
doc := must.NotFail(types.NewDocument(
78+
"title", "large board",
79+
"_id", types.ObjectID{1, 2, 3},
80+
"archived", false,
81+
"nested", nested,
82+
))
83+
b, err := Marshal(doc)
84+
require.NoError(t, err)
85+
86+
actual, err := UnmarshalFields(b, []string{"archived", "_id", "missing"})
87+
require.NoError(t, err)
88+
assert.Equal(t, must.NotFail(types.NewDocument(
89+
"_id", types.ObjectID{1, 2, 3},
90+
"archived", false,
91+
)), actual, "selected fields retain stored order and missing fields remain missing")
92+
assert.False(t, actual.Has("nested"), "unobservable nested values must not be recursively decoded")
93+
}
94+
6995
// TestUnmarshalInvalid checks that in case of invalid data, we return errors and not just ignore issues.
7096
func TestUnmarshalInvalid(t *testing.T) {
7197
t.Parallel()

0 commit comments

Comments
 (0)