File: result_helper.cpp

package info (click to toggle)
duckdb 1.5.1-3
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 299,196 kB
  • sloc: cpp: 865,414; ansic: 57,292; python: 18,871; sql: 12,663; lisp: 11,751; yacc: 7,412; lex: 1,682; sh: 747; makefile: 564
file content (598 lines) | stat: -rw-r--r-- 21,076 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
#include "result_helper.hpp"

#include "catch.hpp"
#include "duckdb/common/crypto/md5.hpp"
#include "duckdb/parser/qualified_name.hpp"
#include "re2/re2.h"
#include "sqllogic_test_logger.hpp"
#include "sqllogic_test_runner.hpp"
#include "termcolor.hpp"
#include "test_helpers.hpp"
#include "test_config.hpp"

#include <thread>

namespace duckdb {

void TestResultHelper::SortQueryResult(SortStyle sort_style, vector<string> &result, idx_t ncols) {
	if (sort_style == SortStyle::NO_SORT) {
		return;
	}
	if (sort_style == SortStyle::VALUE_SORT) {
		// sort values independently
		std::sort(result.begin(), result.end());
		return;
	}
	if (result.size() % ncols != 0) {
		// row-sort failed: result is not row-wise aligned, bail
		FAIL(StringUtil::Format("Failed to sort query result - result is not aligned. Found %d rows with %d columns",
		                        result.size(), ncols));
		return;
	}
	// row-oriented sorting
	idx_t nrows = result.size() / ncols;
	vector<vector<string>> rows;
	rows.reserve(nrows);
	for (idx_t row_idx = 0; row_idx < nrows; row_idx++) {
		vector<string> row;
		row.reserve(ncols);
		for (idx_t col_idx = 0; col_idx < ncols; col_idx++) {
			row.push_back(std::move(result[row_idx * ncols + col_idx]));
		}
		rows.push_back(std::move(row));
	}
	// sort the individual rows
	std::sort(rows.begin(), rows.end(), [](const vector<string> &a, const vector<string> &b) {
		for (idx_t col_idx = 0; col_idx < a.size(); col_idx++) {
			if (a[col_idx] != b[col_idx]) {
				return a[col_idx] < b[col_idx];
			}
		}
		return false;
	});

	// now reconstruct the values from the rows
	for (idx_t row_idx = 0; row_idx < nrows; row_idx++) {
		for (idx_t col_idx = 0; col_idx < ncols; col_idx++) {
			result[row_idx * ncols + col_idx] = std::move(rows[row_idx][col_idx]);
		}
	}
}

bool TestResultHelper::CheckQueryResult(const Query &query, ExecuteContext &context,
                                        duckdb::unique_ptr<MaterializedQueryResult> owned_result) {
	auto &result = *owned_result;
	auto &runner = query.runner;
	auto expected_column_count = query.expected_column_count;
	auto &values = query.values;
	auto sort_style = query.sort_style;
	auto query_has_label = query.query_has_label;
	auto &query_label = query.query_label;

	SQLLogicTestLogger logger(context, query);
	if (result.HasError()) {
		if (SkipErrorMessage(result.GetError())) {
			runner.finished_processing_file = true;
			return true;
		}
		if (!FailureSummary::SkipLoggingSameError(context.error_file)) {
			logger.UnexpectedFailure(result);
		}
		return false;
	}
	idx_t row_count = result.RowCount();
	idx_t column_count = result.ColumnCount();
	idx_t total_value_count = row_count * column_count;
	bool compare_hash =
	    query_has_label || (runner.hash_threshold > 0 && total_value_count > idx_t(runner.hash_threshold));
	bool result_is_hash = false;
	// check if the current line (the first line of the result) is a hash value
	if (values.size() == 1 && ResultIsHash(values[0])) {
		compare_hash = true;
		result_is_hash = true;
	}

	vector<string> result_values_string;
	try {
		DuckDBConvertResult(result, runner.original_sqlite_test, result_values_string);
		if (runner.output_result_mode) {
			logger.OutputResult(result, result_values_string);
		}
	} catch (std::exception &ex) {
		ErrorData error(ex);
		auto &original_error = error.Message();
		logger.LogFailure(original_error);
		return false;
	}

	SortQueryResult(sort_style, result_values_string, column_count);

	vector<string> comparison_values;
	if (values.size() == 1 && ResultIsFile(values[0])) {
		auto fname = StringUtil::Replace(values[0], "<FILE>:", "");
		fname = runner.ReplaceKeywords(fname);
		fname = runner.LoopReplacement(fname, context.running_loops);
		string csv_error;
		comparison_values = LoadResultFromFile(fname, result.names, expected_column_count, csv_error);
		if (!csv_error.empty()) {
			string log_message;
			logger.PrintErrorHeader(csv_error);

			return false;
		}
	} else {
		comparison_values = values;
	}

	// compute the hash of the results if there is a hash label or we are past the hash threshold
	string hash_value;
	if (runner.output_hash_mode || compare_hash) {
		MD5Context context;
		for (idx_t i = 0; i < total_value_count; i++) {
			context.Add(result_values_string[i]);
			context.Add("\n");
		}
		string digest = context.FinishHex();
		hash_value = to_string(total_value_count) + " values hashing to " + digest;
		if (runner.output_hash_mode) {
			logger.OutputHash(hash_value);
			return true;
		}
	}

	if (!compare_hash) {
		// check if the row/column count matches
		idx_t original_expected_columns = expected_column_count;
		bool column_count_mismatch = false;
		if (expected_column_count != result.ColumnCount()) {
			// expected column count is different from the count found in the result
			// we try to keep going with the number of columns in the result
			expected_column_count = result.ColumnCount();
			column_count_mismatch = true;
		}
		if (expected_column_count == 0) {
			return false;
		}
		idx_t expected_rows = comparison_values.size() / expected_column_count;
		// we first check the counts: if the values are equal to the amount of rows we expect the results to be row-wise
		bool row_wise = expected_column_count > 1 && comparison_values.size() == result.RowCount();
		if (!row_wise) {
			// the counts do not match up for it to be row-wise
			// however, this can also be because the query returned an incorrect # of rows
			// we make a guess: if everything contains tabs, we still treat the input as row wise
			bool all_tabs = true;
			for (auto &val : comparison_values) {
				if (val.find('\t') == string::npos) {
					all_tabs = false;
					break;
				}
			}
			row_wise = all_tabs;
		}
		if (row_wise) {
			// values are displayed row-wise, format row wise with a tab
			expected_rows = comparison_values.size();
			row_wise = true;
		} else if (comparison_values.size() % expected_column_count != 0) {
			if (column_count_mismatch) {
				logger.ColumnCountMismatch(result, query.values, original_expected_columns, row_wise);
			} else {
				logger.NotCleanlyDivisible(expected_column_count, comparison_values.size());
			}
			return false;
		}
		if (expected_rows != result.RowCount()) {
			if (column_count_mismatch) {
				logger.ColumnCountMismatch(result, query.values, original_expected_columns, row_wise);
			} else {
				logger.WrongRowCount(expected_rows, result, comparison_values, expected_column_count, row_wise);
			}
			return false;
		}

		if (row_wise) {
			// if the result is row-wise, turn it into a set of values by splitting it
			vector<string> expected_values;
			for (idx_t i = 0; i < total_value_count && i < comparison_values.size(); i++) {
				// split based on tab character
				auto splits = StringUtil::Split(comparison_values[i], "\t");
				if (splits.size() != expected_column_count) {
					if (column_count_mismatch) {
						logger.ColumnCountMismatch(result, query.values, original_expected_columns, row_wise);
					}
					logger.SplitMismatch(i + 1, expected_column_count, splits.size());
					return false;
				}
				for (auto &split : splits) {
					expected_values.push_back(std::move(split));
				}
			}
			comparison_values = std::move(expected_values);
			row_wise = false;
		}
		auto &test_config = TestConfiguration::Get();
		auto default_sort_style = test_config.GetDefaultSortStyle();
		idx_t check_it_count = column_count_mismatch || default_sort_style == SortStyle::NO_SORT ? 1 : 2;
		for (idx_t check_it = 0; check_it < check_it_count; check_it++) {
			bool final_iteration = check_it + 1 == check_it_count;
			idx_t current_row = 0, current_column = 0;
			bool success = true;
			for (idx_t i = 0; i < total_value_count && i < comparison_values.size(); i++) {
				success = CompareValues(logger, result,
				                        result_values_string[current_row * expected_column_count + current_column],
				                        comparison_values[i], current_row, current_column, comparison_values,
				                        expected_column_count, row_wise, result_values_string, final_iteration);
				if (!success) {
					break;
				}
				// we do this just to increment the assertion counter
				string success_log = StringUtil::Format("CheckQueryResult: %s:%d", query.file_name, query.query_line);
				REQUIRE(success_log.c_str());

				current_column++;
				if (current_column == expected_column_count) {
					current_row++;
					current_column = 0;
				}
			}
			if (!success) {
				if (final_iteration) {
					return false;
				}
				SortQueryResult(default_sort_style, result_values_string, column_count);
				SortQueryResult(default_sort_style, comparison_values, query.expected_column_count);
			}
		}
		if (column_count_mismatch) {
			logger.ColumnCountMismatchCorrectResult(original_expected_columns, expected_column_count, result);
			return false;
		}
	} else {
		bool hash_compare_error = false;
		if (query_has_label) {
			runner.hash_label_map.WithLock([&](unordered_map<string, CachedLabelData> &map) {
				// the query has a label: check if the hash has already been computed
				auto entry = map.find(query_label);
				if (entry == map.end()) {
					// not computed yet: add it tot he map
					map.emplace(query_label, CachedLabelData(hash_value, logger.ResultToString(*owned_result)));
				} else {
					hash_compare_error = entry->second.hash != hash_value;
				}
			});
		}
		string expected_hash;
		if (result_is_hash) {
			expected_hash = values[0];
			D_ASSERT(values.size() == 1);
			hash_compare_error = expected_hash != hash_value;
		}
		if (hash_compare_error) {
			string expected_result;
			runner.hash_label_map.WithLock([&](unordered_map<string, CachedLabelData> &map) {
				auto it = map.find(query_label);
				if (it != map.end()) {
					expected_result = it->second.result_str;
				}
				logger.WrongResultHash(expected_result, result, expected_hash, hash_value);
			});
			return false;
		}
		REQUIRE(!hash_compare_error);
	}
	return true;
}

bool TestResultHelper::CheckStatementResult(const Statement &statement, ExecuteContext &context,
                                            duckdb::unique_ptr<MaterializedQueryResult> owned_result) {
	auto &result = *owned_result;
	bool error = result.HasError();
	SQLLogicTestLogger logger(context, statement);
	if (runner.output_result_mode || runner.debug_mode) {
		result.Print();
	}

	/* Check to see if we are expecting success or failure */
	auto expected_result = statement.expected_result;
	if (expected_result != ExpectedResult::RESULT_SUCCESS) {
		// even in the case of "statement error", we do not accept ALL errors
		// internal errors are never expected
		// neither are "unoptimized result differs from original result" errors

		if (result.HasError() && TestIsInternalError(runner.always_fail_error_messages, result.GetError())) {
			logger.InternalException(result);
			return false;
		}
		if (expected_result == ExpectedResult::RESULT_UNKNOWN || expected_result == ExpectedResult::RESULT_DONT_CARE) {
			error = false;
		} else {
			error = !error;
		}
		if (result.HasError() && !statement.expected_error.empty()) {
			// We run both comparions on purpose, we might move to only the second but might require some changes in
			// tests
			// This is due to some errors containing absolute paths, some relatives
			if (!StringUtil::Contains(result.GetError(), statement.expected_error) &&
			    !StringUtil::Contains(result.GetError(), runner.ReplaceKeywords(statement.expected_error))) {
				bool success = false;
				if (StringUtil::StartsWith(statement.expected_error, "<REGEX>:") ||
				    StringUtil::StartsWith(statement.expected_error, "<!REGEX>:")) {
					success = MatchesRegex(logger, result.ToString(), statement.expected_error);
				}
				if (!success) {
					// don't log the same test failure many times:
					// e.g. log only the first failure in
					// `./build/debug/test/unittest --on-init "SET max_memory='400kb';"
					// test/fuzzer/pedro/concurrent_catalog_usage.test`
					if (!SkipErrorMessage(result.GetError()) &&
					    !FailureSummary::SkipLoggingSameError(statement.file_name)) {
						logger.ExpectedErrorMismatch(statement.expected_error, result);
						return false;
					}
				}
				string success_log =
				    StringUtil::Format("CheckStatementResult: %s:%d", statement.file_name, statement.query_line);
				REQUIRE(success_log.c_str());
				return true;
			}
		}
	}

	/* Report an error if the results do not match expectation */
	if (error) {
		if (expected_result == ExpectedResult::RESULT_SUCCESS && SkipErrorMessage(result.GetError())) {
			runner.finished_processing_file = true;
			return true;
		}
		if (!FailureSummary::SkipLoggingSameError(statement.file_name)) {
			logger.UnexpectedStatement(expected_result == ExpectedResult::RESULT_SUCCESS, result);
		}
		return false;
	}
	if (error) {
		REQUIRE(false);
	} else {
		string success_log =
		    StringUtil::Format("CheckStatementResult: %s:%d", statement.file_name, statement.query_line);
		REQUIRE(success_log.c_str());
	}
	return true;
}

vector<string> TestResultHelper::LoadResultFromFile(string fname, vector<string> names, idx_t &expected_column_count,
                                                    string &error) {
	DuckDB db(nullptr);
	Connection con(db);
	auto threads = MaxValue<idx_t>(std::thread::hardware_concurrency(), 1);
	con.Query("PRAGMA threads=" + to_string(threads));

	string struct_definition = "STRUCT_PACK(";
	for (idx_t i = 0; i < names.size(); i++) {
		if (i > 0) {
			struct_definition += ", ";
		}
		struct_definition += StringUtil::Format("%s := VARCHAR", SQLIdentifier(names[i]));
	}
	struct_definition += ")";

	auto csv_result = con.Query("SELECT * FROM read_csv('" + fname +
	                            "', header=1, sep='|', columns=" + struct_definition + ", auto_detect=false)");
	if (csv_result->HasError()) {
		error = StringUtil::Format("Could not read CSV File \"%s\": %s", fname, csv_result->GetError());
		return vector<string>();
	}
	expected_column_count = csv_result->ColumnCount();

	vector<string> values;
	while (true) {
		auto chunk = csv_result->Fetch();
		if (!chunk || chunk->size() == 0) {
			break;
		}
		for (idx_t r = 0; r < chunk->size(); r++) {
			for (idx_t c = 0; c < chunk->ColumnCount(); c++) {
				values.push_back(chunk->GetValue(c, r).CastAs(*runner.con->context, LogicalType::VARCHAR).ToString());
			}
		}
	}
	return values;
}

bool TestResultHelper::SkipErrorMessage(const string &message) {
	for (auto &error_message : runner.ignore_error_messages) {
		if (StringUtil::Contains(message, error_message)) {
			SKIP_TEST(string("skip on error_message matching '") + error_message + string("'"));
			return true;
		}
	}
	return false;
}

string TestResultHelper::SQLLogicTestConvertValue(Value value, LogicalType sql_type, bool original_sqlite_test) {
	if (value.IsNull()) {
		return "NULL";
	} else {
		if (original_sqlite_test) {
			// sqlite test hashes want us to convert floating point numbers to integers
			switch (sql_type.id()) {
			case LogicalTypeId::DECIMAL:
			case LogicalTypeId::FLOAT:
			case LogicalTypeId::DOUBLE:
				return value.CastAs(*runner.con->context, LogicalType::BIGINT).ToString();
			default:
				break;
			}
		}
		switch (sql_type.id()) {
		case LogicalTypeId::BOOLEAN:
			return BooleanValue::Get(value) ? "1" : "0";
		default: {
			string str = value.CastAs(*runner.con->context, LogicalType::VARCHAR).ToString();
			if (str.empty()) {
				return "(empty)";
			} else {
				return StringUtil::Replace(str, string("\0", 1), "\\0");
			}
		}
		}
	}
}

// standard result conversion: one line per value
void TestResultHelper::DuckDBConvertResult(MaterializedQueryResult &result, bool original_sqlite_test,
                                           vector<string> &out_result) {
	size_t r, c;
	idx_t row_count = result.RowCount();
	idx_t column_count = result.ColumnCount();

	out_result.resize(row_count * column_count);
	for (r = 0; r < row_count; r++) {
		for (c = 0; c < column_count; c++) {
			auto value = result.GetValue(c, r);
			auto converted_value = SQLLogicTestConvertValue(value, result.types[c], original_sqlite_test);
			out_result[r * column_count + c] = converted_value;
		}
	}
}

bool TestResultHelper::ResultIsHash(const string &result) {
	idx_t pos = 0;
	// first parse the rows
	while (result[pos] >= '0' && result[pos] <= '9') {
		pos++;
	}
	if (pos == 0) {
		return false;
	}
	string constant_str = " values hashing to ";
	string example_hash = "acd848208cc35c7324ece9fcdd507823";
	if (pos + constant_str.size() + example_hash.size() != result.size()) {
		return false;
	}
	if (result.substr(pos, constant_str.size()) != constant_str) {
		return false;
	}
	pos += constant_str.size();
	// now parse the hash
	while ((result[pos] >= '0' && result[pos] <= '9') || (result[pos] >= 'a' && result[pos] <= 'z')) {
		pos++;
	}
	return pos == result.size();
}

bool TestResultHelper::ResultIsFile(string result) {
	return StringUtil::StartsWith(result, "<FILE>:");
}

bool TestResultHelper::CompareValues(SQLLogicTestLogger &logger, MaterializedQueryResult &result, string lvalue_str,
                                     string rvalue_str, idx_t current_row, idx_t current_column, vector<string> &values,
                                     idx_t expected_column_count, bool row_wise, vector<string> &result_values,
                                     bool print_error) {
	Value lvalue, rvalue;
	bool error = false;
	// simple first test: compare string value directly
	// We run both comparions on purpose, we might move to only the second but might require some changes in tests
	// This is due to some results containing absolute paths, some relatives
	if (lvalue_str == rvalue_str || lvalue_str == runner.ReplaceKeywords(rvalue_str)) {
		return true;
	}
	if (StringUtil::StartsWith(rvalue_str, "<REGEX>:") || StringUtil::StartsWith(rvalue_str, "<!REGEX>:")) {
		if (MatchesRegex(logger, lvalue_str, rvalue_str)) {
			return true;
		}
	}
	// some times require more checking (specifically floating point numbers because of inaccuracies)
	// if not equivalent we need to cast to the SQL type to verify
	auto sql_type = result.types[current_column];
	if (sql_type.IsNumeric()) {
		bool converted_lvalue = false;
		bool converted_rvalue = false;
		if (lvalue_str == "NULL") {
			lvalue = Value(sql_type);
			converted_lvalue = true;
		} else {
			lvalue = Value(lvalue_str);
			if (lvalue.TryCastAs(*runner.con->context, sql_type)) {
				converted_lvalue = true;
			}
		}
		if (rvalue_str == "NULL") {
			rvalue = Value(sql_type);
			converted_rvalue = true;
		} else {
			rvalue = Value(rvalue_str);
			if (rvalue.TryCastAs(*runner.con->context, sql_type)) {
				converted_rvalue = true;
			}
		}
		if (converted_lvalue && converted_rvalue) {
			error = !Value::ValuesAreEqual(*runner.con->context, lvalue, rvalue);
		} else {
			error = true;
		}
	} else if (sql_type == LogicalType::BOOLEAN) {
		auto low_r_val = StringUtil::Lower(rvalue_str);
		auto low_l_val = StringUtil::Lower(lvalue_str);

		string true_str = "true";
		string false_str = "false";
		if (low_l_val == true_str || lvalue_str == "1") {
			lvalue = Value(1);
		} else if (low_l_val == false_str || lvalue_str == "0") {
			lvalue = Value(0);
		}
		if (low_r_val == true_str || rvalue_str == "1") {
			rvalue = Value(1);
		} else if (low_r_val == false_str || rvalue_str == "0") {
			rvalue = Value(0);
		}
		error = !Value::ValuesAreEqual(*runner.con->context, lvalue, rvalue);

	} else {
		// for other types we just mark the result as incorrect
		error = true;
	}
	if (error) {
		if (print_error) {
			std::ostringstream oss;
			logger.PrintErrorHeader("Wrong result in query!");
			logger.PrintLineSep();
			logger.PrintSQL();
			logger.PrintLineSep();
			oss << termcolor::red << termcolor::bold << "Mismatch on row " << current_row + 1 << ", column "
			    << result.ColumnName(current_column) << "(index " << current_column + 1 << ")" << std::endl
			    << termcolor::reset;
			oss << lvalue_str << " <> " << rvalue_str << std::endl;
			logger.LogFailure(oss.str());
			logger.PrintLineSep();
			logger.PrintResultError(result_values, values, expected_column_count, row_wise);
		}
		return false;
	}
	return true;
}

bool TestResultHelper::MatchesRegex(SQLLogicTestLogger &logger, string lvalue_str, string rvalue_str) {
	bool want_match = StringUtil::StartsWith(rvalue_str, "<REGEX>:");
	string regex_str = StringUtil::Replace(StringUtil::Replace(rvalue_str, "<REGEX>:", ""), "<!REGEX>:", "");
	RE2::Options options;
	options.set_dot_nl(true);
	RE2 re(regex_str, options);
	if (!re.ok()) {
		std::ostringstream oss;
		logger.PrintErrorHeader("Test error!");
		logger.PrintLineSep();
		oss << termcolor::red << termcolor::bold << "Failed to parse regex: " << re.error() << termcolor::reset
		    << std::endl;
		logger.LogFailure(oss.str());
		logger.PrintLineSep();
		return false;
	}
	bool regex_matches = RE2::FullMatch(lvalue_str, re);
	if ((want_match && regex_matches) || (!want_match && !regex_matches)) {
		return true;
	}
	return false;
}

} // namespace duckdb