debezium / debezium/dbz

Vitess connector emits NULL for BLOB/TEXT columns omitted from NOBLOB row images (ignores RowChange.data_columns)

Open
#2,607 1 comment 0 reactions 1 assignee Claimed by @mcrauwel View on GitHub
component/vitess-connector type/bug
Dominant language
HTML
Stars
6
Forks
8
Avg merge
2d 19h
Merged PRs (30d)
1

Description

## Bug report

**What Debezium connector do you use and what version?**

Debezium Vitess connector. Observed on 3.2.x in production; the relevant code is unchanged on `main` (3.7.0-SNAPSHOT, `VStreamOutputMessageDecoder.resolveColumns`), so all released versions are affected.

---

**What is the connector configuration?**

Nothing special is needed to trigger it; any Vitess connector configuration streaming from a keyspace whose MySQL runs with `binlog_row_image=NOBLOB`, e.g.

```
connector.class=io.debezium.connector.vitess.VitessConnector
vitess.vtgate.host=...
vitess.vtgate.port=15991
vitess.keyspace=commerce
vitess.tablet.type=REPLICA
topic.prefix=vitess
table.include.list=commerce.t
```

---

**What is the captured database version and mode of deployment?**

Vitess v22 (vtgate/vttablet), MySQL 8.4.6, `binlog_row_image=NOBLOB`, vttablet with the default `--vreplication_experimental_flags=7` (bit 2 = `AllowNoBlobBinlogRowImage`, which makes the vstreamer tolerate partial row images instead of erroring). Hosted (PlanetScale), but the behavior is not deployment specific.

---

**What behavior do you expect?**

The same behavior the MySQL/MariaDB connectors have since debezium/dbz#473: when a BLOB/TEXT column is omitted from a NOBLOB row image, the connector emits the configured `unavailable.value.placeholder` (default `__debezium_unavailable_value`) for that column, so downstream consumers can tell "unchanged, not provided" from an actual `NULL`.

Vitess gives the connector what it needs for the AFTER image: `binlogdata.RowChange.data_columns` is a bitmap with a bit set for every column present in the after image (available in `vitess-grpc-client` since Vitess v17; the currently used 23.0.0-rc1 exposes `hasDataColumns()` / `getDataColumns()`).

---

**What behavior do you see?**

The connector never reads `RowChange.data_columns`. `VStreamOutputMessageDecoder.resolveColumns` maps `Row.lengths` positionally and treats length `-1` as `null`. The vstreamer serializes an omitted column as a NULL cell (length `-1`) and signals the omission only via the bitmap, so every unchanged BLOB/TEXT column in an UPDATE reaches the sink as `null` in `after`, indistinguishable from a real NULL. A sink that upserts full rows silently nulls those columns out.

Example with `create table t (id bigint primary key, name varchar(64), body text)` and `update t set name = 'b' where id = 1`:

- vstreamer sends `after = {1, 'b', NULL}` with `data_columns` bits `{id, name}` set and `body` clear
- connector emits `after.body = null`

---

**Do you see the same behaviour using the latest released Debezium version?**

Yes. Verified against the code on `main` (3.7.0-SNAPSHOT, commit 610950b): no reference to `data_columns` / `DataColumns` anywhere in `debezium-connector-vitess`.

---

**Do you have the connector logs, ideally from start till finish?**

There is nothing in the logs: the connector does not error, it just emits `null`. Happy to provide TRACE logs if useful, but they only show the row events with the NULL cells.

---

**How to reproduce the issue using our [tutorial](https://github.com/debezium/debezium-examples/tree/main/tutorial) deployment?**

The tutorial has no Vitess variant; with the Vitess local example (`examples/local`) plus the connector:

1. Set `binlog_row_image=NOBLOB` in the MySQL config used by the Vitess local example; keep vttablet's default `--vreplication_experimental_flags` (7).
2. `create table t (id bigint not null, name varchar(64), body text, primary key (id)); insert into t values (1, 'a', 'some long text');`
3. Start the Vitess connector on that keyspace.
4. `update t set name = 'b' where id = 1;`
5. The change event has `after.body = null` although the column was not changed and `RowChange.data_columns` marks it as not present. Expected: `after.body = "__debezium_unavailable_value"`.

---

**Implementation notes**

- In `resolveColumns`, take the `RowChange` (not only the `Row`); when `rowChange.hasDataColumns()` and bit `i` is clear, emit the unavailable-value placeholder instead of `null`. `VitessConnectorConfig` already extends `RelationalDatabaseConnectorConfig`, so `unavailable.value.placeholder` / `getUnavailableValuePlaceholder()` are available without new configuration. Scoping the substitution to BLOB/TEXT-typed columns mirrors `RowImageUtils.isBlobOrTextColumn` in the binlog connector (MySQL never omits JSON under NOBLOB).
- Known upstream limitation: VStream currently carries the bitmap only for the AFTER image. The BEFORE image (UPDATE `before`, and DELETE events) has no bitmap even though NOBLOB drops all non-PK BLOB/TEXT columns from it. vitessio/vitess#21065 tracks adding a `before_data_columns` bitmap; once that lands the connector can align `before` the same way the binlog connector does with the before-image included-columns bitmap.
- I can work on a PR for this.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.