• 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

90.91
/pkg/pgsql/server/bmessages/data_row.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 bmessages
18

19
import (
20
        "bytes"
21
        "encoding/binary"
22
        "encoding/hex"
23
        "strings"
24

25
        "github.com/codenotary/immudb/embedded/sql"
26
)
27

28
// DataRow if ResultColumnFormatCodes is nil default text format is used
29
func DataRow(rows []*sql.Row, colNumb int, ResultColumnFormatCodes []int16) []byte {
196✔
30
        rowsB := make([]byte, 0)
196✔
31
        for _, row := range rows {
498✔
32
                rowB := make([]byte, 0)
302✔
33
                // Identifies the message as a data row.
302✔
34
                // Byte1('D')
302✔
35
                messageType := []byte(`D`)
302✔
36

302✔
37
                // The number of column values that follow (possibly zero).
302✔
38
                // Int16
302✔
39
                columnNumb := make([]byte, 2)
302✔
40
                binary.BigEndian.PutUint16(columnNumb, uint16(colNumb))
302✔
41

302✔
42
                for i, val := range row.ValuesByPosition {
805✔
43
                        if val == nil {
504✔
44
                                return nil
1✔
45
                        }
1✔
46

47
                        valueLength := make([]byte, 4)
502✔
48
                        value := make([]byte, 0)
502✔
49

502✔
50
                        BINformat := false
502✔
51
                        if len(ResultColumnFormatCodes) == 1 {
633✔
52
                                BINformat = ResultColumnFormatCodes[0] == 1
131✔
53
                        }
131✔
54
                        if ResultColumnFormatCodes != nil && len(ResultColumnFormatCodes) > i && ResultColumnFormatCodes[i] == 1 {
695✔
55
                                BINformat = true
193✔
56
                        }
193✔
57
                        if BINformat {
696✔
58
                                if val.IsNull() {
197✔
59
                                        n := -1
3✔
60
                                        binary.BigEndian.PutUint32(valueLength, uint32(n))
3✔
61
                                } else {
194✔
62
                                        rv := val.RawValue()
191✔
63
                                        switch val.Type() {
191✔
64
                                        case sql.IntegerType:
158✔
65
                                                {
316✔
66
                                                        binary.BigEndian.PutUint32(valueLength, uint32(8))
158✔
67
                                                        value = make([]byte, 8)
158✔
68
                                                        binary.BigEndian.PutUint64(value, uint64(rv.(int64)))
158✔
69
                                                }
158✔
70
                                        case sql.JSONType:
×
71
                                                {
×
72
                                                        jsonStr := trimQuotes(val.String())
×
73
                                                        binary.BigEndian.PutUint32(valueLength, uint32(len(jsonStr)))
×
74
                                                        value = []byte(jsonStr)
×
75
                                                }
×
76
                                        case sql.VarcharType:
2✔
77
                                                {
4✔
78
                                                        s := rv.(string)
2✔
79
                                                        binary.BigEndian.PutUint32(valueLength, uint32(len(s)))
2✔
80
                                                        value = []byte(s)
2✔
81
                                                }
2✔
82
                                        case sql.BooleanType:
18✔
83
                                                {
36✔
84
                                                        binary.BigEndian.PutUint32(valueLength, uint32(1))
18✔
85
                                                        value = []byte{0}
18✔
86
                                                        if rv.(bool) {
28✔
87
                                                                value = []byte{1}
10✔
88
                                                        }
10✔
89
                                                }
90
                                        case sql.BLOBType:
4✔
91
                                                {
8✔
92
                                                        blob := rv.([]byte)
4✔
93
                                                        binary.BigEndian.PutUint32(valueLength, uint32(len(blob)))
4✔
94
                                                        value = blob
4✔
95
                                                }
4✔
96
                                        }
97
                                }
98
                        } else {
308✔
99
                                // only text format is allowed in simple query
308✔
100
                                value = renderValueAsByte(val)
308✔
101
                        }
308✔
102
                        binary.BigEndian.PutUint32(valueLength, uint32(len(value)))
502✔
103
                        //  As a special case, -1 indicates a NULL column value. No value bytes follow in the NULL case.
502✔
104
                        if value == nil {
512✔
105
                                tm := int32(-1)
10✔
106
                                value = nil
10✔
107
                                binary.BigEndian.PutUint32(valueLength, uint32(tm))
10✔
108
                        }
10✔
109
                        rowB = append(rowB, bytes.Join([][]byte{valueLength, value}, nil)...)
502✔
110
                }
111

112
                // Length of message contents in bytes, including self.
113
                // Int32
114
                selfMessageLength := make([]byte, 4)
301✔
115
                binary.BigEndian.PutUint32(selfMessageLength, uint32(4+2+len(rowB)))
301✔
116
                rowsB = append(rowsB, bytes.Join([][]byte{messageType, selfMessageLength, columnNumb, rowB}, nil)...)
301✔
117
        }
118
        return rowsB
195✔
119
}
120

121
func renderValueAsByte(v sql.TypedValue) []byte {
314✔
122
        if v.IsNull() {
326✔
123
                return nil
12✔
124
        }
12✔
125

126
        var s string
302✔
127
        switch v.Type() {
302✔
128
        case sql.VarcharType:
245✔
129
                s, _ = v.RawValue().(string)
245✔
130
        case sql.JSONType:
×
131
                s = trimQuotes(v.String())
×
132
        case sql.BLOBType:
3✔
133
                // Postgres canonical text format for bytea is `\x<hex>` since PG 9.0.
3✔
134
                // lib/pq / pgx parse this: with the `\x` prefix they hex-decode the
3✔
135
                // rest; without it they fall back to escape format and treat the
3✔
136
                // string bytes as-is — which breaks every Gitea/Rails/Django consumer
3✔
137
                // that stores binary blobs (YAML workflow payloads, push-commit
3✔
138
                // hashes, session data) because they get raw hex characters when
3✔
139
                // they expect the decoded bytes.
3✔
140
                if raw, ok := v.RawValue().([]byte); ok {
6✔
141
                        s = `\x` + hex.EncodeToString(raw)
3✔
142
                } else {
3✔
NEW
143
                        s = `\x` + v.String()
×
NEW
144
                }
×
145
        default:
54✔
146
                s = v.String()
54✔
147
        }
148
        return []byte(s)
302✔
149
}
150

151
func trimQuotes(s string) string {
4✔
152
        return strings.TrimSuffix(strings.TrimPrefix(s, "'"), "'")
4✔
153
}
4✔
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