• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codenotary / immudb / 29190098042

12 Jul 2026 10:59AM UTC coverage: 84.968% (-0.005%) from 84.973%
29190098042

Pull #2110

gh-ci

vchaindz
chore(sql): treat nil row-value slots as SQL NULL in remaining consumers

The projection-pushdown optimization leaves Row.ValuesByPosition[i]
nil for columns a query does not reference. A repo-wide audit after
the fileSorter.encodeRow panic (#2100) found five more consumers that
dereference every positional slot and stay safe only because
projection currently repopulates rows before they run:

- CTE materialization (materializeCTE/materializeRecursiveCTE)
  type-asserted v.(ValueExp) on each slot: hard panic on a nil
  interface. Extracted rowValuesToValueExps, which substitutes a
  typed SQL NULL.
- Row.digest (DISTINCT, diff) called v.Type() on every slot; nil now
  digests the same as NULL.
- setOpRowReader.rowDigest (EXCEPT/INTERSECT) called v.IsNull(); nil
  now digests the same as NULL.
- sqlRowsToProto (gRPC row serialization) dereferenced every slot;
  nil now serializes as SQL NULL.
- embedded/document readers dereferenced positions 0/1 unchecked; nil
  or missing slots now return ErrUnexpectedValue.

None of these is reachable today: every external Resolve passes nil
scanSpecs and SelectStmt.Resolve always projects before returning.
The guards remove the reliance on the "projection always runs first"
invariant, which has already broken once. Unit tests encode the
nil-slot-equals-NULL contract.

The same audit confirmed the empty-statement-slice class from #2100 is
fully closed: every ParseSQL/ParseSQLString call site is now length-
guarded, range-based, or parses hardcoded non-empty input.
Pull Request #2110: fix(pgsql): guard extended-query Parse against zero parsed statements + nil row-value-slot hardening

41 of 62 new or added lines in 7 files covered. (66.13%)

2 existing lines in 1 file now uncovered.

45258 of 53265 relevant lines covered (84.97%)

126001.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.72
/embedded/document/document_reader.go
1
/*
2
Copyright 2026 Codenotary Inc. All rights reserved.
3

4
SPDX-License-Identifier: BUSL-1.1
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    https://mariadb.com/bsl11/
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16
package document
17

18
import (
19
        "context"
20
        "errors"
21

22
        "github.com/codenotary/immudb/embedded/sql"
23
        "github.com/codenotary/immudb/pkg/api/protomodel"
24
        "google.golang.org/protobuf/proto"
25

26
        "google.golang.org/protobuf/types/known/structpb"
27
)
28

29
type DocumentReader interface {
30
        // Read reads a single message from a reader and returns it as a Struct message.
31
        Read(ctx context.Context) (*protomodel.DocumentAtRevision, error)
32
        // ReadN reads n number of messages from a reader and returns them as a slice of Struct messages.
33
        ReadN(ctx context.Context, count int) ([]*protomodel.DocumentAtRevision, error)
34
        Close() error
35
}
36

37
type documentReader struct {
38
        rowReader       sql.RowReader
39
        onCloseCallback func(reader DocumentReader)
40
}
41

42
func newDocumentReader(rowReader sql.RowReader, onCloseCallback func(reader DocumentReader)) DocumentReader {
31✔
43
        return &documentReader{
31✔
44
                rowReader:       rowReader,
31✔
45
                onCloseCallback: onCloseCallback,
31✔
46
        }
31✔
47
}
31✔
48

49
// ReadN reads n number of messages from a reader and returns them as a slice of Struct messages.
50
func (r *documentReader) ReadN(ctx context.Context, count int) ([]*protomodel.DocumentAtRevision, error) {
36✔
51
        if count < 1 {
37✔
52
                return nil, sql.ErrIllegalArguments
1✔
53
        }
1✔
54

55
        revisions := make([]*protomodel.DocumentAtRevision, 0)
35✔
56

35✔
57
        var err error
35✔
58

35✔
59
        for l := 0; l < count; l++ {
241✔
60
                var row *sql.Row
206✔
61
                row, err = r.rowReader.Read(ctx)
206✔
62
                if errors.Is(err, sql.ErrNoMoreRows) {
220✔
63
                        err = ErrNoMoreDocuments
14✔
64
                        break
14✔
65
                }
66
                if err != nil {
192✔
67
                        return nil, mayTranslateError(err)
×
68
                }
×
69

70
                docIDBytes, docBytes, err := docRowBytes(row)
192✔
71
                if err != nil {
192✔
NEW
72
                        return nil, err
×
NEW
73
                }
×
74

75
                doc := &structpb.Struct{}
192✔
76
                err = proto.Unmarshal(docBytes, doc)
192✔
77
                if err != nil {
192✔
78
                        return nil, err
×
79
                }
×
80

81
                revisions = append(revisions, &protomodel.DocumentAtRevision{
192✔
82
                        TransactionId: 0, // TODO: not yet available via SQL row reader
192✔
83
                        Revision:      0, // TODO: not yet available via SQL row reader
192✔
84
                        DocumentId:    DocumentID(docIDBytes).EncodeToHexString(),
192✔
85
                        Document:      doc,
192✔
86
                })
192✔
87
        }
88

89
        return revisions, err
35✔
90
}
91

92
// docRowBytes extracts the raw document-id and encoded-document bytes from
93
// a SQL row. Slots may be nil when an upstream reader skipped decoding
94
// unneeded columns (projection pushdown), so guard before dereferencing.
95
func docRowBytes(row *sql.Row) (docIDBytes, docBytes []byte, err error) {
197✔
96
        if len(row.ValuesByPosition) < 2 || row.ValuesByPosition[0] == nil || row.ValuesByPosition[1] == nil {
197✔
NEW
97
                return nil, nil, ErrUnexpectedValue
×
NEW
98
        }
×
99

100
        docIDBytes, ok := row.ValuesByPosition[0].RawValue().([]byte)
197✔
101
        if !ok {
197✔
NEW
102
                return nil, nil, ErrUnexpectedValue
×
NEW
103
        }
×
104

105
        docBytes, ok = row.ValuesByPosition[1].RawValue().([]byte)
197✔
106
        if !ok {
197✔
NEW
107
                return nil, nil, ErrUnexpectedValue
×
NEW
108
        }
×
109

110
        return docIDBytes, docBytes, nil
197✔
111
}
112

113
func (r *documentReader) Close() error {
31✔
114
        if r.onCloseCallback != nil {
62✔
115
                defer r.onCloseCallback(r)
31✔
116
        }
31✔
117

118
        return r.rowReader.Close()
31✔
119
}
120

121
// Read reads a single message from a reader and returns it as a Struct message.
122
func (r *documentReader) Read(ctx context.Context) (*protomodel.DocumentAtRevision, error) {
8✔
123
        var row *sql.Row
8✔
124
        row, err := r.rowReader.Read(ctx)
8✔
125
        if errors.Is(err, sql.ErrNoMoreRows) {
11✔
126
                err = ErrNoMoreDocuments
3✔
127
        }
3✔
128
        if err != nil {
11✔
129
                return nil, mayTranslateError(err)
3✔
130
        }
3✔
131

132
        docIDBytes, docBytes, err := docRowBytes(row)
5✔
133
        if err != nil {
5✔
NEW
134
                return nil, err
×
NEW
135
        }
×
136

137
        doc := &structpb.Struct{}
5✔
138
        err = proto.Unmarshal(docBytes, doc)
5✔
139
        if err != nil {
5✔
140
                return nil, err
×
141
        }
×
142

143
        revision := &protomodel.DocumentAtRevision{
5✔
144
                TransactionId: 0, // TODO: not yet available via SQL row reader
5✔
145
                Revision:      0, // TODO: not yet available via SQL row reader
5✔
146
                DocumentId:    DocumentID(docIDBytes).EncodeToHexString(),
5✔
147
                Document:      doc,
5✔
148
        }
5✔
149

5✔
150
        return revision, err
5✔
151
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc