diff --git a/.github/actions/mysql-client-tests/action.yaml b/.github/actions/mysql-client-tests/action.yaml deleted file mode 100644 index aa05bed550..0000000000 --- a/.github/actions/mysql-client-tests/action.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: 'Dolt MySQL client integration tests' -description: 'Smoke tests for mysql client integrations' -runs: - using: 'docker' - image: '../../../integration-tests/MySQLDockerfile' diff --git a/.github/workflows/ci-mysql-client-tests.yaml b/.github/workflows/ci-mysql-client-tests.yaml index 18017f8bc9..1ec13cf467 100644 --- a/.github/workflows/ci-mysql-client-tests.yaml +++ b/.github/workflows/ci-mysql-client-tests.yaml @@ -11,14 +11,50 @@ concurrency: cancel-in-progress: true jobs: - mysql_client_integrations_job: + mysql_client_integrations: runs-on: ubuntu-22.04 timeout-minutes: 45 - name: Run tests steps: - name: Checkout uses: actions/checkout@v4 - - name: Copy go package - run: cp -r ./go ./integration-tests/go - - name: Test mysql client integrations - uses: ./.github/actions/mysql-client-tests + with: + path: dolt + + - name: Free disk space + run: | + NAME="DISK-CLEANUP" + echo "[${NAME}] Starting background cleanup..." + [ -d "$AGENT_TOOLSDIRECTORY" ] && sudo rm -rf "$AGENT_TOOLSDIRECTORY" & + [ -d /usr/share/dotnet ] && sudo rm -rf /usr/share/dotnet & + [ -d /usr/local/lib/android ] && sudo rm -rf /usr/local/lib/android & + [ -d /opt/ghc ] && sudo rm -rf /opt/ghc & + [ -d /usr/local/share/boost ] && sudo rm -rf /usr/local/share/boost & + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Ensure cache directory exists + run: | + sudo mkdir -p /mnt/.buildx-cache + sudo chown $USER:$USER /mnt/.buildx-cache + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /mnt/.buildx-cache + key: ${{ runner.os }}-docker-mysql-client-integrations + restore-keys: | + ${{ runner.os }}-docker + + - name: Build MySQL test image + uses: docker/build-push-action@v6 + with: + context: . + file: dolt/integration-tests/mysql-client-tests/Dockerfile + tags: mysql-client-tests:latest + load: true + cache-from: type=local,src=/mnt/.buildx-cache + cache-to: type=local,dest=/mnt/.buildx-cache + + - name: Test MySQL client integrations + run: docker run --rm mysql-client-tests:latest diff --git a/integration-tests/MySQLDockerfile b/integration-tests/MySQLDockerfile deleted file mode 100644 index 7f57cf3c16..0000000000 --- a/integration-tests/MySQLDockerfile +++ /dev/null @@ -1,149 +0,0 @@ -FROM --platform=linux/amd64 ubuntu:20.04 - -# install python, libmysqlclient-dev, java, bats, git ruby, perl, cpan -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update -y && \ - apt install -y \ - curl \ - gnupg \ - libwxbase3.0-0v5 \ - libwxgtk3.0-gtk3-0v5 \ - libncurses5 \ - libsctp1 \ - software-properties-common && \ - curl -sL https://deb.nodesource.com/setup_22.x | bash - && \ - add-apt-repository ppa:deadsnakes/ppa -y && \ - curl -OL https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/esl-erlang/esl-erlang_25.0-1~ubuntu~focal_amd64.deb && \ - dpkg -i esl-erlang_25.0-1~ubuntu~focal_amd64.deb && \ - curl -LO https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/elixir/elixir_1.14.3_1_otp_25.3~ubuntu~focal_all.deb && \ - dpkg -i elixir_1.14.3_1_otp_25.3~ubuntu~focal_all.deb -RUN apt update -y && \ - apt install -y \ - python3.9 \ - python3-pip \ - curl \ - wget \ - pkg-config \ - mysql-client \ - libmysqlclient-dev \ - openjdk-17-jdk \ - ant \ - ca-certificates-java \ - bats \ - perl \ - php \ - php-mysqli \ - cpanminus \ - cmake \ - g++ \ - libmysqlcppconn-dev \ - git \ - ruby \ - ruby-dev \ - gem \ - libc6 \ - libgcc1 \ - libgssapi-krb5-2 \ - libicu66 \ - libssl1.1 \ - libstdc++6 \ - zlib1g \ - r-base \ - postgresql \ - postgresql-contrib \ - libpq-dev \ - nodejs \ - lsof \ - postgresql-server-dev-all && \ - update-ca-certificates -f - -# install go -WORKDIR /root -ENV GO_VERSION=1.25.0 -ENV GOPATH=$HOME/go -ENV PATH=$PATH:$GOPATH/bin -ENV PATH=$PATH:$GOPATH/bin:/usr/local/go/bin -RUN curl -O "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" && \ - sha256sum "go${GO_VERSION}.linux-amd64.tar.gz" && \ - tar -xvf "go${GO_VERSION}.linux-amd64.tar.gz" -C /usr/local && \ - chown -R root:root /usr/local/go && \ - mkdir -p $HOME/go/{bin,src} && \ - go version - -# install MySQL dependency from source -RUN git clone https://github.com/go-sql-driver/mysql.git -WORKDIR mysql -RUN git checkout tags/v1.6.0 -b v1.6.0 -RUN go install . -WORKDIR / - -# install dotnet -RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version 8.0.412 --install-dir /usr/local/bin --no-path && \ - dotnet --version - -# install pip for python3.9 -RUN curl -LO https://bootstrap.pypa.io/get-pip.py && \ - python3.9 get-pip.py && \ - pip --version - -# install mysql connector and pymsql -RUN pip install mysql-connector-python==8.0.32 -RUN pip install PyMySQL==1.0.2 -RUN pip install sqlalchemy==1.4.46 - -# Setup JAVA_HOME -- useful for docker commandline -ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/ - -# install mysql connector java -RUN mkdir -p /mysql-client-tests/java -RUN curl -L -o /mysql-client-tests/java/mysql-connector-java-8.0.21.jar \ - https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.21/mysql-connector-java-8.0.21.jar - -# install node deps -COPY mysql-client-tests/node/package.json /mysql-client-tests/node/ -COPY mysql-client-tests/node/package-lock.json /mysql-client-tests/node/ -WORKDIR /mysql-client-tests/node -RUN npm install - -# install cpan dependencies -RUN cpanm --force DBD::mysql - -# install ruby dependencies -COPY mysql-client-tests/ruby/Gemfile /mysql-client-tests/ruby/ -COPY mysql-client-tests/ruby/Gemfile.lock /mysql-client-tests/ruby/ -WORKDIR /mysql-client-tests/ruby -RUN gem install bundler -v 2.1.4 && bundle install - -# install R packages -RUN Rscript -e 'install.packages(c("DBI", "RMySQL", "RMariaDB"), \ - repos = c(RSPM="https://packagemanager.rstudio.com/cran/__linux__/focal/latest"))' - -# install rust -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y -ENV PATH="/root/.cargo/bin:${PATH}" - -# install postgres and psql -RUN service postgresql start - -# install mysql_fdw -WORKDIR /mysql-client-tests/mysql_fdw -RUN git clone https://github.com/EnterpriseDB/mysql_fdw --branch REL-2_9_0 -WORKDIR /mysql-client-tests/mysql_fdw/mysql_fdw -RUN make USE_PGXS=1 && \ - make USE_PGXS=1 install - -# install dolt from source -WORKDIR /root/building -COPY ./go . -ENV GOFLAGS="-mod=readonly" -RUN go build -o /usr/local/bin/dolt ./cmd/dolt - -COPY mysql-client-tests /mysql-client-tests -COPY mysql-client-tests/mysql-client-tests-entrypoint.sh /mysql-client-tests/entrypoint.sh - -# install rust dependencies -WORKDIR /mysql-client-tests/rust -RUN cargo build --config net.git-fetch-with-cli=true - -WORKDIR /mysql-client-tests -ENTRYPOINT ["/mysql-client-tests/entrypoint.sh"] diff --git a/integration-tests/MySQLDockerfile.dockerignore b/integration-tests/MySQLDockerfile.dockerignore deleted file mode 100644 index efcd3e81bf..0000000000 --- a/integration-tests/MySQLDockerfile.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -mysql-client-tests/c/mysql-client-tests -mysql-client-tests/java/MySQLConnectorTest.class -mysql-client-tests/cpp/_build/**/* -mysql-client-tests/cpp/_build -bats diff --git a/integration-tests/mysql-client-tests/Dockerfile b/integration-tests/mysql-client-tests/Dockerfile new file mode 100644 index 0000000000..01b924e1dd --- /dev/null +++ b/integration-tests/mysql-client-tests/Dockerfile @@ -0,0 +1,186 @@ +# syntax=docker/dockerfile:1 +FROM golang:1.25-alpine AS golang_cgo125 +ENV CGO_ENABLED=1 +ENV GO_LDFLAGS="-linkmode external -extldflags '-static'" +RUN apk add --no-cache build-base + +FROM golang_cgo125 AS dolt_build +RUN apk add --no-cache icu-dev icu-static +COPY dolt/go/go.mod /build/dolt/go/ +WORKDIR /build/dolt/go/ +RUN go mod download +COPY dolt/go/ /build/dolt/go/ +RUN go build -tags icu_static -ldflags "$GO_LDFLAGS" -o /build/bin/dolt ./cmd/dolt + +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/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/sql-driver-mysql-test + +FROM rust:1.90-alpine3.22 AS rust_clients_build +RUN apk add --no-cache musl-dev +COPY dolt/integration-tests/mysql-client-tests/rust/ /build/rust/ +WORKDIR /build/rust/ +RUN cargo build --release --target-dir /build/bin/ && cargo clean # exe is in release/ + +FROM debian:bookworm-slim AS dotnet_clients_build +RUN apt-get update && apt-get install -y wget gnupg ca-certificates && rm -rf /var/lib/apt/lists/* +RUN wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb +RUN apt-get update && apt-get install -y dotnet-sdk-9.0 && rm -rf /var/lib/apt/lists/* +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlClient/*.csproj /build/dotnet/MySqlClient/ +WORKDIR /build/dotnet/MySqlClient/ +RUN dotnet restore +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlClient/ /build/dotnet/MySqlClient/ +RUN dotnet publish -c Release -o /build/bin --no-restore + +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlConnector/*.csproj /build/dotnet/MySqlConnector/ +WORKDIR /build/dotnet/MySqlConnector/ +RUN dotnet restore +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlConnector/ /build/dotnet/MySqlConnector/ +RUN dotnet publish -c Release -o /build/bin --no-restore +# devart dotconnect reqs a license so we've skipped it here + +FROM debian:bookworm-slim 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 unixodbc-dev odbcinst wget gcc make && rm -rf /var/lib/apt/lists/* +RUN wget --progress=dot:giga https://dlm.mariadb.com/4465891/Connectors/odbc/connector-odbc-3.2.7/mariadb-connector-odbc_3.2.7-1+maria~bookworm_amd64.deb \ + && dpkg -i mariadb-connector-odbc_3.2.7-1+maria~bookworm_amd64.deb || apt-get install -y -f \ + && rm mariadb-connector-odbc_3.2.7-1+maria~bookworm_amd64.deb \ + && odbcinst -i -d -f /dev/stdin < +#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/mariadb-odbc-test.c b/integration-tests/mysql-client-tests/c/mariadb-odbc-test.c new file mode 100644 index 0000000000..20d8b4fcbb --- /dev/null +++ b/integration-tests/mysql-client-tests/c/mariadb-odbc-test.c @@ -0,0 +1,186 @@ +#include +#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", +}; + +void check_error(SQLRETURN ret, SQLHANDLE handle, SQLSMALLINT type, const char *msg) { + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + SQLCHAR sqlstate[6]; + SQLCHAR message[SQL_MAX_MESSAGE_LENGTH]; + SQLINTEGER native_error; + SQLSMALLINT text_length; + + SQLGetDiagRec(type, handle, 1, sqlstate, &native_error, message, sizeof(message), &text_length); + fprintf(stderr, "%s\nSQLSTATE: %s\nMessage: %s\n", msg, sqlstate, message); + exit(1); + } +} + +typedef struct prepared_statement_t { + char *query; + int num_params; + int pk_param; + int value_param; + int expect_prepare_error; + int expect_exec_error; +} prepared_statement; + +void test_prepared_statement(SQLHDBC dbc, prepared_statement *pstmt) { + SQLHSTMT stmt; + SQLRETURN ret; + + ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); + check_error(ret, dbc, SQL_HANDLE_DBC, "Failed to allocate statement handle"); + + ret = SQLPrepare(stmt, (SQLCHAR *)pstmt->query, SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + if (!pstmt->expect_prepare_error) { + check_error(ret, stmt, SQL_HANDLE_STMT, "Failed to prepare statement"); + } else { + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + return; + } + } + + if (pstmt->num_params > 0) { + if (pstmt->pk_param) { + SQLINTEGER pk = 1; + ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &pk, 0, NULL); + check_error(ret, stmt, SQL_HANDLE_STMT, "Failed to bind pk parameter"); + } + if (pstmt->num_params > 1 && pstmt->value_param) { + SQLINTEGER value = 12; + ret = SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &value, 0, NULL); + check_error(ret, stmt, SQL_HANDLE_STMT, "Failed to bind value parameter"); + } + } + + ret = SQLExecute(stmt); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + if (!pstmt->expect_exec_error) { + check_error(ret, stmt, SQL_HANDLE_STMT, "Failed to execute statement"); + } + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); +} + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char *user = argv[1]; + int port = atoi(argv[2]); + char *db = argv[3]; + + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + SQLRETURN ret; + + // Allocate environment handle + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); + check_error(ret, env, SQL_HANDLE_ENV, "Failed to allocate environment handle"); + + // Set ODBC version + ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0); + check_error(ret, env, SQL_HANDLE_ENV, "Failed to set ODBC version"); + + // Allocate connection handle + ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + check_error(ret, env, SQL_HANDLE_ENV, "Failed to allocate connection handle"); + + // Build connection string + char connStr[512]; + snprintf(connStr, sizeof(connStr), + "DRIVER=MariaDB ODBC 3.2 Driver;SERVER=127.0.0.1;PORT=%d;DATABASE=%s;UID=%s;PWD=;", + port, db, user); + + // Connect to database + ret = SQLDriverConnect(dbc, NULL, (SQLCHAR *)connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); + check_error(ret, dbc, SQL_HANDLE_DBC, "Failed to connect to database"); + + printf("Connected to database successfully\n"); + + // Execute test queries + for (int i = 0; i < QUERIES_SIZE; i++) { + ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); + check_error(ret, dbc, SQL_HANDLE_DBC, "Failed to allocate statement handle"); + + ret = SQLExecDirect(stmt, (SQLCHAR *)queries[i], SQL_NTS); + if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { + printf("QUERY FAILED: %s\n", queries[i]); + check_error(ret, stmt, SQL_HANDLE_STMT, "Query execution failed"); + } + + // Fetch and discard results + while (SQLFetch(stmt) == SQL_SUCCESS) { + // Just consume the results + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + } + + // Test prepared statements + prepared_statement statements[] = { + { + .query = "select * from test where pk = ?", + .num_params = 1, + .pk_param = 1, + .expect_prepare_error = 0, + .expect_exec_error = 0, + }, + { + .query = "insert into test values (?, ?)", + .num_params = 2, + .pk_param = 1, + .value_param = 1, + .expect_prepare_error = 0, + .expect_exec_error = 0, + }, + { + .query = "select * from test SYNTAX ERROR where pk = ?", + .num_params = 1, + .pk_param = 1, + .expect_prepare_error = 1, + .expect_exec_error = 0, + }, + {NULL, 0, 0, 0, 0, 0}, // Sentinel + }; + + for (int i = 0; statements[i].query; i++) { + test_prepared_statement(dbc, &statements[i]); + } + + printf("All tests passed\n"); + + // Cleanup + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); + + 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/CMakeLists.txt b/integration-tests/mysql-client-tests/cpp/CMakeLists.txt deleted file mode 100644 index fd02d226f2..0000000000 --- a/integration-tests/mysql-client-tests/cpp/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -cmake_minimum_required(VERSION 3.10) - -project(DoltCxxConnectorTest - VERSION 0.1 - DESCRIPTION "A smoke test for mysql-connector-c++ connecting to Dolt" - LANGUAGES CXX) - -add_executable(test_mysql_connector_cxx mysql-connector-cpp-test.cpp) -set_property(TARGET test_mysql_connector_cxx PROPERTY CXX_STANDARD 11) - -if(WITH_JDBC) - add_subdirectory(third_party/mysql-connector-cpp EXCLUDE_FROM_ALL) - target_link_libraries(test_mysql_connector_cxx connector-jdbc) -else() - find_library(LIBMYSQLCPPCONN "mysqlcppconn") - target_link_libraries(test_mysql_connector_cxx "${LIBMYSQLCPPCONN}") -endif() diff --git a/integration-tests/mysql-client-tests/cpp/Makefile b/integration-tests/mysql-client-tests/cpp/Makefile index 1a26239014..d00eeea100 100644 --- a/integration-tests/mysql-client-tests/cpp/Makefile +++ b/integration-tests/mysql-client-tests/cpp/Makefile @@ -1,14 +1,26 @@ -MYSQL_CONCPP_DIR = /usr/local/Cellar/mysql-connector-c++/8.0.21 -CPPFLAGS = -I $(MYSQL_CONCPP_DIR)/include -L $(MYSQL_CONCPP_DIR)/lib64 -LDLIBS = -lmysqlcppconn8 -CXX = clang++ -stdlib=libc++ -CXXFLAGS = -std=c++11 +CXX = g++ -all: mysql-connector-cpp-test +CXXFLAGS_MYSQL = -I/usr/include/cppconn -std=c++11 -Wall -O2 +CXXFLAGS_MARIADB = -I/usr/include/mariadb -std=c++11 -Wall -O2 -mysql-connector-cpp-test: mysql-connector-cpp-test.cpp - $(CXX) $(CXXFLAGS) $(CPPFLAGS) -o $@ $^ $(LDLIBS) +LDFLAGS_MYSQL = -lmysqlcppconn +LDFLAGS_MARIADB = -lmariadbcpp + +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) + +$(TARGET_MYSQL): $(SRCS_MYSQL) + @mkdir -p /build/bin + $(CXX) $(CXXFLAGS_MYSQL) -o $@ $^ $(LDFLAGS_MYSQL) + +$(TARGET_MARIADB): $(SRCS_MARIADB) + @mkdir -p /build/bin + $(CXX) $(CXXFLAGS_MARIADB) -o $@ $^ $(LDFLAGS_MARIADB) -.PHONY: clean clean: - rm -f mysql-connector-cpp-test + rm -f $(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 93% 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 b1d5bd2c05..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 @@ -46,6 +46,11 @@ std::string queries[QUERIES_SIZE] = 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]; @@ -63,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/cpp/third_party/mysql-connector-cpp b/integration-tests/mysql-client-tests/cpp/third_party/mysql-connector-cpp deleted file mode 160000 index 857a8d63d8..0000000000 --- a/integration-tests/mysql-client-tests/cpp/third_party/mysql-connector-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 857a8d63d817a17160ca6062ceef6e9d6d0dd128 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 f06a4d0402..d65c9db305 100644 --- a/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj +++ b/integration-tests/mysql-client-tests/dotnet/MySqlClient/dotnet.csproj @@ -3,6 +3,9 @@ Exe net8.0 + 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 71de47491f..eb01c3e3d0 100755 --- a/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj +++ b/integration-tests/mysql-client-tests/dotnet/MySqlConnector/MySqlConnectorTest.csproj @@ -3,6 +3,9 @@ Exe net8.0 + mysql-connector-test + true + true diff --git a/integration-tests/mysql-client-tests/elixir/mix.exs b/integration-tests/mysql-client-tests/elixir/mix.exs deleted file mode 100644 index 51a20bd6f6..0000000000 --- a/integration-tests/mysql-client-tests/elixir/mix.exs +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Simple.MixProject do - use Mix.Project - - def project do - [ - app: :simple, - version: "0.1.0", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - {:myxql, "~> 0.5.0"}, - ] - end -end 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 72% rename from integration-tests/mysql-client-tests/elixir/lib/simple.ex rename to integration-tests/mysql-client-tests/elixir/myxql/lib/simple.ex index 89a5237f2b..4585787a87 100644 --- a/integration-tests/mysql-client-tests/elixir/lib/simple.ex +++ b/integration-tests/mysql-client-tests/elixir/myxql/lib/simple.ex @@ -1,16 +1,26 @@ defmodule SmokeTest do - def myTestFunc(arg1, arg2) do - if arg1 != arg2 do - raise "Test error" - end + def main(_args \\ []) do + IO.puts("Starting SmokeTest.main/1") + + cli_args = Burrito.Util.Args.get_arguments() + IO.puts("Received CLI args: #{inspect(cli_args)}") + + result = run(cli_args) + System.halt(0) + result end - @spec run :: nil - def run do - args = System.argv() + defp run(args) do + if length(args) < 3 do + IO.puts("Usage: simple ") + System.halt(1) + end + user = Enum.at(args, 0) - {port, _} = Integer.parse(Enum.at(args, 1)) + port_str = Enum.at(args, 1) database = Enum.at(args, 2) + + {port, _} = Integer.parse(port_str) {:ok, pid} = MyXQL.start_link(username: user, port: port, database: database) {:ok, _} = MyXQL.query(pid, "drop table if exists test") @@ -21,8 +31,6 @@ defmodule SmokeTest do myTestFunc(result.num_rows, 0) {:ok, _} = MyXQL.query(pid, "insert into test (pk, `value`) values (0,0)") - - # MyXQL uses the CLIENT_FOUND_ROWS flag so we should return the number of rows matched {:ok, result} = MyXQL.query(pid, "UPDATE test SET pk = pk where pk = 0") myTestFunc(result.num_rows, 1) @@ -31,7 +39,7 @@ defmodule SmokeTest do {:ok, result} = MyXQL.query(pid, "SELECT * FROM test") myTestFunc(result.num_rows, 1) - myTestFunc(result.rows, [[0,0]]) + myTestFunc(result.rows, [[0, 0]]) {:ok, _} = MyXQL.query(pid, "call dolt_add('-A');") {:ok, _} = MyXQL.query(pid, "call dolt_commit('-m', 'my commit')") @@ -45,5 +53,12 @@ defmodule SmokeTest do {:ok, result} = MyXQL.query(pid, "select COUNT(*) FROM dolt_log") myTestFunc(result.num_rows, 1) myTestFunc(result.rows, [[3]]) + :ok + end + + defp myTestFunc(arg1, arg2) do + if arg1 != arg2 do + raise "Test error: expected #{inspect(arg2)}, got #{inspect(arg1)}" + end end end 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/myxql/mix.exs b/integration-tests/mysql-client-tests/elixir/myxql/mix.exs new file mode 100644 index 0000000000..27943f628f --- /dev/null +++ b/integration-tests/mysql-client-tests/elixir/myxql/mix.exs @@ -0,0 +1,43 @@ +defmodule Simple.MixProject do + use Mix.Project + + def project do + [ + app: :simple, + 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: {Simple.Application, []} + ] + end + + defp releases do + [ + simple: [ + steps: [:assemble, &Burrito.wrap/1], + burrito: [ + targets: [ + linux: [os: :linux, cpu: :x86_64] + ], + no_native_archivers: true + ] + ] + ] + end + + defp deps do + [ + {:myxql, "~> 0.5.0"}, + {:burrito, "~> 1.4.0"} + ] + 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..82224dbdb8 --- /dev/null +++ b/integration-tests/mysql-client-tests/java/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + com.dolthub + j + 1.0.0 + + + 17 + 17 + 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/java/MySQLConnectorTest.java b/integration-tests/mysql-client-tests/java/src/main/java/MySQLConnectorTest.java similarity index 97% rename from integration-tests/mysql-client-tests/java/MySQLConnectorTest.java rename to integration-tests/mysql-client-tests/java/src/main/java/MySQLConnectorTest.java index 3bed99ab29..2cbc1a2a3e 100644 --- a/integration-tests/mysql-client-tests/java/MySQLConnectorTest.java +++ b/integration-tests/mysql-client-tests/java/src/main/java/MySQLConnectorTest.java @@ -1,173 +1,173 @@ -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.sql.ResultSetMetaData; - -public class MySQLConnectorTest { - - // test queries to be run against Dolt - private static final String[] 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", - }; - - // We currently only test a single field value in the first row - private static final String[] expectedResults = { - "0", - "pk", - null, - "1", - "0", - "0", - "0", - "2", - "0", - "1", - "1", - "0", - "", - "3" - }; - - // fieldAccessors are the value used to access a field in a row in a result set. Currently, only - // String (i.e column name) and Integer (i.e. field position) values are supported. - private static final Object[] fieldAccessors = { - 1, - 1, - "pk", - 1, - "test.pk", - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - "COUNT(*)", - }; - - public static void main(String[] args) { - testStatements(args); - testServerSideCursors(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 { - String url = "jdbc:mysql://127.0.0.1:" + port + "/" + db + - "?useServerPrepStmts=true&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 { - String url = "jdbc:mysql://127.0.0.1:" + port + "/" + db; - String password = ""; - - conn = DriverManager.getConnection(url, user, password); - Statement st = conn.createStatement(); - - for (int i = 0; i < queries.length; i++) { - String query = queries[i]; - String expected = expectedResults[i]; - if ( st.execute(query) ) { - ResultSet rs = st.getResultSet(); - if (rs.next()) { - String result = ""; - Object fieldAccessor = fieldAccessors[i]; - if (fieldAccessor instanceof String) { - result = rs.getString((String)fieldAccessor); - } else if (fieldAccessor instanceof Integer) { - result = rs.getString((Integer)fieldAccessor); - } else { - System.out.println("Unsupported field accessor value: " + fieldAccessor); - System.exit(1); - } - - if (!expected.equals(result) && !(query.contains("dolt_commit")) && !(query.contains("dolt_merge"))) { - System.out.println("Query: \n" + query); - System.out.println("Expected:\n" + expected); - System.out.println("Result:\n" + result); - System.exit(1); - } - } - } else { - String result = Integer.toString(st.getUpdateCount()); - if ( !expected.equals(result) ) { - System.out.println("Query: \n" + query); - System.out.println("Expected:\n" + expected); - System.out.println("Rows Updated:\n" + result); - System.exit(1); - } - } - } - } catch (SQLException ex) { - System.out.println("An error occurred."); - ex.printStackTrace(); - System.exit(1); - } - } -} +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.sql.ResultSetMetaData; + +public class MySQLConnectorTest { + + // test queries to be run against Dolt + private static final String[] 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", + }; + + // We currently only test a single field value in the first row + private static final String[] expectedResults = { + "0", + "pk", + null, + "1", + "0", + "0", + "0", + "2", + "0", + "1", + "1", + "0", + "", + "3" + }; + + // fieldAccessors are the value used to access a field in a row in a result set. Currently, only + // String (i.e column name) and Integer (i.e. field position) values are supported. + private static final Object[] fieldAccessors = { + 1, + 1, + "pk", + 1, + "test.pk", + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + "COUNT(*)", + }; + + public static void main(String[] args) { + testStatements(args); + testServerSideCursors(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 { + String url = "jdbc:mysql://127.0.0.1:" + port + "/" + db + + "?useServerPrepStmts=true&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 { + String url = "jdbc:mysql://127.0.0.1:" + port + "/" + db; + String password = ""; + + conn = DriverManager.getConnection(url, user, password); + Statement st = conn.createStatement(); + + for (int i = 0; i < queries.length; i++) { + String query = queries[i]; + String expected = expectedResults[i]; + if ( st.execute(query) ) { + ResultSet rs = st.getResultSet(); + if (rs.next()) { + String result = ""; + Object fieldAccessor = fieldAccessors[i]; + if (fieldAccessor instanceof String) { + result = rs.getString((String)fieldAccessor); + } else if (fieldAccessor instanceof Integer) { + result = rs.getString((Integer)fieldAccessor); + } else { + System.out.println("Unsupported field accessor value: " + fieldAccessor); + System.exit(1); + } + + if (!expected.equals(result) && !(query.contains("dolt_commit")) && !(query.contains("dolt_merge"))) { + System.out.println("Query: \n" + query); + System.out.println("Expected:\n" + expected); + System.out.println("Result:\n" + result); + System.exit(1); + } + } + } else { + String result = Integer.toString(st.getUpdateCount()); + if ( !expected.equals(result) ) { + System.out.println("Query: \n" + query); + System.out.println("Expected:\n" + expected); + System.out.println("Rows Updated:\n" + result); + System.exit(1); + } + } + } + } catch (SQLException ex) { + System.out.println("An error occurred."); + ex.printStackTrace(); + System.exit(1); + } + } +} diff --git a/integration-tests/mysql-client-tests/java/MySQLConnectorTest_Collation.java b/integration-tests/mysql-client-tests/java/src/main/java/MySQLConnectorTest_Collation.java similarity index 100% rename from integration-tests/mysql-client-tests/java/MySQLConnectorTest_Collation.java rename to integration-tests/mysql-client-tests/java/src/main/java/MySQLConnectorTest_Collation.java diff --git a/integration-tests/mysql-client-tests/mysql-client-tests-entrypoint.sh b/integration-tests/mysql-client-tests/mysql-client-tests-entrypoint.sh index 9ee3bc7e83..f89dc6a08c 100755 --- a/integration-tests/mysql-client-tests/mysql-client-tests-entrypoint.sh +++ b/integration-tests/mysql-client-tests/mysql-client-tests-entrypoint.sh @@ -7,4 +7,4 @@ dolt config --global --add user.name mysql-test-runner dolt config --global --add user.email mysql-test-runner@liquidata.co echo "Running mysql-client-tests:" -bats /mysql-client-tests/mysql-client-tests.bats +bats /build/bin/mysql-client-tests.bats diff --git a/integration-tests/mysql-client-tests/mysql-client-tests.bats b/integration-tests/mysql-client-tests/mysql-client-tests.bats index 1f8bd2172a..7d95832b9c 100644 --- a/integration-tests/mysql-client-tests/mysql-client-tests.bats +++ b/integration-tests/mysql-client-tests/mysql-client-tests.bats @@ -1,5 +1,5 @@ #!/usr/bin/env bats -load $BATS_TEST_DIRNAME/helpers.bash +load /build/bin/helpers.bash # MySQL client tests are set up to test Dolt as a MySQL server and # standard MySQL Clients in a wide array of languages. I used BATS because @@ -25,107 +25,112 @@ teardown() { fi } -@test "go go-sql-drive/mysql test" { - (cd $BATS_TEST_DIRNAME/go; go build .) - $BATS_TEST_DIRNAME/go/go $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" { - (cd $BATS_TEST_DIRNAME/go-mysql; go build .) - $BATS_TEST_DIRNAME/go-mysql/go $USER $PORT $REPO_NAME +@test "go go-mysql" { + /build/bin/go/mysql-client-test $USER $PORT $REPO_NAME } -@test "python mysql.connector client" { - python3.9 $BATS_TEST_DIRNAME/python/mysql.connector-test.py $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" { - python3.9 $BATS_TEST_DIRNAME/python/pymysql-test.py $USER $PORT $REPO_NAME + /build/bin/python/pymysql-test $USER $PORT $REPO_NAME } @test "python sqlachemy client" { - python3.9 $BATS_TEST_DIRNAME/python/sqlalchemy-test.py $USER $PORT $REPO_NAME + /build/bin/python/sqlalchemy-test $USER $PORT $REPO_NAME } -@test "mysql-connector-java client" { - javac $BATS_TEST_DIRNAME/java/MySQLConnectorTest.java - java -cp $BATS_TEST_DIRNAME/java:$BATS_TEST_DIRNAME/java/mysql-connector-java-8.0.21.jar MySQLConnectorTest $USER $PORT $REPO_NAME +@test "java mysql-connector-j" { + java -jar /build/bin/java/mysql-connector-test.jar $USER $PORT $REPO_NAME } -@test "mysql-connector-java client collations" { - javac $BATS_TEST_DIRNAME/java/MySQLConnectorTest_Collation.java - java -cp $BATS_TEST_DIRNAME/java:$BATS_TEST_DIRNAME/java/mysql-connector-java-8.0.21.jar MySQLConnectorTest_Collation $USER $PORT $REPO_NAME +@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 $BATS_TEST_DIRNAME/node/index.js $USER $PORT $REPO_NAME - node $BATS_TEST_DIRNAME/node/knex.js $USER $PORT $REPO_NAME + 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 $BATS_TEST_DIRNAME/node/workbench.js $USER $PORT $REPO_NAME $BATS_TEST_DIRNAME/node/testdata + node /build/bin/node/workbench.js $USER $PORT $REPO_NAME /build/bin/node/testdata } -@test "c mysql connector" { - (cd $BATS_TEST_DIRNAME/c; make clean; make) - $BATS_TEST_DIRNAME/c/mysql-connector-c-test $USER $PORT $REPO_NAME +@test "c mysql client" { + /build/bin/c/mysql-client-test $USER $PORT $REPO_NAME +} + +@test "c mariadb client" { + /build/bin/c/mariadb-client-test $USER $PORT $REPO_NAME +} + +@test "c mariadb odbc connector" { + /build/bin/c/mariadb-odbc-test $USER $PORT $REPO_NAME } @test "cpp mysql connector" { - if [ -d $BATS_TEST_DIRNAME/cpp/_build ] - then - rm -rf $BATS_TEST_DIRNAME/cpp/_build/* - else - mkdir $BATS_TEST_DIRNAME/cpp/_build - fi - cd $BATS_TEST_DIRNAME/cpp/_build - if [[ `uname` = "Darwin" ]]; then - PATH=/usr/local/Cellar/mysql-client/8.0.21/bin/:"$PATH" cmake .. -DWITH_SSL=/usr/local/Cellar/openssl@1.1/1.1.1g/ -DWITH_JDBC=yes; - else - cmake .. - fi -cmake .. - make -j 10 - $BATS_TEST_DIRNAME/cpp/_build/test_mysql_connector_cxx $USER $PORT $REPO_NAME - cd - + /build/bin/cpp/mysql-connector-test $USER $PORT $REPO_NAME +} + +@test "cpp mariadb connector" { + /build/bin/cpp/mariadb-connector-test $USER $PORT $REPO_NAME } @test "dotnet mysql connector" { - cd $BATS_TEST_DIRNAME/dotnet/MySqlConnector - # dotnet run uses output channel 3 which conflicts with bats so we pipe it to null - dotnet run -- $USER $PORT $REPO_NAME 3>&- + /build/bin/dotnet/mysql-connector-test $USER $PORT $REPO_NAME } @test "dotnet mysql client" { - cd $BATS_TEST_DIRNAME/dotnet/MySqlClient - # dotnet run uses output channel 3 which conflicts with bats so we pipe it to null - dotnet run -- $USER $PORT $REPO_NAME 3>&- + /build/bin/dotnet/mysql-client-test $USER $PORT $REPO_NAME } @test "perl DBD:mysql client" { - perl $BATS_TEST_DIRNAME/perl/dbd-mysql-test.pl $USER $PORT $REPO_NAME + perl /build/bin/perl/dbd-mysql-test.pl $USER $PORT $REPO_NAME } -@test "ruby ruby/mysql test" { - ruby $BATS_TEST_DIRNAME/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" { - ruby $BATS_TEST_DIRNAME/ruby/mysql2-test.rb $USER $PORT $REPO_NAME +@test "ruby ruby/mysql client" { + ruby /build/bin/ruby/mysql-client-test.rb $USER $PORT $REPO_NAME } -@test "elixir myxql test" { - cd $BATS_TEST_DIRNAME/elixir/ - # install some mix dependencies - mix local.hex --force - mix local.rebar --force - mix deps.get - - # run the test - mix run -e "IO.puts(SmokeTest.run())" $USER $PORT $REPO_NAME +@test "ruby mysql2" { + ruby /build/bin/ruby/mysql2-test.rb $USER $PORT $REPO_NAME } -@test "mysqldump works" { +@test "elixir myxql" { + /build/bin/elixir/myxql-driver-test $USER $PORT $REPO_NAME +} + +@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 } @@ -163,27 +168,26 @@ EOF" -m "postgres" } @test "R RMySQL client" { - Rscript $BATS_TEST_DIRNAME/r/rmysql-test.r $USER $PORT $REPO_NAME + Rscript /build/bin/r/rmysql-test.r $USER $PORT $REPO_NAME } @test "R RMariaDB client" { - skip "Error loading RMariaDB library" - # ex: https://github.com/dolthub/dolt/actions/runs/4428743682/jobs/7770282852 - Rscript $BATS_TEST_DIRNAME/r/rmariadb-test.r $USER $PORT $REPO_NAME + Rscript /build/bin/r/rmariadb-test.r $USER $PORT $REPO_NAME } @test "rust mysql client" { - cd $BATS_TEST_DIRNAME/rust - ./target/debug/mysql_connector_test $USER $PORT $REPO_NAME + /build/bin/rust/mysql-client-test $USER $PORT $REPO_NAME +} + +@test "swift perfect-mariadb client" { + /build/bin/swift/mariadb-swift-test $USER $PORT $REPO_NAME } @test "php mysqli mysql client" { - cd $BATS_TEST_DIRNAME/php - php mysqli_connector_test.php $USER $PORT $REPO_NAME + php /build/bin/php/mysqli_connector_test.php $USER $PORT $REPO_NAME } @test "php pdo mysql client" { - cd $BATS_TEST_DIRNAME/php - php pdo_connector_test.php $USER $PORT $REPO_NAME + php /build/bin/php/pdo_connector_test.php $USER $PORT $REPO_NAME } 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/mysql.connector-test.py b/integration-tests/mysql-client-tests/python/mysql-connector-test.py similarity index 95% rename from integration-tests/mysql-client-tests/python/mysql.connector-test.py rename to integration-tests/mysql-client-tests/python/mysql-connector-test.py index 5edde38009..8ea0544709 100644 --- a/integration-tests/mysql-client-tests/python/mysql.connector-test.py +++ b/integration-tests/mysql-client-tests/python/mysql-connector-test.py @@ -1,70 +1,70 @@ -import mysql.connector -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)]}, - # We used to have a bug where spaces after a semicolon in a query - # would cause a client/server disconnect. - # https://github.com/dolthub/vitess/pull/65 - # The following regression tests it. - {"select * from test; ": [(0, 0)]}, - {"select * from test; ": [(0, 0)]}, - # Test the Dolt SQL functions - {"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 = sys.argv[2] - db = sys.argv[3] - - connection = mysql.connector.connect(user=user, - host="127.0.0.1", - port=port, - database=db) - - for query_response in QUERY_RESPONSE: - query = list(query_response.keys())[0] - exp_results = query_response[query] - cursor = connection.cursor() - cursor.execute(query) - try: - results = cursor.fetchall() - print(exp_results) - print(results) - 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) - exit(1) - except mysql.connector.errors.InterfaceError: - - # This is a write query - pass - - cursor.close() - - connection.close() - - exit(0) - - -main() +import mysql.connector +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)]}, + # We used to have a bug where spaces after a semicolon in a query + # would cause a client/server disconnect. + # https://github.com/dolthub/vitess/pull/65 + # The following regression tests it. + {"select * from test; ": [(0, 0)]}, + {"select * from test; ": [(0, 0)]}, + # Test the Dolt SQL functions + {"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 = sys.argv[2] + db = sys.argv[3] + + connection = mysql.connector.connect(user=user, + host="127.0.0.1", + port=port, + database=db) + + for query_response in QUERY_RESPONSE: + query = list(query_response.keys())[0] + exp_results = query_response[query] + cursor = connection.cursor() + cursor.execute(query) + try: + results = cursor.fetchall() + print(exp_results) + print(results) + 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) + except mysql.connector.errors.InterfaceError: + + # This is a write query + pass + + cursor.close() + + connection.close() + + sys.exit(0) + + +main() diff --git a/integration-tests/mysql-client-tests/python/pymysql-test.py b/integration-tests/mysql-client-tests/python/pymysql-test.py index dbbd8d61a8..ab542ce06c 100644 --- a/integration-tests/mysql-client-tests/python/pymysql-test.py +++ b/integration-tests/mysql-client-tests/python/pymysql-test.py @@ -44,11 +44,11 @@ def main(): print(exp_results) print("Received:") print(results) - exit(1) + sys.exit(1) connection.close() - exit(0) + sys.exit(0) main() diff --git a/integration-tests/mysql-client-tests/python/sqlalchemy-test.py b/integration-tests/mysql-client-tests/python/sqlalchemy-test.py index a9a4b7b9fe..41c882a798 100644 --- a/integration-tests/mysql-client-tests/python/sqlalchemy-test.py +++ b/integration-tests/mysql-client-tests/python/sqlalchemy-test.py @@ -1,67 +1,67 @@ -import sqlalchemy - -from sqlalchemy.engine import Engine -from sqlalchemy import create_engine - -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] - - conn_string_base = "mysql+mysqlconnector://" - - engine = create_engine(conn_string_base + - "{user}@127.0.0.1:{port}/{db}".format(user=user, - port=port, - db=db) - ) - - with engine.connect() as con: - for query_response in QUERY_RESPONSE: - query = list(query_response.keys())[0] - exp_results = query_response[query] - - result_proxy = con.execute(query) - - try: - results = result_proxy.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) - exit(1) - # You can't call fetchall on an insert - # so we'll just ignore the exception - except sqlalchemy.exc.ResourceClosedError: - pass - - con.close() - exit(0) - - -main() +import sqlalchemy + +from sqlalchemy.engine import Engine +from sqlalchemy import create_engine + +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] + + conn_string_base = "mysql+mysqlconnector://" + + engine = create_engine(conn_string_base + + "{user}@127.0.0.1:{port}/{db}".format(user=user, + port=port, + db=db) + ) + + with engine.connect() as con: + for query_response in QUERY_RESPONSE: + query = list(query_response.keys())[0] + exp_results = query_response[query] + + result_proxy = con.execute(query) + + try: + results = result_proxy.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) + # You can't call fetchall on an insert + # so we'll just ignore the exception + except sqlalchemy.exc.ResourceClosedError: + pass + + con.close() + sys.exit(0) + + +main() diff --git a/integration-tests/mysql-client-tests/r/rmariadb-test.r b/integration-tests/mysql-client-tests/r/rmariadb-test.r index c4175ebbe3..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, NA), 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(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(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(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(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(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 diff --git a/integration-tests/mysql-client-tests/rust/Cargo.toml b/integration-tests/mysql-client-tests/rust/Cargo.toml index 0a26070a76..db8cc4cf37 100644 --- a/integration-tests/mysql-client-tests/rust/Cargo.toml +++ b/integration-tests/mysql-client-tests/rust/Cargo.toml @@ -8,5 +8,5 @@ edition = "2018" mysql = "*" [[bin]] -name = "mysql_connector_test" +name = "mysql-client-test" path = "src/mysql_connector_test.rs" diff --git a/integration-tests/mysql-client-tests/swift/Package.swift b/integration-tests/mysql-client-tests/swift/Package.swift new file mode 100644 index 0000000000..6b965ee246 --- /dev/null +++ b/integration-tests/mysql-client-tests/swift/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.5 +import PackageDescription + +let package = Package( + name: "MariaDBTest", + platforms: [ + .macOS(.v10_15) + ], + dependencies: [ + .package(url: "https://github.com/PerfectlySoft/Perfect-MariaDB.git", from: "3.0.0") + ], + targets: [ + .executableTarget( + name: "MariaDBTest", + dependencies: [ + .product(name: "MariaDB", package: "Perfect-MariaDB") + ], + path: "Sources", + linkerSettings: [ + .linkedLibrary("mariadb") + ] + ) + ] +) + diff --git a/integration-tests/mysql-client-tests/swift/Sources/main.swift b/integration-tests/mysql-client-tests/swift/Sources/main.swift new file mode 100644 index 0000000000..e3dceb2175 --- /dev/null +++ b/integration-tests/mysql-client-tests/swift/Sources/main.swift @@ -0,0 +1,127 @@ +import Foundation +import MariaDB + +func main() throws { + // Get connection parameters from command line + guard CommandLine.arguments.count >= 4 else { + print("Usage: MariaDBTest ") + exit(1) + } + + let user = CommandLine.arguments[1] + let port = UInt32(CommandLine.arguments[2]) ?? 3306 + let database = CommandLine.arguments[3] + + // Connect to the database + let mysql = MySQL() + + guard mysql.connect( + host: "127.0.0.1", + user: user, + password: "", + db: database, + port: port + ) else { + print("Connection failed: \(mysql.errorMessage())") + exit(1) + } + + print("Connected to MariaDB successfully") + + // Test queries + let queries = [ + "CREATE TABLE test (pk INT, value INT, PRIMARY KEY(pk))", + "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" + ] + + for query in queries { + guard mysql.query(statement: query) else { + print("Query failed: \(query)") + print("Error: \(mysql.errorMessage())") + exit(1) + } + + // Consume results if any + let results = mysql.storeResults() + results?.close() + } + + // Test prepared statements + print("Testing prepared statements...") + + let stmt = MySQLStmt(mysql) + + // Test SELECT with prepared statement + let selectQuery = "SELECT * FROM test WHERE pk = ?" + guard stmt.prepare(statement: selectQuery) else { + print("Failed to prepare SELECT statement: \(stmt.errorMessage())") + exit(1) + } + + stmt.bindParam(1) + + guard stmt.execute() else { + print("Failed to execute SELECT statement: \(stmt.errorMessage())") + exit(1) + } + + let resultSet = stmt.results() + resultSet.close() + stmt.close() + + // Test INSERT with prepared statement + let insertStmt = MySQLStmt(mysql) + + let insertQuery = "INSERT INTO test VALUES (?, ?)" + guard insertStmt.prepare(statement: insertQuery) else { + print("Failed to prepare INSERT statement: \(insertStmt.errorMessage())") + exit(1) + } + + insertStmt.bindParam(2) + insertStmt.bindParam(20) + + guard insertStmt.execute() else { + print("Failed to execute INSERT statement: \(insertStmt.errorMessage())") + exit(1) + } + + insertStmt.close() + + // Verify the insert + guard mysql.query(statement: "SELECT * FROM test WHERE pk = 2") else { + print("Failed to verify insert") + exit(1) + } + + if let verifyResults = mysql.storeResults() { + let row = verifyResults.next() + guard row != nil else { + print("Expected row not found after insert") + exit(1) + } + verifyResults.close() + } + + print("All Swift tests passed!") + + mysql.close() +} + +do { + try main() +} catch { + print("Error: \(error)") + exit(1) +} +