/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 |