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

codenotary / immudb / 24841644892

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

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%)

115 existing lines in 18 files now uncovered.

44599 of 52298 relevant lines covered (85.28%)

127676.6 hits per line

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

69.86
/embedded/sql/hash_grouped_row_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 "context"
20

21
// hashGroupedRowReader implements GROUP BY aggregation using a hash map
22
// rather than requiring sorted input.  It reads all inner rows in a single
23
// pass, accumulates aggregations per group, then streams results in insertion
24
// order.
25
//
26
// Used when the scan index does not provide GROUP BY order (i.e.
27
// scanSpecs.groupBySortExps is non-empty), replacing the sort+groupedRowReader
28
// pipeline with a single hash-aggregate phase.
29
type hashGroupedRowReader struct {
30
        // Embeds groupedRowReader to reuse column-descriptor setup,
31
        // aggregation init/update helpers, and all RowReader interface methods.
32
        // Read() is overridden below and takes precedence over the promoted one.
33
        *groupedRowReader
34

35
        groups  []*Row // aggregated groups, in insertion order
36
        nextIdx int
37
        built   bool
38
}
39

40
func newHashGroupedRowReader(
41
        rowReader RowReader,
42
        allAggregations bool,
43
        selectors []*AggColSelector,
44
        groupBy []*ColSelector,
45
) (*hashGroupedRowReader, error) {
19✔
46
        gr, err := newGroupedRowReader(rowReader, allAggregations, selectors, groupBy)
19✔
47
        if err != nil {
19✔
NEW
48
                return nil, err
×
NEW
49
        }
×
50
        return &hashGroupedRowReader{groupedRowReader: gr}, nil
19✔
51
}
52

53
// groupKey encodes the GROUP BY column values from row into a byte string
54
// used as the hash-map key.  NULL values are distinguished from non-NULL by a
55
// leading 0/1 byte; variable-length types carry a length prefix inside
56
// EncodeValue, making the concatenation collision-free.
57
func (h *hashGroupedRowReader) groupKey(row *Row) (string, error) {
360✔
58
        tableAlias := h.rowReader.TableAlias()
360✔
59
        var key []byte
360✔
60
        for _, col := range h.groupByCols {
845✔
61
                encSel := EncodeSelector(col.resolve(tableAlias))
485✔
62
                val := row.ValuesBySelector[encSel]
485✔
63
                if val == nil || val.IsNull() {
485✔
NEW
64
                        key = append(key, 0) // NULL sentinel
×
NEW
65
                        continue
×
66
                }
67
                key = append(key, 1) // non-NULL
485✔
68
                b, err := EncodeValue(val, val.Type(), 0)
485✔
69
                if err != nil {
485✔
NEW
70
                        return "", err
×
NEW
71
                }
×
72
                key = append(key, b...)
485✔
73
        }
74
        return string(key), nil
360✔
75
}
76

77
// buildGroups performs one full scan of the inner reader and populates h.groups.
78
func (h *hashGroupedRowReader) buildGroups(ctx context.Context) error {
10✔
79
        order := make([]string, 0)
10✔
80
        groups := make(map[string]*Row)
10✔
81

10✔
82
        for {
380✔
83
                row, err := h.rowReader.Read(ctx)
370✔
84
                if err == ErrNoMoreRows {
380✔
85
                        break
10✔
86
                }
87
                if err != nil {
360✔
NEW
88
                        return err
×
NEW
89
                }
×
90

91
                key, err := h.groupKey(row)
360✔
92
                if err != nil {
360✔
NEW
93
                        return err
×
NEW
94
                }
×
95

96
                if existing, ok := groups[key]; !ok {
512✔
97
                        // First row for this group — initialise aggregation state.
152✔
98
                        if err := h.initAggregations(row); err != nil {
152✔
NEW
99
                                return err
×
NEW
100
                        }
×
101
                        groups[key] = row
152✔
102
                        order = append(order, key)
152✔
103
                } else {
208✔
104
                        // Subsequent row — update aggregation accumulators.
208✔
105
                        if err := updateRow(existing, row); err != nil {
208✔
NEW
106
                                return err
×
NEW
107
                        }
×
108
                }
109
        }
110

111
        // When the inner scan is empty and the query is a pure aggregation
112
        // (no GROUP BY), return a single zero-value row — e.g. COUNT(*) → 0.
113
        if len(groups) == 0 && h.allAggregations && len(h.groupByCols) == 0 {
10✔
NEW
114
                zr, err := h.zeroRow(ctx)
×
NEW
115
                if err != nil {
×
NEW
116
                        return err
×
NEW
117
                }
×
NEW
118
                h.groups = []*Row{zr}
×
NEW
119
                return nil
×
120
        }
121

122
        h.groups = make([]*Row, len(order))
10✔
123
        for i, key := range order {
162✔
124
                h.groups[i] = groups[key]
152✔
125
        }
152✔
126
        return nil
10✔
127
}
128

129
// Read overrides groupedRowReader.Read.  On the first call it builds the
130
// complete hash-aggregate result; subsequent calls stream the groups.
131
func (h *hashGroupedRowReader) Read(ctx context.Context) (*Row, error) {
162✔
132
        if !h.built {
172✔
133
                if err := h.buildGroups(ctx); err != nil {
10✔
NEW
134
                        return nil, err
×
NEW
135
                }
×
136
                h.built = true
10✔
137
        }
138
        if h.nextIdx >= len(h.groups) {
172✔
139
                return nil, ErrNoMoreRows
10✔
140
        }
10✔
141
        row := h.groups[h.nextIdx]
152✔
142
        h.nextIdx++
152✔
143
        return row, nil
152✔
144
}
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