Coverage Report

Created: 2026-11-11 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/duckdb/extension/json/include/json_reader.hpp
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//                         DuckDB
3
//
4
// json_reader.hpp
5
//
6
//
7
//===----------------------------------------------------------------------===//
8
9
#pragma once
10
11
#include "duckdb/common/atomic.hpp"
12
#include "duckdb/common/enum_util.hpp"
13
#include "duckdb/common/enums/file_compression_type.hpp"
14
#include "duckdb/common/file_system.hpp"
15
#include "duckdb/common/open_file_info.hpp"
16
#include "json_reader_options.hpp"
17
#include "duckdb/common/mutex.hpp"
18
#include "duckdb/common/query_context.hpp"
19
#include "json_common.hpp"
20
#include "json_enums.hpp"
21
#include "yyjson_memory.hpp"
22
23
namespace duckdb {
24
struct JSONScanGlobalState;
25
class JSONReader;
26
27
struct JSONBufferHandle {
28
public:
29
  JSONBufferHandle(JSONReader &reader, idx_t buffer_index, idx_t readers, AllocatedData buffer, idx_t buffer_size,
30
                   idx_t buffer_start);
31
32
public:
33
  //! The reader this buffer comes from
34
  JSONReader &reader;
35
  //! Buffer index (within same file)
36
  const idx_t buffer_index;
37
38
  //! Number of readers for this buffer
39
  atomic<idx_t> readers;
40
  //! The buffer
41
  AllocatedData buffer;
42
  //! The size of the data in the buffer (can be less than buffer.GetSize())
43
  const idx_t buffer_size;
44
  //! The start position in the buffer
45
  idx_t buffer_start;
46
  //! The start of the line copied in from the previous buffer, set once it is copied
47
  atomic<idx_t> remainder_start;
48
};
49
50
struct JSONFileHandle {
51
public:
52
  JSONFileHandle(QueryContext context, unique_ptr<FileHandle> file_handle, Allocator &allocator);
53
54
  bool IsOpen() const;
55
  void Close();
56
57
  void Reset();
58
  bool RequestedReadsComplete();
59
  bool LastReadRequested() const;
60
61
  idx_t FileSize() const;
62
  idx_t Remaining() const;
63
  //! The fraction of the file that has been read, in [0, 1]
64
  double GetProgress() const;
65
66
  bool CanSeek() const;
67
  bool IsPipe() const;
68
69
  FileHandle &GetHandle();
70
71
  //! The next two functions return whether the read was successful
72
  bool GetPositionAndSize(idx_t &position, idx_t &size, idx_t requested_size);
73
  bool Read(char *pointer, idx_t &read_size, idx_t requested_size);
74
  //! Read at position optionally allows passing a custom handle to read from, otherwise the default one is used
75
  void ReadAtPosition(char *pointer, idx_t size, idx_t position, optional_ptr<FileHandle> override_handle = nullptr);
76
  //! The bytes of the stored file read since the last call - for a compressed file, its compressed bytes. Bytes read
77
  //! again after a reset are only returned once
78
  idx_t TakeStoredBytesRead();
79
80
private:
81
  idx_t ReadInternal(char *pointer, const idx_t requested_size);
82
  idx_t ReadFromCache(char *&pointer, idx_t &size, atomic<idx_t> &position);
83
84
private:
85
  QueryContext context;
86
87
  //! The JSON file handle
88
  unique_ptr<FileHandle> file_handle;
89
  Allocator &allocator;
90
91
  //! File properties
92
  const bool can_seek;
93
  const idx_t file_size;
94
  //! Whether the file is compressed - the read position then refers to the decompressed data
95
  const bool compressed;
96
97
  //! Read properties
98
  atomic<idx_t> read_position;
99
  atomic<idx_t> requested_reads;
100
  atomic<idx_t> actual_reads;
101
  atomic<bool> last_read_requested;
102
103
  //! The bytes of the stored file read and not yet returned by TakeStoredBytesRead
104
  atomic<idx_t> stored_bytes_untaken {0};
105
  //! For a compressed file, the compressed bytes added to stored_bytes_untaken since the file was (re)opened
106
  idx_t compressed_bytes_counted = 0;
107
108
  //! Cached buffers for resetting when reading stream
109
  vector<AllocatedData> cached_buffers;
110
  idx_t cached_size;
111
};
112
113
struct JSONString {
114
public:
115
11.8M
  JSONString() {
116
11.8M
  }
117
52.7k
  JSONString(const char *pointer_p, idx_t size_p) : pointer(pointer_p), size(size_p) {
118
52.7k
  }
119
120
  const char *pointer;
121
  idx_t size;
122
123
public:
124
0
  string ToString() {
125
0
    return string(pointer, size);
126
0
  }
127
128
108k
  const char &operator[](size_t i) const {
129
108k
    return pointer[i];
130
108k
  }
131
};
132
133
enum class JSONFileReadType { SCAN_ENTIRE_FILE, SCAN_PARTIAL };
134
135
struct JSONReaderScanState {
136
  explicit JSONReaderScanState(ClientContext &context, Allocator &global_allocator,
137
                               idx_t reconstruct_buffer_capacity);
138
139
  FileSystem &fs;
140
  Allocator &global_allocator;
141
  //! Thread-local allocator
142
  JSONAllocator allocator;
143
  idx_t buffer_capacity;
144
  bool initialized = false;
145
  // if we have a buffer already - this is our buffer index
146
  optional_idx buffer_index;
147
  //! Whether or not we are scanning the entire file
148
  //! If we are scanning the entire file we don't share reads between threads and just read the file until we are done
149
  JSONFileReadType file_read_type = JSONFileReadType::SCAN_PARTIAL;
150
  // Data for reading (if we have postponed reading)
151
  //! Buffer (if we have one)
152
  AllocatedData read_buffer;
153
  bool needs_to_read = false;
154
  idx_t request_size;
155
  idx_t read_position;
156
  idx_t read_size;
157
  //! Current scan data
158
  idx_t scan_count = 0;
159
  JSONString units[STANDARD_VECTOR_SIZE];
160
  yyjson_val *values[STANDARD_VECTOR_SIZE];
161
  optional_ptr<JSONBufferHandle> current_buffer_handle;
162
  //! Current buffer read info
163
  optional_ptr<JSONReader> current_reader;
164
  char *buffer_ptr = nullptr;
165
  idx_t buffer_size = 0;
166
  idx_t buffer_offset = 0;
167
  idx_t prev_buffer_remainder = 0;
168
  idx_t prev_buffer_offset = 0;
169
  idx_t lines_or_objects_in_buffer = 0;
170
  //! Whether this is the first time scanning this buffer
171
  bool is_first_scan = false;
172
  //! Whether this is the last batch of the file
173
  bool is_last = false;
174
  //! Whether the remainder of the file is deliberately not read (everything after a FeatureCollection's
175
  //! "features" array), i.e. no further buffers should be read even if the file has more data
176
  bool skip_remainder_of_file = false;
177
  //! Buffer to reconstruct split values
178
  optional_idx batch_index;
179
180
  //! For some filesystems (e.g. S3), using a filehandle per thread increases performance
181
  unique_ptr<FileHandle> thread_local_filehandle;
182
183
public:
184
  //! Reset for parsing the next batch of JSON from the current buffer
185
  void ResetForNextParse();
186
  //! Reset state for reading the next buffer
187
  void ResetForNextBuffer();
188
  //! Clear the buffer handle (if any)
189
  void ClearBufferHandle();
190
};
191
192
struct JSONError {
193
  idx_t buf_index;
194
  idx_t line_or_object_in_buf;
195
  string error_msg;
196
};
197
198
//! Reads a single JSON file
199
class JSONReader {
200
public:
201
  JSONReader(ClientContext &context, JSONReaderOptions options, OpenFileInfo file);
202
203
  void OpenJSONFile();
204
  void CloseHandle();
205
  void Reset();
206
207
  bool HasFileHandle() const;
208
  bool IsOpen() const;
209
5.58k
  bool IsInitialized() const {
210
5.58k
    return initialized;
211
5.58k
  }
212
213
  JSONReaderOptions &GetOptions();
214
215
  JSONFormat GetFormat() const;
216
  void SetFormat(JSONFormat format);
217
218
  JSONRecordType GetRecordType() const;
219
  void SetRecordType(JSONRecordType type);
220
221
  const string &GetFileName() const;
222
  JSONFileHandle &GetFileHandle() const;
223
224
public:
225
  //! Get a new buffer index (grabs the lock)
226
  idx_t GetBufferIndex();
227
  //! The index the next buffer will receive (grabs the lock)
228
  idx_t GetNextBufferIndex();
229
  //! Set line count for a buffer that is done (grabs the lock)
230
  void SetBufferLineOrObjectCount(JSONBufferHandle &handle, idx_t count);
231
  //! Records a parse error in the specified buffer
232
  void AddParseError(JSONReaderScanState &scan_state, idx_t line_or_object_in_buf, yyjson_read_err &err,
233
                     const string &extra = "");
234
  //! Records a transform error in the specified buffer
235
  void AddTransformError(JSONReaderScanState &scan_state, idx_t object_index, const string &error_message);
236
  //! Whether this reader has thrown if an error has occurred
237
  bool HasThrown();
238
239
  void Initialize(Allocator &allocator, idx_t buffer_size);
240
  bool InitializeScan(JSONReaderScanState &state, JSONFileReadType file_read_type);
241
  bool ParseJSON(JSONReaderScanState &scan_state, char *const json_start, const idx_t json_size,
242
                 const idx_t remaining);
243
  bool ParseNextChunk(JSONReaderScanState &scan_state);
244
  idx_t Scan(JSONReaderScanState &scan_state);
245
  bool ReadNextBuffer(JSONReaderScanState &scan_state);
246
  bool PrepareBufferForRead(JSONReaderScanState &scan_state);
247
248
  //! Scan progress
249
  double GetProgress() const;
250
251
  //! Counts what the file was read for as scanned, once a query scans it
252
  void CountBytesScanned();
253
  //! Set when a query starts scanning the file, so reading samples of it to detect its schema is not counted
254
  bool scanning = false;
255
  //! The bytes of the stored file scanned but not yet reported to the profiler
256
  atomic<idx_t> bytes_scanned_unreported {0};
257
258
  void DecrementBufferUsage(JSONBufferHandle &handle, idx_t lines_or_object_in_buffer, AllocatedData &buffer);
259
260
private:
261
  void SkipOverArrayStart(JSONReaderScanState &scan_state);
262
  void AutoDetect(Allocator &allocator, idx_t buffer_size);
263
  bool CopyRemainderFromPreviousBuffer(JSONReaderScanState &scan_state);
264
  void FinalizeBufferInternal(JSONReaderScanState &scan_state, AllocatedData &buffer, idx_t buffer_index);
265
  void PrepareForReadInternal(JSONReaderScanState &scan_state);
266
  void PrepareForScan(JSONReaderScanState &scan_state);
267
  bool PrepareBufferSeek(JSONReaderScanState &scan_state);
268
  void ReadNextBufferSeek(JSONReaderScanState &scan_state);
269
  bool ReadNextBufferNoSeek(JSONReaderScanState &scan_state);
270
  void FinalizeBuffer(JSONReaderScanState &scan_state);
271
272
  //! Insert/get/remove buffer (grabs the lock)
273
  void InsertBuffer(idx_t buffer_idx, unique_ptr<JSONBufferHandle> buffer);
274
  optional_ptr<JSONBufferHandle> GetBuffer(idx_t buffer_idx);
275
  AllocatedData RemoveBuffer(JSONBufferHandle &handle);
276
277
  void ThrowObjectSizeError(const idx_t object_size);
278
279
private:
280
  //! Add an error to the buffer - requires the lock to be held
281
  void AddError(idx_t buf_index, idx_t line_or_object_in_buf, const string &error_msg);
282
  //! Throw errors if possible - requires the lock to be held
283
  void ThrowErrorsIfPossible();
284
  //! Try to get the line number - requires the lock to be held
285
  optional_idx TryGetLineNumber(idx_t buf_index, idx_t line_or_object_in_buf);
286
287
private:
288
  //! The file that is read
289
  OpenFileInfo file;
290
  ClientContext &context;
291
  JSONReaderOptions options;
292
  //! The flags documents are parsed with: numbers are kept as text when a column is or contains a DECIMAL
293
  yyjson_read_flag read_flag;
294
295
  //! File handle
296
  unique_ptr<JSONFileHandle> file_handle;
297
298
  //! Whether or not the reader has been initialized
299
  bool initialized;
300
  //! Next buffer index within the file
301
  idx_t next_buffer_index;
302
  //! Mapping from batch index to currently held buffers
303
  unordered_map<idx_t, unique_ptr<JSONBufferHandle>> buffer_map;
304
305
  //! Line count per buffer
306
  vector<int64_t> buffer_line_or_object_counts;
307
  //! Whether any of the reading threads has thrown an error
308
  bool thrown;
309
310
  //! If we have auto-detected, this is the buffer read by the auto-detection
311
  AllocatedData auto_detect_data;
312
  idx_t auto_detect_data_size = 0;
313
314
  //! Whether this file is a GeoJSON FeatureCollection, i.e. the rows live in its "features" array rather than at
315
  //! the top level. The array itself is then read using the regular JSONFormat::ARRAY handling
316
  bool skip_feature_collection_prefix = false;
317
318
  //! The first error we found in the file (if any)
319
  unique_ptr<JSONError> error;
320
321
public:
322
  mutable mutex lock;
323
};
324
325
} // namespace duckdb