Coverage Report

Created: 2026-11-11 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/duckdb/extension/json/json_schema_inference.cpp
Line
Count
Source
1
#include "json_schema_inference.hpp"
2
3
#include "duckdb/common/enum_util.hpp"
4
#include "json_geojson.hpp"
5
#include "json_scan.hpp"
6
#include "json_transform.hpp"
7
#include "duckdb/common/types/timestamp.hpp"
8
9
namespace duckdb {
10
11
740
static bool IsNumeric(LogicalTypeId type) {
12
740
  return type == LogicalTypeId::DOUBLE || type == LogicalTypeId::UBIGINT || type == LogicalTypeId::BIGINT ||
13
179
         type == LogicalTypeId::HUGEINT;
14
740
}
15
16
104
static LogicalTypeId MaxNumericType(const LogicalTypeId &a, const LogicalTypeId &b) {
17
104
  D_ASSERT(a != b);
18
104
  if (a == LogicalTypeId::DOUBLE || b == LogicalTypeId::DOUBLE) {
19
99
    return LogicalTypeId::DOUBLE;
20
99
  }
21
5
  if (a == LogicalTypeId::HUGEINT || b == LogicalTypeId::HUGEINT) {
22
2
    return LogicalTypeId::HUGEINT;
23
2
  }
24
  // One is BIGINT and the other is UBIGINT - need HUGEINT to represent both ranges
25
3
  if ((a == LogicalTypeId::BIGINT && b == LogicalTypeId::UBIGINT) ||
26
3
      (a == LogicalTypeId::UBIGINT && b == LogicalTypeId::BIGINT)) {
27
3
    return LogicalTypeId::HUGEINT;
28
3
  }
29
0
  return LogicalTypeId::BIGINT;
30
3
}
31
32
JSONStructureNode::JSONStructureNode(const bool case_insensitive_keys_p)
33
10.1k
    : count(0), null_count(0), case_insensitive_keys(case_insensitive_keys_p) {
34
10.1k
}
35
36
JSONStructureNode::JSONStructureNode(const char *key_ptr, const size_t key_len, const bool case_insensitive_keys_p)
37
80
    : JSONStructureNode(case_insensitive_keys_p) {
38
80
  key = make_uniq<string>(key_ptr, key_len);
39
80
}
40
41
8
JSONStructureNode JSONStructureNode::EmptyLike(const JSONStructureNode &parent) {
42
8
  return JSONStructureNode(parent.case_insensitive_keys);
43
8
}
44
45
0
static void SwapJSONStructureNode(JSONStructureNode &a, JSONStructureNode &b) noexcept {
46
0
  std::swap(a.key, b.key);
47
0
  std::swap(a.initialized, b.initialized);
48
0
  std::swap(a.descriptions, b.descriptions);
49
0
  std::swap(a.count, b.count);
50
0
  std::swap(a.null_count, b.null_count);
51
0
  std::swap(a.case_insensitive_keys, b.case_insensitive_keys);
52
0
}
53
54
0
JSONStructureNode::JSONStructureNode(JSONStructureNode &&other) noexcept {
55
0
  SwapJSONStructureNode(*this, other);
56
0
}
57
58
0
JSONStructureNode &JSONStructureNode::operator=(JSONStructureNode &&other) noexcept {
59
0
  SwapJSONStructureNode(*this, other);
60
0
  return *this;
61
0
}
62
63
4.54k
JSONStructureDescription &JSONStructureNode::GetOrCreateDescription(const LogicalTypeId type) {
64
4.54k
  if (descriptions.empty()) {
65
    // Empty, just put this type in there
66
3.91k
    descriptions.emplace_back(type, case_insensitive_keys);
67
3.91k
    return descriptions.back();
68
3.91k
  }
69
70
628
  if (descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::SQLNULL) {
71
    // Only a NULL in there, override
72
0
    descriptions[0].type = type;
73
0
    return descriptions[0];
74
0
  }
75
76
628
  if (type == LogicalTypeId::SQLNULL) {
77
    // 'descriptions' is non-empty, so let's not add NULL
78
0
    return descriptions.back();
79
0
  }
80
81
  // Check if type is already in there or if we can merge numerics
82
628
  const auto is_numeric = IsNumeric(type);
83
732
  for (auto &description : descriptions) {
84
732
    if (type == description.type) {
85
480
      return description;
86
480
    }
87
252
    if (is_numeric && IsNumeric(description.type)) {
88
104
      description.type = MaxNumericType(type, description.type);
89
104
      return description;
90
104
    }
91
252
  }
92
  // Type was not there, create a new description
93
44
  descriptions.emplace_back(type, case_insensitive_keys);
94
44
  return descriptions.back();
95
628
}
96
97
2.41k
bool JSONStructureNode::ContainsVarchar() const {
98
2.41k
  if (descriptions.size() != 1) {
99
    // We can't refine types if we have more than 1 description (yet), defaults to JSON type for now
100
106
    return false;
101
106
  }
102
2.31k
  auto &description = descriptions[0];
103
2.31k
  if (description.type == LogicalTypeId::VARCHAR) {
104
757
    return true;
105
757
  }
106
1.55k
  for (auto &child : description.children) {
107
685
    if (child.ContainsVarchar()) {
108
22
      return true;
109
22
    }
110
685
  }
111
112
1.53k
  return false;
113
1.55k
}
114
115
void JSONStructureNode::InitializeCandidateTypes(const idx_t max_depth, const bool convert_strings_to_integers,
116
384
                                                 const bool user_specified_timestamp_format, const idx_t depth) {
117
384
  if (depth >= max_depth) {
118
0
    return;
119
0
  }
120
384
  if (descriptions.size() != 1) {
121
    // We can't refine types if we have more than 1 description (yet), defaults to JSON type for now
122
0
    return;
123
0
  }
124
384
  auto &description = descriptions[0];
125
384
  if (description.type == LogicalTypeId::VARCHAR && !initialized) {
126
    // We loop through the candidate types and format templates from back to front.
127
    // TIMESTAMP_TZ is only a candidate when the user did not pass timestamp_format=; never infer that
128
    // from the date-format map (auto-detect can also converge on a single format).
129
    // Trial order puts TIMESTAMP_TZ before TIMESTAMP so back-to-front tries TZ first (issue #14919).
130
373
    if (convert_strings_to_integers) {
131
0
      if (user_specified_timestamp_format) {
132
0
        description.candidate_types = {LogicalTypeId::UUID, LogicalTypeId::BIGINT, LogicalTypeId::TIMESTAMP,
133
0
                                       LogicalTypeId::DATE, LogicalTypeId::TIME};
134
0
      } else {
135
0
        description.candidate_types = {LogicalTypeId::UUID,      LogicalTypeId::BIGINT,
136
0
                                       LogicalTypeId::TIMESTAMP, LogicalTypeId::TIMESTAMP_TZ,
137
0
                                       LogicalTypeId::DATE,      LogicalTypeId::TIME};
138
0
      }
139
373
    } else if (user_specified_timestamp_format) {
140
0
      description.candidate_types = {LogicalTypeId::UUID, LogicalTypeId::TIMESTAMP, LogicalTypeId::DATE,
141
0
                                     LogicalTypeId::TIME};
142
373
    } else {
143
373
      description.candidate_types = {LogicalTypeId::UUID, LogicalTypeId::TIMESTAMP, LogicalTypeId::TIMESTAMP_TZ,
144
373
                                     LogicalTypeId::DATE, LogicalTypeId::TIME};
145
373
    }
146
373
    initialized = true;
147
373
  } else {
148
11
    for (auto &child : description.children) {
149
11
      child.InitializeCandidateTypes(max_depth, convert_strings_to_integers, user_specified_timestamp_format,
150
11
                                     depth + 1);
151
11
    }
152
11
  }
153
384
}
154
155
void JSONStructureNode::RefineCandidateTypes(yyjson_val *vals[], const idx_t val_count, Vector &string_vector,
156
                                             ArenaAllocator &allocator, MutableDateFormatMap &date_format_map,
157
384
                                             const bool user_specified_timestamp_format) {
158
384
  if (descriptions.size() != 1) {
159
    // We can't refine types if we have more than 1 description (yet), defaults to JSON type for now
160
0
    return;
161
0
  }
162
384
  if (!ContainsVarchar()) {
163
0
    return;
164
0
  }
165
384
  auto &description = descriptions[0];
166
384
  switch (description.type) {
167
5
  case LogicalTypeId::LIST:
168
5
    return RefineCandidateTypesArray(vals, val_count, string_vector, allocator, date_format_map,
169
5
                                     user_specified_timestamp_format);
170
6
  case LogicalTypeId::STRUCT:
171
6
    return RefineCandidateTypesObject(vals, val_count, string_vector, allocator, date_format_map,
172
6
                                      user_specified_timestamp_format);
173
373
  case LogicalTypeId::VARCHAR:
174
373
    return RefineCandidateTypesString(vals, val_count, string_vector, date_format_map,
175
373
                                      user_specified_timestamp_format);
176
0
  default:
177
0
    return;
178
384
  }
179
384
}
180
181
void JSONStructureNode::RefineCandidateTypesArray(yyjson_val *vals[], const idx_t val_count, Vector &string_vector,
182
                                                  ArenaAllocator &allocator, MutableDateFormatMap &date_format_map,
183
5
                                                  const bool user_specified_timestamp_format) {
184
5
  D_ASSERT(descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::LIST);
185
5
  auto &desc = descriptions[0];
186
5
  D_ASSERT(desc.children.size() == 1);
187
5
  auto &child = desc.children[0];
188
189
5
  idx_t total_list_size = 0;
190
10
  for (idx_t i = 0; i < val_count; i++) {
191
5
    if (vals[i] && !unsafe_yyjson_is_null(vals[i])) {
192
5
      D_ASSERT(yyjson_is_arr(vals[i]));
193
5
      total_list_size += unsafe_yyjson_get_len(vals[i]);
194
5
    }
195
5
  }
196
197
5
  idx_t offset = 0;
198
5
  auto child_vals =
199
5
      reinterpret_cast<yyjson_val **>(allocator.AllocateAligned(total_list_size * sizeof(yyjson_val *)));
200
201
5
  size_t idx, max;
202
5
  yyjson_val *child_val;
203
10
  for (idx_t i = 0; i < val_count; i++) {
204
5
    if (vals[i] && !unsafe_yyjson_is_null(vals[i])) {
205
9
      yyjson_arr_foreach(vals[i], idx, max, child_val) {
206
9
        child_vals[offset++] = child_val;
207
9
      }
208
5
    }
209
5
  }
210
5
  child.RefineCandidateTypes(child_vals, total_list_size, string_vector, allocator, date_format_map,
211
5
                             user_specified_timestamp_format);
212
5
}
213
214
void JSONStructureNode::RefineCandidateTypesObject(yyjson_val *vals[], const idx_t val_count, Vector &string_vector,
215
                                                   ArenaAllocator &allocator, MutableDateFormatMap &date_format_map,
216
6
                                                   const bool user_specified_timestamp_format) {
217
6
  D_ASSERT(descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::STRUCT);
218
6
  auto &desc = descriptions[0];
219
220
6
  const idx_t child_count = desc.children.size();
221
6
  vector<yyjson_val **> child_vals;
222
6
  child_vals.reserve(child_count);
223
12
  for (idx_t child_idx = 0; child_idx < child_count; child_idx++) {
224
6
    child_vals.emplace_back(
225
6
        reinterpret_cast<yyjson_val **>(allocator.AllocateAligned(val_count * sizeof(yyjson_val *))));
226
6
  }
227
228
6
  const auto found_keys = reinterpret_cast<bool *>(allocator.AllocateAligned(sizeof(bool) * child_count));
229
230
6
  const auto &key_map = desc.key_map;
231
6
  size_t idx, max;
232
6
  yyjson_val *child_key, *child_val;
233
12
  for (idx_t i = 0; i < val_count; i++) {
234
6
    if (vals[i] && !unsafe_yyjson_is_null(vals[i])) {
235
6
      idx_t found_key_count = 0;
236
6
      memset(found_keys, false, child_count);
237
238
6
      D_ASSERT(yyjson_is_obj(vals[i]));
239
6
      yyjson_obj_foreach(vals[i], idx, max, child_key, child_val) {
240
6
        D_ASSERT(yyjson_is_str(child_key));
241
6
        const auto key_ptr = unsafe_yyjson_get_str(child_key);
242
6
        const auto key_len = unsafe_yyjson_get_len(child_key);
243
6
        auto it = key_map.find({key_ptr, key_len});
244
6
        D_ASSERT(it != key_map.end());
245
6
        const auto child_idx = it->second;
246
6
        child_vals[child_idx][i] = child_val;
247
6
        found_key_count += !found_keys[child_idx];
248
6
        found_keys[child_idx] = true;
249
6
      }
250
251
6
      if (found_key_count != child_count) {
252
        // Set child val to nullptr so recursion doesn't break
253
0
        for (idx_t child_idx = 0; child_idx < child_count; child_idx++) {
254
0
          if (!found_keys[child_idx]) {
255
0
            child_vals[child_idx][i] = nullptr;
256
0
          }
257
0
        }
258
0
      }
259
6
    } else {
260
0
      for (idx_t child_idx = 0; child_idx < child_count; child_idx++) {
261
0
        child_vals[child_idx][i] = nullptr;
262
0
      }
263
0
    }
264
6
  }
265
266
12
  for (idx_t child_idx = 0; child_idx < child_count; child_idx++) {
267
6
    desc.children[child_idx].RefineCandidateTypes(child_vals[child_idx], val_count, string_vector, allocator,
268
6
                                                  date_format_map, user_specified_timestamp_format);
269
6
  }
270
6
}
271
272
void JSONStructureNode::RefineCandidateTypesString(yyjson_val *vals[], const idx_t val_count, Vector &string_vector,
273
                                                   MutableDateFormatMap &date_format_map,
274
373
                                                   const bool user_specified_timestamp_format) {
275
373
  D_ASSERT(descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::VARCHAR);
276
373
  if (descriptions[0].candidate_types.empty()) {
277
0
    return;
278
0
  }
279
373
  static JSONTransformOptions OPTIONS;
280
373
  JSONTransform::GetStringVector(vals, val_count, LogicalType::SQLNULL, string_vector, OPTIONS);
281
373
  EliminateCandidateTypes(val_count, string_vector, date_format_map, user_specified_timestamp_format);
282
373
}
283
284
// Single commutative transition table, shared by per-vector refinement and parallel merges,
285
// so thread scheduling cannot change the detected type. UNKNOWN is the identity, MIXED is sticky.
286
368
static TimestampOffsetState MergeTimestampOffsetState(const TimestampOffsetState a, const TimestampOffsetState b) {
287
368
  if (a == TimestampOffsetState::UNKNOWN) {
288
368
    return b;
289
368
  }
290
0
  if (b == TimestampOffsetState::UNKNOWN) {
291
0
    return a;
292
0
  }
293
0
  if (a != b) {
294
0
    return TimestampOffsetState::MIXED;
295
0
  }
296
0
  return a;
297
0
}
298
299
// Date::TryConvertDate reuses has_offset for specials; only Z / ±HH[:MM] is a real offset.
300
3
static bool TimestampStringHasUtcOffset(const string_t &value) {
301
3
  const auto str = value.GetData();
302
3
  const auto len = value.GetSize();
303
3
  idx_t time_start = 0;
304
18
  for (idx_t i = 0; i < len; i++) {
305
15
    if (str[i] == 'T' || str[i] == ' ') {
306
0
      time_start = i + 1;
307
0
      break;
308
0
    }
309
15
  }
310
3
  if (time_start == 0 || time_start >= len) {
311
3
    return false;
312
3
  }
313
0
  for (idx_t i = time_start; i < len; i++) {
314
0
    if (str[i] == 'Z') {
315
0
      return true;
316
0
    }
317
0
    if (str[i] == '+' || str[i] == '-') {
318
0
      idx_t pos = i;
319
0
      int hh, mm, ss;
320
0
      return Timestamp::TryParseUTCOffset(str, pos, len, hh, mm, ss);
321
0
    }
322
0
  }
323
0
  return false;
324
0
}
325
326
// Update the sticky offset state from this vector's non-null strings.
327
// MIXED → VARCHAR: neither TIMESTAMPTZ (would invent session TZ for naive values)
328
// nor TIMESTAMP (would drop offsets) is safe.
329
static void UpdateTimestampOffsetState(JSONStructureDescription &description, const Vector &string_vector,
330
373
                                       const idx_t count) {
331
373
  const auto strings = FlatVector::GetData<string_t>(string_vector);
332
373
  const auto &validity = FlatVector::Validity(string_vector);
333
750
  for (idx_t i = 0; i < count; i++) {
334
377
    if (!validity.RowIsValid(i)) {
335
0
      continue; // nulls leave the state unchanged
336
0
    }
337
377
    timestamp_t ts;
338
377
    bool has_offset = false;
339
377
    string_t tz(nullptr, 0);
340
377
    auto res =
341
377
        Timestamp::TryConvertTimestampTZ(strings[i].GetData(), strings[i].GetSize(), ts, true, has_offset, tz);
342
377
    if (res != TimestampCastResult::SUCCESS && res != TimestampCastResult::STRICT_UTC) {
343
373
      continue; // not a timestamp string — other candidates handle it
344
373
    }
345
4
    if (!ts.IsFinite()) {
346
0
      continue; // ±infinity is valid for TIMESTAMP and TIMESTAMPTZ
347
0
    }
348
4
    if (has_offset && !TimestampStringHasUtcOffset(strings[i])) {
349
3
      continue; // date special such as epoch, not a real offset
350
3
    }
351
1
    description.timestamp_offset_state = MergeTimestampOffsetState(
352
1
        description.timestamp_offset_state,
353
1
        has_offset ? TimestampOffsetState::WITH_OFFSET : TimestampOffsetState::WITHOUT_OFFSET);
354
1
    if (description.timestamp_offset_state == TimestampOffsetState::MIXED) {
355
0
      return;
356
0
    }
357
1
  }
358
373
}
359
360
void JSONStructureNode::EliminateCandidateTypes(const idx_t vec_count, Vector &string_vector,
361
                                                MutableDateFormatMap &date_format_map,
362
373
                                                const bool user_specified_timestamp_format) {
363
373
  D_ASSERT(descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::VARCHAR);
364
373
  auto &description = descriptions[0];
365
373
  auto &candidate_types = description.candidate_types;
366
367
  // Monotonic across vectors: once MIXED, refuse to guess.
368
373
  if (!user_specified_timestamp_format) {
369
373
    UpdateTimestampOffsetState(description, string_vector, vec_count);
370
373
    if (description.timestamp_offset_state == TimestampOffsetState::MIXED) {
371
0
      candidate_types.clear();
372
0
      return;
373
0
    }
374
373
  }
375
376
2.18k
  while (true) {
377
2.17k
    if (candidate_types.empty()) {
378
358
      return;
379
358
    }
380
1.81k
    const auto type = candidate_types.back();
381
1.81k
    Vector result_vector(type, vec_count);
382
1.81k
    if (type == LogicalTypeId::TIMESTAMP_TZ) {
383
      // Defense in depth: InitializeCandidateTypes already omits TZ when the user set timestamp_format=.
384
359
      if (user_specified_timestamp_format) {
385
0
        candidate_types.pop_back();
386
0
        continue;
387
0
      }
388
      // Only accept TIMESTAMPTZ when every non-null value seen so far carries an offset/Z.
389
      // UNKNOWN (e.g., null-only vectors) and WITHOUT_OFFSET drop TZ and try TIMESTAMP (etc.).
390
359
      if (description.timestamp_offset_state != TimestampOffsetState::WITH_OFFSET) {
391
359
        candidate_types.pop_back();
392
359
        continue;
393
359
      }
394
0
      string error_message;
395
0
      if (!VectorOperations::DefaultTryCast(string_vector, result_vector, vec_count, &error_message, true)) {
396
0
        candidate_types.pop_back();
397
0
      } else {
398
0
        return;
399
0
      }
400
1.46k
    } else if (date_format_map.HasFormats(type)) {
401
729
      if (EliminateCandidateFormats(vec_count, string_vector, result_vector, date_format_map)) {
402
3
        return;
403
726
      } else {
404
726
        candidate_types.pop_back();
405
726
      }
406
731
    } else {
407
731
      string error_message;
408
731
      if (!VectorOperations::DefaultTryCast(string_vector, result_vector, vec_count, &error_message, true)) {
409
728
        candidate_types.pop_back();
410
728
      } else {
411
3
        return;
412
3
      }
413
731
    }
414
1.81k
  }
415
373
}
416
417
template <class OP, class T>
418
6.46k
bool TryParse(const Vector &string_vector, StrpTimeFormat &format, const idx_t count) {
419
6.46k
  const auto strings = FlatVector::GetData<string_t>(string_vector);
420
6.46k
  const auto &validity = FlatVector::Validity(string_vector);
421
422
6.46k
  T result;
423
6.46k
  string error_message;
424
6.46k
  if (validity.CannotHaveNull()) {
425
0
    for (idx_t i = 0; i < count; i++) {
426
0
      if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
427
0
        return false;
428
0
      }
429
0
    }
430
6.46k
  } else {
431
6.47k
    for (idx_t i = 0; i < count; i++) {
432
6.46k
      if (validity.RowIsValid(i)) {
433
6.46k
        if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
434
6.45k
          return false;
435
6.45k
        }
436
6.46k
      }
437
6.46k
    }
438
6.46k
  }
