Skip to content

3 Dolt JSON Bugs for large JSON payloads / parallel execution of JSON payloads. #11210

Description

@tianhuil

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:

  1. JSON input text > ~4004 bytes
  2. 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:

  1. Multiple concurrent SQL connections to the same Dolt server (8 connections observed)
  2. Concurrent reads of a JSON column containing values that trigger Bug 1 (i.e., > ~4004 bytes with \uXXXX escapes)
  3. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcorrectnessWe don't return the same result as MySQLcustomer issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions