diff --git a/.github/workflows/issue236-windows.yml b/.github/workflows/issue236-windows.yml new file mode 100644 index 0000000..700a1db --- /dev/null +++ b/.github/workflows/issue236-windows.yml @@ -0,0 +1,200 @@ +name: Issue 236 Windows diagnostics + +on: + pull_request: + paths: + - ".github/workflows/issue236-windows.yml" + - "test/issue236_windows.jl" + - "src/**" + - "Project.toml" + workflow_dispatch: + +jobs: + build-yggdrasil-variant: + name: Build Yggdrasil variant / ${{ matrix.variant }} + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - variant: fno-strict-aliasing + cflags: -fno-strict-aliasing + - variant: fno-tree-vectorize + cflags: -fno-tree-vectorize + steps: + - uses: julia-actions/setup-julia@v2 + with: + version: "1.12" + + - uses: actions/checkout@v5 + with: + repository: JuliaPackaging/Yggdrasil + path: Yggdrasil + + - name: Patch MariaDB Connector/C recipe + shell: bash + run: | + set -euo pipefail + cd Yggdrasil/M/MariaDB_Connector_C + perl -0pi -e 's/\Q-Wno-error -Wno-incompatible-pointer-types\E/-Wno-error -Wno-incompatible-pointer-types $ENV{EXTRA_CFLAGS}/' build_tarballs.jl + grep -n -- "$EXTRA_CFLAGS" build_tarballs.jl + env: + EXTRA_CFLAGS: ${{ matrix.cflags }} + + - name: Build Windows tarball + shell: bash + run: | + set -euo pipefail + cd Yggdrasil/M/MariaDB_Connector_C + julia --project=../.. --startup-file=no -e 'using Pkg; Pkg.instantiate()' + julia --project=../.. --startup-file=no build_tarballs.jl --verbose x86_64-w64-mingw32 + mkdir -p "$GITHUB_WORKSPACE/variant" + cp products/MariaDB_Connector_C.v*.x86_64-w64-mingw32.tar.gz "$GITHUB_WORKSPACE/variant/" + cp products/MariaDB_Connector_C-logs.v*.x86_64-w64-mingw32.tar.gz "$GITHUB_WORKSPACE/variant/" + + - uses: actions/upload-artifact@v5 + with: + name: mariadb-connector-c-${{ matrix.variant }} + path: variant/*.tar.gz + + issue236: + name: Windows ${{ matrix.os }} / ${{ matrix.connector }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - windows-2022 + - windows-2025 + connector: + - jll-default + - jll-3.3.9 + - official-3.4.9 + + steps: + - uses: actions/checkout@v5 + + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + arch: x64 + + - name: Start MySQL + id: setup-mysql + uses: shogo82148/actions-setup-mysql@v1.51.0 + with: + mysql-version: "8.4" + root-password: root + + - name: Download official Connector/C + if: matrix.connector == 'official-3.4.9' + shell: pwsh + run: | + $msi = Join-Path $env:RUNNER_TEMP "mariadb-connector-c-3.4.9-win64.msi" + $target = Join-Path $env:RUNNER_TEMP "mariadb-connector-c-3.4.9" + Invoke-WebRequest ` + -Uri "https://dlm.mariadb.com/4751028/Connectors/c/connector-c-3.4.9/mariadb-connector-c-3.4.9-win64.msi" ` + -OutFile $msi + Start-Process msiexec.exe -Wait -ArgumentList @("/a", $msi, "/qn", "TARGETDIR=$target") + + $dll = Get-ChildItem $target -Recurse -Filter libmariadb.dll | Select-Object -First 1 + if ($null -eq $dll) { + throw "Could not find libmariadb.dll under $target" + } + + $plugin = Get-ChildItem $target -Recurse -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "caching_sha2_password.dll") } | + Select-Object -First 1 + if ($null -eq $plugin) { + throw "Could not find Connector/C plugin directory under $target" + } + + "MYSQLJL_LIBMARIADB_OVERRIDE=$($dll.FullName)" >> $env:GITHUB_ENV + "MYSQLJL_PLUGIN_DIR_OVERRIDE=$($plugin.FullName)" >> $env:GITHUB_ENV + "$($dll.DirectoryName)" >> $env:GITHUB_PATH + + - name: Run issue 236 diagnostic + shell: pwsh + env: + MYSQLJL_HOST: 127.0.0.1 + MYSQLJL_PORT: "3306" + MYSQLJL_USER: root + MYSQLJL_PASSWORD: root + MYSQLJL_CONNECTOR: ${{ matrix.connector }} + MYSQLJL_JLL_VERSION: ${{ matrix.connector == 'jll-3.3.9' && '3.3.9' || 'default' }} + run: julia --startup-file=no test/issue236_windows.jl + + issue236-yggdrasil-variant: + name: Windows ${{ matrix.os }} / yggdrasil-${{ matrix.variant }} + if: github.event_name == 'workflow_dispatch' + needs: build-yggdrasil-variant + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - windows-2022 + - windows-2025 + variant: + - fno-strict-aliasing + - fno-tree-vectorize + + steps: + - uses: actions/checkout@v5 + + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + arch: x64 + + - name: Start MySQL + uses: shogo82148/actions-setup-mysql@v1.51.0 + with: + mysql-version: "8.4" + root-password: root + + - uses: actions/download-artifact@v5 + with: + name: mariadb-connector-c-${{ matrix.variant }} + path: ${{ runner.temp }}/variant + + - name: Use Yggdrasil variant + shell: pwsh + run: | + $target = Join-Path $env:RUNNER_TEMP "mariadb-connector-c-${env:MYSQLJL_CONNECTOR_VARIANT}" + New-Item -ItemType Directory -Force -Path $target | Out-Null + $tarball = Get-ChildItem (Join-Path $env:RUNNER_TEMP "variant") -Filter "MariaDB_Connector_C.v*.x86_64-w64-mingw32.tar.gz" | Select-Object -First 1 + if ($null -eq $tarball) { + throw "Could not find built Connector/C tarball" + } + tar -xzf $tarball.FullName -C $target + + $dll = Join-Path $target "bin/libmariadb.dll" + $plugin = Join-Path $target "lib/mariadb/plugin" + if (!(Test-Path $dll)) { + throw "Could not find libmariadb.dll at $dll" + } + if (!(Test-Path (Join-Path $plugin "caching_sha2_password.dll"))) { + throw "Could not find Connector/C plugin directory at $plugin" + } + + "MYSQLJL_LIBMARIADB_OVERRIDE=$dll" >> $env:GITHUB_ENV + "MYSQLJL_PLUGIN_DIR_OVERRIDE=$plugin" >> $env:GITHUB_ENV + "$(Split-Path -Parent $dll)" >> $env:GITHUB_PATH + env: + MYSQLJL_CONNECTOR_VARIANT: ${{ matrix.variant }} + + - name: Run issue 236 diagnostic + shell: pwsh + env: + MYSQLJL_HOST: 127.0.0.1 + MYSQLJL_PORT: "3306" + MYSQLJL_USER: root + MYSQLJL_PASSWORD: root + MYSQLJL_CONNECTOR: yggdrasil-${{ matrix.variant }} + MYSQLJL_JLL_VERSION: default + run: julia --startup-file=no test/issue236_windows.jl diff --git a/src/api/API.jl b/src/api/API.jl index b0b2a60..1910488 100644 --- a/src/api/API.jl +++ b/src/api/API.jl @@ -7,7 +7,7 @@ export DateAndTime using MariaDB_Connector_C_jll using OpenSSL_jll: libssl, libcrypto -const PLUGIN_DIR = joinpath(MariaDB_Connector_C_jll.artifact_dir, "lib", "mariadb", "plugin") +const PLUGIN_DIR = get(ENV, "MYSQLJL_PLUGIN_DIR_OVERRIDE", joinpath(MariaDB_Connector_C_jll.artifact_dir, "lib", "mariadb", "plugin")) # Pre-load OpenSSL libraries so they're available when MariaDB loads plugins. # MariaDB authentication plugins (e.g., caching_sha2_password) depend on OpenSSL, @@ -37,4 +37,4 @@ include("capi.jl") # Prepared statement API functions include("papi.jl") -end # module \ No newline at end of file +end # module diff --git a/src/api/ccalls.jl b/src/api/ccalls.jl index 95fc51c..bf4f7ef 100644 --- a/src/api/ccalls.jl +++ b/src/api/ccalls.jl @@ -1,11 +1,13 @@ +const libmariadb_for_ccall = get(ENV, "MYSQLJL_LIBMARIADB_OVERRIDE", libmariadb) + macro c(func, ret, args, vals...) if Sys.iswindows() esc(quote - ret = ccall( ($func, libmariadb), stdcall, $ret, $args, $(vals...)) + ret = ccall( ($func, libmariadb_for_ccall), stdcall, $ret, $args, $(vals...)) end) else esc(quote - ret = ccall( ($func, libmariadb), $ret, $args, $(vals...)) + ret = ccall( ($func, libmariadb_for_ccall), $ret, $args, $(vals...)) end) end end diff --git a/test/issue236_windows.jl b/test/issue236_windows.jl new file mode 100644 index 0000000..e866387 --- /dev/null +++ b/test/issue236_windows.jl @@ -0,0 +1,185 @@ +if get(ENV, "MYSQLJL_BOOTSTRAPPED", "false") != "true" + using Pkg + + jll_pkg_version(v::String) = VersionNumber(occursin("+", v) ? v : string(v, "+0")) + + Pkg.activate(; temp=true) + Pkg.develop(PackageSpec(path=dirname(@__DIR__))) + Pkg.add([ + PackageSpec(name="DBInterface"), + PackageSpec(name="Tables"), + ]) + + jll_version = get(ENV, "MYSQLJL_JLL_VERSION", "default") + if jll_version != "default" + Pkg.add(PackageSpec(name="MariaDB_Connector_C_jll", version=jll_pkg_version(jll_version))) + else + Pkg.add(PackageSpec(name="MariaDB_Connector_C_jll")) + end + + Pkg.instantiate() + Pkg.status() + + ENV["MYSQLJL_BOOTSTRAPPED"] = "true" + include(@__FILE__) + exit() +end + +using DBInterface +using Libdl +using MariaDB_Connector_C_jll +using MySQL +using Random +using Tables + +function logline(xs...) + println(xs...) + flush(stdout) + return nothing +end + +function log_mysql_bind_layout() + names = fieldnames(MySQL.API.MYSQL_BIND) + logline("MYSQL_BIND_SIZE ", sizeof(MySQL.API.MYSQL_BIND)) + for (i, name) in pairs(names) + logline("MYSQL_BIND_FIELD ", name, " offset=", fieldoffset(MySQL.API.MYSQL_BIND, i)) + end + return nothing +end + +function connect_root(; db=nothing) + host = get(ENV, "MYSQLJL_HOST", "127.0.0.1") + port = parse(Int, get(ENV, "MYSQLJL_PORT", "3306")) + user = get(ENV, "MYSQLJL_USER", "root") + password = get(ENV, "MYSQLJL_PASSWORD", "root") + + if db === nothing + return DBInterface.connect(MySQL.Connection, host, user, password; port=port, connect_timeout=10) + else + return DBInterface.connect(MySQL.Connection, host, user, password; db=db, port=port, connect_timeout=10) + end +end + +function wait_for_connection(; db=nothing, timeout=30.0) + start = time() + last_err = nothing + while time() - start < timeout + try + return connect_root(; db=db) + catch err + last_err = err + logline("WAIT_CONNECTION db=", db, " error=", sprint(showerror, err)) + sleep(1) + end + end + error("MySQL did not become ready: ", sprint(showerror, last_err)) +end + +function placeholders(n::Integer) + return join(fill("?", n), ",") +end + +function query_for(shape::Symbol, n::Integer) + marks = placeholders(n) + if shape === :table + return "SELECT id FROM myTable WHERE id IN ($marks)" + elseif shape === :constant + return "SELECT 1 AS id WHERE 1 IN ($marks)" + elseif shape === :echo + return "SELECT $marks" + else + error("unknown query shape: $shape") + end +end + +function consume(cursor) + rows = 0 + for _ in cursor + rows += 1 + end + return rows +end + +function run_case(conn, name::String, shape::Symbol, params; mysql_store_result::Bool=true) + n = length(params) + query = query_for(shape, n) + logline("BEGIN name=", name, + " shape=", shape, + " n=", n, + " eltype=", eltype(params), + " mysql_store_result=", mysql_store_result) + logline("QUERY ", query) + logline("FIRST_VALUES ", collect(Iterators.take(params, min(n, 5)))) + + stmt = DBInterface.prepare(conn, query) + logline("PREPARED nparams=", stmt.nparams, " nfields=", stmt.nfields) + + cursor = DBInterface.execute(stmt, params; mysql_store_result=mysql_store_result) + rows = consume(cursor) + logline("DONE name=", name, " rows=", rows) + + DBInterface.close!(stmt) + return nothing +end + +function prepare_database() + conn = wait_for_connection() + try + version = DBInterface.execute(conn, "SELECT VERSION() AS version") |> Tables.columntable + logline("SERVER_VERSION ", only(version.version)) + + DBInterface.execute(conn, "DROP DATABASE IF EXISTS issue236") + DBInterface.execute(conn, "CREATE DATABASE issue236") + finally + DBInterface.close!(conn) + end + + conn = wait_for_connection(db="issue236") + DBInterface.execute(conn, "CREATE TABLE myTable (id BIGINT UNSIGNED NOT NULL PRIMARY KEY)") + + stmt = DBInterface.prepare(conn, "INSERT INTO myTable (id) VALUES (?)") + try + for id in UInt64(1):UInt64(512) + DBInterface.execute(stmt, id) + end + finally + DBInterface.close!(stmt) + end + return conn +end + +function main() + logline("JULIA_VERSION ", VERSION) + logline("OS ", Sys.KERNEL, " MACHINE ", Sys.MACHINE) + logline("WORD_SIZE ", Sys.WORD_SIZE) + logline("CONNECTOR ", get(ENV, "MYSQLJL_CONNECTOR", "unknown")) + logline("JLL_LIBMARIADB ", MariaDB_Connector_C_jll.libmariadb) + logline("CCALL_LIBMARIADB ", MySQL.API.libmariadb_for_ccall) + logline("PLUGIN_DIR ", MySQL.API.PLUGIN_DIR) + logline("LIBMARIADB_HANDLE ", Libdl.dlopen_e(MySQL.API.libmariadb_for_ccall)) + log_mysql_bind_layout() + + conn = prepare_database() + try + Random.seed!(0x236) + + for n in (31, 32, 33, 34, 64, 128) + run_case(conn, "table-small-uint64", :table, UInt64.(1:n)) + end + + for n in (32, 33, 34) + run_case(conn, "table-small-int64", :table, Int64.(1:n)) + run_case(conn, "table-random-uint64", :table, rand(UInt64, n)) + run_case(conn, "constant-small-uint64", :constant, UInt64.(1:n)) + run_case(conn, "table-small-uint64-unbuffered", :table, UInt64.(1:n); mysql_store_result=false) + end + + run_case(conn, "echo-small-uint64", :echo, UInt64.(1:33)) + finally + DBInterface.close!(conn) + end + + logline("ISSUE236_DIAGNOSTIC_COMPLETE") +end + +main()