439
12
  return true;
440
6.46k
}
bool duckdb::TryParse<duckdb::TryParseDate, duckdb::date_t>(duckdb::Vector const&, duckdb::StrpTimeFormat&, unsigned long)
Line
Count
Source
418
2.16k
bool TryParse(const Vector &string_vector, StrpTimeFormat &format, const idx_t count) {
419
2.16k
  const auto strings = FlatVector::GetData<string_t>(string_vector);
420
2.16k
  const auto &validity = FlatVector::Validity(string_vector);
421
422
2.16k
  T result;
423
2.16k
  string error_message;
424
2.16k
  if (validity.CannotHaveNull()) {
425
0
    for (idx_t i = 0; i < count; i++) {
426
0
      if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
427
0
        return false;
428
0
      }
429
0
    }
430
2.16k
  } else {
431
2.17k
    for (idx_t i = 0; i < count; i++) {
432
2.16k
      if (validity.RowIsValid(i)) {
433
2.16k
        if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
434
2.15k
          return false;
435
2.15k
        }
436
2.16k
      }
437
2.16k
    }
438
2.16k
  }
439
11
  return true;
440
2.16k
}
bool duckdb::TryParse<duckdb::TryParseTimeStamp, duckdb::timestamp_base_t<1000000l, false> >(duckdb::Vector const&, duckdb::StrpTimeFormat&, unsigned long)
Line
Count
Source
418
4.29k
bool TryParse(const Vector &string_vector, StrpTimeFormat &format, const idx_t count) {
419
4.29k
  const auto strings = FlatVector::GetData<string_t>(string_vector);
420
4.29k
  const auto &validity = FlatVector::Validity(string_vector);
421
422
4.29k
  T result;
423
4.29k
  string error_message;
424
4.29k
  if (validity.CannotHaveNull()) {
425
0
    for (idx_t i = 0; i < count; i++) {
426
0
      if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
427
0
        return false;
428
0
      }
429
0
    }
430
4.29k
  } else {
431
4.29k
    for (idx_t i = 0; i < count; i++) {
432
4.29k
      if (validity.RowIsValid(i)) {
433
4.29k
        if (!OP::template Operation<T>(format, strings[i], result, error_message)) {
434
4.29k
          return false;
435
4.29k
        }
436
4.29k
      }
437
4.29k
    }
438
4.29k
  }
