diff --git a/duckdb b/duckdb index 4a89d97..20b1486 160000 --- a/duckdb +++ b/duckdb @@ -1 +1 @@ -Subproject commit 4a89d97db8a5a23a15f3025c8d2d2885337c2637 +Subproject commit 20b1486d1192f9fbd2328d1122b5afe5f1747fce diff --git a/src/mysql_utils.cpp b/src/mysql_utils.cpp index 7802127..18bc53a 100644 --- a/src/mysql_utils.cpp +++ b/src/mysql_utils.cpp @@ -4,21 +4,76 @@ namespace duckdb { +static bool ParseValue(const string &dsn, idx_t &pos, string &result) { + // skip leading spaces + while(pos < dsn.size() && StringUtil::CharacterIsSpace(dsn[pos])) { + pos++; + } + if (pos >= dsn.size()) { + return false; + } + // check if we are parsing a quoted value or not + if (dsn[pos] == '"') { + pos++; + // scan until we find another quote + bool found_quote = false; + for(; pos < dsn.size(); pos++) { + if (dsn[pos] == '"') { + found_quote = true; + pos++; + break; + } + if (dsn[pos] == '\\') { + // backslash escapes the backslash or double-quote + if (pos + 1 >= dsn.size()) { + throw InvalidInputException("Invalid dsn \"%s\" - backslash at end of dsn", dsn); + } + if (dsn[pos + 1] != '\\' && dsn[pos + 1] != '"') { + throw InvalidInputException("Invalid dsn \"%s\" - backslash can only escape \\ or \"", dsn); + } + result += dsn[pos + 1]; + pos++; + } else { + result += dsn[pos]; + } + } + if (!found_quote) { + throw InvalidInputException("Invalid dsn \"%s\" - unterminated quote", dsn); + } + } else { + // unquoted value, continue until space, equality sign or end of string + for(; pos < dsn.size(); pos++) { + if (dsn[pos] == '=') { + break; + } + if (StringUtil::CharacterIsSpace(dsn[pos])) { + break; + } + result += dsn[pos]; + } + } + return true; +} + MySQLConnectionParameters MySQLUtils::ParseConnectionParameters(const string &dsn) { MySQLConnectionParameters result; // parse options - auto parameters = StringUtil::Split(dsn, ' '); - for (auto ¶m : parameters) { - StringUtil::Trim(param); - if (param.empty()) { - continue; + + idx_t pos = 0; + while(pos < dsn.size()) { + string key; + string value; + if (!ParseValue(dsn, pos, key)) { + break; + } + if (pos >= dsn.size() || dsn[pos] != '=') { + throw InvalidInputException("Invalid dsn \"%s\" - expected key=value pairs separated by spaces", dsn); } - auto splits = StringUtil::Split(param, '='); - if (splits.size() != 2) { + pos++; + if (!ParseValue(dsn, pos, value)) { throw InvalidInputException("Invalid dsn \"%s\" - expected key=value pairs separated by spaces", dsn); } - auto key = StringUtil::Lower(splits[0]); - auto &value = splits[1]; + key = StringUtil::Lower(key); if (key == "host") { result.host = value; } else if (key == "user") { diff --git a/test/sql/attach_dsn.test b/test/sql/attach_dsn.test new file mode 100644 index 0000000..6653089 --- /dev/null +++ b/test/sql/attach_dsn.test @@ -0,0 +1,38 @@ +# name: test/sql/attach_dsn.test +# description: Test attaching with complex DSNs +# group: [sql] + +require mysql_scanner + +require-env MYSQL_TEST_DATABASE_AVAILABLE + +# dsn parsing failures +statement error +ATTACH 'host' AS s (TYPE MYSQL_SCANNER) +---- +expected key=value pairs separated by spaces + +statement error +ATTACH 'host=' AS s (TYPE MYSQL_SCANNER) +---- +expected key=value pairs separated by spaces + +statement error +ATTACH 'host="' AS s (TYPE MYSQL_SCANNER) +---- +unterminated quote + +statement error +ATTACH 'host="\' AS s (TYPE MYSQL_SCANNER) +---- +backslash at end of dsn + +statement error +ATTACH 'host="\a' AS s (TYPE MYSQL_SCANNER) +---- +backslash can only escape + +statement error +ATTACH 'host="this string contains \"quoted\" \\spaces"' AS s (TYPE MYSQL_SCANNER) +---- +this string contains "quoted" \spaces diff --git a/test/sql/attach_fake_booleans.test b/test/sql/attach_fake_booleans.test index 524f828..decc736 100644 --- a/test/sql/attach_fake_booleans.test +++ b/test/sql/attach_fake_booleans.test @@ -10,7 +10,7 @@ statement ok SET GLOBAL mysql_experimental_filter_pushdown=true; statement ok -ATTACH 'host=localhost user=root port=0 database=mysqlscanner' AS s1 (TYPE MYSQL_SCANNER) +ATTACH ' host="localhost" user=root port=0 database=mysqlscanner ' AS s1 (TYPE MYSQL_SCANNER) query II SELECT * FROM s1.fake_booleans diff --git a/test/sql/attach_filter_pushdown.test b/test/sql/attach_filter_pushdown.test index 6599450..c532a9d 100644 --- a/test/sql/attach_filter_pushdown.test +++ b/test/sql/attach_filter_pushdown.test @@ -10,7 +10,7 @@ statement ok SET GLOBAL mysql_experimental_filter_pushdown=true; statement ok -ATTACH 'host=localhost user=root port=0 database=mysqlscanner' AS s1 (TYPE MYSQL_SCANNER) +ATTACH 'host=localhost user=root port="0" database="mysqlscanner"' AS s1 (TYPE MYSQL_SCANNER) statement ok CREATE OR REPLACE TABLE s1.filter_pushdown(i INTEGER)