diff --git a/.github/workflows/ci-mysql-client-tests.yaml b/.github/workflows/ci-mysql-client-tests.yaml index fa2b1ae104..e472be9eaf 100644 --- a/.github/workflows/ci-mysql-client-tests.yaml +++ b/.github/workflows/ci-mysql-client-tests.yaml @@ -15,11 +15,14 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - name: Clean up preinstalled .NET and agent tools - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - + - name: Maximize build space + uses: easimon/maximize-build-space@v10 + with: + remove-dotnet: 'true' + remove-codeql: 'true' + remove-haskell: 'true' + + - name: Checkout uses: actions/checkout@v4 with: @@ -29,14 +32,14 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Build MySQL test image - uses: docker/build-push-action@v4 + 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=gha - cache-to: type=gha,mode=max + cache-to: type=gha,mode=min - name: Test MySQL client integrations run: docker run --rm mysql-client-tests:latest diff --git a/integration-tests/mysql-client-tests/Dockerfile b/integration-tests/mysql-client-tests/Dockerfile index c40c7d3163..2ead533844 100644 --- a/integration-tests/mysql-client-tests/Dockerfile +++ b/integration-tests/mysql-client-tests/Dockerfile @@ -5,10 +5,10 @@ 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 -RUN apk add --no-cache icu-dev icu-static COPY dolt/go/ /build/dolt/go/ RUN go build -tags icu_static -ldflags "$GO_LDFLAGS" -o /build/bin/dolt ./cmd/dolt @@ -22,31 +22,43 @@ 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 apk add --no-cache musl-dev RUN cargo build --release --target-dir /build/bin/ # exe is in release/ FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS dotnet_clients_build -COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlClient/ /build/dotnet/MySqlClient/ +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlClient/*.csproj /build/dotnet/MySqlClient/ WORKDIR /build/dotnet/MySqlClient/ -RUN dotnet publish -c Release -o /build/bin +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/ /build/dotnet/MySqlConnector/ +COPY dolt/integration-tests/mysql-client-tests/dotnet/MySqlConnector/*.csproj /build/dotnet/MySqlConnector/ WORKDIR /build/dotnet/MySqlConnector/ -RUN dotnet publish -c Release -o /build/bin +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 gcc:12.5-bookworm AS c_clients_build # default-libmysqlclient-dev uses libmariadb under the hood but here we test both header interfaces -RUN apt-get update && apt-get install -y default-libmysqlclient-dev libmariadb-dev && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y default-libmysqlclient-dev libmariadb-dev unixodbc-dev odbcinst wget && 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 +#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/mysql-client-tests.bats b/integration-tests/mysql-client-tests/mysql-client-tests.bats index 71978d4835..7d95832b9c 100644 --- a/integration-tests/mysql-client-tests/mysql-client-tests.bats +++ b/integration-tests/mysql-client-tests/mysql-client-tests.bats @@ -86,6 +86,10 @@ teardown() { /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" { /build/bin/cpp/mysql-connector-test $USER $PORT $REPO_NAME } @@ -175,6 +179,10 @@ EOF" -m "postgres" /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" { php /build/bin/php/mysqli_connector_test.php $USER $PORT $REPO_NAME } 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) +} +