439
1
  return true;
440
4.29k
}
441
442
bool JSONStructureNode::EliminateCandidateFormats(const idx_t vec_count, Vector &string_vector,
443
729
                                                  const Vector &result_vector, MutableDateFormatMap &date_format_map) {
444
729
  D_ASSERT(descriptions.size() == 1 && descriptions[0].type == LogicalTypeId::VARCHAR);
445
446
729
  const auto type = result_vector.GetType().id();
447
729
  auto i = date_format_map.NumberOfFormats(type);
448
7.17k
  for (; i != 0; i--) {
449
6.46k
    StrpTimeFormat format;
450
6.46k
    if (!date_format_map.GetFormatAtIndex(type, i - 1, format)) {
451
0
      continue;
452
0
    }
453
454
6.46k
    bool success;
455
6.46k
    switch (type) {
456
2.16k
    case LogicalTypeId::DATE:
457
2.16k
      success = TryParse<TryParseDate, date_t>(string_vector, format, vec_count);
458
2.16k
      break;
459
4.29k
    case LogicalTypeId::TIMESTAMP:
460
4.29k
      success = TryParse<TryParseTimeStamp, timestamp_t>(string_vector, format, vec_count);
461
4.29k
      break;
462
0
    default:
463
0
      throw InternalException("No date/timestamp formats for %s", EnumUtil::ToString(type));
464
6.46k
    }
465
466
6.45k
    if (success) {
467
3
      return true;
468
3
    }
469
6.45k
  }
470
471
717
  return false;
472
729
}
473
474
JSONStructureDescription::JSONStructureDescription(const LogicalTypeId type_p, const bool case_insensitive_keys)
475
3.96k
    : type(type_p),
