Files
email-tracker/external/duckdb/test/sqlite/sqllogic_test_runner.cpp
2025-10-24 19:21:19 -05:00

1238 lines
40 KiB
C++

#include "sqllogic_test_runner.hpp"
#include "catch.hpp"
#include "duckdb/common/file_open_flags.hpp"
#include "duckdb/common/virtual_file_system.hpp"
#include "duckdb/main/extension/generated_extension_loader.hpp"
#include "duckdb/common/types/uuid.hpp"
#include "duckdb/main/extension_entries.hpp"
#include "duckdb/main/extension_helper.hpp"
#include "sqllogic_parser.hpp"
#include "test_helpers.hpp"
#include "sqllogic_test_logger.hpp"
#include "duckdb/common/random_engine.hpp"
#ifdef DUCKDB_OUT_OF_TREE
#include DUCKDB_EXTENSION_HEADER
#endif
namespace duckdb {
SQLLogicTestRunner::SQLLogicTestRunner(string dbpath) : dbpath(std::move(dbpath)), finished_processing_file(false) {
config = GetTestConfig();
config->options.allow_unredacted_secrets = true;
config->options.load_extensions = false;
auto &test_config = TestConfiguration::Get();
autoloading_mode = test_config.GetExtensionAutoLoadingMode();
config->options.autoload_known_extensions = false;
config->options.autoinstall_known_extensions = false;
config->options.allow_unsigned_extensions = true;
local_extension_repo = "";
autoinstall_is_checked = false;
switch (autoloading_mode) {
case TestConfiguration::ExtensionAutoLoadingMode::NONE: {
break;
}
case TestConfiguration::ExtensionAutoLoadingMode::AVAILABLE: {
autoinstall_is_checked = true;
config->options.autoload_known_extensions = true;
break;
}
case TestConfiguration::ExtensionAutoLoadingMode::ALL: {
autoinstall_is_checked = false;
config->options.autoload_known_extensions = true;
config->options.autoinstall_known_extensions = true;
break;
}
}
auto env_var = std::getenv("LOCAL_EXTENSION_REPO");
if (env_var) {
local_extension_repo = env_var;
config->options.autoload_known_extensions = true;
config->options.autoinstall_known_extensions = true;
} else if (config->options.autoload_known_extensions) {
local_extension_repo = string(DUCKDB_BUILD_DIRECTORY) + "/repository";
}
for (auto &entry : test_config.GetConfigSettings()) {
config->SetOptionByName(entry.name, entry.value);
}
}
SQLLogicTestRunner::~SQLLogicTestRunner() {
config.reset();
con.reset();
db.reset();
for (auto &loaded_path : loaded_databases) {
if (loaded_path.empty()) {
continue;
}
// only delete database files that were created during the tests
if (!StringUtil::StartsWith(loaded_path, TestDirectoryPath())) {
continue;
}
DeleteDatabase(loaded_path);
}
}
void SQLLogicTestRunner::ExecuteCommand(duckdb::unique_ptr<Command> command) {
if (InLoop()) {
auto &current_loop = *active_loops.back();
current_loop.loop_commands.push_back(std::move(command));
} else {
ExecuteContext context;
command->Execute(context);
}
}
void SQLLogicTestRunner::StartLoop(LoopDefinition definition) {
auto loop = make_uniq<LoopCommand>(*this, std::move(definition));
auto loop_ptr = loop.get();
if (InLoop()) {
auto &current_loop = *active_loops.back();
current_loop.loop_commands.push_back(std::move(loop));
} else {
// not in a loop yet: new top-level loop
top_level_loop = std::move(loop);
}
active_loops.push_back(loop_ptr);
}
void SQLLogicTestRunner::EndLoop() {
// finish a loop: pop it from the active_loop queue
if (active_loops.empty()) {
throw std::runtime_error("endloop without active loop!");
}
active_loops.pop_back();
if (active_loops.empty()) {
// not in a loop
ExecuteContext context;
top_level_loop->Execute(context);
top_level_loop.reset();
}
}
ExtensionLoadResult SQLLogicTestRunner::LoadExtension(DuckDB &db, const std::string &extension) {
auto &test_config = TestConfiguration::Get();
if (test_config.GetExtensionAutoLoadingMode() != TestConfiguration::ExtensionAutoLoadingMode::NONE) {
// try LOAD extension
Connection con(db);
auto result = con.Query("LOAD " + extension);
if (!result->HasError()) {
return ExtensionLoadResult::LOADED_EXTENSION;
}
}
return ExtensionHelper::LoadExtension(db, extension);
}
void SQLLogicTestRunner::LoadDatabase(string dbpath, bool load_extensions) {
loaded_databases.push_back(dbpath);
// restart the database with the specified db path
db.reset();
con.reset();
named_connection_map.clear();
// now re-open the current database
try {
db = make_uniq<DuckDB>(dbpath, config.get());
// always load core functions
auto &test_config = TestConfiguration::Get();
for (auto ext : test_config.ExtensionToBeLoadedOnLoad()) {
SQLLogicTestRunner::LoadExtension(*db, ext);
}
} catch (std::exception &ex) {
ErrorData err(ex);
SQLLogicTestLogger::LoadDatabaseFail(file_name, dbpath, err.Message());
FAIL();
}
Reconnect();
// load any previously loaded extensions again
if (load_extensions) {
for (auto &extension : extensions) {
SQLLogicTestRunner::LoadExtension(*db, extension);
}
}
}
void SQLLogicTestRunner::Reconnect() {
con = make_uniq<Connection>(*db);
if (original_sqlite_test) {
con->Query("SET integer_division=true");
}
con->Query("SET secret_directory='" + TestCreatePath("test_secret_dir") + "'");
#ifdef DUCKDB_ALTERNATIVE_VERIFY
con->Query("SET pivot_filter_threshold=0");
#endif
auto &client_config = ClientConfig::GetConfig(*con->context);
client_config.enable_progress_bar = true;
client_config.print_progress_bar = false;
if (enable_verification) {
con->EnableQueryVerification();
}
// Set the local extension repo for autoinstalling extensions
if (!local_extension_repo.empty()) {
auto res1 = con->Query("SET autoinstall_extension_repository='" + local_extension_repo + "'");
}
auto &test_config = TestConfiguration::Get();
auto init_cmd = test_config.OnInitCommand() + ";" + test_config.OnConnectionCommand();
if (!init_cmd.empty()) {
test_config.ProcessPath(init_cmd, file_name);
auto res = con->Query(ReplaceKeywords(init_cmd));
if (res->HasError()) {
FAIL("Startup queries provided via on_init failed: " + res->GetError());
}
}
}
string SQLLogicTestRunner::ReplaceLoopIterator(string text, string loop_iterator_name, string replacement) {
replacement = ReplaceKeywords(replacement);
if (StringUtil::Contains(loop_iterator_name, ",")) {
auto name_splits = StringUtil::Split(loop_iterator_name, ",");
auto replacement_splits = StringUtil::Split(replacement, ",");
if (name_splits.size() != replacement_splits.size()) {
FAIL("foreach loop: number of commas in loop iterator (" + loop_iterator_name +
") does not match number of commas in replacement (" + replacement + ")");
}
for (idx_t i = 0; i < name_splits.size(); i++) {
text = StringUtil::Replace(text, "${" + name_splits[i] + "}", replacement_splits[i]);
}
return text;
} else {
return StringUtil::Replace(text, "${" + loop_iterator_name + "}", replacement);
}
}
string SQLLogicTestRunner::LoopReplacement(string text, const vector<LoopDefinition> &loops) {
for (auto &active_loop : loops) {
if (active_loop.tokens.empty()) {
// regular loop
text = ReplaceLoopIterator(text, active_loop.loop_iterator_name, to_string(active_loop.loop_idx));
} else {
// foreach loop
text = ReplaceLoopIterator(text, active_loop.loop_iterator_name, active_loop.tokens[active_loop.loop_idx]);
}
}
return text;
}
string SQLLogicTestRunner::ReplaceKeywords(string input) {
// Replace environment variables in the SQL
for (auto &it : environment_variables) {
auto &name = it.first;
auto &value = it.second;
input = StringUtil::Replace(input, StringUtil::Format("${%s}", name), value);
}
auto &test_config = TestConfiguration::Get();
test_config.ProcessPath(input, file_name);
input = StringUtil::Replace(input, "__BUILD_DIRECTORY__", DUCKDB_BUILD_DIRECTORY);
string data_location = test_config.DataLocation();
input = StringUtil::Replace(input, "'data/", string("'") + data_location);
input = StringUtil::Replace(input, "\"data/", string("\"") + data_location);
if (StringUtil::StartsWith(input, "data/")) {
input = data_location + input.substr(5);
}
return input;
}
bool SQLLogicTestRunner::ForEachTokenReplace(const string &parameter, vector<string> &result) {
if (parameter.empty()) {
return true;
}
auto token_name = StringUtil::Lower(parameter);
StringUtil::Trim(token_name);
bool collection = false;
bool is_compression = token_name == "<compression>";
bool is_all = token_name == "<alltypes>";
bool is_numeric = is_all || token_name == "<numeric>";
bool is_integral = is_numeric || token_name == "<integral>";
bool is_signed = is_integral || token_name == "<signed>";
bool is_unsigned = is_integral || token_name == "<unsigned>";
bool is_all_types_column = token_name == "<all_types_columns>";
if (token_name[0] == '!') {
// !token tries to remove the token from the list of tokens
auto entry = std::find(result.begin(), result.end(), parameter.substr(1));
if (entry == result.end()) {
// not found - insert as-is
return false;
}
// found - erase the entry
result.erase(entry);
collection = true;
}
if (is_signed) {
result.push_back("tinyint");
result.push_back("smallint");
result.push_back("integer");
result.push_back("bigint");
result.push_back("hugeint");
collection = true;
}
if (is_unsigned) {
result.push_back("utinyint");
result.push_back("usmallint");
result.push_back("uinteger");
result.push_back("ubigint");
result.push_back("uhugeint");
collection = true;
}
if (is_numeric) {
result.push_back("float");
result.push_back("double");
collection = true;
}
if (is_all) {
result.push_back("bool");
result.push_back("interval");
result.push_back("varchar");
collection = true;
}
if (is_compression) {
result.push_back("none");
result.push_back("uncompressed");
result.push_back("rle");
result.push_back("bitpacking");
result.push_back("dictionary");
result.push_back("fsst");
result.push_back("dict_fsst");
result.push_back("alp");
result.push_back("alprd");
collection = true;
}
if (is_all_types_column) {
result.push_back("bool");
result.push_back("tinyint");
result.push_back("smallint");
result.push_back("int");
result.push_back("bigint");
result.push_back("hugeint");
result.push_back("uhugeint");
result.push_back("utinyint");
result.push_back("usmallint");
result.push_back("uint");
result.push_back("ubigint");
result.push_back("date");
result.push_back("time");
result.push_back("timestamp");
result.push_back("timestamp_s");
result.push_back("timestamp_ms");
result.push_back("timestamp_ns");
result.push_back("time_tz");
result.push_back("timestamp_tz");
result.push_back("float");
result.push_back("double");
result.push_back("dec_4_1");
result.push_back("dec_9_4");
result.push_back("dec_18_6");
result.push_back("dec38_10");
result.push_back("uuid");
result.push_back("interval");
result.push_back("varchar");
result.push_back("blob");
result.push_back("bit");
result.push_back("small_enum");
result.push_back("medium_enum");
result.push_back("large_enum");
result.push_back("int_array");
result.push_back("double_array");
result.push_back("date_array");
result.push_back("timestamp_array");
result.push_back("timestamptz_array");
result.push_back("varchar_array");
result.push_back("nested_int_array");
result.push_back("struct");
result.push_back("struct_of_arrays");
result.push_back("array_of_structs");
result.push_back("map");
result.push_back("union");
result.push_back("fixed_int_array");
result.push_back("fixed_varchar_array");
result.push_back("fixed_nested_int_array");
result.push_back("fixed_nested_varchar_array");
result.push_back("fixed_struct_array");
result.push_back("struct_of_fixed_array");
result.push_back("fixed_array_of_int_list");
result.push_back("list_of_fixed_int_array");
collection = true;
}
return collection;
}
static string ParseExplanation(SQLLogicParser &parser, const vector<string> &params, size_t &index) {
string res;
if (params[index].empty() || params[index][0] != '"') {
parser.Fail("Quoted parameter should start with double quotes");
}
res += params[index].substr(1);
index++;
while (index < params.size()) {
res += " " + params[index];
index++;
if (res.back() == '"') {
res.pop_back();
break;
}
}
return res;
}
RequireResult SQLLogicTestRunner::CheckRequire(SQLLogicParser &parser, const vector<string> &params) {
if (params.size() < 1) {
parser.Fail("require requires a single parameter");
}
// require command
string param = StringUtil::Lower(params[0]);
// os specific stuff
if (param == "notmusl") {
#ifdef __MUSL_ENABLED__
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "notmingw") {
#ifdef __MINGW32__
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "mingw") {
#ifndef __MINGW32__
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "notwindows") {
#ifdef _WIN32
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "windows") {
#ifndef _WIN32
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "longdouble") {
#if LDBL_MANT_DIG < 54
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "64bit") {
if (sizeof(void *) != 8) {
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "noforcestorage") {
if (TestConfiguration::TestForceStorage()) {
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "nothreadsan") {
#ifdef DUCKDB_THREAD_SANITIZER
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "strinline") {
#ifdef DUCKDB_DEBUG_NO_INLINE
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "ram") {
if (params.size() != 2) {
parser.Fail("require ram requires a parameter");
}
// require a minimum amount of ram
auto required_limit = DBConfig::ParseMemoryLimit(params[1]);
auto limit = FileSystem::GetAvailableMemory();
if (!limit.IsValid()) {
return RequireResult::MISSING;
}
if (limit.GetIndex() < required_limit) {
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "disk_space") {
if (params.size() != 2) {
parser.Fail("require disk_space requires a parameter");
}
// require a minimum amount of disk space
auto required_limit = DBConfig::ParseMemoryLimit(params[1]);
auto available_space = FileSystem::GetAvailableDiskSpace(".");
if (!available_space.IsValid()) {
return RequireResult::MISSING;
}
if (available_space.GetIndex() < required_limit) {
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "vector_size") {
if (params.size() != 2) {
parser.Fail("require vector_size requires a parameter");
}
// require a specific vector size
auto required_vector_size = NumericCast<idx_t>(std::stoi(params[1]));
if (STANDARD_VECTOR_SIZE < required_vector_size) {
// vector size is too low for this test: skip it
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "exact_vector_size") {
if (params.size() != 2) {
parser.Fail("require exact_vector_size requires a parameter");
}
// require an exact vector size
auto required_vector_size = NumericCast<idx_t>(std::stoi(params[1]));
if (STANDARD_VECTOR_SIZE != required_vector_size) {
// vector size does not match the required vector size: skip it
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "block_size") {
if (params.size() != 2) {
parser.Fail("require block_size requires a parameter");
}
// require a specific block size
auto required_block_size = NumericCast<idx_t>(std::stoi(params[1]));
if (config->options.default_block_alloc_size != required_block_size) {
// block size does not match the required block size: skip it
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "skip_reload") {
skip_reload = true;
return RequireResult::PRESENT;
}
if (param == "no_alternative_verify") {
#ifdef DUCKDB_ALTERNATIVE_VERIFY
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "no_latest_storage") {
#ifdef DUCKDB_LATEST_STORAGE
return RequireResult::MISSING;
#elif defined(DUCKDB_ALTERNATIVE_VERIFY)
//! ALTERNATIVE_VERIFY also forces latest storage
return RequireResult::MISSING;
#else
return RequireResult::PRESENT;
#endif
}
if (param == "no_vector_verification") {
auto &test_config = TestConfiguration::Get();
if (test_config.GetVectorVerification() != DebugVectorVerification::NONE) {
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "no_extension_autoloading") {
if (params.size() < 2) {
parser.Fail("require no_extension_autoloading needs an explanation string");
}
size_t index = 1;
string explanation = ParseExplanation(parser, params, index);
if (explanation.rfind("EXPECTED", 0) == 0 || explanation.rfind("FIXME", 0) == 0) {
// good, explanation is properly formatted
} else {
parser.Fail(
"require no_extension_autoloading explanation string should begin with either 'EXPECTED' or FIXME'");
}
if (config->options.autoload_known_extensions) {
// If autoloading is on, we skip this test
return RequireResult::MISSING;
}
return RequireResult::PRESENT;
}
if (param == "allow_unsigned_extensions") {
if (config->options.allow_unsigned_extensions) {
return RequireResult::PRESENT;
}
return RequireResult::MISSING;
}
bool excluded_from_autoloading = true;
for (const auto &ext : AUTOLOADABLE_EXTENSIONS) {
if (ext == param) {
excluded_from_autoloading = false;
break;
}
}
bool perform_install = false;
bool perform_load = false;
if (!config->options.autoload_known_extensions) {
auto result = ExtensionLoadResult::NOT_LOADED;
try {
result = SQLLogicTestRunner::LoadExtension(*db, param);
} catch (std::exception &ex) {
ErrorData error_data(ex);
parser.Fail("extension '%s' load threw an exception: %s", param, error_data.Message());
}
if (result == ExtensionLoadResult::LOADED_EXTENSION) {
// add the extension to the list of loaded extensions
extensions.insert(param);
} else if (result == ExtensionLoadResult::EXTENSION_UNKNOWN) {
parser.Fail("unknown extension type: %s", params[0]);
} else if (result == ExtensionLoadResult::NOT_LOADED) {
// extension known but not build: skip this test
return RequireResult::MISSING;
}
} else if (excluded_from_autoloading) {
if (autoloading_mode == TestConfiguration::ExtensionAutoLoadingMode::NONE) {
// This is needed to still support LOCAL_EXTENSION_REPO
return RequireResult::MISSING;
}
perform_install = true;
perform_load = true;
} else if (autoloading_mode != TestConfiguration::ExtensionAutoLoadingMode::NONE && autoinstall_is_checked) {
perform_install = true;
}
if (perform_install) {
auto res = con->Query("INSTALL " + param + " FROM '" + local_extension_repo + "';");
if (res->HasError()) {
return RequireResult::MISSING;
}
}
if (perform_load) {
auto res = con->Query("LOAD " + param + ";");
if (res->HasError()) {
return RequireResult::MISSING;
}
extensions.insert(param);
}
return RequireResult::PRESENT;
}
bool TryParseConditions(SQLLogicParser &parser, const string &condition_text, vector<Condition> &conditions,
bool skip_if) {
bool is_condition = false;
for (auto &c : condition_text) {
switch (c) {
case '=':
case '>':
case '<':
is_condition = true;
break;
default:
break;
}
}
if (!is_condition) {
// not a condition
return false;
}
// split based on &&
auto condition_strings = StringUtil::Split(condition_text, "&&");
for (auto &condition_str : condition_strings) {
vector<pair<string, ExpressionType>> comparators {
{"<>", ExpressionType::COMPARE_NOTEQUAL}, {">=", ExpressionType::COMPARE_GREATERTHANOREQUALTO},
{">", ExpressionType::COMPARE_GREATERTHAN}, {"<=", ExpressionType::COMPARE_LESSTHANOREQUALTO},
{"<", ExpressionType::COMPARE_LESSTHAN}, {"=", ExpressionType::COMPARE_EQUAL}};
ExpressionType comparison_type = ExpressionType::INVALID;
vector<string> splits;
for (auto &comparator : comparators) {
if (!StringUtil::Contains(condition_str, comparator.first)) {
continue;
}
splits = StringUtil::Split(condition_str, comparator.first);
comparison_type = comparator.second;
break;
}
// loop condition, e.g. skipif threadid=0
if (splits.size() != 2) {
parser.Fail("skipif/onlyif must be in the form of x=y or x>y, potentially separated by &&");
}
// strip white space
for (auto &split : splits) {
StringUtil::Trim(split);
}
// now create the condition
Condition condition;
condition.keyword = splits[0];
condition.value = splits[1];
condition.comparison = comparison_type;
condition.skip_if = skip_if;
conditions.push_back(condition);
}
return true;
}
// add implicit tags from environment variables, with value if available
void add_env_tag(vector<string> &tags, const string &name, const string *value = nullptr) {
tags.emplace_back(StringUtil::Format("env[%s]", name));
if (value != nullptr) {
tags.emplace_back(StringUtil::Format("env[%s]=%s", name, *value));
}
}
void SQLLogicTestRunner::ExecuteFile(string script) {
auto &test_config = TestConfiguration::Get();
if (test_config.ShouldSkipTest(script)) {
SKIP_TEST("config skip_tests");
return;
}
file_name = script;
SQLLogicParser parser;
idx_t skip_level = 0;
bool test_expr_executed = false;
bool file_tags_expr_seen = false;
vector<string> file_tags; // gets both implicit and file-spec'd
// for the original SQLite tests we convert floating point numbers to integers
// for our own tests this is undesirable since it hides certain errors
if (script.find("test/sqlite/select") != string::npos) {
original_sqlite_test = true;
}
if (script.find("third_party/sqllogictest") != string::npos) {
original_sqlite_test = true;
}
if (!dbpath.empty()) {
// delete the target database file, if it exists
DeleteDatabase(dbpath);
}
ignore_error_messages.clear();
for (auto ignore : test_config.ErrorMessagesToBeSkipped()) {
ignore_error_messages.insert(ignore);
}
// initialize the database with the default dbpath
LoadDatabase(dbpath, true);
// open the file and parse it
bool success = parser.OpenFile(script);
if (!success) {
FAIL("Could not find test script '" + script + "'. Perhaps run `make sqlite`. ");
}
if (StringUtil::EndsWith(script, ".test_slow")) {
file_tags.emplace_back("slow");
}
if (StringUtil::EndsWith(script, ".test_coverage")) {
file_tags.emplace_back("coverage");
}
/* Loop over all records in the file */
while (parser.NextStatement()) {
// tokenize the current line
auto token = parser.Tokenize();
// throw explicit error on single line statements that are not separated by a comment or newline
if (parser.IsSingleLineStatement(token) && !parser.NextLineEmptyOrComment()) {
parser.Fail("all test statements need to be separated by an empty line");
}
// Check tags first time we hit test statements, since all explicit & implicit tags now present
if (parser.IsTestCommand(token.type) && !test_expr_executed) {
if (test_config.GetPolicyForTagSet(file_tags) == TestConfiguration::SelectPolicy::SKIP) {
SKIP_TEST("select tag-set");
return;
}
test_expr_executed = true;
}
vector<Condition> conditions;
bool skip_statement = false;
while (token.type == SQLLogicTokenType::SQLLOGIC_SKIP_IF || token.type == SQLLogicTokenType::SQLLOGIC_ONLY_IF) {
// skipif/onlyif
bool skip_if = token.type == SQLLogicTokenType::SQLLOGIC_SKIP_IF;
if (token.parameters.size() < 1) {
parser.Fail("skipif/onlyif requires a single parameter (e.g. skipif duckdb)");
}
auto system_name = StringUtil::Lower(token.parameters[0]);
// we support two kinds of conditions here
// (for original sqllogictests) system comparisons, e.g.:
// (1) skipif duckdb
// (2) onlyif <other_system>
// conditions on loop variables, e.g.:
// (1) skipif i=2
// (2) onlyif threadid=0
// the latter is only supported in our own tests (not in original sqllogic tests)
bool is_system_comparison;
if (original_sqlite_test) {
is_system_comparison = true;
} else {
is_system_comparison = !TryParseConditions(parser, system_name, conditions, skip_if);
}
if (is_system_comparison) {
bool our_system = system_name == "duckdb";
if (original_sqlite_test) {
our_system = our_system || system_name == "postgresql";
}
if (our_system == skip_if) {
// we skip this command in two situations
// (1) skipif duckdb
// (2) onlyif <other_system>
skip_statement = true;
break;
}
}
parser.NextLine();
token = parser.Tokenize();
}
if (skip_statement) {
continue;
}
if (skip_level > 0 && token.type != SQLLogicTokenType::SQLLOGIC_MODE) {
continue;
}
if (token.type == SQLLogicTokenType::SQLLOGIC_STATEMENT) {
// statement
if (token.parameters.size() < 1) {
parser.Fail("statement requires at least one parameter (statement ok/error)");
}
auto command = make_uniq<Statement>(*this);
// parse the first parameter
if (token.parameters[0] == "ok") {
command->expected_result = ExpectedResult::RESULT_SUCCESS;
} else if (token.parameters[0] == "error") {
command->expected_result = ExpectedResult::RESULT_ERROR;
} else if (token.parameters[0] == "maybe") {
command->expected_result = ExpectedResult::RESULT_UNKNOWN;
} else {
parser.Fail("statement argument should be 'ok' or 'error");
}
command->file_name = script;
command->query_line = parser.current_line + 1;
// extract the SQL statement
parser.NextLine();
auto statement_text = parser.ExtractStatement();
if (statement_text.empty()) {
parser.Fail("Unexpected empty statement text");
}
command->expected_error = parser.ExtractExpectedError(
command->expected_result == ExpectedResult::RESULT_SUCCESS, original_sqlite_test);
// perform any renames in the text
command->base_sql_query = ReplaceKeywords(std::move(statement_text));
if (token.parameters.size() >= 2) {
command->connection_name = token.parameters[1];
}
command->conditions = std::move(conditions);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_QUERY) {
if (token.parameters.size() < 1) {
parser.Fail("query requires at least one parameter (query III)");
}
auto command = make_uniq<Query>(*this);
// parse the expected column count
command->expected_column_count = 0;
auto &column_text = token.parameters[0];
for (idx_t i = 0; i < column_text.size(); i++) {
command->expected_column_count++;
if (column_text[i] != 'T' && column_text[i] != 'I' && column_text[i] != 'R') {
parser.Fail("unknown type character '%s' in string, expected T, I or R only",
string(1, column_text[i]));
}
}
if (command->expected_column_count == 0) {
parser.Fail("query requires at least a single column in the result");
}
command->file_name = script;
command->query_line = parser.current_line + 1;
// extract the SQL statement
parser.NextLine();
auto statement_text = parser.ExtractStatement();
// perform any renames in the text
command->base_sql_query = ReplaceKeywords(std::move(statement_text));
// extract the expected result
command->values = parser.ExtractExpectedResult();
// figure out the sort style/connection style
string sort_style = "none";
if (token.parameters.size() > 1) {
if (!TestConfiguration::TryParseSortStyle(token.parameters[1], command->sort_style)) {
// if this is not a known sort style, we use this as the connection name
// this is a bit dirty, but well
command->connection_name = token.parameters[1];
} else {
sort_style = token.parameters[1];
}
}
if (!TestConfiguration::TryParseSortStyle(sort_style, command->sort_style)) {
throw std::runtime_error("eek invalid sort style set, this should not happen");
}
// check the label of the query
if (token.parameters.size() > 2) {
command->query_has_label = true;
command->query_label = token.parameters[2];
} else {
command->query_has_label = false;
}
command->conditions = std::move(conditions);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_HASH_THRESHOLD) {
if (token.parameters.size() != 1) {
parser.Fail("hash-threshold requires a parameter");
}
try {
hash_threshold = std::stoi(token.parameters[0]);
} catch (...) {
parser.Fail("hash-threshold must be a number");
}
} else if (token.type == SQLLogicTokenType::SQLLOGIC_HALT) {
break;
} else if (token.type == SQLLogicTokenType::SQLLOGIC_MODE) {
if (token.parameters.size() != 1) {
parser.Fail("mode requires one parameter");
}
string parameter = token.parameters[0];
if (parameter == "skip") {
skip_level++;
} else if (parameter == "unskip") {
skip_level--;
} else {
auto command = make_uniq<ModeCommand>(*this, std::move(parameter));
ExecuteCommand(std::move(command));
}
} else if (token.type == SQLLogicTokenType::SQLLOGIC_SET) {
if (token.parameters.size() < 1) {
parser.Fail("set requires at least 1 parameter (e.g. set ignore_error_messages HTTP Error)");
}
if (token.parameters[0] == "ignore_error_messages" || token.parameters[0] == "always_fail_error_messages") {
unordered_set<string> *string_set;
if (token.parameters[0] == "ignore_error_messages") {
string_set = &ignore_error_messages;
} else {
string_set = &always_fail_error_messages;
}
// the set command overrides the default values
string_set->clear();
// Parse the parameter list as a comma separated list of strings that can contain spaces
// e.g. `set ignore_error_messages This is an error message, This_is_another, and another`
if (token.parameters.size() > 1) {
string current_string = "";
unsigned int token_idx = 1;
unsigned int substr_idx = 0;
while (token_idx < token.parameters.size()) {
auto comma_pos = token.parameters[token_idx].find(',', substr_idx);
if (comma_pos == string::npos) {
current_string += token.parameters[token_idx].substr(substr_idx) + " ";
token_idx++;
substr_idx = 0;
} else {
current_string += token.parameters[token_idx].substr(substr_idx, comma_pos);
StringUtil::Trim(current_string);
string_set->insert(current_string);
current_string = "";
substr_idx = comma_pos + 1;
}
}
StringUtil::Trim(current_string);
string_set->insert(current_string);
string_set->erase("");
}
} else if (token.parameters[0] == "seed") {
if (token.parameters.size() != 2) {
parser.Fail("set seed requires a single seed value");
}
Value seed(token.parameters[1]);
if (!seed.DefaultTryCastAs(LogicalType::DOUBLE)) {
parser.Fail("set seed requires a floating point parameter");
}
auto res = con->Query("SELECT SETSEED(" + seed.ToString() + ")");
if (res->HasError()) {
parser.Fail("Failed to set seed: %s", res->GetError());
}
skip_reload = true;
} else {
parser.Fail("unrecognized set parameter: %s", token.parameters[0]);
}
} else if (token.type == SQLLogicTokenType::SQLLOGIC_RESET) {
if (token.parameters.size() != 2) {
parser.Fail("Expected reset [type] [name] (e.g reset label my_label)");
}
auto &reset_type = token.parameters[0];
auto &reset_item = token.parameters[1];
if (StringUtil::CIEquals("label", reset_type)) {
auto reset_label_command = make_uniq<ResetLabel>(*this);
reset_label_command->query_label = reset_item;
ExecuteCommand(std::move(reset_label_command));
} else {
parser.Fail("unrecognized reset parameter: %s", reset_type);
}
} else if (token.type == SQLLogicTokenType::SQLLOGIC_LOOP ||
token.type == SQLLogicTokenType::SQLLOGIC_CONCURRENT_LOOP) {
if (token.parameters.size() != 3) {
parser.Fail("Expected loop [iterator_name] [start] [end] (e.g. loop i 1 300)");
}
LoopDefinition def;
def.loop_iterator_name = token.parameters[0];
try {
def.loop_start = std::stoi(token.parameters[1].c_str());
def.loop_end = std::stoi(token.parameters[2].c_str());
} catch (...) {
parser.Fail("loop_start and loop_end must be a number");
}
def.loop_idx = def.loop_start;
def.is_parallel = token.type == SQLLogicTokenType::SQLLOGIC_CONCURRENT_LOOP;
StartLoop(def);
} else if (token.type == SQLLogicTokenType::SQLLOGIC_FOREACH ||
token.type == SQLLogicTokenType::SQLLOGIC_CONCURRENT_FOREACH) {
if (token.parameters.size() < 2) {
parser.Fail("expected foreach [iterator_name] [m1] [m2] [etc...] (e.g. foreach type integer "
"smallint float)");
}
LoopDefinition def;
def.loop_iterator_name = token.parameters[0];
for (idx_t i = 1; i < token.parameters.size(); i++) {
D_ASSERT(!token.parameters[i].empty());
if (!ForEachTokenReplace(token.parameters[i], def.tokens)) {
def.tokens.push_back(token.parameters[i]);
}
}
def.loop_idx = 0;
def.loop_start = 0;
def.loop_end = def.tokens.size();
def.is_parallel = token.type == SQLLogicTokenType::SQLLOGIC_CONCURRENT_FOREACH;
StartLoop(def);
} else if (token.type == SQLLogicTokenType::SQLLOGIC_ENDLOOP) {
EndLoop();
} else if (token.type == SQLLogicTokenType::SQLLOGIC_REQUIRE) {
auto require_result = CheckRequire(parser, token.parameters);
if (require_result == RequireResult::MISSING) {
auto &param = token.parameters[0];
if (IsRequired(param)) {
// This extension / setting was explicitly required
parser.Fail(StringUtil::Format("require %s: FAILED", param));
}
SKIP_TEST("require " + token.parameters[0]);
return;
}
} else if (token.type == SQLLogicTokenType::SQLLOGIC_TEST_ENV) {
if (InLoop()) {
parser.Fail("test-env cannot be called in a loop");
}
if (token.parameters.size() != 2) {
parser.Fail("test-env requires 2 arguments: <env name> <default env val>");
}
auto env_var = token.parameters[0];
auto env_actual = test_config.GetTestEnv(env_var, token.parameters[1]);
// Check if we have something defining from our test
if (environment_variables.count(env_var)) {
parser.Fail(StringUtil::Format("Environment/Test variable '%s' has already been defined", env_var));
}
environment_variables[env_var] = env_actual;
add_env_tag(file_tags, env_var, &env_actual);
} else if (token.type == SQLLogicTokenType::SQLLOGIC_REQUIRE_ENV) {
if (InLoop()) {
parser.Fail("require-env cannot be called in a loop");
}
if (token.parameters.size() != 1 && token.parameters.size() != 2) {
parser.Fail("require-env requires 1 argument: <env name> [optional: <expected env val>]");
}
auto env_var = token.parameters[0];
const char *env_actual = std::getenv(env_var.c_str());
string default_local_repo = string(DUCKDB_BUILD_DIRECTORY) + "/repository";
if (env_actual == nullptr && env_var == "LOCAL_EXTENSION_REPO" &&
config->options.autoload_known_extensions) {
// Overriding LOCAL_EXTENSION_REPO here is a hacky
// More proper solution is wrapping std::getenv in a duckdb::test_getenv, and having a way to inject env
// variables
env_actual = default_local_repo.c_str();
}
if (env_actual == nullptr) {
// Environment variable was not found, this test should not be run
SKIP_TEST("require-env " + token.parameters[0]);
return;
}
if (token.parameters.size() == 2) {
// Check that the value is the same as the expected value
auto env_value = token.parameters[1];
if (std::strcmp(env_actual, env_value.c_str()) != 0) {
// It's not, check the test
SKIP_TEST("require-env " + token.parameters[0] + " " + token.parameters[1]);
return;
}
file_tags.emplace_back(StringUtil::Format("env[%s]=%s", token.parameters[0], token.parameters[1]));
}
if (environment_variables.count(env_var)) {
parser.Fail(StringUtil::Format("Environment variable '%s' has already been defined", env_var));
}
environment_variables[env_var] = env_actual;
add_env_tag(file_tags, token.parameters[0], token.parameters.size() == 2 ? &token.parameters[1] : nullptr);
} else if (token.type == SQLLogicTokenType::SQLLOGIC_LOAD) {
auto &test_config = TestConfiguration::Get();
if (test_config.OnLoadCommand() == "skip") {
SKIP_TEST("config on_load skip");
return;
}
bool is_read_only = false;
if (token.parameters.size() > 1) {
auto param = token.parameters[1];
if (StringUtil::CIEquals("readonly", param)) {
is_read_only = true;
} else if (StringUtil::CIEquals("readwrite", param)) {
is_read_only = false;
} else {
parser.Fail(StringUtil::Format(
"parameter to 'load' is invalid, received '%s', accepted options are 'readonly' and 'readwrite",
param));
}
}
string version;
if (token.parameters.size() > 2) {
version = token.parameters[2];
}
string load_db_path;
if (!token.parameters.empty()) {
load_db_path = ReplaceKeywords(token.parameters[0]);
} else {
load_db_path = string();
}
auto command = make_uniq<LoadCommand>(*this, load_db_path, is_read_only, version);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_RESTART) {
bool load_extensions = !(token.parameters.size() == 1 && token.parameters[0] == "no_extension_load");
// restart the current database
auto command = make_uniq<RestartCommand>(*this, load_extensions);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_RECONNECT) {
auto command = make_uniq<ReconnectCommand>(*this);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_SLEEP) {
if (token.parameters.size() != 2) {
parser.Fail("sleep requires two parameter (e.g. sleep 1 second)");
}
// require a specific block size
auto sleep_duration = std::stoull(token.parameters[0]);
auto sleep_unit = SleepCommand::ParseUnit(token.parameters[1]);
auto command = make_uniq<SleepCommand>(*this, sleep_duration, sleep_unit);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_UNZIP) {
if (token.parameters.size() != 1 && token.parameters.size() != 2) {
parser.Fail("unzip requires 1 argument: <path/to/file.db.gz> [optional: "
"<path/to/unzipped_file.db>, default: __TEST_DIR__/<file.db>]");
}
// set input path
auto input_path = ReplaceKeywords(token.parameters[0]);
// file name
idx_t filename_start_pos = input_path.find_last_of("/") + 1;
if (!StringUtil::EndsWith(input_path, CompressionExtensionFromType(FileCompressionType::GZIP))) {
parser.Fail("unzip: input has not a GZIP extension");
}
string filename = input_path.substr(filename_start_pos, input_path.size() - filename_start_pos - 3);
// extraction path
string default_extraction_path = ReplaceKeywords("__TEST_DIR__/" + filename);
string extraction_path =
(token.parameters.size() == 2) ? ReplaceKeywords(token.parameters[1]) : default_extraction_path;
if (extraction_path == "NULL") {
extraction_path = default_extraction_path;
}
auto command = make_uniq<UnzipCommand>(*this, input_path, extraction_path);
ExecuteCommand(std::move(command));
} else if (token.type == SQLLogicTokenType::SQLLOGIC_TAGS) {
// NOTE: tags-before-test-commands is the low bar right now
// 1 better: all non-command lines precede command lines
// Mo better: parse first, build entire context before execution; allows e.g.
// - implicit tag scans of e.g. strings, vars, etc., like '${ENVVAR}', '__TEST_DIR__', 'ATTACH'
// - faster subset runs
// - tag match runs to generate lists
if (test_expr_executed) {
parser.Fail("tags expression must precede test commands");
}
if (file_tags_expr_seen) {
parser.Fail("tags may be only specified once");
}
file_tags_expr_seen = true;
if (token.parameters.empty()) {
parser.Fail("tags requires >= 1 argument, e.g.: <tag1> [tag2 .. tagN]");
}
// extend file_tags for jit eval
file_tags.insert(file_tags.begin(), token.parameters.begin(), token.parameters.end());
}
}
if (InLoop()) {
parser.Fail("Missing endloop!");
}
}
} // namespace duckdb