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

codenotary / immudb / 24841571249

23 Apr 2026 02:44PM UTC coverage: 85.275% (-4.0%) from 89.306%
24841571249

push

gh-ci

web-flow
feat: v1.11.0 PostgreSQL compatibility and SQL feature expansion (#2090)

* Add structured audit logging with immutable audit trail

Introduces a new --audit-log flag that records all gRPC operations as
structured JSON events in immudb's tamper-proof KV store. Events are
stored under the audit: key prefix in systemdb, queryable via Scan and
verifiable via VerifiableGet. An async buffered writer ensures minimal
latency impact. Configurable event filtering (all/write/admin) via
--audit-log-events flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add PostgreSQL ORM compatibility layer and verification functions

Extend the pgsql wire protocol with immudb verification functions
(immudb_state, immudb_verify_row, immudb_verify_tx, immudb_history,
immudb_tx) accessible via standard SQL SELECT statements.

Add pg_catalog resolvers (pg_attribute, pg_index, pg_constraint,
pg_type, pg_settings, pg_description) and information_schema
resolvers (tables, columns, schemata, key_column_usage) to support
ORM introspection from Django, SQLAlchemy, GORM, and ActiveRecord.

Add PostgreSQL compatibility functions: current_database,
current_schema, current_user, format_type, pg_encoding_to_char,
pg_get_expr, pg_get_constraintdef, obj_description, col_description,
has_table_privilege, has_schema_privilege, and others.

Add SHOW statement emulation for common ORM config queries and
schema-qualified name stripping for information_schema and public
schema references.

* Implement EXISTS and IN subquery support in SQL engine

Replace the previously stubbed ExistsBoolExp and InSubQueryExp
implementations with working non-correlated subquery execution.

EXISTS subqueries resolve the inner SELECT and check if any rows
are returned. IN subqueries resolve the inner SELECT, iterate the
result set, and compare each value against the outer expression.
Both support NOT variants (NOT EXISTS, NOT IN).

Correlated subqueries (referencing outer query columns) ar... (continued)

7254 of 10471 new or added lines in 124 files covered. (69.28%)

119 existing lines in 18 files now uncovered.

44597 of 52298 relevant lines covered (85.27%)

127591.66 hits per line

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

84.7
/embedded/sql/full_outer_join_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

17
package sql
18

19
import (
20
        "context"
21
        "fmt"
22

23
        "github.com/codenotary/immudb/embedded/multierr"
24
)
25

26
// fullOuterJoinRowReader materializes both sides and produces:
27
// 1. All left rows with matching right rows (or NULLs if no match)
28
// 2. Unmatched right rows with NULLs for left columns
29
type fullOuterJoinRowReader struct {
30
        leftReader  RowReader
31
        rightReader RowReader
32

33
        leftCols  []ColDescriptor
34
        rightCols []ColDescriptor
35

36
        cond     ValueExp
37
        tx       *SQLTx
38

39
        // materialized state
40
        loaded       bool
41
        resultRows   []*Row
42
        resultIdx    int
43

44
        onCloseCallback func()
45
}
46

47
func newFullOuterJoinRowReader(ctx context.Context, leftReader, rightReader RowReader, cond ValueExp) (*fullOuterJoinRowReader, error) {
7✔
48
        leftCols, err := leftReader.Columns(ctx)
7✔
49
        if err != nil {
7✔
NEW
50
                return nil, err
×
NEW
51
        }
×
52

53
        rightCols, err := rightReader.Columns(ctx)
7✔
54
        if err != nil {
7✔
NEW
55
                return nil, err
×
NEW
56
        }
×
57

58
        return &fullOuterJoinRowReader{
7✔
59
                leftReader:  leftReader,
7✔
60
                rightReader: rightReader,
7✔
61
                leftCols:    leftCols,
7✔
62
                rightCols:   rightCols,
7✔
63
                cond:        cond,
7✔
64
                tx:          leftReader.Tx(),
7✔
65
        }, nil
7✔
66
}
67

68
func (r *fullOuterJoinRowReader) materialize(ctx context.Context) error {
33✔
69
        if r.loaded {
61✔
70
                return nil
28✔
71
        }
28✔
72
        r.loaded = true
5✔
73

5✔
74
        maxRows := 0
5✔
75
        if r.tx != nil && r.tx.engine != nil {
10✔
76
                maxRows = r.tx.engine.maxWindowRows
5✔
77
        }
5✔
78

79
        // Read all left rows
80
        var leftRows []*Row
5✔
81
        for {
29✔
82
                row, err := r.leftReader.Read(ctx)
24✔
83
                if err == ErrNoMoreRows {
29✔
84
                        break
5✔
85
                }
86
                if err != nil {
19✔
NEW
87
                        return err
×
NEW
88
                }
×
89
                leftRows = append(leftRows, row)
19✔
90
        }
91

92
        // Read all right rows
93
        var rightRows []*Row
5✔
94
        for {
29✔
95
                row, err := r.rightReader.Read(ctx)
24✔
96
                if err == ErrNoMoreRows {
29✔
97
                        break
5✔
98
                }
99
                if err != nil {
19✔
NEW
100
                        return err
×
NEW
101
                }
×
102
                rightRows = append(rightRows, row)
19✔
103
        }
104

105
        totalRows := len(leftRows) + len(rightRows)
5✔
106
        if maxRows > 0 && totalRows > maxRows {
5✔
NEW
107
                return fmt.Errorf("%w: %d rows exceed limit of %d",
×
NEW
108
                        ErrWindowRowsLimitExceeded, totalRows, maxRows)
×
NEW
109
        }
×
110

111
        rightMatched := make([]bool, len(rightRows))
5✔
112
        tableAlias := r.leftReader.TableAlias()
5✔
113

5✔
114
        // Phase 1: For each left row, find matching right rows
5✔
115
        for _, lRow := range leftRows {
24✔
116
                matched := false
19✔
117

19✔
118
                for ri, rRow := range rightRows {
140✔
119
                        combined := combineRows(lRow, rRow)
121✔
120

121✔
121
                        condVal, err := r.cond.reduce(r.tx, combined, tableAlias)
121✔
122
                        if err != nil {
121✔
NEW
123
                                continue
×
124
                        }
125

126
                        boolVal, ok := condVal.RawValue().(bool)
121✔
127
                        if !ok || !boolVal {
232✔
128
                                continue
111✔
129
                        }
130

131
                        // Match found
132
                        r.resultRows = append(r.resultRows, combined)
10✔
133
                        rightMatched[ri] = true
10✔
134
                        matched = true
10✔
135
                }
136

137
                if !matched {
28✔
138
                        // Left row with NULLs for right columns
9✔
139
                        r.resultRows = append(r.resultRows, combineRowWithNulls(lRow, r.rightCols))
9✔
140
                }
9✔
141
        }
142

143
        // Phase 2: Unmatched right rows with NULLs for left columns
144
        for ri, rRow := range rightRows {
24✔
145
                if !rightMatched[ri] {
28✔
146
                        r.resultRows = append(r.resultRows, combineNullsWithRow(r.leftCols, rRow))
9✔
147
                }
9✔
148
        }
149

150
        return nil
5✔
151
}
152

153
func combineRows(left, right *Row) *Row {
121✔
154
        row := &Row{
121✔
155
                ValuesByPosition: make([]TypedValue, 0, len(left.ValuesByPosition)+len(right.ValuesByPosition)),
121✔
156
                ValuesBySelector: make(map[string]TypedValue, len(left.ValuesBySelector)+len(right.ValuesBySelector)),
121✔
157
        }
121✔
158

121✔
159
        row.ValuesByPosition = append(row.ValuesByPosition, left.ValuesByPosition...)
121✔
160
        row.ValuesByPosition = append(row.ValuesByPosition, right.ValuesByPosition...)
121✔
161

121✔
162
        for k, v := range left.ValuesBySelector {
251✔
163
                row.ValuesBySelector[k] = v
130✔
164
        }
130✔
165
        for k, v := range right.ValuesBySelector {
363✔
166
                row.ValuesBySelector[k] = v
242✔
167
        }
242✔
168

169
        return row
121✔
170
}
171

172
func combineRowWithNulls(left *Row, rightCols []ColDescriptor) *Row {
9✔
173
        row := &Row{
9✔
174
                ValuesByPosition: make([]TypedValue, 0, len(left.ValuesByPosition)+len(rightCols)),
9✔
175
                ValuesBySelector: make(map[string]TypedValue, len(left.ValuesBySelector)+len(rightCols)),
9✔
176
        }
9✔
177

9✔
178
        row.ValuesByPosition = append(row.ValuesByPosition, left.ValuesByPosition...)
9✔
179
        for k, v := range left.ValuesBySelector {
19✔
180
                row.ValuesBySelector[k] = v
10✔
181
        }
10✔
182

183
        for _, col := range rightCols {
27✔
184
                nv := NewNull(col.Type)
18✔
185
                row.ValuesByPosition = append(row.ValuesByPosition, nv)
18✔
186
                row.ValuesBySelector[col.Selector()] = nv
18✔
187
        }
18✔
188

189
        return row
9✔
190
}
191

192
func combineNullsWithRow(leftCols []ColDescriptor, right *Row) *Row {
9✔
193
        row := &Row{
9✔
194
                ValuesByPosition: make([]TypedValue, 0, len(leftCols)+len(right.ValuesByPosition)),
9✔
195
                ValuesBySelector: make(map[string]TypedValue, len(leftCols)+len(right.ValuesBySelector)),
9✔
196
        }
9✔
197

9✔
198
        for _, col := range leftCols {
27✔
199
                nv := NewNull(col.Type)
18✔
200
                row.ValuesByPosition = append(row.ValuesByPosition, nv)
18✔
201
                row.ValuesBySelector[col.Selector()] = nv
18✔
202
        }
18✔
203

204
        row.ValuesByPosition = append(row.ValuesByPosition, right.ValuesByPosition...)
9✔
205
        for k, v := range right.ValuesBySelector {
27✔
206
                row.ValuesBySelector[k] = v
18✔
207
        }
18✔
208

209
        return row
9✔
210
}
211

212
func (r *fullOuterJoinRowReader) onClose(callback func()) {
6✔
213
        r.onCloseCallback = callback
6✔
214
}
6✔
215

216
func (r *fullOuterJoinRowReader) Tx() *SQLTx {
64✔
217
        return r.tx
64✔
218
}
64✔
219

220
func (r *fullOuterJoinRowReader) TableAlias() string {
224✔
221
        return r.leftReader.TableAlias()
224✔
222
}
224✔
223

NEW
224
func (r *fullOuterJoinRowReader) OrderBy() []ColDescriptor {
×
NEW
225
        return nil
×
NEW
226
}
×
227

NEW
228
func (r *fullOuterJoinRowReader) ScanSpecs() *ScanSpecs {
×
NEW
229
        return nil
×
NEW
230
}
×
231

NEW
232
func (r *fullOuterJoinRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) {
×
NEW
233
        cols := make([]ColDescriptor, 0, len(r.leftCols)+len(r.rightCols))
×
NEW
234
        cols = append(cols, r.leftCols...)
×
NEW
235
        cols = append(cols, r.rightCols...)
×
NEW
236
        return cols, nil
×
NEW
237
}
×
238

239
func (r *fullOuterJoinRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) {
4✔
240
        result := make(map[string]ColDescriptor, len(r.leftCols)+len(r.rightCols))
4✔
241
        for _, c := range r.leftCols {
12✔
242
                result[c.Selector()] = c
8✔
243
        }
8✔
244
        for _, c := range r.rightCols {
12✔
245
                result[c.Selector()] = c
8✔
246
        }
8✔
247
        return result, nil
4✔
248
}
249

250
func (r *fullOuterJoinRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error {
1✔
251
        if err := r.leftReader.InferParameters(ctx, params); err != nil {
1✔
NEW
252
                return err
×
NEW
253
        }
×
254
        return r.rightReader.InferParameters(ctx, params)
1✔
255
}
256

257
func (r *fullOuterJoinRowReader) Parameters() map[string]interface{} {
64✔
258
        return r.leftReader.Parameters()
64✔
259
}
64✔
260

261
func (r *fullOuterJoinRowReader) Read(ctx context.Context) (*Row, error) {
33✔
262
        if err := r.materialize(ctx); err != nil {
33✔
NEW
263
                return nil, err
×
NEW
264
        }
×
265

266
        if r.resultIdx >= len(r.resultRows) {
38✔
267
                return nil, ErrNoMoreRows
5✔
268
        }
5✔
269

270
        row := r.resultRows[r.resultIdx]
28✔
271
        r.resultIdx++
28✔
272
        return row, nil
28✔
273
}
274

275
func (r *fullOuterJoinRowReader) Close() error {
7✔
276
        merr := multierr.NewMultiErr()
7✔
277
        merr.Append(r.leftReader.Close())
7✔
278
        merr.Append(r.rightReader.Close())
7✔
279

7✔
280
        if r.onCloseCallback != nil {
13✔
281
                r.onCloseCallback()
6✔
282
        }
6✔
283

284
        return merr.Reduce()
7✔
285
}
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