diff --git a/.github/actions/mysql-client-tests/Dockerfile b/.github/actions/mysql-client-tests/Dockerfile index 96c543e3c2..c5a651b6e5 100644 --- a/.github/actions/mysql-client-tests/Dockerfile +++ b/.github/actions/mysql-client-tests/Dockerfile @@ -17,11 +17,11 @@ RUN go build -tags icu_static -ldflags "$GO_LDFLAGS" -o /build/bin/dolt ./cmd/do FROM golang_cgo125 AS go_clients_build COPY dolt/integration-tests/mysql-client-tests/go /build/go/ WORKDIR /build/go/ -RUN go build -ldflags "$GO_LDFLAGS" -o /build/bin/go-mysql-client-test +RUN go build -ldflags "$GO_LDFLAGS" -o /build/bin/mysql-client-test COPY dolt/integration-tests/mysql-client-tests/go-mysql/ /build/go-mysql/ WORKDIR /build/go-mysql/ -RUN go build -ldflags "$GO_LDFLAGS" -o /build/bin/go-sql-driver-test +RUN go build -ldflags "$GO_LDFLAGS" -o /build/bin/sql-driver-mysql-test FROM rust:1.90-alpine3.22 AS rust_clients_build COPY dolt/integration-tests/mysql-client-tests/rust/ /build/rust/ @@ -37,8 +37,10 @@ RUN dotnet publish -c Release -o /build/bin COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlConnector/ /build/dotnet/MySqlConnector/ WORKDIR /build/dotnet/MySqlConnector/ RUN dotnet publish -c Release -o /build/bin +# devart dotconnect reqs a license so we've skipped it here FROM gcc:12.5-bookworm AS c_clients_build +# default-libmysqlclient-dev uses libmariadb under the hood but here we test both header interfaces RUN apt-get update && apt-get install -y default-libmysqlclient-dev libmariadb-dev && rm -rf /var/lib/apt/lists/* COPY dolt/integration-tests/mysql-client-tests/c/ /build/c/ WORKDIR /build/c/ @@ -54,25 +56,34 @@ WORKDIR /build/cpp/ RUN make FROM python:3.14-slim-bookworm AS python_clients_build -RUN apt-get update && apt-get install -y binutils && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir mysql-connector-python==8.0.33 PyMySQL==1.0.2 sqlalchemy==1.4.46 pyinstaller +RUN apt-get update && apt-get install -y binutils libmariadb-dev gcc && rm -rf /var/lib/apt/lists/* +RUN pip install --no-cache-dir mysql-connector-python==8.0.33 PyMySQL==1.0.2 sqlalchemy==1.4.46 mariadb pyinstaller COPY dolt/integration-tests/mysql-client-tests/python/ /build/python/ WORKDIR /build/python/ RUN pyinstaller --onefile pymysql-test.py -RUN pyinstaller --onefile --collect-all mysql.connector py-sqlalchemy-test.py -RUN pyinstaller --onefile --collect-all mysql.connector py-mysql-connector-test.py +RUN pyinstaller --onefile --collect-all mysql.connector sqlalchemy-test.py +RUN pyinstaller --onefile --collect-all mysql.connector mysql-connector-test.py +RUN pyinstaller --onefile mariadb-connector-test.py FROM elixir:1.18-slim AS elixir_clients_build RUN apt-get update && apt-get install -y ca-certificates xz-utils curl && rm -rf /var/lib/apt/lists/* RUN curl -sSL https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz | tar -xJ ENV PATH="/zig-x86_64-linux-0.14.1:${PATH}" -COPY dolt/integration-tests/mysql-client-tests/elixir/mix.exs /build/elixir/ -WORKDIR /build/elixir/ +COPY dolt/integration-tests/mysql-client-tests/elixir/myxql/mix.exs /build/elixir/myxql/ +WORKDIR /build/elixir/myxql/ RUN mix local.hex --force && mix local.rebar --force RUN mix deps.get -COPY dolt/integration-tests/mysql-client-tests/elixir/ /build/elixir/ -RUN MIX_ENV=prod mix release -RUN mkdir -p /build/bin && cp burrito_out/simple_linux /build/bin/elixir-mysql-client-test +COPY dolt/integration-tests/mysql-client-tests/elixir/myxql/ /build/elixir/myxql/ +RUN MIX_ENV=prod mix release simple +COPY dolt/integration-tests/mysql-client-tests/elixir/mysql/mix.exs /build/elixir/mysql/ +WORKDIR /build/elixir/mysql/ +RUN mix local.hex --force && mix local.rebar --force +RUN mix deps.get +COPY dolt/integration-tests/mysql-client-tests/elixir/mysql/ /build/elixir/mysql/ +RUN MIX_ENV=prod mix release mysql_otp +RUN mkdir -p /build/bin && \ + cp /build/elixir/myxql/burrito_out/simple_linux /build/bin/myxql-driver-test && \ + cp /build/elixir/mysql/burrito_out/mysql_otp_linux /build/bin/mysql-otp-test FROM maven:3.9.11-amazoncorretto-21-debian-bookworm AS java_clients_build RUN apt-get update && apt-get install -y binutils && rm -rf /var/lib/apt/lists/* @@ -92,9 +103,8 @@ WORKDIR /build/bin/ RUN npm install --omit=dev COPY dolt/integration-tests/mysql-client-tests/node/ /build/bin/ -FROM php:8.3-bookworm AS php_clients_build +FROM php:8.3-bookworm AS php_deps RUN docker-php-ext-install mysqli pdo_mysql -COPY dolt/integration-tests/mysql-client-tests/php/ /build/bin/ FROM ruby:3.4-bookworm AS ruby_clients_build RUN apt-get update && apt-get install -y default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/* @@ -103,23 +113,24 @@ WORKDIR /build/ruby/ RUN bundle install COPY dolt/integration-tests/mysql-client-tests/ruby/ /build/bin/ +FROM debian:bookworm-slim AS r_deps +RUN apt-get update && apt-get install -y r-base-core libmariadb-dev && rm -rf /var/lib/apt/lists/* +WORKDIR /build/r/ +RUN R -e "install.packages('DBI', INSTALL_opts='--build', repos='https://cloud.r-project.org/')" +RUN R -e "install.packages('RMySQL', INSTALL_opts='--build', repos='https://cloud.r-project.org/')" +RUN R -e "install.packages('RMariaDB', INSTALL_opts='--build', repos='https://cloud.r-project.org/')" + FROM php:8.3-bookworm AS runtime RUN apt-get update && apt-get install -y \ - default-libmysqlclient-dev \ libmysqlcppconn-dev \ - libmariadb3 \ libdbd-mysql-perl \ + libdbd-mariadb-perl \ default-mysql-client \ - postgresql \ - postgresql-contrib \ postgresql-15-mysql-fdw \ r-base-core \ - perl-base \ lsof \ bats \ && rm -rf /var/lib/apt/lists/* -# compiled, must come after above -RUN R -e "install.packages(c('RMariaDB', 'RMySQL', 'DBI'), repos='https://cloud.r-project.org/')" COPY --from=dolt_build /build/bin/ /usr/local/bin/ COPY --from=go_clients_build /build/bin/ /build/bin/go/ @@ -131,7 +142,7 @@ COPY --from=python_clients_build /build/python/dist/ /build/bin/python/ COPY --from=elixir_clients_build /build/bin/ /build/bin/elixir/ COPY --from=java_clients_build /build/java/target/ /build/bin/java/ COPY --from=node_clients_build /build/bin/ /build/bin/node/ -COPY --from=php_clients_build /build/bin/ /build/bin/php/ +COPY dolt/integration-tests/mysql-client-tests/php/ /build/bin/php/ COPY --from=ruby_clients_build /build/bin/ /build/bin/ruby/ COPY dolt/integration-tests/mysql-client-tests/r/ /build/bin/r/ COPY dolt/integration-tests/mysql-client-tests/perl/ /build/bin/perl/ @@ -140,11 +151,13 @@ COPY --from=cpp_clients_build /usr/lib/x86_64-linux-gnu/libmariadb* /usr/lib/x86 COPY --from=cpp_clients_build /usr/include/mariadb/ /usr/include/mariadb/ COPY --from=java_clients_build /build/jre/ /opt/jre/ COPY --from=node_clients_build /usr/local/bin/node /usr/local/bin/node -COPY --from=php_clients_build /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ -COPY --from=php_clients_build /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ +COPY --from=php_deps /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ +COPY --from=php_deps /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ COPY --from=ruby_clients_build /usr/local/bin/ruby /usr/local/bin/ruby COPY --from=ruby_clients_build /usr/local/lib/ /usr/local/lib/ COPY --from=ruby_clients_build /usr/local/bundle/ /usr/local/bundle/ +COPY --from=r_deps /build/r/*.tar.gz /tmp/r-packages/ +RUN R CMD INSTALL /tmp/r-packages/*.tar.gz && rm -rf /tmp/r-packages/ ENV PATH="/opt/jre/bin:/usr/local/bundle/bin:${PATH}" ENV GEM_HOME="/usr/local/bundle" ENV BUNDLE_APP_CONFIG="/usr/local/bundle" diff --git a/integration-tests/mysql-client-tests/c/Makefile b/integration-tests/mysql-client-tests/c/Makefile index 393dfd15a3..6c5526a70c 100644 --- a/integration-tests/mysql-client-tests/c/Makefile +++ b/integration-tests/mysql-client-tests/c/Makefile @@ -6,11 +6,11 @@ CFLAGS_MARIADB = -I/usr/include/mariadb -Wall -O2 LDFLAGS_MYSQL = -lmysqlclient LDFLAGS_MARIADB = -lmariadb -TARGET_MYSQL = /build/bin/c-mysql-client-test -TARGET_MARIADB = /build/bin/c-mariadb-client-test +TARGET_MYSQL = /build/bin/mysql-client-test +TARGET_MARIADB = /build/bin/mariadb-client-test -SRCS_MYSQL = mysql-connector-c-test.c -SRCS_MARIADB = mariadb-connector-c-test.c +SRCS_MYSQL = mysql-connector-test.c +SRCS_MARIADB = mariadb-connector-test.c all: $(TARGET_MYSQL) $(TARGET_MARIADB) diff --git a/integration-tests/mysql-client-tests/c/mariadb-connector-test.c b/integration-tests/mysql-client-tests/c/mariadb-connector-test.c new file mode 100644 index 0000000000..02e2b426fe --- /dev/null +++ b/integration-tests/mysql-client-tests/c/mariadb-connector-test.c @@ -0,0 +1,199 @@ +#include +#include +#include +#include + +#define QUERIES_SIZE 14 + +char *queries[QUERIES_SIZE] = + { + "create table test (pk int, `value` int, primary key(pk))", + "describe test", + "select * from test", + "insert into test (pk, `value`) values (0,0)", + "select * from test", + "call dolt_add('-A');", + "call dolt_commit('-m', 'my commit')", + "select COUNT(*) FROM dolt_log", + "call dolt_checkout('-b', 'mybranch')", + "insert into test (pk, `value`) values (10,10)", + "call dolt_commit('-a', '-m', 'my commit2')", + "call dolt_checkout('main')", + "call dolt_merge('mybranch')", + "select COUNT(*) FROM dolt_log", + }; + +typedef struct statement_t { + char *query; + MYSQL_BIND bind[10]; + int expect_prepare_error; + int expect_exec_error; + int expect_result_metadata; +} statement; + +void test_statement(MYSQL *con, statement *stmt) { + MYSQL_STMT *mstmt = mysql_stmt_init(con); + if (!mstmt) { + fprintf(stderr, "failed to init stmt: %s\n", mysql_error(con)); + exit(1); + } + if ( mysql_stmt_prepare(mstmt, stmt->query, strlen(stmt->query)) ) { + if ( !stmt->expect_prepare_error) { + fprintf(stderr, "failed to prepare stmt: %s: %s\n", stmt->query, mysql_stmt_error(mstmt)); + exit(1); + } else { + goto close; + } + } + if ( mysql_stmt_bind_param(mstmt, stmt->bind) ) { + fprintf(stderr, "failed to bind stmt: %s: %s\n", stmt->query, mysql_stmt_error(mstmt)); + exit(1); + } + MYSQL_RES *metadata = mysql_stmt_result_metadata(mstmt); + if (stmt->expect_result_metadata && metadata == NULL) { + fprintf(stderr, "result metadata was unexpectedly NULL: %s\n", stmt->query); + exit(1); + } else if (!stmt->expect_result_metadata && metadata != NULL) { + fprintf(stderr, "result metadata was unexpectedly non-NULL: %s\n", stmt->query); + exit(1); + } + if ( mysql_stmt_execute(mstmt) ) { + if ( !stmt->expect_exec_error) { + fprintf(stderr, "failed to execute stmt: %s: %s\n", stmt->query, mysql_stmt_error(mstmt)); + exit(1); + } + } + // TODO: Add test for mysql_stmt_store_result when supported +close: + if ( mysql_stmt_close(mstmt) ) { + fprintf(stderr, "failed to close stmt: %s: %s\n", stmt->query, mysql_error(con)); + exit(1); + } +} + +statement LAST_STATEMENT = { +}; + +int main(int argc, char **argv) { + + char* user = argv[1]; + int port = atoi(argv[2]); + char* db = argv[3]; + + MYSQL *con = mysql_init(NULL); + + if ( con == NULL ) { + fprintf(stderr, "%s\n", mysql_error(con)); + exit(1); + } + + #ifndef MARIADB_CLIENT_VERSION_STR + fprintf(stderr, "Error: Not using MariaDB connector!\n"); + exit(1); + #endif + + if ( mysql_real_connect(con, + "127.0.0.1", + user, + "", + db, + port, + NULL, + 0 ) == NULL) { + fprintf(stderr, "%s\n", mysql_error(con)); + mysql_close(con); + exit(1); + } + + for ( int i = 0; i < QUERIES_SIZE; i++ ) { + if ( mysql_query(con, queries[i]) ) { + printf("QUERY FAILED: %s\n", queries[i]); + fprintf(stderr, "%s\n", mysql_error(con)); + mysql_close(con); + exit(1); + } else { + // Not checking validity of results for now + MYSQL_RES* result = mysql_use_result(con); + mysql_free_result(result); + } + } + + int pk = 1; + int value = 12; + unsigned long string_len = 16; + statement statements[] = { + { + .query = "select * from test where pk = ?", + .bind = { + [0] = { + .buffer_type = MYSQL_TYPE_LONG, + .buffer = (void *)(&pk), + .buffer_length = sizeof(pk), + }, + }, + .expect_result_metadata = 1, + }, + { + .query = "select * from test where pk = ?", + .bind = { + [0] = { + .buffer_type = MYSQL_TYPE_LONG, + .buffer = (void *)(&pk), + .buffer_length = sizeof(pk), + .is_unsigned = 1, + }, + }, + .expect_result_metadata = 1, + }, + { + .query = "insert into test values (?, ?)", + .bind = { + [0] = { + .buffer_type = MYSQL_TYPE_LONG, + .buffer = (void *)(&pk), + .buffer_length = sizeof(pk), + }, + [1] = { + .buffer_type = MYSQL_TYPE_LONG, + .buffer = (void *)(&value), + .buffer_length = sizeof(value), + }, + }, + .expect_result_metadata = 0, + }, + { + .query = "update test set `value` = ?", + .bind = { + [0] = { + .buffer_type = MYSQL_TYPE_STRING, + .buffer = (void *)"test string here", + .buffer_length = string_len, + .length = &string_len, + }, + }, + .expect_exec_error = 1, + .expect_result_metadata = 0, + }, + { + .query = "select * from test SYNTAX ERROR where pk = ?", + .bind = { + [0] = { + .buffer_type = MYSQL_TYPE_LONG, + .buffer = (void *)(&pk), + .buffer_length = sizeof(pk), + }, + }, + .expect_prepare_error = 1, + }, + LAST_STATEMENT, + }; + + for (int i = 0; statements[i].query; i++) { + test_statement(con, &statements[i]); + } + + mysql_close(con); + + return 0; +} + diff --git a/integration-tests/mysql-client-tests/c/mysql-connector-c-test.c b/integration-tests/mysql-client-tests/c/mysql-connector-test.c similarity index 100% rename from integration-tests/mysql-client-tests/c/mysql-connector-c-test.c rename to integration-tests/mysql-client-tests/c/mysql-connector-test.c diff --git a/integration-tests/mysql-client-tests/cpp/Makefile b/integration-tests/mysql-client-tests/cpp/Makefile index cd5210a7e2..441b42ef18 100644 --- a/integration-tests/mysql-client-tests/cpp/Makefile +++ b/integration-tests/mysql-client-tests/cpp/Makefile @@ -1,12 +1,12 @@ CXX = g++ CXXFLAGS_MYSQL = -I/usr/include/cppconn -std=c++11 -Wall -O2 -CXXFLAGS_MARIADB = -std=c++11 -Wall -O2 +CXXFLAGS_MARIADB = -I/usr/include/mariadb -std=c++11 -Wall -O2 LDFLAGS_MYSQL = -lmysqlcppconn LDFLAGS_MARIADB = -lmariadbcpp -TARGET_MYSQL = /build/bin/cpp-mysql-connector-test -TARGET_MARIADB = /build/bin/cpp-mariadb-connector-test -SRCS_MYSQL = mysql-connector-cpp-test.cpp -SRCS_MARIADB = mariadb-connector-cpp-test.cpp +TARGET_MYSQL = /build/bin/mysql-connector-test +TARGET_MARIADB = /build/bin/mariadb-connector-test +SRCS_MYSQL = mysql-connector-test.cpp +SRCS_MARIADB = mariadb-connector-test.cpp all: $(TARGET_MYSQL) $(TARGET_MARIADB) diff --git a/integration-tests/mysql-client-tests/cpp/README.md b/integration-tests/mysql-client-tests/cpp/README.md deleted file mode 100644 index 860d255c7b..0000000000 --- a/integration-tests/mysql-client-tests/cpp/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# General - -This code uses git submodules. You need to recursively pull all the submodules -in order for it to build. - -# Building on OS X - -```sh -$ brew install cmake openssl mysql-client boost -$ export PATH=/usr/local/Cellar/mysql-client/8.0.21/bin/:"$PATH" -$ mkdir _build -$ cd _build -$ cmake .. -DWITH_SSL=/usr/local/Cellar/openssl@1.1/1.1.1g/ -DWITH_JDBC=yes -$ make -j 10 -``` - -TODO: These instructions are coupled to openssl and mysql-client version that -happen to be installed... - -# Build on Ubuntu / Debian - -```sh -$ apt-get install g++ cmake libmysqlcppconn-dev -$ mkdir _build -$ cd _build -$ cmake .. -$ make -j 10 -``` diff --git a/integration-tests/mysql-client-tests/cpp/mariadb-connector-test.cpp b/integration-tests/mysql-client-tests/cpp/mariadb-connector-test.cpp new file mode 100644 index 0000000000..c7b4da1a80 --- /dev/null +++ b/integration-tests/mysql-client-tests/cpp/mariadb-connector-test.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include + +#define QUERIES_SIZE 14 + +std::string queries[QUERIES_SIZE] = + { + "create table test (pk int, `value` int, primary key(pk))", + "describe test", + "select * from test", + "insert into test (pk, `value`) values (0,0)", + "select * from test", + "call dolt_add('-A');", + "call dolt_commit('-m', 'my commit')", + "select COUNT(*) FROM dolt_log", + "call dolt_checkout('-b', 'mybranch')", + "insert into test (pk, `value`) values (1,1)", + "call dolt_commit('-a', '-m', 'my commit2')", + "call dolt_checkout('main')", + "call dolt_merge('mybranch')", + "select COUNT(*) FROM dolt_log", + }; + +int is_update[QUERIES_SIZE] = {1,0,0,1,0,0,0,0,0,1,0,0,0,0}; + +int main(int argc, char **argv) { + if (argc < 4) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::string user = argv[1]; + std::string port = argv[2]; + std::string db = argv[3]; + + try { + // Get the driver instance + sql::Driver* driver = sql::mariadb::get_driver_instance(); + + // Create connection properties + sql::SQLString url("jdbc:mariadb://127.0.0.1:" + port + "/" + db); + sql::Properties properties({{"user", user}, {"password", ""}}); + + // Establish connection + std::unique_ptr con(driver->connect(url, properties)); + + for ( int i = 0; i < QUERIES_SIZE; i++ ) { + try { + std::unique_ptr stmt(con->createStatement()); + + if ( is_update[i] ) { + stmt->executeUpdate(queries[i]); + } else { + std::unique_ptr res(stmt->executeQuery(queries[i])); + + // Assert that all columns have column name metadata populated + sql::ResultSetMetaData* metadata = res->getMetaData(); + const uint32_t columnCount = metadata->getColumnCount(); + for (uint32_t columnIndex = 1; columnIndex <= columnCount; ++columnIndex) { + sql::SQLString columnName = metadata->getColumnName(columnIndex); + if (columnName.length() == 0) { + std::cerr << "Column name is empty at index " << columnIndex << std::endl; + return 1; + } + } + } + } catch (sql::SQLException &e) { + std::cout << "QUERY: " << queries[i] << std::endl; + std::cout << "# ERR: " << e.what(); + std::cout << " (MariaDB error code: " << e.getErrorCode(); + std::cout << ", SQLState: " << e.getSQLState() << " )" << std::endl; + return 1; + } + } + + return 0; + } catch (sql::SQLException &e) { + std::cerr << "Connection error: " << e.what() << std::endl; + std::cerr << " (MariaDB error code: " << e.getErrorCode(); + std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl; + return 1; + } +} + diff --git a/integration-tests/mysql-client-tests/cpp/mysql-connector-cpp-test.cpp b/integration-tests/mysql-client-tests/cpp/mysql-connector-test.cpp similarity index 97% rename from integration-tests/mysql-client-tests/cpp/mysql-connector-cpp-test.cpp rename to integration-tests/mysql-client-tests/cpp/mysql-connector-test.cpp index 050dfed275..d95bccb683 100644 --- a/integration-tests/mysql-client-tests/cpp/mysql-connector-cpp-test.cpp +++ b/integration-tests/mysql-client-tests/cpp/mysql-connector-test.cpp @@ -68,7 +68,7 @@ int main(int argc, char **argv) { sql::Statement *stmt = con->createStatement(); if ( is_update[i] ) { - int affected_rows = stmt->executeUpdate(queries[i]); + stmt->executeUpdate(queries[i]); } else { sql::ResultSet *res = stmt->executeQuery(queries[i]); diff --git a/integration-tests/mysql-client-tests/dotnet/.gitignore b/integration-tests/mysql-client-tests/dotnet/.gitignore deleted file mode 100644 index da19d74625..0000000000 --- a/integration-tests/mysql-client-tests/dotnet/.gitignore +++ /dev/null @@ -1,136 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -.vs - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.svclog -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.azurePubxml - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -packages/ -## TODO: If the tool you use requires repositories.config, also uncomment the next line -!packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -![Ss]tyle[Cc]op.targets -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml - -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -# ========================= -# Windows detritus -# ========================= - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac desktop service store files -.DS_Store - -_NCrunch* -ZZ \ No newline at end of file diff --git a/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj b/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj index c73549a89f..d65c9db305 100644 --- a/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj +++ b/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj @@ -3,7 +3,7 @@ Exe net8.0 - dotnet-mysql-client-test + mysql-client-test true true diff --git a/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj b/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj index dcb8f7feb4..eb01c3e3d0 100755 --- a/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj +++ b/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj @@ -3,7 +3,7 @@ Exe net8.0 - dotnet-mysql-connector-test + mysql-connector-test true true diff --git a/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp/application.ex b/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp/application.ex new file mode 100644 index 0000000000..4f1a021333 --- /dev/null +++ b/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp/application.ex @@ -0,0 +1,25 @@ +defmodule MySQLOTP.Application do + use Application + + @impl true + def start(_type, _args) do + IO.puts("MySQLOTP.Application.start/2 called") + + # This is a CLI app, so we run the command and exit + # Start the application supervisor first + children = [] + opts = [strategy: :one_for_one, name: MySQLOTP.Supervisor] + + {:ok, pid} = Supervisor.start_link(children, opts) + IO.puts("Supervisor started") + + # Spawn a task to run the CLI command + IO.puts("Spawning MySQLOTPTest.main/1") + spawn(fn -> + MySQLOTPTest.main([]) + end) + + {:ok, pid} + end +end + diff --git a/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp_test.ex b/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp_test.ex new file mode 100644 index 0000000000..8cc01a032d --- /dev/null +++ b/integration-tests/mysql-client-tests/elixir/mysql/lib/mysql_otp_test.ex @@ -0,0 +1,93 @@ +defmodule MySQLOTPTest do + @moduledoc """ + Test for MySQL/OTP (Erlang native MySQL client) + Uses the :mysql Erlang library + """ + + def main(_args \\ []) do + IO.puts("Starting MySQL/OTP Test") + + cli_args = Burrito.Util.Args.get_arguments() + IO.puts("Received CLI args: #{inspect(cli_args)}") + + result = run(cli_args) + System.halt(0) + result + end + + defp run(args) do + if length(args) < 3 do + IO.puts("Usage: mysql-otp-test ") + System.halt(1) + end + + user = Enum.at(args, 0) + port_str = Enum.at(args, 1) + database = Enum.at(args, 2) + + {port, _} = Integer.parse(port_str) + + # Start MySQL/OTP connection using Erlang :mysql module + {:ok, pid} = :mysql.start_link([ + host: '127.0.0.1', + port: port, + user: String.to_charlist(user), + password: '', + database: String.to_charlist(database) + ]) + + IO.puts("Connected using MySQL/OTP (Erlang connector)") + + # Test queries + queries = [ + "create table test (pk int, `value` int, primary key(pk))", + "describe test", + "select * from test", + "insert into test (pk, `value`) values (0,0)", + "select * from test", + "call dolt_add('-A')", + "call dolt_commit('-m', 'my commit')", + "select COUNT(*) FROM dolt_log", + "call dolt_checkout('-b', 'mybranch')", + "insert into test (pk, `value`) values (1,1)", + "call dolt_commit('-a', '-m', 'my commit2')", + "call dolt_checkout('main')", + "call dolt_merge('mybranch')", + "select COUNT(*) FROM dolt_log" + ] + + # Execute each query + Enum.each(queries, fn query -> + IO.puts("Executing: #{query}") + + case :mysql.query(pid, query) do + :ok -> + IO.puts(" → OK") + + {:ok, column_names, rows} -> + IO.puts(" → #{length(rows)} row(s) returned with #{length(column_names)} column(s)") + + {:error, reason} -> + IO.puts("Query failed: #{query}") + IO.puts("Error: #{inspect(reason)}") + :mysql.stop(pid) + System.halt(1) + end + end) + + # Verify final log count + {:ok, _columns, rows} = :mysql.query(pid, "select COUNT(*) FROM dolt_log") + [[count]] = rows + + if count != 3 do + IO.puts("Expected 3 commits in dolt_log, got #{count}") + :mysql.stop(pid) + System.halt(1) + end + + :mysql.stop(pid) + IO.puts("\nAll MySQL/OTP tests passed!") + :ok + end +end + diff --git a/integration-tests/mysql-client-tests/elixir/mysql/mix.exs b/integration-tests/mysql-client-tests/elixir/mysql/mix.exs new file mode 100644 index 0000000000..31972424b8 --- /dev/null +++ b/integration-tests/mysql-client-tests/elixir/mysql/mix.exs @@ -0,0 +1,43 @@ +defmodule MySQLOTP.MixProject do + use Mix.Project + + def project do + [ + app: :mysql_otp_test, + version: "0.1.0", + elixir: "~> 1.18", + start_permanent: Mix.env() == :prod, + deps: deps(), + releases: releases() + ] + end + + def application do + [ + extra_applications: [:logger, :crypto, :public_key, :ssl], + mod: {MySQLOTP.Application, []} + ] + end + + defp releases do + [ + mysql_otp: [ + steps: [:assemble, &Burrito.wrap/1], + burrito: [ + targets: [ + linux: [os: :linux, cpu: :x86_64] + ], + no_native_archivers: true + ] + ] + ] + end + + defp deps do + [ + {:mysql, "~> 1.9.0"}, + {:burrito, "~> 1.4.0"} + ] + end +end + diff --git a/integration-tests/mysql-client-tests/elixir/lib/simple.ex b/integration-tests/mysql-client-tests/elixir/myxql/lib/simple.ex similarity index 100% rename from integration-tests/mysql-client-tests/elixir/lib/simple.ex rename to integration-tests/mysql-client-tests/elixir/myxql/lib/simple.ex diff --git a/integration-tests/mysql-client-tests/elixir/myxql/lib/simple/application.ex b/integration-tests/mysql-client-tests/elixir/myxql/lib/simple/application.ex new file mode 100644 index 0000000000..11a1363659 --- /dev/null +++ b/integration-tests/mysql-client-tests/elixir/myxql/lib/simple/application.ex @@ -0,0 +1,25 @@ +defmodule Simple.Application do + use Application + + @impl true + def start(_type, _args) do + IO.puts("Simple.Application.start/2 called") + + # This is a CLI app, so we run the command and exit + # Start the application supervisor first + children = [] + opts = [strategy: :one_for_one, name: Simple.Supervisor] + + {:ok, pid} = Supervisor.start_link(children, opts) + IO.puts("Supervisor started") + + # Spawn a task to run the CLI command + IO.puts("Spawning SmokeTest.main/1") + spawn(fn -> + SmokeTest.main([]) + end) + + {:ok, pid} + end +end + diff --git a/integration-tests/mysql-client-tests/elixir/mix.exs b/integration-tests/mysql-client-tests/elixir/myxql/mix.exs similarity index 99% rename from integration-tests/mysql-client-tests/elixir/mix.exs rename to integration-tests/mysql-client-tests/elixir/myxql/mix.exs index 5bfb76a824..27943f628f 100644 --- a/integration-tests/mysql-client-tests/elixir/mix.exs +++ b/integration-tests/mysql-client-tests/elixir/myxql/mix.exs @@ -40,3 +40,4 @@ defmodule Simple.MixProject do ] end end + diff --git a/integration-tests/mysql-client-tests/go-mysql/go-mysql-test.go b/integration-tests/mysql-client-tests/go-mysql/mysql-client-test.go similarity index 100% rename from integration-tests/mysql-client-tests/go-mysql/go-mysql-test.go rename to integration-tests/mysql-client-tests/go-mysql/mysql-client-test.go diff --git a/integration-tests/mysql-client-tests/go/go-sql-driver-mysql-test.go b/integration-tests/mysql-client-tests/go/sql-driver-mysql-test.go similarity index 100% rename from integration-tests/mysql-client-tests/go/go-sql-driver-mysql-test.go rename to integration-tests/mysql-client-tests/go/sql-driver-mysql-test.go diff --git a/integration-tests/mysql-client-tests/java/pom.xml b/integration-tests/mysql-client-tests/java/pom.xml new file mode 100644 index 0000000000..7792f5ed13 --- /dev/null +++ b/integration-tests/mysql-client-tests/java/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + com.dolthub + j + 1.0.0 + + + 21 + 21 + UTF-8 + + + + + com.mysql + mysql-connector-j + 8.0.33 + + + org.mariadb.jdbc + mariadb-java-client + 3.5.3 + + + org.mariadb + r2dbc-mariadb + 1.2.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.target} + ${project.build.sourceEncoding} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + mysql-connector-test + package + shade + + mysql-connector-test + + + MySQLConnectorTest + + + + + + + + mysql-connector-test-collation + package + shade + + mysql-connector-test-collation + + + MySQLConnectorTest_Collation + + + + + + + + mariadb-connector-test + package + shade + + mariadb-connector-test + + + MariaDBConnectorTest + + + + + + + + mariadb-R2DBC-test + package + shade + + mariadb-R2DBC-test + + + MariaDBR2DBCTest + + + + + + + + + + diff --git a/integration-tests/mysql-client-tests/java/src/main/java/MariaDBConnectorTest.java b/integration-tests/mysql-client-tests/java/src/main/java/MariaDBConnectorTest.java new file mode 100644 index 0000000000..aa7e2f3e66 --- /dev/null +++ b/integration-tests/mysql-client-tests/java/src/main/java/MariaDBConnectorTest.java @@ -0,0 +1,179 @@ +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.ResultSet; +import java.util.Objects; + +public class MariaDBConnectorTest { + + // TestCase represents a single query test case + static class TestCase { + public String query; + public String expectedResult; + public Object fieldAccessor; // String (column name) or Integer (field position) + + public TestCase(String query, String expectedResult, Object fieldAccessor) { + this.query = query; + this.expectedResult = expectedResult; + this.fieldAccessor = fieldAccessor; + } + } + + // test queries to be run against Dolt + private static final TestCase[] testCases = { + new TestCase("create table test (pk int, `value` int, primary key(pk))", "0", 1), + new TestCase("describe test", "pk", 1), + new TestCase("select * from test", null, "pk"), + new TestCase("insert into test (pk, `value`) values (0,0)", "1", 1), + new TestCase("select * from test", "0", "test.pk"), + new TestCase("call dolt_add('-A')", "0", 1), + new TestCase("call dolt_commit('-m', 'my commit')", "0", 1), + new TestCase("select COUNT(*) FROM dolt_log", "2", 1), + new TestCase("call dolt_checkout('-b', 'mybranch')", "0", 1), + new TestCase("insert into test (pk, `value`) values (1,1)", "1", 1), + new TestCase("call dolt_commit('-a', '-m', 'my commit2')", "1", 1), + new TestCase("call dolt_checkout('main')", "0", 1), + new TestCase("call dolt_merge('mybranch')", "", 1), + new TestCase("select COUNT(*) FROM dolt_log", "3", "COUNT(*)"), + }; + + public static void main(String[] args) { + testStatements(args); + testServerSideCursors(args); + testCollation(args); + System.exit(0); + } + + // testServerSideCursors does a simple smoke test with server-side cursors to make sure + // results can be read. Note that we don't test results here; this is just a high level + // smoke test that we can execute server-side cursors logic without the server erroring out. + // This test was added for a regression where server-side cursor logic was getting + // corrupted result set memory and sending invalid data to the client, which caused the + // server to error out and crash the connection. If any errors are encountered, a stack trace + // is printed and this function exits with a non-zero code. + // For more details, see: https://github.com/dolthub/dolt/issues/9125 + private static void testServerSideCursors(String[] args) { + String user = args[0]; + String port = args[1]; + String db = args[2]; + + try { + // MariaDB JDBC URL format + String url = "jdbc:mariadb://127.0.0.1:" + port + "/" + db + + "?useCursorFetch=true"; + Connection conn = DriverManager.getConnection(url, user, ""); + + executePreparedQuery(conn, "SELECT 1;"); + executePreparedQuery(conn, "SELECT database();"); + executePreparedQuery(conn, "SHOW COLLATION;"); + executePreparedQuery(conn, "SHOW COLLATION;"); + executePreparedQuery(conn, "SHOW COLLATION;"); + } catch (SQLException ex) { + System.out.println("An error occurred."); + ex.printStackTrace(); + System.exit(1); + } + } + + // executePreparedQuery executes the specified |query| using |conn| as a prepared statement, + // and uses server-side cursor to fetch results. This method does not do any validation of + // results from the query. It is simply a smoke test to ensure the connection doesn't crash. + private static void executePreparedQuery(Connection conn, String query) throws SQLException { + PreparedStatement stmt = conn.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY); + stmt.setFetchSize(25); // needed to ensure a server-side cursor is used + + ResultSet rs = stmt.executeQuery(); + while (rs.next()) {} + + rs.close(); + stmt.close(); + } + + // testStatements executes the queries from |queries| and asserts their results from + // |expectedResults|. If any errors are encountered, a stack trace is printed and this + // function exits with a non-zero code. + private static void testStatements(String[] args) { + Connection conn = null; + + String user = args[0]; + String port = args[1]; + String db = args[2]; + + try { + // MariaDB JDBC URL format + String url = "jdbc:mariadb://127.0.0.1:" + port + "/" + db; + String password = ""; + + conn = DriverManager.getConnection(url, user, password); + Statement st = conn.createStatement(); + + for (TestCase test : testCases) { + if ( st.execute(test.query) ) { + ResultSet rs = st.getResultSet(); + if (rs.next()) { + String result = ""; + if (test.fieldAccessor instanceof String) { + result = rs.getString((String)test.fieldAccessor); + } else if (test.fieldAccessor instanceof Integer) { + result = rs.getString((Integer)test.fieldAccessor); + } else { + System.out.println("Unsupported field accessor value: " + test.fieldAccessor); + System.exit(1); + } + + if (!Objects.equals(test.expectedResult, result) && + !(test.query.contains("dolt_commit")) && + !(test.query.contains("dolt_merge"))) { + System.out.println("Query: \n" + test.query); + System.out.println("Expected:\n" + test.expectedResult); + System.out.println("Result:\n" + result); + System.exit(1); + } + } + } else { + String result = Integer.toString(st.getUpdateCount()); + if ( !Objects.equals(test.expectedResult, result) ) { + System.out.println("Query: \n" + test.query); + System.out.println("Expected:\n" + test.expectedResult); + System.out.println("Rows Updated:\n" + result); + System.exit(1); + } + } + } + } catch (SQLException ex) { + System.out.println("An error occurred."); + ex.printStackTrace(); + System.exit(1); + } + } + + // testCollation tests that metadata queries work properly with collations. + // This is a regression test for https://github.com/dolthub/dolt/issues/9890 + private static void testCollation(String[] args) { + String user = args[0]; + String port = args[1]; + String db = args[2]; + + try { + // MariaDB JDBC URL format + String url = "jdbc:mariadb://127.0.0.1:" + port + "/" + db; + Connection conn = DriverManager.getConnection(url, user, ""); + + // This should not throw an exception + ResultSet result = conn.getMetaData().getColumns(null, null, null, null); + + // Close the result set + if (result != null) { + result.close(); + } + conn.close(); + } catch (SQLException ex) { + System.out.println("Collation test failed."); + ex.printStackTrace(); + System.exit(1); + } + } +} + diff --git a/integration-tests/mysql-client-tests/java/src/main/java/MariaDBR2DBCTest.java b/integration-tests/mysql-client-tests/java/src/main/java/MariaDBR2DBCTest.java new file mode 100644 index 0000000000..7c2658db58 --- /dev/null +++ b/integration-tests/mysql-client-tests/java/src/main/java/MariaDBR2DBCTest.java @@ -0,0 +1,107 @@ +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.Result; +import org.mariadb.r2dbc.MariadbConnectionConfiguration; +import org.mariadb.r2dbc.MariadbConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +public class MariaDBR2DBCTest { + + // TestCase represents a single query test case + static class TestCase { + public String query; + public boolean isUpdate; + + public TestCase(String query, boolean isUpdate) { + this.query = query; + this.isUpdate = isUpdate; + } + } + + // test queries to be run against Dolt + private static final TestCase[] testCases = { + new TestCase("create table test (pk int, `value` int, primary key(pk))", true), + new TestCase("insert into test (pk, `value`) values (0,0)", true), + new TestCase("select * from test", false), + new TestCase("call dolt_add('-A')", false), + new TestCase("call dolt_commit('-m', 'my commit')", false), + new TestCase("select COUNT(*) FROM dolt_log", false), + new TestCase("call dolt_checkout('-b', 'mybranch')", false), + new TestCase("insert into test (pk, `value`) values (1,1)", true), + new TestCase("call dolt_commit('-a', '-m', 'my commit2')", false), + new TestCase("call dolt_checkout('main')", false), + new TestCase("call dolt_merge('mybranch')", false), + new TestCase("select COUNT(*) FROM dolt_log", false), + }; + + public static void main(String[] args) { + if (args.length < 3) { + System.err.println("Usage: MariaDBR2DBCTest "); + System.exit(1); + } + + String user = args[0]; + int port = Integer.parseInt(args[1]); + String database = args[2]; + + try { + runTests(user, port, database); + System.out.println("All R2DBC tests passed!"); + } catch (Exception e) { + System.err.println("R2DBC test failed: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static void runTests(String user, int port, String database) { + // Create connection configuration + MariadbConnectionConfiguration config = MariadbConnectionConfiguration.builder() + .host("127.0.0.1") + .port(port) + .username(user) + .password("") + .database(database) + .build(); + + ConnectionFactory connectionFactory = new MariadbConnectionFactory(config); + + // Run tests reactively - block at the end for the test + Mono.from(connectionFactory.create()) + .flatMapMany(connection -> + Flux.fromArray(testCases) + .concatMap(testCase -> executeTest(connection, testCase)) + .doFinally(signalType -> + Mono.from(connection.close()).subscribe() + ) + ) + .blockLast(Duration.ofSeconds(30)); + } + + private static Mono executeTest(Connection connection, TestCase testCase) { + System.out.println("Executing: " + testCase.query); + + return Mono.from(connection.createStatement(testCase.query).execute()) + .flatMap(result -> { + if (testCase.isUpdate) { + // For updates, just get the rows affected + return Mono.from(result.getRowsUpdated()).then(); + } else { + // For selects, consume all rows + return Flux.from(result.map((row, metadata) -> row)) + .then(); + } + }) + .onErrorResume(e -> { + System.err.println("Error executing query: " + testCase.query); + System.err.println("Error: " + e.getMessage()); + return Mono.error(e); + }); + } +} + diff --git a/integration-tests/mysql-client-tests/mysql-client-tests.bats b/integration-tests/mysql-client-tests/mysql-client-tests.bats index 177b1d59d9..15e4860cb0 100644 --- a/integration-tests/mysql-client-tests/mysql-client-tests.bats +++ b/integration-tests/mysql-client-tests/mysql-client-tests.bats @@ -25,16 +25,20 @@ teardown() { fi } -@test "go go-sql-drive/mysql test" { - /build/bin/go/go-mysql-client-test $USER $PORT $REPO_NAME +@test "go go-sql-driver/mysql" { + /build/bin/go/sql-driver-mysql-test $USER $PORT $REPO_NAME } -@test "go go-mysql test" { - /build/bin/go/go-sql-driver-test $USER $PORT $REPO_NAME +@test "go go-mysql" { + /build/bin/go/mysql-client-test $USER $PORT $REPO_NAME } -@test "python mysql.connector client" { - /build/bin/python/py-mysql-connector-test $USER $PORT $REPO_NAME +@test "python mysql.connector" { + /build/bin/python/mysql-connector-test $USER $PORT $REPO_NAME +} + +@test "python mariadb connector" { + /build/bin/python/mariadb-connector-test $USER $PORT $REPO_NAME } @test "python pymysql client" { @@ -42,67 +46,87 @@ teardown() { } @test "python sqlachemy client" { - /build/bin/python/py-sqlalchemy-test $USER $PORT $REPO_NAME + /build/bin/python/sqlalchemy-test $USER $PORT $REPO_NAME } -@test "mysql-connector-java client" { +@test "java mysql-connector-j" { java -jar /build/bin/java/mysql-connector-test.jar $USER $PORT $REPO_NAME } -@test "mysql-connector-java client collations" { +@test "java mysql-connector-j collation" { java -jar /build/bin/java/mysql-connector-test-collation.jar $USER $PORT $REPO_NAME } +@test "java mariadb-java-client" { + java -jar /build/bin/java/mariadb-connector-test.jar $USER $PORT $REPO_NAME +} + +@test "java r2dbc-mariadb connector" { + java -jar /build/bin/java/mariadb-R2DBC-test.jar $USER $PORT $REPO_NAME +} + @test "node mysql client" { node /build/bin/node/index.js $USER $PORT $REPO_NAME node /build/bin/node/knex.js $USER $PORT $REPO_NAME } +@test "node mariadb connector" { + node /build/bin/node/mariadb-connector.js $USER $PORT $REPO_NAME +} + @test "node mysql client, hosted workbench stability" { node /build/bin/node/workbench.js $USER $PORT $REPO_NAME /build/bin/node/testdata } -@test "c mysql connector" { - /build/bin/c/c-mysql-client-test $USER $PORT $REPO_NAME +@test "c mysql client" { + /build/bin/c/mysql-client-test $USER $PORT $REPO_NAME } -@test "c mariadb connector" { - /build/bin/c/c-mariadb-client-test $USER $PORT $REPO_NAME +@test "c mariadb client" { + /build/bin/c/mariadb-client-test $USER $PORT $REPO_NAME } @test "cpp mysql connector" { - /build/bin/cpp/cpp-mysql-connector-test $USER $PORT $REPO_NAME + /build/bin/cpp/mysql-connector-test $USER $PORT $REPO_NAME } @test "cpp mariadb connector" { - /build/bin/cpp/cpp-mariadb-connector-test $USER $PORT $REPO_NAME + /build/bin/cpp/mariadb-connector-test $USER $PORT $REPO_NAME } @test "dotnet mysql connector" { - /build/bin/dotnet/dotnet-mysql-connector-test $USER $PORT $REPO_NAME + /build/bin/dotnet/mysql-connector-test $USER $PORT $REPO_NAME } @test "dotnet mysql client" { - /build/bin/dotnet/dotnet-mysql-client-test $USER $PORT $REPO_NAME + /build/bin/dotnet/mysql-client-test $USER $PORT $REPO_NAME } @test "perl DBD:mysql client" { perl /build/bin/perl/dbd-mysql-test.pl $USER $PORT $REPO_NAME } -@test "ruby ruby/mysql test" { - ruby /build/bin/ruby/ruby-mysql-test.rb $USER $PORT $REPO_NAME +@test "perl DBD:MariaDB client" { + perl /build/bin/perl/dbd-mariadb-test.pl $USER $PORT $REPO_NAME } -@test "ruby mysql2 test" { +@test "ruby ruby/mysql client" { + ruby /build/bin/ruby/mysql-client-test.rb $USER $PORT $REPO_NAME +} + +@test "ruby mysql2" { ruby /build/bin/ruby/mysql2-test.rb $USER $PORT $REPO_NAME } -@test "elixir myxql test" { - /build/bin/elixir/elixir-mysql-client-test $USER $PORT $REPO_NAME +@test "elixir myxql" { + /build/bin/elixir/myxql-driver-test $USER $PORT $REPO_NAME } -@test "mysqldump works" { +@test "elixir mysql-otp" { + /build/bin/elixir/mysql-otp-test $USER $PORT $REPO_NAME +} + +@test "mysqldump" { mysqldump $REPO_NAME -P $PORT -h 0.0.0.0 -u $USER } diff --git a/integration-tests/mysql-client-tests/node/mariadb-connector.js b/integration-tests/mysql-client-tests/node/mariadb-connector.js new file mode 100644 index 0000000000..c76e0ebcb7 --- /dev/null +++ b/integration-tests/mysql-client-tests/node/mariadb-connector.js @@ -0,0 +1,70 @@ +import mariadb from "mariadb"; +import { getArgs } from "./helpers.js"; + +const tests = [ + { q: "create table test (pk int, `value` int, primary key(pk))", isQuery: false }, + { q: "describe test", isQuery: true }, + { q: "select * from test", isQuery: true }, + { q: "insert into test (pk, `value`) values (0,0)", isQuery: false }, + { q: "select * from test", isQuery: true }, + { q: "call dolt_add('-A');", isQuery: true }, + { q: "call dolt_commit('-m', 'my commit')", isQuery: true }, + { q: "select COUNT(*) FROM dolt_log", isQuery: true }, + { q: "call dolt_checkout('-b', 'mybranch')", isQuery: true }, + { q: "insert into test (pk, `value`) values (1,1)", isQuery: false }, + { q: "call dolt_commit('-a', '-m', 'my commit2')", isQuery: true }, + { q: "call dolt_checkout('main')", isQuery: true }, + { q: "call dolt_merge('mybranch')", isQuery: true }, + { q: "select COUNT(*) FROM dolt_log", isQuery: true }, +]; + +async function main() { + const { user, port, dbName } = getArgs(); + + let conn; + try { + // Create connection pool + const pool = mariadb.createPool({ + host: "127.0.0.1", + port: port, + user: user, + database: dbName, + connectionLimit: 5, + }); + + // Get a connection from the pool + conn = await pool.getConnection(); + console.log("Connected to MariaDB!"); + + // Run all tests + for (const test of tests) { + console.log(`Executing: ${test.q}`); + + if (test.isQuery) { + // Execute query and fetch results + const rows = await conn.query(test.q); + console.log(` → ${rows.length} row(s) returned`); + } else { + // Execute update/insert + const result = await conn.query(test.q); + console.log(` → ${result.affectedRows} row(s) affected`); + } + } + + console.log("\nAll MariaDB connector tests passed!"); + + // Close connection and pool + if (conn) conn.release(); + await pool.end(); + + process.exit(0); + } catch (err) { + console.error("MariaDB connector test failed:"); + console.error(err); + if (conn) conn.release(); + process.exit(1); + } +} + +main(); + diff --git a/integration-tests/mysql-client-tests/node/package.json b/integration-tests/mysql-client-tests/node/package.json index ef66fdd14a..add06a0339 100644 --- a/integration-tests/mysql-client-tests/node/package.json +++ b/integration-tests/mysql-client-tests/node/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "knex": "^2.4.0", + "mariadb": "^3.4.5", "mysql": "^2.18.1", "mysql2": "^3.9.8", "wtfnode": "^0.9.1", diff --git a/integration-tests/mysql-client-tests/perl/dbd-mariadb-test.pl b/integration-tests/mysql-client-tests/perl/dbd-mariadb-test.pl new file mode 100644 index 0000000000..588dba06e5 --- /dev/null +++ b/integration-tests/mysql-client-tests/perl/dbd-mariadb-test.pl @@ -0,0 +1,46 @@ +use strict; + +use DBI; + +my $QUERY_RESPONSE = [ + { "create table test (pk int, `value` int, primary key(pk))" => '0E0' }, + { "describe test" => 2 }, + { "insert into test (pk, `value`) values (0,0)" => 1 }, + { "select * from test" => 1 }, + {"call dolt_add('-A');" => 1 }, + {"call dolt_commit('-m', 'my commit')" => 1}, + {"call dolt_checkout('-b', 'mybranch')" => 1 }, + {"insert into test (pk, `value`) values (1,1)" => 1 }, + {"call dolt_commit('-a', '-m', 'my commit2')" => 1 }, + {"call dolt_checkout('main')" => 1 }, + {"call dolt_merge('mybranch')" => 1 }, + {"select COUNT(*) FROM dolt_log" => 1 }, +]; + +my $user = $ARGV[0]; +my $port = $ARGV[1]; +my $db = $ARGV[2]; + +# Use DBD::MariaDB driver +my $dsn = "DBI:MariaDB:database=$db;host=127.0.0.1;port=$port"; +my $dbh = DBI->connect($dsn, $user, "", {PrintError => 1, RaiseError => 1}); + +print "Connected using DBD::MariaDB\n"; + +foreach my $query_response ( @{$QUERY_RESPONSE} ) { + my @query_keys = keys %{$query_response}; + my $query = $query_keys[0]; + my $exp_result = $query_response->{$query}; + + my $result = $dbh->do($query); + if ( $result != $exp_result ) { + print "QUERY: $query\n"; + print "EXPECTED: $exp_result\n"; + print "RESULT: $result\n"; + exit 1 + } +} + +print "All DBD::MariaDB tests passed!\n"; +exit 0; + diff --git a/integration-tests/mysql-client-tests/python/mariadb-connector-test.py b/integration-tests/mysql-client-tests/python/mariadb-connector-test.py new file mode 100644 index 0000000000..ff9af39c3c --- /dev/null +++ b/integration-tests/mysql-client-tests/python/mariadb-connector-test.py @@ -0,0 +1,81 @@ +import mariadb +import sys + +QUERY_RESPONSE = [ + {"create table test (pk int, `value` int, primary key(pk))": ()}, + {"describe test": ( + ('pk', 'int', 'NO', 'PRI', None, ''), + ('value', 'int', 'YES', '', None, '') + )}, + {"insert into test (pk, `value`) values (0,0)": ()}, + {"select * from test": ((0, 0),)}, + {"call dolt_add('-A');": ((0,),)}, + {"call dolt_commit('-m', 'my commit')": (('',),)}, + {"select COUNT(*) FROM dolt_log": ((2,),)}, + {"call dolt_checkout('-b', 'mybranch')": ((0, "Switched to branch 'mybranch'"),)}, + {"insert into test (pk, `value`) values (1,1)": ()}, + {"call dolt_commit('-a', '-m', 'my commit2')": (('',),)}, + {"call dolt_checkout('main')": ((0, "Switched to branch 'main'"),)}, + {"call dolt_merge('mybranch')": (('',1,0,),)}, + {"select COUNT(*) FROM dolt_log": ((3,),)}, +] + + +def main(): + user = sys.argv[1] + port = int(sys.argv[2]) + db = sys.argv[3] + + try: + # Connect using MariaDB Connector/Python + connection = mariadb.connect( + user=user, + host="127.0.0.1", + port=port, + database=db + ) + + print(f"Connected to MariaDB using MariaDB Connector/Python v{mariadb.__version__}") + + cursor = connection.cursor() + + for query_response in QUERY_RESPONSE: + query = list(query_response.keys())[0] + exp_results = query_response[query] + + cursor.execute(query) + + try: + results = cursor.fetchall() + + # MariaDB Connector/Python returns lists, convert to tuples for comparison + results = tuple(tuple(row) if isinstance(row, list) else row for row in results) + + # Skip validation for dolt_commit and dolt_merge as their results vary + if ("dolt_commit" not in query) and ("dolt_merge" not in query): + if results != exp_results: + print("Query:") + print(query) + print("Expected:") + print(exp_results) + print("Received:") + print(results) + sys.exit(1) + except mariadb.Error: + # This is a write query with no results + pass + + cursor.close() + connection.close() + + print("All MariaDB Connector/Python tests passed!") + sys.exit(0) + + except mariadb.Error as e: + print(f"Error connecting to MariaDB: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/integration-tests/mysql-client-tests/python/py-mysql-connector-test.py b/integration-tests/mysql-client-tests/python/mysql-connector-test.py similarity index 100% rename from integration-tests/mysql-client-tests/python/py-mysql-connector-test.py rename to integration-tests/mysql-client-tests/python/mysql-connector-test.py diff --git a/integration-tests/mysql-client-tests/python/pymysql-test.py b/integration-tests/mysql-client-tests/python/pymysql-test.py index ab542ce06c..f7cf4385a3 100644 --- a/integration-tests/mysql-client-tests/python/pymysql-test.py +++ b/integration-tests/mysql-client-tests/python/pymysql-test.py @@ -1,54 +1,54 @@ -import pymysql -import sys - -QUERY_RESPONSE = [ - {"create table test (pk int, `value` int, primary key(pk))": ()}, - {"describe test": ( - ('pk', 'int', 'NO', 'PRI', None, ''), - ('value', 'int', 'YES', '', None, '') - )}, - {"insert into test (pk, `value`) values (0,0)": ()}, - {"select * from test": ((0, 0),)}, - {"call dolt_add('-A');": ((0,),)}, - {"call dolt_commit('-m', 'my commit')": (('',),)}, - {"select COUNT(*) FROM dolt_log": ((2,),)}, - {"call dolt_checkout('-b', 'mybranch')": ((0, "Switched to branch 'mybranch'"),)}, - {"insert into test (pk, `value`) values (1,1)": ()}, - {"call dolt_commit('-a', '-m', 'my commit2')": (('',),)}, - {"call dolt_checkout('main')": ((0, "Switched to branch 'main'"),)}, - {"call dolt_merge('mybranch')": (('',1,0,),)}, - {"select COUNT(*) FROM dolt_log": ((3,),)}, -] - - -def main(): - user = sys.argv[1] - port = int(sys.argv[2]) - db = sys.argv[3] - - connection = pymysql.connect(host="127.0.0.1", - port=port, - user=user, - db=db) - - for query_response in QUERY_RESPONSE: - query = list(query_response.keys())[0] - exp_results = query_response[query] - with connection.cursor() as cursor: - cursor.execute(query) - results = cursor.fetchall() - if (results != exp_results) and ("dolt_commit" not in query) and ("dolt_merge" not in query): - print("Query:") - print(query) - print("Expected:") - print(exp_results) - print("Received:") - print(results) - sys.exit(1) - - connection.close() - - sys.exit(0) - - -main() +import pymysql +import sys + +QUERY_RESPONSE = [ + {"create table test (pk int, `value` int, primary key(pk))": ()}, + {"describe test": ( + ('pk', 'int', 'NO', 'PRI', None, ''), + ('value', 'int', 'YES', '', None, '') + )}, + {"insert into test (pk, `value`) values (0,0)": ()}, + {"select * from test": ((0, 0),)}, + {"call dolt_add('-A');": ((0,),)}, + {"call dolt_commit('-m', 'my commit')": (('',),)}, + {"select COUNT(*) FROM dolt_log": ((2,),)}, + {"call dolt_checkout('-b', 'mybranch')": ((0, "Switched to branch 'mybranch'"),)}, + {"insert into test (pk, `value`) values (1,1)": ()}, + {"call dolt_commit('-a', '-m', 'my commit2')": (('',),)}, + {"call dolt_checkout('main')": ((0, "Switched to branch 'main'"),)}, + {"call dolt_merge('mybranch')": (('',1,0,),)}, + {"select COUNT(*) FROM dolt_log": ((3,),)}, +] + + +def main(): + user = sys.argv[1] + port = int(sys.argv[2]) + db = sys.argv[3] + + connection = pymysql.connect(host="127.0.0.1", + port=port, + user=user, + db=db) + + for query_response in QUERY_RESPONSE: + query = list(query_response.keys())[0] + exp_results = query_response[query] + with connection.cursor() as cursor: + cursor.execute(query) + results = cursor.fetchall() + if (results != exp_results) and ("dolt_commit" not in query) and ("dolt_merge" not in query): + print("Query:") + print(query) + print("Expected:") + print(exp_results) + print("Received:") + print(results) + sys.exit(1) + + connection.close() + + sys.exit(0) + + +main() diff --git a/integration-tests/mysql-client-tests/python/py-sqlalchemy-test.py b/integration-tests/mysql-client-tests/python/sqlalchemy-test.py similarity index 100% rename from integration-tests/mysql-client-tests/python/py-sqlalchemy-test.py rename to integration-tests/mysql-client-tests/python/sqlalchemy-test.py diff --git a/integration-tests/mysql-client-tests/r/rmariadb-test.r b/integration-tests/mysql-client-tests/r/rmariadb-test.r index 7cae8d7de0..0481b4f347 100644 --- a/integration-tests/mysql-client-tests/r/rmariadb-test.r +++ b/integration-tests/mysql-client-tests/r/rmariadb-test.r @@ -1,89 +1,89 @@ -library(RMariaDB) -library(DBI) - -args = commandArgs(trailingOnly=TRUE) - -user = args[1] -port = args[2] -db = args[3] - -conn = dbConnect(RMariaDB::MariaDB(), host="127.0.0.1", port = port, - username = user, dbname = db) - -# check standard queries -queries = list("create table test (pk int, value int, primary key(pk))", - "describe test", - "insert into test (pk, `value`) values (0,0)", - "select * from test") - -responses = list(NULL, - data.frame(Field = c("pk", "value"), Type = c("int", "int"), Null = c("NO", "YES"), Key = c("PRI", ""), Default = c(NA_character_, NA_character_), Extra = c("", ""), stringsAsFactors = FALSE), - NULL, - data.frame(pk = c(0), value = c(0), stringsAsFactors = FALSE)) - -for(i in 1:length(queries)) { - q = queries[[i]] - want = responses[[i]] - if (!is.null(want)) { - got <- dbGetQuery(conn, q) - if (!isTRUE(all.equal(want, got))) { - print(q) - print(want) - print(got) - quit(save="no", status=1) - } - } else { - dbExecute(conn, q) - } -} - -# check prepared statements -stmt <- dbSendStatement(conn, "INSERT INTO test values (?, ?)") -rs <- dbBind(stmt, list(1,1)) -rowsAff <- dbGetRowsAffected(rs) -dbClearResult(rs) - -if (rowsAff != 1) { - print("failed to execute prepared statement") - quit(save="no", status=1) -} - -got <- dbGetQuery(conn, "select * from test where pk = 1") -want = data.frame(pk = c(1), value = c(1)) -if (!isTRUE(all.equal(want, got))) { - print("unexpected prepared statement result") - print(got) - quit(save="no", status=1) -} - -dolt_queries = list("call DOLT_ADD('-A')", - "call dolt_commit('-m', 'my commit')", - "call dolt_checkout('-b', 'mybranch')", - "insert into test (pk, `value`) values (2,2)", - "call dolt_commit('-a', '-m', 'my commit2')", - "call dolt_checkout('main')", - "call dolt_merge('mybranch')") - -for(i in 1:length(dolt_queries)) { - q = dolt_queries[[i]] - dbExecute(conn, q) -} - -count <- dbGetQuery(conn, "select COUNT(*) as c from dolt_log") -want <- data.frame(c = c(3)) -ret <- all.equal(count, want) -if (!ret) { - print("Number of commits is incorrect") - quit(save="no", status=1) -} - -# Add a failing query and ensure that the connection does not quit. -# cc. https://github.com/dolthub/dolt/issues/3418 -try(dbExecute(conn, "insert into test values (0, 1)"), silent = TRUE) -one <- dbGetQuery(conn, "select 1 as pk") -ret <- one == data.frame(pk=1) -if (!ret) { - print("Number of commits is incorrect") - quit(save="no", status=1) -} - +library(RMariaDB) +library(DBI) + +args = commandArgs(trailingOnly=TRUE) + +user = args[1] +port = args[2] +db = args[3] + +conn = dbConnect(RMariaDB::MariaDB(), host="127.0.0.1", port = port, + username = user, dbname = db) + +# check standard queries +queries = list("create table test (pk int, value int, primary key(pk))", + "describe test", + "insert into test (pk, `value`) values (0,0)", + "select * from test") + +responses = list(NULL, + data.frame(Field = c("pk", "value"), Type = c("int", "int"), Null = c("NO", "YES"), Key = c("PRI", ""), Default = c(NA_character_, NA_character_), Extra = c("", ""), stringsAsFactors = FALSE), + NULL, + data.frame(pk = c(0), value = c(0), stringsAsFactors = FALSE)) + +for(i in 1:length(queries)) { + q = queries[[i]] + want = responses[[i]] + if (!is.null(want)) { + got <- dbGetQuery(conn, q) + if (!isTRUE(all.equal(want, got))) { + print(q) + print(want) + print(got) + quit(save="no", status=1) + } + } else { + dbExecute(conn, q) + } +} + +# check prepared statements +stmt <- dbSendStatement(conn, "INSERT INTO test values (?, ?)") +rs <- dbBind(stmt, list(1,1)) +rowsAff <- dbGetRowsAffected(rs) +dbClearResult(rs) + +if (rowsAff != 1) { + print("failed to execute prepared statement") + quit(save="no", status=1) +} + +got <- dbGetQuery(conn, "select * from test where pk = 1") +want = data.frame(pk = c(1), value = c(1)) +if (!isTRUE(all.equal(want, got))) { + print("unexpected prepared statement result") + print(got) + quit(save="no", status=1) +} + +dolt_queries = list("call DOLT_ADD('-A')", + "call dolt_commit('-m', 'my commit')", + "call dolt_checkout('-b', 'mybranch')", + "insert into test (pk, `value`) values (2,2)", + "call dolt_commit('-a', '-m', 'my commit2')", + "call dolt_checkout('main')", + "call dolt_merge('mybranch')") + +for(i in 1:length(dolt_queries)) { + q = dolt_queries[[i]] + dbExecute(conn, q) +} + +count <- dbGetQuery(conn, "select COUNT(*) as c from dolt_log") +want <- data.frame(c = c(3)) +ret <- all.equal(count, want) +if (!ret) { + print("Number of commits is incorrect") + quit(save="no", status=1) +} + +# Add a failing query and ensure that the connection does not quit. +# cc. https://github.com/dolthub/dolt/issues/3418 +try(dbExecute(conn, "insert into test values (0, 1)"), silent = TRUE) +one <- dbGetQuery(conn, "select 1 as pk") +ret <- one == data.frame(pk=1) +if (!ret) { + print("Number of commits is incorrect") + quit(save="no", status=1) +} + diff --git a/integration-tests/mysql-client-tests/ruby/ruby-mysql-test.rb b/integration-tests/mysql-client-tests/ruby/mysql-client-test.rb similarity index 100% rename from integration-tests/mysql-client-tests/ruby/ruby-mysql-test.rb rename to integration-tests/mysql-client-tests/ruby/mysql-client-test.rb