The following bugs are reproduced on CI tests in a Github repo here and a 2nd Github repo.
It affects at least Dolt version 2.1.7.
See @zachmu and @nicktobey
Bug 1: JSON column silently stores NULL for large values
When inserting JSON values > ~4004 bytes containing certain control characters into
a JSON column, Dolt silently stores NULL instead of the value. No error is
raised. The same value stored in a LONGTEXT column round-trips correctly
byte-for-byte.
Trigger conditions
The bug requires both conditions:
- JSON input text > ~4004 bytes
- Value contains control characters that
JSON.stringify serializes as \uXXXX (6-byte escape sequences)
These control characters trigger the bug (no short escape → \uXXXX):
- U+0000–U+0007 (NUL through BEL)
- U+000B (VT, vertical tab)
- U+000E–U+001F
These do not trigger (short escape or literal):
- U+0008 (BS) →
\b (2 bytes)
- U+0009 (TAB) →
\t (2 bytes)
- U+000A (LF) →
\n (2 bytes)
- U+000C (FF) →
\f (2 bytes)
- U+000D (CR) →
\r (2 bytes)
- U+007F (DEL) → literal (1 byte)
- Normal ASCII / UTF-8 → literal (1 byte each)
Minimum reproduction
{"d":"\u000b\u000b\u000b...666 times...\u000b"}
Total JSON input = 4004 bytes. A single \u000b in a 50KB string also triggers.
But 100KB of plain x with no escapes works fine.
Hypothesis
Dolt's JSON serializer allocates an output buffer proportional to the input
string length. Control characters needing \uXXXX escaping expand from 1 byte
(internal) to 6 bytes (JSON output). The serializer writes past the end of the
allocated buffer, corrupting internal state and causing a silent NULL. Characters
with shorter escapes or no expansion fit within the original buffer.
Bug 2: JSON_VALID returns 0 for LONGTEXT values > 2040 bytes
JSON_VALID() returns 0 for valid JSON stored in LONGTEXT columns when the
value exceeds 2040 bytes and the table has no PRIMARY KEY. The same value
passes JSON_VALID() as a query parameter literal, and parses correctly in
JavaScript.
Trigger conditions
| Condition |
JSON_VALID |
| Value ≤ 2039 bytes, no PK |
1 (correct) |
| Value ≥ 2040 bytes, no PK |
0 (bug) |
| Value ≥ 2040 bytes, with PK |
1 (correct) |
| Any size, as query param |
1 (correct) |
The bug is purely size-based — content does not matter.
Hypothesis
Dolt's JSON_VALID implementation uses a fixed 2048-byte internal buffer.
The 8-byte overhead of JSON wrapping ({"d":"..."}) reduces usable payload to
2040 bytes. The presence of a PRIMARY KEY changes Dolt's internal storage format
(row format) such that the buffer limit is not hit.
Bug 3: JSON column returns garbled bytes under concurrent read load
Under concurrent read load, CAST(data_json AS CHAR) returns either NULL or garbled, unparseable bytes for every read of a JSON column — including small values that round-trip correctly when read single-threaded. The same reads against a LONGTEXT column succeed with zero failures under identical load. No error is raised; data is silently lost.
Trigger conditions
The bug requires all conditions:
- Multiple concurrent SQL connections to the same Dolt server (8 connections observed)
- Concurrent reads of a
JSON column containing values that trigger Bug 1 (i.e., > ~4004 bytes with \uXXXX escapes)
- Small JSON values on the same connection also get corrupted once any large value has been read
LONGTEXT columns under the same load show zero corruption across all sizes.
Minimum reproduction
CREATE TABLE repro_test (
id INT AUTO_INCREMENT PRIMARY KEY,
label TEXT NOT NULL,
data_json JSON,
data_text LONGTEXT
);
-- Insert small (1.3KB), medium (224KB), large (21MB) JSON
-- containing \u000b (VT) characters into both data_json and data_text
// 8 connections, 20 read iterations via Promise.all
const pool = await Promise.all(
Array.from({ length: 8 }, () => mysql.createConnection({ ... })),
)
await Promise.all(Array.from({ length: 20 }, async (_, i) => {
const conn = pool[i % 8]
await conn.query("SELECT label, CAST(data_json AS CHAR) AS raw_json FROM repro_test")
await conn.query("SELECT label, CAST(data_text AS CHAR) AS raw_text FROM repro_test")
}))
Results
| Column |
Reads attempted |
Unparseable |
NULL |
Wrong format |
LONGTEXT |
60 |
0 |
0 |
0 |
JSON |
60 |
60 |
40 |
20 |
Single-threaded, JSON reads of the small value succeed; only medium/large return NULL (Bug 1). Under concurrent load, all 60 JSON reads fail — 40 return NULL, 20 return unparseable garbage. Repro: repro.test.ts "Concurrent reads".
Hypothesis
Bug 1's buffer overflow appears to corrupt per-connection or per-server state. Once any connection reads a large JSON value, subsequent reads on that connection return garbage even for small values that single-threaded reads handle correctly. The LONGTEXT path uses a different serializer that does not suffer this issue, suggesting the corruption lives in Dolt's JSON column serialization code path.
The following bugs are reproduced on CI tests in a Github repo here and a 2nd Github repo.
It affects at least Dolt version 2.1.7.
See @zachmu and @nicktobey
Bug 1: JSON column silently stores NULL for large values
When inserting JSON values > ~4004 bytes containing certain control characters into
a
JSONcolumn, Dolt silently storesNULLinstead of the value. No error israised. The same value stored in a
LONGTEXTcolumn round-trips correctlybyte-for-byte.
Trigger conditions
The bug requires both conditions:
JSON.stringifyserializes as\uXXXX(6-byte escape sequences)These control characters trigger the bug (no short escape →
\uXXXX):These do not trigger (short escape or literal):
\b(2 bytes)\t(2 bytes)\n(2 bytes)\f(2 bytes)\r(2 bytes)Minimum reproduction
{"d":"\u000b\u000b\u000b...666 times...\u000b"}Total JSON input = 4004 bytes. A single
\u000bin a 50KB string also triggers.But 100KB of plain
xwith no escapes works fine.Hypothesis
Dolt's JSON serializer allocates an output buffer proportional to the input
string length. Control characters needing
\uXXXXescaping expand from 1 byte(internal) to 6 bytes (JSON output). The serializer writes past the end of the
allocated buffer, corrupting internal state and causing a silent NULL. Characters
with shorter escapes or no expansion fit within the original buffer.
Bug 2: JSON_VALID returns 0 for LONGTEXT values > 2040 bytes
JSON_VALID()returns0for valid JSON stored inLONGTEXTcolumns when thevalue exceeds 2040 bytes and the table has no PRIMARY KEY. The same value
passes
JSON_VALID()as a query parameter literal, and parses correctly inJavaScript.
Trigger conditions
The bug is purely size-based — content does not matter.
Hypothesis
Dolt's
JSON_VALIDimplementation uses a fixed 2048-byte internal buffer.The 8-byte overhead of JSON wrapping (
{"d":"..."}) reduces usable payload to2040 bytes. The presence of a PRIMARY KEY changes Dolt's internal storage format
(row format) such that the buffer limit is not hit.
Bug 3: JSON column returns garbled bytes under concurrent read load
Under concurrent read load,
CAST(data_json AS CHAR)returns eitherNULLor garbled, unparseable bytes for every read of aJSONcolumn — including small values that round-trip correctly when read single-threaded. The same reads against aLONGTEXTcolumn succeed with zero failures under identical load. No error is raised; data is silently lost.Trigger conditions
The bug requires all conditions:
JSONcolumn containing values that trigger Bug 1 (i.e., > ~4004 bytes with\uXXXXescapes)LONGTEXTcolumns under the same load show zero corruption across all sizes.Minimum reproduction
Results
LONGTEXTJSONSingle-threaded,
JSONreads of the small value succeed; only medium/large returnNULL(Bug 1). Under concurrent load, all 60JSONreads fail — 40 returnNULL, 20 return unparseable garbage. Repro:repro.test.ts"Concurrent reads".Hypothesis
Bug 1's buffer overflow appears to corrupt per-connection or per-server state. Once any connection reads a large JSON value, subsequent reads on that connection return garbage even for small values that single-threaded reads handle correctly. The
LONGTEXTpath uses a different serializer that does not suffer this issue, suggesting the corruption lives in Dolt'sJSONcolumn serialization code path.