476
3.96k
      key_map(0, JSONKeyOptionalCaseHash {case_insensitive_keys}, JSONKeyOptionalCaseEquality {case_insensitive_keys}) {
477
3.96k
}
478
479
44
static void SwapJSONStructureDescription(JSONStructureDescription &a, JSONStructureDescription &b) noexcept {
480
44
  std::swap(a.type, b.type);
481
44
  std::swap(a.key_map, b.key_map);
482
44
  std::swap(a.children, b.children);
483
44
  std::swap(a.candidate_types, b.candidate_types);
484
44
  std::swap(a.has_large_ubigint, b.has_large_ubigint);
485
44
  std::swap(a.timestamp_offset_state, b.timestamp_offset_state);
486
44
}
487
488
44
JSONStructureDescription::JSONStructureDescription(JSONStructureDescription &&other) noexcept {
489
44
  SwapJSONStructureDescription(*this, other);
490
44
}
491
492
0
JSONStructureDescription &JSONStructureDescription::operator=(JSONStructureDescription &&other) noexcept {
493
0
  SwapJSONStructureDescription(*this, other);
494
0
  return *this;
495
0
}
496
497
1.34k
JSONStructureNode &JSONStructureDescription::GetOrCreateChild() {
498
1.34k
  D_ASSERT(type == LogicalTypeId::LIST);
499
1.34k
  if (children.empty()) {
500
1.33k
    children.emplace_back(key_map.hash_function().case_insensitive);
501
1.33k
  }
502
1.34k
  D_ASSERT(children.size() == 1);
503
1.34k
  return children.back();
504
1.34k
}
505
506
88
JSONStructureNode &JSONStructureDescription::GetOrCreateChild(const char *key_ptr, const size_t key_size) {
507
  // Check if there is already a child with the same key
508
88
  const JSONKey temp_key {key_ptr, key_size};
509
88
  const auto it = key_map.find(temp_key);
510
88
  if (it != key_map.end()) {
511
8
    return children[it->second]; // Found it
512
8
  }
513
514
  // Didn't find, create a new child (the first spelling of a key names it when keys are case-insensitive)
515
80
  children.emplace_back(key_ptr, key_size, key_map.hash_function().case_insensitive);
516
80
  const auto &persistent_key_string = *children.back().key;
517
80
  JSONKey new_key {persistent_key_string.c_str(), persistent_key_string.length()};
518
80
  key_map.emplace(new_key, children.size() - 1);
519
80
  return children.back();
520
88
}
521
522
JSONStructureNode &JSONStructureDescription::GetOrCreateChild(yyjson_val *key, yyjson_val *val,
523
                                                              const bool ignore_errors, const bool detect_geojson,
524
44
                                                              const idx_t depth) {
525
44
  D_ASSERT(yyjson_is_str(key));
526
44
  auto &child = GetOrCreateChild(unsafe_yyjson_get_str(key), unsafe_yyjson_get_len(key));
527
44
  JSONStructure::ExtractStructure(val, child, ignore_errors, detect_geojson, depth);
528
44
  return child;
529
44
}
530
531
static void ExtractStructureArray(yyjson_val *arr, JSONStructureNode &node, const bool ignore_errors,
532
673
                                  const bool detect_geojson, const idx_t depth) {
533
673
  D_ASSERT(yyjson_is_arr(arr));
534
673
  auto &description = node.GetOrCreateDescription(LogicalTypeId::LIST);
535
673
  auto &child = description.GetOrCreateChild();
536
537
673
  size_t idx, max;
538
673
  yyjson_val *val;
539
1.16k
  yyjson_arr_foreach(arr, idx, max, val) {
540
1.16k
    JSONStructure::ExtractStructure(val, child, ignore_errors, detect_geojson, depth + 1);
541
1.16k
  }
542
673
}
543
544
static void ExtractStructureObject(yyjson_val *obj, JSONStructureNode &node, const bool ignore_errors,
545
85
                                   const bool detect_geojson, const idx_t depth) {
546
85
  D_ASSERT(yyjson_is_obj(obj));
547
85
  auto &description = node.GetOrCreateDescription(LogicalTypeId::STRUCT);
548
549
  // Keep track of keys so we can detect duplicates
550
85
  unordered_set<string> obj_keys;
551
85
  case_insensitive_set_t ci_obj_keys;
552
553
85
  size_t idx, max;
554
85
  yyjson_val *key, *val;
555
85
  yyjson_obj_foreach(obj, idx, max, key, val) {
556
44
    const string obj_key(unsafe_yyjson_get_str(key), unsafe_yyjson_get_len(key));
557
44
    auto insert_result = obj_keys.insert(obj_key);
558
44
    if (!ignore_errors && !insert_result.second) { // Exact match
559
0
      JSONCommon::ThrowValFormatError("Duplicate key \"" + obj_key + "\" in object %s", obj);
560
0
    }
561
44
    insert_result = ci_obj_keys.insert(obj_key);
562
44
    if (!ignore_errors && !insert_result.second) { // Case-insensitive match
563
0
      JSONCommon::ThrowValFormatError("Duplicate key (different case) \"" + obj_key + "\" and \"" +
564
0
                                          *insert_result.first + "\" in object %s",
565
0
                                      obj);
566
0
    }
567
44
    description.GetOrCreateChild(key, val, ignore_errors, detect_geojson, depth + 1);
568
44
  }
569
85
}
570
571
1.79k
static void ExtractStructureVal(yyjson_val *val, JSONStructureNode &node) {
572
1.79k
  D_ASSERT(!yyjson_is_arr(val) && !yyjson_is_obj(val));
573
1.79k
  const auto val_type = JSONCommon::ValTypeToLogicalTypeId(val);
574
1.79k
  auto &desc = node.GetOrCreateDescription(val_type);
575
1.79k
  if (val_type == LogicalTypeId::UBIGINT &&
576
576
      unsafe_yyjson_get_uint(val) > static_cast<uint64_t>(NumericLimits<int64_t>::Maximum())) {
577
29
    desc.has_large_ubigint = true;
578
29
  }
579
1.79k
}
580
581
void JSONStructure::ExtractStructure(yyjson_val *val, JSONStructureNode &node, const bool ignore_errors,
582
2.55k
                                     const bool detect_geojson, const idx_t depth) {
583
2.55k
  if (depth >= JSONCommon::MAX_RECURSION_DEPTH) {
584
0
    throw InvalidInputException("JSON exceeds maximum recursion depth of %d", JSONCommon::MAX_RECURSION_DEPTH);
585
0
  }
586
2.55k
  node.count++;
587
2.55k
  const auto tag = yyjson_get_tag(val);
588
2.55k
  if (tag == (YYJSON_TYPE_NULL | YYJSON_SUBTYPE_NONE)) {
589
4
    node.null_count++;
590
4
  }
591
592
2.55k
  switch (tag) {
593
673
  case YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE:
594
673
    return ExtractStructureArray(val, node, ignore_errors, detect_geojson, depth);
595
85
  case YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE:
596
    // A GeoJSON geometry is a leaf: it becomes GEOMETRY rather than a struct of type/coordinates. Detecting it
597
    // here (rather than on the finished tree) means geometries of different kinds still merge into one type.
598
85
    if (detect_geojson && JSONGeometry::IsGeoJSONGeometry(val)) {
599
0
      node.GetOrCreateDescription(LogicalTypeId::GEOMETRY);
600
0
      return;
601
0
    }
602
85
    return ExtractStructureObject(val, node, ignore_errors, detect_geojson, depth);
603
1.79k
  default:
604
1.79k
    return ExtractStructureVal(val, node);
605
2.55k
  }
606
2.55k
}
607
608
static LogicalType StructureToTypeArray(ClientContext &context, const JSONStructureNode &node, const idx_t max_depth,
609
                                        const double field_appearance_threshold, const idx_t map_inference_threshold,
610
646
                                        const idx_t depth, const LogicalType &null_type) {
611
646
  D_ASSERT(node.descriptions.size() == 1 && node.descriptions[0].type == LogicalTypeId::LIST);
612
646
  const auto &desc = node.descriptions[0];
613
646
  D_ASSERT(desc.children.size() == 1);
614
615
646
  return LogicalType::LIST(JSONStructure::StructureToType(context, desc.children[0], max_depth,
616
646
                                                          field_appearance_threshold, map_inference_threshold,
617
646
                                                          depth + 1, null_type));
618
646
}
619
620
673
static void MergeNodeArray(JSONStructureNode &merged, const JSONStructureDescription &child_desc) {
621
673
  D_ASSERT(child_desc.type == LogicalTypeId::LIST);
622
673
  auto &merged_desc = merged.GetOrCreateDescription(LogicalTypeId::LIST);
623
673
  auto &merged_child = merged_desc.GetOrCreateChild();
624
673
  for (auto &list_child : child_desc.children) {
625
673
    JSONStructure::MergeNodes(merged_child, list_child);
626
673
  }
627
673
}
628
629
85
static void MergeNodeObject(JSONStructureNode &merged, const JSONStructureDescription &child_desc) {
630
85
  D_ASSERT(child_desc.type == LogicalTypeId::STRUCT);
631
85
  auto &merged_desc = merged.GetOrCreateDescription(LogicalTypeId::STRUCT);
632
85
  for (auto &struct_child : child_desc.children) {
633
44
    const auto &struct_child_key = *struct_child.key;
634
44
    auto &merged_child = merged_desc.GetOrCreateChild(struct_child_key.c_str(), struct_child_key.length());
635
44
    JSONStructure::MergeNodes(merged_child, struct_child);
636
44
  }
637
85
}
638
639
static void MergeNodeVal(JSONStructureNode &merged, const JSONStructureDescription &child_desc,
640
1.23k
                         const bool node_initialized) {
641
1.23k
  D_ASSERT(child_desc.type != LogicalTypeId::LIST && child_desc.type != LogicalTypeId::STRUCT);
642
1.23k
  auto &merged_desc = merged.GetOrCreateDescription(child_desc.type);
643
1.23k
  if (child_desc.has_large_ubigint) {
644
29
    merged_desc.has_large_ubigint = true;
645
29
  }
646
1.23k
  if (merged_desc.type != LogicalTypeId::VARCHAR || !node_initialized || merged.descriptions.size() != 1) {
647
866
    return;
648
866
  }
649
  // Sticky offset state across parallel auto-detect tasks
650
367
  merged_desc.timestamp_offset_state =
651
367
      MergeTimestampOffsetState(merged_desc.timestamp_offset_state, child_desc.timestamp_offset_state);
652
367
  if (merged_desc.timestamp_offset_state == TimestampOffsetState::MIXED) {
653
0
    merged_desc.candidate_types.clear(); // mixed offset/plain → VARCHAR
654
0
    merged.initialized = true;
655
0
    return;
656
0
  }
657
367
  if (!merged.initialized) {
658
367
    merged_desc.candidate_types = child_desc.candidate_types;
659
367
  } else if (merged_desc.candidate_types.empty() != child_desc.candidate_types.empty() // both empty or neither empty
660
0
             || (!merged_desc.candidate_types.empty() &&
661
0
                 merged_desc.candidate_types.back() != child_desc.candidate_types.back())) { // non-empty: check type
662
0
    merged_desc.candidate_types.clear(); // Not the same, default to VARCHAR
663
0
  }
664
665
367
  merged.initialized = true;
666
367
}
667
668
2.15k
void JSONStructure::MergeNodes(JSONStructureNode &merged, const JSONStructureNode &node) {
669
2.15k
  merged.count += node.count;
670
2.15k
  merged.null_count += node.null_count;
671
2.15k
  for (const auto &child_desc : node.descriptions) {
672
1.99k
    switch (child_desc.type) {
673
673
    case LogicalTypeId::LIST:
674
673
      MergeNodeArray(merged, child_desc);
675
673
      break;
676
85
    case LogicalTypeId::STRUCT:
677
85
      MergeNodeObject(merged, child_desc);
678
85
      break;
679
1.23k
    default:
680
1.23k
      MergeNodeVal(merged, child_desc, node.initialized);
681
1.23k
      break;
682
1.99k
    }
683
1.99k
  }
684
2.15k
}
685
686
static double CalculateTypeSimilarity(const LogicalType &merged, const LogicalType &type, idx_t max_depth, idx_t depth);
687
688
static double CalculateMapAndStructSimilarity(const LogicalType &map_type, const LogicalType &struct_type,
689
0
                                              const bool swapped, const idx_t max_depth, const idx_t depth) {
690
0
  const auto &map_value_type = MapType::ValueType(map_type);
691
0
  const auto &struct_child_types = StructType::GetChildTypes(struct_type);
692
0
  double total_similarity = 0;
693
0
  for (const auto &struct_child_type : struct_child_types) {
694
0
    const auto similarity =
695
0
        swapped ? CalculateTypeSimilarity(struct_child_type.second, map_value_type, max_depth, depth + 1)
696
0
                : CalculateTypeSimilarity(map_value_type, struct_child_type.second, max_depth, depth + 1);
697
0
    if (similarity < 0) {
698
0
      return similarity;
699
0
    }
700
0
    total_similarity += similarity;
701
0
  }
702
0
  return total_similarity / static_cast<double>(struct_child_types.size());
703
0
}
704
705
static double CalculateTypeSimilarity(const LogicalType &merged, const LogicalType &type, const idx_t max_depth,
706
0
                                      const idx_t depth) {
707
0
  if (depth >= max_depth || merged.id() == LogicalTypeId::SQLNULL || type.id() == LogicalTypeId::SQLNULL) {
708
0
    return 1;
709
0
  }
710
0
  if (merged.IsJSONType()) {
711
    // Incompatible types
712
0
    return -1;
713
0
  }
714
0
  if (type.IsJSONType() || merged == type) {
715
0
    return 1;
716
0
  }
717
718
0
  switch (merged.id()) {
719
0
  case LogicalTypeId::STRUCT: {
720
0
    if (type.id() == LogicalTypeId::MAP) {
721
      // This can happen for empty structs/maps ("{}"), or in rare cases where an inconsistent struct becomes
722
      // consistent when merged, but does not have enough children to be considered a map.
723
0
      return CalculateMapAndStructSimilarity(type, merged, true, max_depth, depth);
724
0
    } else if (type.id() != LogicalTypeId::STRUCT) {
725
0
      return -1;
726
0
    }
727
728
    // Only structs can be merged into a struct
729
0
    D_ASSERT(type.id() == LogicalTypeId::STRUCT);
730
0
    const auto &merged_child_types = StructType::GetChildTypes(merged);
731
0
    const auto &type_child_types = StructType::GetChildTypes(type);
732
733
0
    identifier_map_t<const LogicalType &> merged_child_types_map;
734
0
    for (const auto &merged_child : merged_child_types) {
735
0
      merged_child_types_map.emplace(merged_child.first, merged_child.second);
736
0
    }
737
738
0
    double total_similarity = 0;
739
0
    for (const auto &type_child_type : type_child_types) {
740
0
      const auto it = merged_child_types_map.find(type_child_type.first);
741
0
      if (it == merged_child_types_map.end()) {
742
0
        return -1;
743
0
      }
744
0
      const auto similarity = CalculateTypeSimilarity(it->second, type_child_type.second, max_depth, depth + 1);
745
0
      if (similarity < 0) {
746
0
        return similarity;
747
0
      }
748
0
      total_similarity += similarity;
749
0
    }
750
0
    return total_similarity / static_cast<double>(merged_child_types.size());
751
0
  }
752
0
  case LogicalTypeId::MAP: {
753
0
    if (type.id() == LogicalTypeId::MAP) {
754
0
      return CalculateTypeSimilarity(MapType::ValueType(merged), MapType::ValueType(type), max_depth, depth + 1);
755
0
    }
756
757
    // Only maps and structs can be merged into a map
758
0
    if (type.id() != LogicalTypeId::STRUCT) {
759
0
      return -1;
760
0
    }
761
0
    return CalculateMapAndStructSimilarity(merged, type, false, max_depth, depth);
762
0
  }
763
0
  case LogicalTypeId::LIST: {
764
    // Only lists can be merged into a list
765
0
    if (type.id() != LogicalTypeId::LIST) {
766
0
      return -1;
767
0
    }
768
0
    D_ASSERT(type.id() == LogicalTypeId::LIST);
769
0
    const auto &merged_child_type = ListType::GetChildType(merged);
770
0
    const auto &type_child_type = ListType::GetChildType(type);
771
0
    return CalculateTypeSimilarity(merged_child_type, type_child_type, max_depth, depth + 1);
772
0
  }
773
0
  default:
774
    // This is only reachable if type has been inferred using candidate_types, but candidate_types were not
775
    // consistent among all map values
776
0
    return 1;
777
0
  }
778
0
}
779
780
static bool IsStructureInconsistent(const JSONStructureDescription &desc, const idx_t sample_count,
781
20
                                    const idx_t null_count, const double field_appearance_threshold) {
782
20
  D_ASSERT(sample_count > null_count);
783
20
  double total_child_counts = 0;
784
20
  for (const auto &child : desc.children) {
785
20
    total_child_counts += static_cast<double>(child.count) / static_cast<double>(sample_count - null_count);
786
20
  }
787
20
  const auto avg_occurrence = total_child_counts / static_cast<double>(desc.children.size());
788
20
  return avg_occurrence < field_appearance_threshold;
789
20
}
790
791
static LogicalType GetMergedType(ClientContext &context, const JSONStructureNode &node, const idx_t max_depth,
792
                                 const double field_appearance_threshold, const idx_t map_inference_threshold,
793
8
                                 const idx_t depth, const LogicalType &null_type) {
794
8
  D_ASSERT(node.descriptions.size() == 1);
795
8
  auto &desc = node.descriptions[0];
796
8
  auto merged = JSONStructureNode::EmptyLike(node);
797
8
  for (const auto &child : desc.children) {
798
8
    JSONStructure::MergeNodes(merged, child);
799
8
  }
800
8
  return JSONStructure::StructureToType(context, merged, max_depth, field_appearance_threshold,
801
8
                                        map_inference_threshold, depth + 1, null_type);
802
8
}
803
804
static LogicalType StructureToTypeObject(ClientContext &context, const JSONStructureNode &node, const idx_t max_depth,
805
                                         const double field_appearance_threshold, const idx_t map_inference_threshold,
806
69
                                         const idx_t depth, const LogicalType &null_type) {
807
69
  D_ASSERT(node.descriptions.size() == 1 && node.descriptions[0].type == LogicalTypeId::STRUCT);
808
69
  auto &desc = node.descriptions[0];
809
810
69
  if (desc.children.empty()) {
811
41
    if (map_inference_threshold != DConstants::INVALID_INDEX) {
812
      // Empty struct - use MAP(VARCHAR, JSON) as a generic container;
813
      // JSON() is safe for all output formats and is not affected by ExchangeNullType
814
41
      return LogicalType::MAP(LogicalType::VARCHAR, LogicalType::JSON());
815
41
    } else {
816
0
      return LogicalType::JSON();
817
0
    }
818
41
  }
819
820
  // An empty key can't be a STRUCT member name, so such objects can only be a MAP or JSON
821
28
  bool has_empty_key = false;
822
28
  for (auto &child : desc.children) {
823
28
    D_ASSERT(child.key);
824
28
    has_empty_key = has_empty_key || child.key->empty();
825
28
  }
826
28
  if (has_empty_key && map_inference_threshold == DConstants::INVALID_INDEX) {
827
0
    return LogicalType::JSON();
828
0
  }
829
830
  // If it's an inconsistent object we also just do MAP with the best-possible, recursively-merged value type
831
28
  if (has_empty_key || (map_inference_threshold != DConstants::INVALID_INDEX &&
832
20
                        IsStructureInconsistent(desc, node.count, node.null_count, field_appearance_threshold))) {
833
8
    return LogicalType::MAP(LogicalType::VARCHAR,
834
8
                            GetMergedType(context, node, max_depth, field_appearance_threshold,
835
8
                                          map_inference_threshold, depth + 1, null_type));
836
8
  }
837
838
  // We have a consistent object
839
20
  child_list_t<LogicalType> child_types;
840
20
  child_types.reserve(desc.children.size());
841
20
  for (auto &child : desc.children) {
842
20
    D_ASSERT(child.key);
843
20
    child_types.emplace_back(*child.key,
844
20
                             JSONStructure::StructureToType(context, child, max_depth, field_appearance_threshold,
845
20
                                                            map_inference_threshold, depth + 1, null_type));
846
20
  }
847
848
  // If we have many children and all children have similar-enough types we infer map
849
20
  if (desc.children.size() >= map_inference_threshold) {
850
0
    LogicalType map_value_type = GetMergedType(context, node, max_depth, field_appearance_threshold,
851
0
                                               map_inference_threshold, depth + 1, LogicalTypeId::SQLNULL);
852
853
0
    double total_similarity = 0;
854
0
    for (const auto &child_type : child_types) {
855
0
      const auto similarity = CalculateTypeSimilarity(map_value_type, child_type.second, max_depth, depth + 1);
856
0
      if (similarity < 0) {
857
0
        total_similarity = similarity;
858
0
        break;
859
0
      }
860
0
      total_similarity += similarity;
861
0
    }
862
0
    const auto avg_similarity = total_similarity / static_cast<double>(child_types.size());
863
0
    if (avg_similarity >= 0.8) {
864
0
      if (null_type != LogicalTypeId::SQLNULL) {
865
0
        map_value_type = GetMergedType(context, node, max_depth, field_appearance_threshold,
866
0
                                       map_inference_threshold, depth + 1, null_type);
867
0
      }
868
0
      return LogicalType::MAP(LogicalType::VARCHAR, map_value_type);
869
0
    }
870
0
  }
871
872
20
  return LogicalType::STRUCT(child_types);
873
20
}
874
875
361
static LogicalType StructureToTypeString(const JSONStructureNode &node) {
876
361
  D_ASSERT(node.descriptions.size() == 1 && node.descriptions[0].type == LogicalTypeId::VARCHAR);
877
361
  auto &desc = node.descriptions[0];
878
361
  if (desc.candidate_types.empty()) {
879
358
    return LogicalTypeId::VARCHAR;
880
358
  }
881
3
  return desc.candidate_types.back();
882
361
}
883
884
LogicalType JSONStructure::StructureToType(ClientContext &context, const JSONStructureNode &node, const idx_t max_depth,
885
                                           const double field_appearance_threshold, const idx_t map_inference_threshold,
886
2.10k
                                           const idx_t depth, const LogicalType &null_type) {
887
2.10k
  if (depth >= max_depth) {
888
0
    return LogicalType::JSON();
889
0
  }
890
2.10k
  if (node.descriptions.empty()) {
891
191
    return LogicalType::JSON(); // no data observed, fall back to generic JSON type
892
191
  }
893
1.91k
  if (node.descriptions.size() != 1) { // Inconsistent types, so we resort to JSON
894
11
    return LogicalType::JSON();
895
11
  }
896
1.90k
  auto &desc = node.descriptions[0];
897
1.90k
  D_ASSERT(desc.type != LogicalTypeId::INVALID);
898
1.90k
  switch (desc.type) {
899
646
  case LogicalTypeId::LIST:
900
646
    return StructureToTypeArray(context, node, max_depth, field_appearance_threshold, map_inference_threshold,
901
646
                                depth, null_type);
902
69
  case LogicalTypeId::STRUCT:
903
69
    return StructureToTypeObject(context, node, max_depth, field_appearance_threshold, map_inference_threshold,
904
69
                                 depth, null_type);
905
361
  case LogicalTypeId::VARCHAR:
906
361
    return StructureToTypeString(node);
907
382
  case LogicalTypeId::UBIGINT:
908
382
    if (desc.has_large_ubigint) {
909
29
      return LogicalTypeId::HUGEINT;
910
29
    }
911
353
    return LogicalTypeId::BIGINT;
912
4
  case LogicalTypeId::SQLNULL:
913
4
    return LogicalType::SQLNULL;
914
0
  case LogicalTypeId::GEOMETRY:
915
0
    return LogicalType::GEOMETRY();
916
441
  default:
917
441
    return desc.type;
918
1.90k
  }
919
1.90k
}
920
921
} // namespace duckdb