diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86e2a30..e264843 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,26 @@ on: branches: [main] tags: ["*"] pull_request: + schedule: + # nightly deterministic fuzz budget (plan §8.4); the smoke batch runs in every test job + - cron: "17 6 * * *" + workflow_dispatch: jobs: + cleanroom: + name: Native clean-room source check + if: github.event_name != 'schedule' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: julia-actions/setup-julia@v2 + with: + version: "1.10" + - run: julia --startup-file=no scripts/check_native_cleanroom.jl test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + if: github.event_name != 'schedule' runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -17,25 +33,131 @@ jobs: - nightly os: - ubuntu-latest + - macOS-latest + - windows-latest arch: - x64 + exclude: + - os: macOS-latest + arch: x64 + include: + - os: macOS-latest + arch: aarch64 + version: "1.10" + - os: macOS-latest + arch: aarch64 + version: 1 + - os: macOS-latest + arch: aarch64 + version: nightly steps: - uses: actions/checkout@v5 + # The wire-protocol tests need no server; the server integration tests run only + # where Docker is available (Linux runners). - run: docker info + if: runner.os == 'Linux' - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 + # The cross-platform matrix runs the serverless protocol suite only (MYSQL_INTEGRATION=0): + # it is fast, needs no server, and the Docker integration (live lanes + leak soak + + # the server integration tests) runs in the dedicated `integration` job instead. + # Coverage is measured by the `coverage` job. + - uses: julia-actions/julia-runtest@v1 + with: + coverage: false + env: + MYSQL_INTEGRATION: "0" + integration: + # Live lanes (mysql:8.4, mariadb:11.4), the golden behavior manifest, the leak/lifecycle + # soak, and the server integration tests, all against real servers via Harbor. Linux-only + # (Docker) and pinned to Julia 1.10, whose GC is stable under the soak's GC-thrash (1.12+ + # can hang on shared runners). + name: Integration (Docker) + if: github.event_name != 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v5 + - run: docker info + - uses: julia-actions/setup-julia@v2 + with: + version: "1.10" + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 + with: + coverage: false + coverage: + # Line-coverage gate (plan §8.15). The serverless fake-peer suite alone covers ~96% of + # src/, so the gate needs neither Docker nor the leak soak and stays fast; it also + # feeds codecov. + name: Native coverage + if: github.event_name != 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - run: julia --project=. --code-coverage=@src --startup-file=no -e 'include("test/protocol/runtests.jl")' - uses: julia-actions/julia-processcoverage@v1 + - run: julia --startup-file=no scripts/check_native_coverage.jl lcov.info 0.85 - uses: codecov/codecov-action@v5 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} + fuzz: + # Budgeted mutation fuzzing in isolated worker processes (test/protocol/fuzz.jl via + # scripts/fuzz.jl); scheduled nightly, or run on demand with workflow_dispatch. + name: Nightly fuzz + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - run: julia --project=. scripts/fuzz.jl --minutes 30 --seed $(date +%Y%m%d)000000 --out fuzz_failures + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: fuzz-failures + path: fuzz_failures/ + perf: + # §8.9 gates (correctness/allocation) plus the wall-clock timing report. They are too + # heavy for the shared PR test lanes, so they run here on the nightly schedule and on + # demand (MYSQL_PERF_GATES=1), with coverage off. Cross-driver comparisons (MySQL.jl 1.x, + # other clients) live in bench/, outside CI. + name: Performance gates (§8.9) + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - run: docker info + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + with: + coverage: false + env: + MYSQL_PERF_GATES: "1" docs: name: Documentation + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/.gitignore b/.gitignore index 425e40f..12c0b33 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,8 @@ deps/build.log Manifest.toml .vscode/ + +# Documentation build and native-backend artifacts +docs/build/ +fuzz_failures/ +*.cov diff --git a/Project.toml b/Project.toml index a45026e..bae928a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,33 +1,38 @@ name = "MySQL" uuid = "39abe10b-433b-5dbd-92d4-e302a9df00cd" author = ["quinnj"] -version = "1.6.0" +version = "2.0.0" [deps] DBInterface = "a10d1c49-ce27-4219-8d33-6db1a4562965" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DecFP = "55939f99-70c6-5e9b-8bb0-5071ed7d61fd" -Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -MariaDB_Connector_C_jll = "aabc7e14-95f1-5e66-9f32-aea603782360" OpenSSL_jll = "458c3c95-2e84-50aa-8efc-19380b2a3a95" Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] +Chairmarks = "1.3" DBInterface = "2.5" DecFP = "0.4.9, 0.4.10, 1" Harbor = "1.0.3" -MariaDB_Connector_C_jll = "3.1.12" OpenSSL_jll = "3" -Parsers = "0.3, 1, 2" +Parsers = "2" +Random = "1" +Reseau = "1.4.1" +SHA = "0.7.0, 1" Tables = "1" julia = "1.10" [extras] +Chairmarks = "0ca39b1e-fe0b-4e98-acfc-b1656634c4de" Harbor = "af79dbb9-1a80-47ad-8928-192a4af69376" -Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Harbor", "Sockets", "Test"] +test = ["Chairmarks", "Harbor", "Logging", "Printf", "Test"] diff --git a/README.md b/README.md index 269b414..c571222 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,41 @@ - -# MySQL - -[![docs](https://img.shields.io/badge/docs-latest-blue&logo=julia)](https://mysql.juliadatabases.org/dev/) -[![CI](https://github.com/JuliaDatabases/MySQL.jl/workflows/CI/badge.svg)](https://github.com/JuliaDatabases/MySQL.jl/actions?query=workflow%3ACI) -[![codecov](https://codecov.io/gh/JuliaDatabases/MySQL.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaDatabases/MySQL.jl) - -[![deps](https://juliahub.com/docs/MySQL/deps.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU?t=2) -[![version](https://juliahub.com/docs/MySQL/version.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU) -[![pkgeval](https://juliahub.com/docs/MySQL/pkgeval.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU) - -Package for interfacing with MySQL databases from Julia via the MariaDB C connector library, version 3.1.6. - -## Documentation - -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://mysql.juliadatabases.org/stable) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://mysql.juliadatabases.org/dev) - + +# MySQL + +[![docs](https://img.shields.io/badge/docs-latest-blue&logo=julia)](https://mysql.juliadatabases.org/dev/) +[![CI](https://github.com/JuliaDatabases/MySQL.jl/workflows/CI/badge.svg)](https://github.com/JuliaDatabases/MySQL.jl/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/JuliaDatabases/MySQL.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaDatabases/MySQL.jl) + +[![deps](https://juliahub.com/docs/MySQL/deps.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU?t=2) +[![version](https://juliahub.com/docs/MySQL/version.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU) +[![pkgeval](https://juliahub.com/docs/MySQL/pkgeval.svg)](https://juliahub.com/ui/Packages/MySQL/xeTdU) + +Package for interfacing with MySQL databases from Julia. + +Since 2.0, MySQL.jl implements the MySQL client/server wire protocol natively in Julia +(built on [Reseau.jl](https://github.com/JuliaServices/Reseau.jl) for TCP/TLS) — no C +client library. 1.x used the MariaDB Connector/C library; see the +[migration guide](https://mysql.juliadatabases.org/dev/migration/) for the differences. + +## Documentation + +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://mysql.juliadatabases.org/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://mysql.juliadatabases.org/dev) + +## Usage + +```julia +conn = DBInterface.connect(MySQL.Connection, host, user, passwd; db="mydb", port=3306) +cursor = DBInterface.execute(conn, "SELECT * FROM mytable") # a Tables.jl-compatible cursor +stmt = DBInterface.prepare(conn, "INSERT INTO mytable (a, b) VALUES (?, ?)") +DBInterface.execute(stmt, (1, "two")) +DBInterface.close!(conn) +``` + +`DBInterface.execute`/`prepare`/`executemany`/`executemultiple`, Tables.jl cursors, +`MySQL.load`, and transactions are all supported; see the +[documentation](https://mysql.juliadatabases.org/dev/). The transport is TCP or TLS +(no Unix sockets, named pipes, or compression yet — these raise clear errors). + ## Contributing The test suite manages its own temporary MySQL container via Harbor.jl. The only prerequisite is a working Docker daemon: diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..aebb483 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,40 @@ +# Cross-driver benchmarks + +Before 2.0, `test/perf/perf_gates.jl` asserted native-vs-Connector/C timing ratios inside +`Pkg.test`. Those ratio gates retired with the C backend; the in-repo §8.9 gates now assert +correctness, allocation budgets, and buffer limits, and print a wall-clock timing report. + +This directory keeps the cross-driver comparison repeatable: + +```sh +julia --project=. bench/run.jl +``` + +builds two temp environments — the current checkout (native) and `MySQL@1` (MariaDB +Connector/C) from the registry — starts the same Docker fixture servers the §8.9 gates use +(`mysql:8.4`, `--max-allowed-packet=128M`, TLS disabled), runs `bench/child.jl` once per +environment, and prints per-benchmark seconds plus the mysql@1/native ratio (>1x means the +native client is faster). + +Reference numbers (macOS ARM, Docker, Julia 1.12, this harness after the 2.0 round-trip +perf round; ratio is mysql@1 time / native time, >1x = native faster): + +| benchmark | native | mysql@1 (Connector/C) | speed | +|---|---|---|---| +| 100k executemany | 16.97s | 23.46s | 1.38x | +| 1M-row text scan | 0.4512s | 0.5098s | 1.13x | +| 1M tiny/NULL rows | 0.1780s | 0.2018s | 1.13x | +| 1M-row binary (prepared) scan | 0.2866s | 0.2978s | 1.04x | +| 64 MiB blob fetch | 0.0591s | 0.0551s | 0.93x | +| 10k round trips (plain) | 2.701s | 2.292s | 0.85x | + +Per-command costs after the round: COM_PING 3 allocs, buffered `SELECT 1` 35 allocs, +repeated prepared execute 22 allocs (framed directly into the output buffer, cached +statement schema, lazy name lookup). The remaining round-trip gap vs the C client is +task-wakeup latency in the transport layer (a blocking `recv` wakes the C client directly; +the native client parks on the poller and pays a second thread wake ≈17µs). The +`direct_wait_ms` connect option (with a Reseau newer than 1.4.1, see Reseau PR #152) +removes that second wake by blocking the waiting task's own OS thread in a bounded +`poll(2)`: measured ~10-15µs less per round trip against Docker (COM_PING ~150 → ~135µs, +20k-row executemany −13%) at the cost of other tasks on that thread waiting up to the +budget. diff --git a/bench/child.jl b/bench/child.jl new file mode 100644 index 0000000..750c1a5 --- /dev/null +++ b/bench/child.jl @@ -0,0 +1,112 @@ +# One benchmark pass against an already-running perf fixture (see bench/run.jl), using +# whatever MySQL.jl version is in the active project — the 2.0 native client or a 1.x +# Connector/C client. Prints `nameseconds` lines for the parent to collect. +# +# usage: julia --project= bench/child.jl