From 047023dc2a020e891a212df87cfb755e67b923ba Mon Sep 17 00:00:00 2001 From: NullSablex <244216261+NullSablex@users.noreply.github.com> Date: Mon, 18 May 2026 12:26:48 -0300 Subject: [PATCH 1/2] Bump to v1.1.0: rust-samp v3, universal binary and general cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate to rust-samp v3.0.0 (git tag) and produce a single .so/.dll that loads on SA-MP and on Open Multiplayer — as a native component or in legacy mode. The unified on_tick removes the need for SetTimer + mysql_tick() on open.mp. The Pawn API stays compatible (nothing removed or renamed; mysql_tick is kept only for backwards compatibility). Plugin - Migrated to enable_tick() / on_tick(TickContext); added on_component_free to help correlate incidents across neighbouring components - Strict typing: 36 `as` casts replaced by explicit TryFrom/From (saturate on overflow, reject negative for u16/u32/usize) - mysql_format refactored into parse_format / collect_format_values / render_format / truncate_to_buffer (UTF-8-safe truncation + warning, no more abort) - mysql_options_set_int rejects out-of-range port/timeout instead of silently wrapping - parse_variadic_params logs a warning for unknown format chars - Logger: I/O failure on logs/mysql.log is reported once via samp::log::error! and then suppressed - ORM raw natives (select/update/insert/delete/save) deduplicated via OrmOp enum + run_orm_op helper (-120 lines, same error semantics) - Pawn natives return bare T where infallible (no AmxResult wrapper) - Named constants: MAX_ORM_STRING_LEN, PARALLEL_KEY_THRESHOLD - 113 unit tests (up from 93) Build & tooling - Scripts replaced: scripts/build-linux.sh + scripts/build-windows.sh (was scripts/build.sh); Windows target moved to MSVC via cargo-xwin (required for the Open Multiplayer native ABI) - build.rs generates include/mysql_samp.inc from include/mysql_samp.inc.in, injecting CARGO_PKG_VERSION; runs on every build (no rerun-if-changed) so a version bump propagates without `touch` or `cargo clean` - Exposes #define MYSQL_SAMP_VERSION in the .inc - Removed: samp-only feature and .cargo/config.toml (both unused) - Cargo.toml: samp pinned to git tag v3.0.0; log dropped as direct dep CI (.github) - Per-job explicit permissions (contents:read at top level, opt-in where needed) - Actions bumped to the latest major: checkout v6, setup-python v6, cache v5, upload-artifact v7, action-gh-release v3, audit-check v2 - rust.yml: new fmt (cargo fmt --check), audit (rustsec/audit-check) and coverage (cargo llvm-cov) jobs - release.yml: tag-vs-Cargo.toml sanity check; automatic injection of the relevant CHANGELOG section into the release body; raw artifacts (.so/.dll/.inc) instead of zipped folders - docs.yml: strict build (mkdocs build --strict) separated from deploy Documentation - Everything migrated to en-US (CLAUDE.md and CHECKLIST.md are explicit exceptions: pt-BR and en-US respectively) - Files renamed: installation.md, connection.md, errors.md, security.md, migration.md, migration-changes.md, migration-examples.md, api-reference.md - mkdocs.yml with full MkDocs Material setup + site_url pointing to mysql-samp.nullsablex.com; docs/CNAME synced with origin/master - Factual corrections in the old docs: native count (was 51, actually 55), removed references to a nonexistent cache_next_row(), added an honest caveat about SSL options being accepted but no-op (TODO in src/connection.rs:67) - README rewritten in en-US, CHECKLIST.md synced with the .inc - CHANGELOG.md reorganised: current major (v1.x) at the root file + changelog/v0.x.md for history - docs/requirements.txt pinning mkdocs / material / pymdown-extensions Cargo.toml: 1.0.0 → 1.1.0 --- .github/workflows/docs.yml | 52 ++- .github/workflows/release.yml | 128 ++++-- .github/workflows/rust.yml | 125 ++++-- .gitignore | 7 +- CHANGELOG.md | 237 +++++++---- CHECKLIST.md | 207 ++++----- Cargo.lock | 23 +- Cargo.toml | 14 +- README.md | 145 ++++--- build.rs | 26 ++ changelog/v0.x.md | 14 + docs/CNAME | 1 + docs/api-reference.md | 202 +++++++++ docs/api.md | 158 ------- docs/benchmark.md | 377 ++++++++--------- docs/cache.md | 210 +++++---- docs/conexao.md | 184 -------- docs/connection.md | 158 +++++++ docs/errors.md | 137 ++++++ docs/erros.md | 132 ------ docs/exemplos-migracao.md | 752 --------------------------------- docs/index.md | 89 ++-- docs/instalacao.md | 104 ----- docs/installation.md | 135 ++++++ docs/migracao.md | 296 ------------- docs/migration-changes.md | 287 +++++++++++++ docs/migration-examples.md | 772 ++++++++++++++++++++++++++++++++++ docs/migration.md | 312 ++++++++++++++ docs/mudancas.md | 282 ------------- docs/options.md | 190 +++------ docs/orm.md | 341 +++++++-------- docs/queries.md | 180 ++++---- docs/requirements.txt | 13 + docs/security.md | 141 +++++++ docs/seguranca.md | 160 ------- include/mysql_samp.inc | 28 +- include/mysql_samp.inc.in | 129 ++++++ mkdocs.yml | 106 +++-- scripts/build-linux.sh | 87 ++++ scripts/build-windows.sh | 129 ++++++ scripts/build.sh | 84 ---- src/cache.rs | 22 +- src/callback.rs | 26 +- src/connection.rs | 52 ++- src/lib.rs | 3 +- src/logger.rs | 49 ++- src/natives/cache.rs | 290 ++++++------- src/natives/connection.rs | 59 +-- src/natives/error.rs | 8 +- src/natives/options.rs | 19 +- src/natives/orm.rs | 632 +++++++++------------------- src/natives/query.rs | 633 ++++++++++++++++++++-------- src/options.rs | 24 +- src/orm.rs | 59 ++- src/plugin.rs | 27 +- src/query.rs | 19 +- 56 files changed, 4874 insertions(+), 4202 deletions(-) create mode 100644 changelog/v0.x.md create mode 100644 docs/CNAME create mode 100644 docs/api-reference.md delete mode 100644 docs/api.md delete mode 100644 docs/conexao.md create mode 100644 docs/connection.md create mode 100644 docs/errors.md delete mode 100644 docs/erros.md delete mode 100644 docs/exemplos-migracao.md delete mode 100644 docs/instalacao.md create mode 100644 docs/installation.md delete mode 100644 docs/migracao.md create mode 100644 docs/migration-changes.md create mode 100644 docs/migration-examples.md create mode 100644 docs/migration.md delete mode 100644 docs/mudancas.md create mode 100644 docs/requirements.txt create mode 100644 docs/security.md delete mode 100644 docs/seguranca.md create mode 100644 include/mysql_samp.inc.in create mode 100755 scripts/build-linux.sh create mode 100755 scripts/build-windows.sh delete mode 100755 scripts/build.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3e9d436..7df6cbd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,23 +7,61 @@ on: paths: - 'docs/**' - 'mkdocs.yml' + - 'docs/requirements.txt' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'docs/requirements.txt' + - '.github/workflows/docs.yml' workflow_dispatch: permissions: - contents: write + contents: read jobs: + build: + name: Build site + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install MkDocs and theme + run: pip install -r docs/requirements.txt + + - name: Strict build (fails on warnings) + run: mkdocs build --strict + deploy: + name: Deploy to GitHub Pages + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' runs-on: ubuntu-latest + # `mkdocs gh-deploy` pushes to the gh-pages branch. + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.x' + cache: pip + cache-dependency-path: docs/requirements.txt - - name: Instalar MkDocs - run: pip install mkdocs-material + - name: Install MkDocs and theme + run: pip install -r docs/requirements.txt - - name: Deploy GitHub Pages - run: mkdocs gh-deploy --force + - name: Deploy + run: mkdocs gh-deploy --force --clean diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 693a74f..175e33c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: types: [published, prereleased] permissions: - contents: write + contents: read env: CARGO_TERM_COLOR: always @@ -14,73 +14,113 @@ env: jobs: build-release: runs-on: ubuntu-latest + # Needs write access to attach .so/.dll/.inc to the GitHub release + # and to amend the release body with the auto-generated notes. + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Verify Cargo.toml version matches release tag + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + CARGO_VERSION="$(grep -m1 '^version' Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')" + echo "Release tag: ${GITHUB_REF_NAME} (version=${TAG_VERSION})" + echo "Cargo.toml: ${CARGO_VERSION}" + if [ "${TAG_VERSION}" != "${CARGO_VERSION}" ]; then + echo "::error::Release tag (${GITHUB_REF_NAME}) does not match Cargo.toml version (${CARGO_VERSION})." + echo "::error::Bump Cargo.toml to ${TAG_VERSION} (or retag) and try again." + exit 1 + fi - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - targets: i686-unknown-linux-gnu,i686-pc-windows-gnu + targets: i686-unknown-linux-gnu,i686-pc-windows-msvc - name: Install cross-compilation tools run: | sudo apt-get update - sudo apt-get install -y gcc-multilib g++-multilib mingw-w64 + sudo apt-get install -y gcc-multilib g++-multilib clang llvm + + - name: Install cargo-xwin + run: cargo install cargo-xwin --locked - name: Cache cargo - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.cargo/registry ~/.cargo/git + ~/.cache/cargo-xwin target key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - - name: Build Linux (i686) + - name: Build Linux (i686, SA-MP + native OMP) run: cargo build --release --target i686-unknown-linux-gnu - - name: Build Windows (i686) - run: cargo build --release --target i686-pc-windows-gnu + - name: Build Windows (i686 MSVC, SA-MP + native OMP) + run: cargo xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc + + - name: Stage release artifacts + run: | + mkdir -p dist + cp "target/i686-unknown-linux-gnu/release/lib${PLUGIN_NAME}.so" "dist/${PLUGIN_NAME}.so" + cp "target/i686-pc-windows-msvc/release/${PLUGIN_NAME}.dll" "dist/${PLUGIN_NAME}.dll" + cp "include/${PLUGIN_NAME}.inc" "dist/${PLUGIN_NAME}.inc" - - name: Package release assets + - name: Generate release notes run: | - VERSION="${{ github.ref_name }}" - LINUX_SO="target/i686-unknown-linux-gnu/release/lib${PLUGIN_NAME}.so" - WINDOWS_DLL="target/i686-pc-windows-gnu/release/${PLUGIN_NAME}.dll" - - # SA:MP Linux — plugins/ + include/ - mkdir -p pkg/samp-linux/plugins pkg/samp-linux/include - cp "$LINUX_SO" "pkg/samp-linux/plugins/${PLUGIN_NAME}.so" - cp "include/${PLUGIN_NAME}.inc" "pkg/samp-linux/include/" - - # SA:MP Windows — plugins/ + pawno/include/ - mkdir -p pkg/samp-windows/plugins pkg/samp-windows/pawno/include - cp "$WINDOWS_DLL" "pkg/samp-windows/plugins/${PLUGIN_NAME}.dll" - cp "include/${PLUGIN_NAME}.inc" "pkg/samp-windows/pawno/include/" - - # OMP Linux — plugins/ + qawno/include/ - mkdir -p pkg/omp-linux/plugins pkg/omp-linux/qawno/include - cp "$LINUX_SO" "pkg/omp-linux/plugins/${PLUGIN_NAME}.so" - cp "include/${PLUGIN_NAME}.inc" "pkg/omp-linux/qawno/include/" - - # OMP Windows — plugins/ + qawno/include/ - mkdir -p pkg/omp-windows/plugins pkg/omp-windows/qawno/include - cp "$WINDOWS_DLL" "pkg/omp-windows/plugins/${PLUGIN_NAME}.dll" - cp "include/${PLUGIN_NAME}.inc" "pkg/omp-windows/qawno/include/" - - # Compactar - cd pkg - zip -r "../${PLUGIN_NAME}-${VERSION}-linux-samp.zip" samp-linux/ - zip -r "../${PLUGIN_NAME}-${VERSION}-windows-samp.zip" samp-windows/ - zip -r "../${PLUGIN_NAME}-${VERSION}-linux-omp.zip" omp-linux/ - zip -r "../${PLUGIN_NAME}-${VERSION}-windows-omp.zip" omp-windows/ + SDK_VERSION="$(grep -oP 'tag\s*=\s*"\Kv[0-9][^"]*' Cargo.toml | head -n1)" + PLUGIN_VERSION="$(grep -m1 '^version' Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')" + + # Extract the section for the current version from CHANGELOG.md. + # Format: "## [X.Y.Z] — yyyy/mm/dd" up to the next "## " heading. + CHANGELOG_SECTION="" + if [ -f CHANGELOG.md ]; then + CHANGELOG_SECTION="$(awk -v ver="${PLUGIN_VERSION}" ' + $0 ~ "^## \\[" ver "\\]" { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md)" + fi + + cat > release_body.md <> release_body.md + fi + + cat release_body.md - name: Upload release assets - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: + body_path: release_body.md + append_body: true files: | - ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}-linux-samp.zip - ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}-windows-samp.zip - ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}-linux-omp.zip - ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}-windows-omp.zip + dist/${{ env.PLUGIN_NAME }}.so + dist/${{ env.PLUGIN_NAME }}.dll + dist/${{ env.PLUGIN_NAME }}.inc diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a243df5..153ada0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,41 +9,100 @@ on: env: CARGO_TERM_COLOR: always +# Default: read-only. Each job opts in to extra scopes when needed. +permissions: + contents: read + jobs: build: + name: Build, test, clippy runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Install Rust (stable + clippy) + uses: dtolnay/rust-toolchain@stable + with: + targets: i686-unknown-linux-gnu + components: clippy + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib + + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --target i686-unknown-linux-gnu --verbose + - name: Test + run: cargo test --target i686-unknown-linux-gnu --verbose + + - name: Clippy + run: cargo clippy --target i686-unknown-linux-gnu --all-targets -- -D warnings + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: i686-unknown-linux-gnu - - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v3 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v3 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Build - run: cargo build --verbose - - - name: Run tests - run: cargo test --verbose - - - name: Run clippy - run: cargo clippy --all-targets -- -D warnings \ No newline at end of file + - uses: actions/checkout@v6 + - name: Install Rust (stable + rustfmt) + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + audit: + name: Security audit + runs-on: ubuntu-latest + # rustsec/audit-check opens issues for new advisories and posts + # review comments on PRs that introduce vulnerable dependencies. + permissions: + contents: read + issues: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v6 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + coverage: + name: Coverage (llvm-cov) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Install Rust (stable + llvm-tools) + uses: dtolnay/rust-toolchain@stable + with: + targets: i686-unknown-linux-gnu + components: llvm-tools-preview + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Generate LCOV report + run: cargo llvm-cov --target i686-unknown-linux-gnu --lcov --output-path lcov.info + + - name: Upload coverage artifact + uses: actions/upload-artifact@v7 + with: + name: coverage-lcov + path: lcov.info + if-no-files-found: error diff --git a/.gitignore b/.gitignore index e89b7db..717e3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ .claude CLAUDE.md /dist -/reference \ No newline at end of file +/reference + +# MkDocs +/site +/.venv +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e34dc30..69f7614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,105 +1,156 @@ # Changelog -Todas as alterações notáveis deste projeto serão documentadas neste arquivo. +All notable changes to this project are documented in this file. -## [1.0.0] - 2026-03-09 +Format inspired by [Keep a Changelog](https://keepachangelog.com/). Versioning follows [Semantic Versioning](https://semver.org/). Older entries live under [`changelog/`](changelog/). -### Adicionado +## [1.1.0] — 2026/05/18 + +Built on top of [rust-samp v3.0.0](https://github.com/NullSablex/rust-samp/releases/tag/v3.0.0). The same `.so` / `.dll` now loads on SA-MP and on Open Multiplayer (native component or legacy mode). No Pawn-visible API was removed or renamed. + +### Added + +- **Universal binary.** A single artifact runs on SA-MP and on Open Multiplayer. Open Multiplayer can load it as a native component (registered under `components` in `config.json`) or in legacy mode (registered under `legacy_plugins`). +- `mysql_tick()` — drains the dispatch queue manually. Kept for backwards compatibility only; with rust-samp v3 the unified `on_tick` callback already pumps the queue on both SA-MP (`ProcessTick`) and Open Multiplayer (`ITimersComponent`, 5 ms by default). +- `MYSQL_SAMP_VERSION` constant in `mysql_samp.inc` — string with the plugin version, auto-generated from `CARGO_PKG_VERSION` by `build.rs`. +- `on_component_free` lifecycle hook — emits a single informational log line when any neighbouring Open Multiplayer component is released. Useful when correlating "mysql_samp misbehaved after plugin X was unloaded" reports. +- CI: new `fmt` job (`cargo fmt --all -- --check`), `audit` job (`rustsec/audit-check`) and `coverage` job (`cargo llvm-cov`, LCOV uploaded as a workflow artifact). +- Release workflow: tag-vs-`Cargo.toml` sanity check. A `v1.2.3` release tag whose `Cargo.toml` still declares `1.2.2` fails before any artifact is built. +- Release workflow: the relevant `## [X.Y.Z]` section of `CHANGELOG.md` is now extracted and appended to the GitHub release body automatically — no copy-paste required. + +### Changed + +- **Tick dispatch is automatic.** `mysql_query` / `mysql_pquery` callbacks no longer require `SetTimer` + `mysql_tick()` in Open Multiplayer. The unified `on_tick` from rust-samp v3 fires on both servers and the plugin processes pending results inside it. +- `mysql_format` now truncates the rendered string at the destination buffer boundary (respecting UTF-8 char boundaries) and logs a warning, instead of aborting the native and returning `0`. The returned value is always the number of bytes written into `dest`. +- `mysql_format` and the callback format string now log a warning when they find an unknown specifier (`%z`, `%q`, …). Previously these were silently passed through or dropped. +- `mysql_options_set_int` rejects out-of-range integers explicitly: + - `MYSQL_OPT_PORT` requires `0..=65535`. Negative or oversized values return `false` (the old code silently wrapped to `u16`). + - `MYSQL_OPT_CONNECT_TIMEOUT` requires `>= 0`. Negative values return `false` (the old code silently wrapped to `u32`). +- The `logs/mysql.log` writer reports failures: when the file or the `logs/` directory cannot be written, the plugin emits exactly one console error (`[MySQL] Failed to write logs/mysql.log: . Further file-write errors will be suppressed.`) and then stops trying, instead of silently dropping every log line. +- Build scripts replaced. `scripts/build.sh` is gone; the project now ships `scripts/build-linux.sh` (Linux + Windows from Linux via `cargo-xwin`) and `scripts/build-windows.sh` (Windows + Linux via WSL or Docker/cross), matching the rust-samp upstream layout. +- Windows target moved from `i686-pc-windows-gnu` to `i686-pc-windows-msvc`. The MSVC toolchain is required so the binary can implement the Open Multiplayer `ComponentEntryPoint` ABI (Itanium on Linux, MSVC on Windows). `cargo-xwin` performs the cross-compile from Linux. +- Release workflow ships **raw artifacts** instead of zips: `mysql_samp.so`, `mysql_samp.dll`, `mysql_samp.inc`. Release notes are auto-generated from `Cargo.toml`, pinning the rust-samp SDK version. +- CI workflows declare per-job permissions (least privilege). `contents: read` at the top level; jobs opt in to `contents: write`, `issues: write` or `pull-requests: write` only when needed. +- Documentation site upgraded to a full MkDocs Material configuration with strict build (`mkdocs build --strict`). Documentation language switched to en-US; doc files renamed to English filenames (`installation.md`, `connection.md`, `errors.md`, `api-reference.md`, `security.md`, `migration.md`, `migration-changes.md`, `migration-examples.md`). +- README rewritten in en-US. Project links and table of contents updated to the new filenames. +- Internal: every cross-width or sign-changing integer conversion uses explicit `TryFrom` / `From` instead of the silent `as` cast. Negative Pawn cell values used as container indices are rejected explicitly instead of wrapping to `usize::MAX`. +- Internal: ORM raw natives (`orm_select` / `orm_update` / `orm_insert` / `orm_delete` / `orm_save`) deduplicated through a shared `run_orm_op` helper and an `OrmOp` enum — about 120 lines of repetition removed, error semantics preserved (same `MYSQL_ERROR_*` codes, same messages). +- Internal: `mysql_format` refactored into pure helpers (`parse_format`, `collect_format_values`, `render_format`, `truncate_to_buffer`) that are testable in isolation. +- Internal: Pawn native return types simplified to bare `T` (`bool`, `i32`, `f32`) where the native cannot fail; the macros from rust-samp v3 accept this without an `AmxResult` wrapper. + +### Fixed + +- Open Multiplayer log levels: with rust-samp v3 the default routing maps `log::Level` to `samp_sdk::omp::LogLevel` (`Error → Error`, `Warn → Warning`, `Info → Message`, `Debug → Debug`). Prior to v3 every line landed as `Message` regardless of severity. +- Documentation corrected: prior pages referenced a `cache_next_row()` native that does not exist, miscounted the natives (51 vs the actual 55), and omitted `MYSQL_OPT_AUTO_RECONNECT`, `cache_get_field_type`, `cache_is_any_active`, `cache_is_valid`, `cache_warning_count`. All fixed against the source of truth (`include/mysql_samp.inc.in` and `src/lib.rs`). +- `build.rs` no longer caches its `rerun-if-changed` directive on the `.inc.in` template only. The `MYSQL_SAMP_VERSION` literal in `include/mysql_samp.inc` is regenerated on every `cargo build`, so a `Cargo.toml` version bump propagates without `touch build.rs` or `cargo clean`. The write itself is idempotent — no spurious timestamp churn when nothing changed. + +### Documentation + +- New honest SSL caveat: `MYSQL_OPT_SSL` and `MYSQL_OPT_SSL_CA` are accepted by `mysql_options_set_int` / `mysql_options_set_str`, but the connection builder does not wire them through to the `mysql` crate yet (`TODO` in `src/connection.rs:67`). The connection is always plaintext today. This was true in 1.0.0 as well; only the documentation was honest about it in 1.1.0. +- Documented the universal-binary registration paths (`plugins=` for SA-MP, `components` for Open Multiplayer native, `legacy_plugins` for Open Multiplayer legacy). + +### Tests + +- Unit tests: 93 → 113 (+20). New coverage for `parse_format`, `render_format`, `truncate_to_buffer` and additional `escape_string` edge cases (consecutive quotes, double-escape invariant, all special characters at once, low-ASCII passthrough). + +### Removed + +- `samp-only` Cargo feature — never actually used, dropped to reduce surface area. +- `.cargo/config.toml` — was forcing every `cargo` invocation to `i686-unknown-linux-gnu`. The release scripts and CI workflows pass `--target` explicitly, so the override only slowed local `cargo check`/`test`/`clippy` without benefit. + +### Dependencies + +- `samp` switched from a local path dependency to git (`tag = "v3.0.0"`). +- `log` removed as a direct dependency (re-exported by `samp`). +- GitHub Actions bumped to the current major: `actions/checkout` v4 → v6, `actions/setup-python` v5 → v6, `actions/cache` v4 → v5, `actions/upload-artifact` v4 → v7, `softprops/action-gh-release` v2 → v3, `rustsec/audit-check` v2.0.0 → v2 (floats within the major for patches). + +--- + +## [1.0.0] — 2026/03/09 + +First stable release. Added ORM, cache, threaded query pipeline, full safety net. + +### Added #### Options -- `MYSQL_OPT_AUTO_RECONNECT` — option que ativa reconexão automática em caso de queda da conexão com o MySQL durante uma query (padrão: ativado) +- `MYSQL_OPT_AUTO_RECONNECT` — turns on the one-shot retry when a query fails because the server dropped the connection (enabled by default). #### Queries (non-blocking) -- `mysql_query` — Query threaded com callback e ordenação FIFO (substitui `mysql_tquery` do R41-4) -- `mysql_pquery` — Query paralela sem garantia de ordem (fire-and-forget) -- `mysql_escape_string` — Escape de strings para uso seguro em SQL (função pura, sem conexão) -- `mysql_format` — Formatação printf-like de queries (`%d`, `%f`, `%s`/`%e` com escape automático, `%r` raw) +- `mysql_query` — threaded query with callback and FIFO ordering (replaces `mysql_tquery` from R41-4). +- `mysql_pquery` — parallel query with no ordering guarantee. +- `mysql_escape_string` — pure SQL escape function. +- `mysql_format` — `printf`-style query builder (`%d`, `%f`, `%s` / `%e` with automatic escape, `%r` raw, `%%` literal). #### Cache -- `cache_get_row_count` / `cache_get_field_count` — Dimensões do resultado -- `cache_get_field_name` / `cache_get_field_type` — Metadados das colunas -- `cache_get_value_index` / `cache_get_value_index_int` / `cache_get_value_index_float` — Valor por índice -- `cache_get_value_name` / `cache_get_value_name_int` / `cache_get_value_name_float` — Valor por nome -- `cache_is_value_index_null` / `cache_is_value_name_null` — Verificação de NULL -- `cache_affected_rows` / `cache_insert_id` — Metadados de INSERT/UPDATE/DELETE -- `cache_warning_count` — Contagem de warnings do MySQL -- `cache_get_query_exec_time` / `cache_get_query_string` — Debug da query -- `cache_save` / `cache_delete` — Persistência de cache -- `cache_set_active` / `cache_unset_active` — Ativação manual de cache salvo -- `cache_is_any_active` / `cache_is_valid` — Verificação de estado do cache +- `cache_get_row_count` / `cache_get_field_count` — result dimensions. +- `cache_get_field_name` / `cache_get_field_type` — column metadata. +- `cache_get_value_index` / `cache_get_value_index_int` / `cache_get_value_index_float` — value by index. +- `cache_get_value_name` / `cache_get_value_name_int` / `cache_get_value_name_float` — value by name (case-insensitive). +- `cache_is_value_index_null` / `cache_is_value_name_null` — NULL checks. +- `cache_affected_rows` / `cache_insert_id` — write metadata. +- `cache_warning_count` — server warning count. +- `cache_get_query_exec_time` / `cache_get_query_string` — query debugging. +- `cache_save` / `cache_delete` — persist a cache across callbacks. +- `cache_set_active` / `cache_unset_active` — manually activate a saved cache. +- `cache_is_any_active` / `cache_is_valid` — state checks. #### ORM -- `orm_create` / `orm_destroy` — Criação e destruição de instâncias ORM -- `orm_errno` — Código de erro da última operação ORM -- `orm_select` / `orm_update` / `orm_insert` / `orm_delete` — Operações CRUD non-blocking -- `orm_save` — INSERT ou UPDATE automático baseado no valor da chave -- `orm_apply_cache` — Aplica resultado do cache nas variáveis Pawn vinculadas -- `orm_addvar_int` / `orm_addvar_float` / `orm_addvar_string` — Binding de variáveis -- `orm_delvar` / `orm_clear_vars` — Remoção de bindings -- `orm_setkey` — Define coluna de chave primária - -#### Callbacks -- `OnQueryError(errorid, error[], callback[], query[], connId)` — Forward chamado quando uma query falha - -#### Utilidades -- `mysql_error` — Obtém a mensagem do último erro em texto -- `mysql_set_charset` / `mysql_get_charset` — Configuração de charset da conexão -- `mysql_unprocessed_queries` — Contagem de queries pendentes em execução -- `mysql_log` — Configuração do nível de log em runtime - -#### Infraestrutura -- Pool de conexões (`mysql::Pool`) para reutilização segura de conexões em threads -- Sistema de cache com stack (push/pop) para callbacks e armazenamento manual -- QueryManager com threading via `mpsc` e reordenação FIFO -- Callback dispatcher com suporte a parâmetros variádicos (int, float, string) -- Limpeza automática de ORMs quando o AMX é descarregado - -#### Testes unitários -- 93 testes cobrindo toda a lógica pura do plugin (sem dependência de MySQL ou SA:MP runtime) -- `error.rs` — Códigos de erro, ErrorState, construtores, clone, igualdade -- `options.rs` — MysqlOptionKind, MysqlOptions defaults, OptionsManager CRUD, wrapping de IDs -- `cache.rs` — CacheEntry (campos, valores, NULL, tipos), CacheManager (stack, save/delete, ativação manual) -- `connection.rs` — escape_string (caracteres especiais, UTF-8, SQL injection), escape_identifier, ConnectionManager -- `query.rs` — QueryManager (ordenação FIFO, dispatch parcial, resultados paralelos, modo misto) -- `orm.rs` — OrmVarBinding, OrmError, OrmManager CRUD, destroy_by_amx, wrapping de IDs - -#### Documentação -- Banner no `include/mysql_samp.inc` com nome do projeto, autor, repositório e licença -- `docs/options.md` — documentação completa de todas as options de conexão com exemplos -- `docs/benchmark.md` — benchmark comparativo com o R41-4, incluindo arquivos executáveis (`benchmark/setup.sql`, `benchmark/bench_mysql_samp.pwn`, `benchmark/bench_r41.pwn`) -- `docs/mudancas.md` — Referência de mudanças práticas R41-4 → mysql_samp -- `docs/exemplos-migracao.md` — 14 exemplos de código antes/depois para migração -- `docs/migracao.md` — Guia passo a passo de migração do R41-4 -- `docs/api.md` — Referência completa de todas as 51 natives + 1 forward -- `docs/queries.md`, `docs/cache.md`, `docs/orm.md` — Documentação detalhada por módulo -- `docs/erros.md`, `docs/conexao.md`, `docs/seguranca.md`, `docs/instalacao.md` - -### Alterado -- `ConnectionEntry` migrado de `mysql::Conn` para `mysql::Pool` (Send+Sync+Clone) -- `MysqlPlugin` agora implementa `on_amx_load`, `on_amx_unload` e `process_tick` -- `enable_process_tick()` habilitado na inicialização do plugin - -### Removido -- `MysqlError::Unknown` — variante sem uso removida; variantes seguintes renumeradas (`QueryFailed=5`, `NoCacheActive=6`, `InvalidOrm=7`, `OrmKeyNotSet=8`) -- Limite artificial de 128 queries concorrentes (`MAX_CONCURRENT_QUERIES`) — o `mysql::Pool` já provê backpressure natural; o limite causava rejeição silenciosa de queries acima do teto - -### Segurança -- **CWE-89** — `%s` no `mysql_format` agora escapa automaticamente (antes era raw). Novo `%r` para strings cruas -- **CWE-89** — Identificadores SQL no ORM escapados via `escape_identifier()` (remoção de backticks) -- **CWE-787** — Proteção contra escrita fora dos limites no ORM (`max_len` limitado a 4096) -- **CWE-770** — Limite de 1024 caches salvos e 100.000 rows por resultado -- **CWE-252** — Verificação de erros no push de parâmetros para callbacks AMX -- **CWE-190** — Proteção contra overflow de IDs com `wrapping_add` em todos os managers -- **UTF-8 forçado** — `SET NAMES utf8mb4` via `init()` do Pool em todas as conexões (previne ataques multi-byte) -- **Reconexão automática** — queries não falham silenciosamente após queda de conexão; o plugin tenta uma segunda vez em uma conexão fresca antes de reportar erro via `OnQueryError` - -## [0.1.0] - 2026-02-22 - -### Adicionado -- Lançamento inicial: conexão, options, errno, logger com banner -- `mysql_connect` / `mysql_close` / `mysql_status` -- `mysql_options_new` / `mysql_options_set_int` / `mysql_options_set_str` -- `mysql_errno` -- Suporte a Unix socket (detecção por `/` no host) -- TLS via rustls (zero dependências externas) -- Logs detalhados em `logs/mysql.log` +- `orm_create` / `orm_destroy` — ORM lifecycle. +- `orm_errno` — last error code for the instance. +- `orm_select` / `orm_update` / `orm_insert` / `orm_delete` — non-blocking CRUD. +- `orm_save` — INSERT or UPDATE depending on the key value. +- `orm_apply_cache` — copy a cache row into the bound Pawn variables. +- `orm_addvar_int` / `orm_addvar_float` / `orm_addvar_string` — bind a Pawn variable to a column. +- `orm_delvar` / `orm_clear_vars` — remove bindings. +- `orm_setkey` — declare the primary-key column. + +#### Forward +- `OnQueryError(errorid, error[], callback[], query[], connId)` — fired when a threaded query fails. + +#### Utility +- `mysql_error` — fetch the last error message into a buffer. +- `mysql_set_charset` / `mysql_get_charset` — read/write the connection charset. +- `mysql_unprocessed_queries` — count of queries currently in flight + buffered. +- `mysql_log` — runtime log-level switch. + +#### Infrastructure +- Connection pool (`mysql::Pool`) shared between worker threads. +- Cache stack (push/pop around callbacks) plus persistent slots. +- `QueryManager` with `mpsc` channel and FIFO reordering. +- Callback dispatcher with variadic-parameter support (int, float, string). +- ORM auto-cleanup when the owning AMX unloads. + +#### Tests +- 93 unit tests covering every pure path: `error.rs`, `options.rs`, `cache.rs`, `connection.rs` (`escape_string`/`escape_identifier`/`ConnectionManager`), `query.rs` (FIFO ordering, partial dispatch, parallel, mixed), `orm.rs`. + +#### Documentation +- Banner in `include/mysql_samp.inc` with project metadata. +- `docs/options.md`, `docs/benchmark.md`, `docs/mudancas.md`, `docs/exemplos-migracao.md`, `docs/migracao.md`, `docs/api.md`, `docs/queries.md`, `docs/cache.md`, `docs/orm.md`, `docs/erros.md`, `docs/conexao.md`, `docs/seguranca.md`, `docs/instalacao.md`. + +### Changed + +- `ConnectionEntry` migrated from `mysql::Conn` to `mysql::Pool` (`Send + Sync + Clone`). +- `MysqlPlugin` now implements `on_amx_load`, `on_amx_unload`, and `process_tick`. +- `enable_process_tick()` enabled in the plugin constructor. + +### Removed + +- `MysqlError::Unknown` variant — never used; remaining variants renumbered (`QueryFailed = 5`, `NoCacheActive = 6`, `InvalidOrm = 7`, `OrmKeyNotSet = 8`). +- `MAX_CONCURRENT_QUERIES` artificial cap of 128 — `mysql::Pool` already applies backpressure; the cap was silently rejecting queries above the ceiling. + +### Security + +- **CWE-89** — `%s` in `mysql_format` now escapes by default (was raw). New `%r` for raw strings. +- **CWE-89** — SQL identifiers in the ORM are sanitised via `escape_identifier()`. +- **CWE-787** — ORM string writes capped at 4096 bytes. +- **CWE-770** — 1024 saved caches and 100 000 rows per result, both enforced. +- **CWE-252** — every push to the AMX stack is checked. +- **CWE-190** — id counters use `wrapping_add(...).max(1)` in every manager. +- UTF-8 forced via `SET NAMES utf8mb4` on every pool connection — blocks multi-byte escape-bypass attacks. +- Auto reconnect retries dropped connections once before reporting through `OnQueryError`. + +--- + +## Historical releases + +- [v0.x](changelog/v0.x.md) — 0.1.0 diff --git a/CHECKLIST.md b/CHECKLIST.md index cc80755..1bc99e1 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,117 +1,136 @@ -# Checklist: mysql_samp vs R41-4 +# Checklist: mysql_samp vs MySQL R41-4 -## Conexão +Coverage of the MySQL R41-4 (BlueG / maddinat0r) Pawn API by **mysql_samp 1.1.0**. Source of truth: [`include/mysql_samp.inc.in`](include/mysql_samp.inc.in) and [`src/lib.rs`](src/lib.rs). -| Funcionalidade | R41-4 | mysql_samp | Notas | +## Connection + +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| `mysql_connect` | Sim | Sim | Nosso não usa tags | -| `mysql_connect_file` | Sim | - | Conexão via .ini | -| `mysql_close` | Sim | Sim | | -| `mysql_errno` | Sim | Sim | Retorna código interno do plugin | -| `mysql_error` | Sim | - | Retorna string do erro | -| `mysql_escape_string` | Sim | Sim | Escape puro (sem connId) | -| `mysql_format` | Sim | Sim | printf-like com `%d`, `%f`, `%s`, `%e` | -| `mysql_set_charset` | Sim | - | | -| `mysql_get_charset` | Sim | - | | -| `mysql_stat` / `mysql_status` | Sim | Sim | Nosso usa `mysql_status` | -| `mysql_unprocessed_queries` | Sim | - | Queries pendentes na fila | -| `mysql_log` | Sim | - | Nível de log configurável | -| Socket Unix | - | Sim | Detecta por `/` no host | +| `mysql_connect` | Yes | Yes | No custom Pawn tags | +| `mysql_connect_file` | Yes | — | `.ini`-based connect (not supported) | +| `mysql_close` | Yes | Yes | | +| `mysql_errno` | Yes | Yes | Returns the MySQL error code (1062, 1045, …) or `0` for no error / `MYSQL_ERROR_*` (1..=8) for plugin-side errors | +| `mysql_error` | Yes | Yes | Writes the last error message into the destination buffer | +| `mysql_escape_string` | Yes | Yes | Pure function — no `connId` required | +| `mysql_format` | Yes | Yes | `printf`-like with `%d`, `%i`, `%f`, `%s`, `%e`, `%r`, `%%` | +| `mysql_set_charset` | Yes | Yes | Runs `SET NAMES ''` on the connection | +| `mysql_get_charset` | Yes | Yes | Reads `@@character_set_connection` | +| `mysql_stat` / `mysql_status` | Yes | Yes | We export `mysql_status` | +| `mysql_unprocessed_queries` | Yes | Yes | In-flight + buffered count | +| `mysql_log` | Yes | Yes | Runtime level switch (`MYSQL_LOG_*`) | +| `mysql_tick` | — | Yes | Manual drain. Optional — `on_tick` (rust-samp v3) already pumps the queue automatically | +| Unix socket | — | Yes | Auto-detected when `host` starts with `/` | ## Options -| Funcionalidade | R41-4 | mysql_samp | Notas | +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| Criar options | `mysql_init_options` | `mysql_options_new` | | -| Definir opção | `mysql_set_option` (variadic) | `mysql_options_set_int` / `_set_str` | Nosso separa int e str | -| `mysql_global_options` | Sim | - | Options globais (duplicatas) | -| AUTO_RECONNECT | Sim | - | | -| MULTI_STATEMENTS | Sim | - | | -| POOL_SIZE | Sim | - | Nosso usa Pool interno (2 conns) | -| SERVER_PORT | Sim | Sim | `MYSQL_OPT_PORT` | -| SSL_ENABLE | Sim | Sim | `MYSQL_OPT_SSL` | -| SSL_KEY_FILE | Sim | - | | -| SSL_CERT_FILE | Sim | - | | -| SSL_CA_FILE | Sim | Sim | `MYSQL_OPT_SSL_CA` | -| SSL_CA_PATH | Sim | - | | -| SSL_CIPHER | Sim | - | | -| CONNECT_TIMEOUT | - | Sim | Exclusivo nosso | +| Create handle | `mysql_init_options` | `mysql_options_new` | | +| Set value | `mysql_set_option` (variadic) | `mysql_options_set_int` / `_set_str` | int and string setters are split | +| `mysql_global_options` | Yes | — | Global option pool (not supported) | +| AUTO_RECONNECT | Yes | Yes | `MYSQL_OPT_AUTO_RECONNECT`; one-shot retry on connection-loss errors | +| MULTI_STATEMENTS | Yes | — | | +| POOL_SIZE | Yes | — | We use `mysql::Pool` internally; size is not exposed | +| SERVER_PORT | Yes | Yes | `MYSQL_OPT_PORT` (`u16`; negative or `> 65535` rejected) | +| SSL_ENABLE | Yes | Accepted **but no-op** | `MYSQL_OPT_SSL` is stored in the options struct; the connection builder does not wire it through to the `mysql` crate yet (TODO in `src/connection.rs:67`) | +| SSL_KEY_FILE | Yes | — | | +| SSL_CERT_FILE | Yes | — | | +| SSL_CA_FILE | Yes | Accepted **but no-op** | `MYSQL_OPT_SSL_CA` — same caveat as `MYSQL_OPT_SSL` | +| SSL_CA_PATH | Yes | — | | +| SSL_CIPHER | Yes | — | | +| CONNECT_TIMEOUT | — | Yes | Exclusive — `MYSQL_OPT_CONNECT_TIMEOUT` (`u32`; negative rejected) | ## Queries -| Funcionalidade | R41-4 | mysql_samp | Notas | +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| `mysql_query` | Sim (sync) | Sim (non-blocking FIFO) | Nosso é sempre threaded, substitui tquery | -| `mysql_tquery` | Sim | - | Substituído por `mysql_query` (non-blocking) | -| `mysql_pquery` | Sim | Sim | Query paralela sem ordem | -| `mysql_query_file` | Sim | - | Query de arquivo SQL | -| `mysql_tquery_file` | Sim | - | Query threaded de arquivo | +| `mysql_query` | Yes (sync) | Yes (non-blocking, FIFO) | Always threaded — replaces `tquery` | +| `mysql_tquery` | Yes | — | Subsumed by `mysql_query` (which is already non-blocking) | +| `mysql_pquery` | Yes | Yes | Parallel, no ordering guarantee | +| `mysql_query_file` | Yes | — | Load SQL from a file | +| `mysql_tquery_file` | Yes | — | Threaded variant of the above | ## Cache -| Funcionalidade | R41-4 | mysql_samp | Notas | +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| `cache_get_row_count` | Sim | Sim | | -| `cache_get_field_count` | Sim | Sim | | -| `cache_get_result_count` | Sim | - | Multi-result sets (desnecessário) | -| `cache_get_field_name` | Sim | Sim | | -| `cache_get_field_type` | Sim | - | | -| `cache_set_result` | Sim | - | Multi-result sets (desnecessário) | -| `cache_get_value_index` | Sim | Sim | String por índice | -| `cache_get_value_index_int` | Sim | Sim | Int por índice | -| `cache_get_value_index_float` | Sim | Sim | Float por índice | -| `cache_is_value_index_null` | Sim | Sim | | -| `cache_get_value_name` | Sim | Sim | String por nome | -| `cache_get_value_name_int` | Sim | Sim | Int por nome | -| `cache_get_value_name_float` | Sim | Sim | Float por nome | -| `cache_is_value_name_null` | Sim | Sim | | -| `cache_save` | Sim | Sim | Salva cache para uso posterior | -| `cache_delete` | Sim | Sim | | -| `cache_set_active` | Sim | Sim | | -| `cache_unset_active` | Sim | Sim | | -| `cache_is_any_active` | Sim | - | Disponível internamente | -| `cache_is_valid` | Sim | - | Disponível internamente | -| `cache_affected_rows` | Sim | Sim | | -| `cache_insert_id` | Sim | Sim | | -| `cache_warning_count` | Sim | - | Raramente usado | -| `cache_get_query_exec_time` | Sim | Sim | | -| `cache_get_query_string` | Sim | Sim | | +| `cache_get_row_count` | Yes | Yes | | +| `cache_get_field_count` | Yes | Yes | | +| `cache_get_result_count` | Yes | — | Multi-result sets (not supported) | +| `cache_get_field_name` | Yes | Yes | | +| `cache_get_field_type` | Yes | Yes | Returns the raw `mysql::consts::ColumnType` byte | +| `cache_set_result` | Yes | — | Multi-result sets (not supported) | +| `cache_get_value_index` | Yes | Yes | String by index | +| `cache_get_value_index_int` | Yes | Yes | Int by index | +| `cache_get_value_index_float` | Yes | Yes | Float by index | +| `cache_is_value_index_null` | Yes | Yes | | +| `cache_get_value_name` | Yes | Yes | String by name (case-insensitive) | +| `cache_get_value_name_int` | Yes | Yes | Int by name | +| `cache_get_value_name_float` | Yes | Yes | Float by name | +| `cache_is_value_name_null` | Yes | Yes | | +| `cache_save` | Yes | Yes | Persists the active entry for later reuse | +| `cache_delete` | Yes | Yes | | +| `cache_set_active` | Yes | Yes | | +| `cache_unset_active` | Yes | Yes | | +| `cache_is_any_active` | Yes | Yes | | +| `cache_is_valid` | Yes | Yes | | +| `cache_affected_rows` | Yes | Yes | | +| `cache_insert_id` | Yes | Yes | | +| `cache_warning_count` | Yes | Yes | Reported by the server after each query | +| `cache_get_query_exec_time` | Yes | Yes | Always in milliseconds | +| `cache_get_query_string` | Yes | Yes | | ## ORM -| Funcionalidade | R41-4 | mysql_samp | Notas | +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| `orm_create` | Sim | Sim | | -| `orm_destroy` | Sim | Sim | | -| `orm_errno` | Sim | Sim | | -| `orm_apply_cache` | Sim | Sim | | -| `orm_select` / `orm_load` | Sim | Sim | Non-blocking | -| `orm_update` | Sim | Sim | Non-blocking | -| `orm_insert` | Sim | Sim | Non-blocking | -| `orm_delete` | Sim | Sim | Non-blocking | -| `orm_save` | Sim | Sim | INSERT se key=0, UPDATE caso contrário | -| `orm_addvar_int` | Sim | Sim | | -| `orm_addvar_float` | Sim | Sim | | -| `orm_addvar_string` | Sim | Sim | | -| `orm_clear_vars` | Sim | Sim | | -| `orm_delvar` | Sim | Sim | | -| `orm_setkey` | Sim | Sim | | - -## Callbacks - -| Funcionalidade | R41-4 | mysql_samp | Notas | +| `orm_create` | Yes | Yes | | +| `orm_destroy` | Yes | Yes | | +| `orm_errno` | Yes | Yes | `ORM_OK` / `ORM_NO_DATA` | +| `orm_apply_cache` | Yes | Yes | | +| `orm_select` / `orm_load` | Yes | Yes | Non-blocking | +| `orm_update` | Yes | Yes | Non-blocking | +| `orm_insert` | Yes | Yes | Non-blocking | +| `orm_delete` | Yes | Yes | Non-blocking | +| `orm_save` | Yes | Yes | INSERT when the key is empty, UPDATE otherwise | +| `orm_addvar_int` | Yes | Yes | | +| `orm_addvar_float` | Yes | Yes | | +| `orm_addvar_string` | Yes | Yes | `var_max_len` capped at `MAX_ORM_STRING_LEN` (4096) | +| `orm_clear_vars` | Yes | Yes | | +| `orm_delvar` | Yes | Yes | | +| `orm_setkey` | Yes | Yes | | + +## Forwards + +| Feature | R41-4 | mysql_samp | Notes | |---|---|---|---| -| `OnQueryError` | Sim | Sim | Forward chamado em erro de query | +| `OnQueryError` | Yes | Yes | Fired on every loaded AMX when a threaded query fails | -## Extras (exclusivo mysql_samp) +## Extras (mysql_samp only) -| Funcionalidade | Notas | +| Feature | Notes | |---|---| -| Zero dependências externas | Sem libmysqlclient, sem OpenSSL | -| TLS via rustls | Embutido no binário | -| `MYSQL_OPT_CONNECT_TIMEOUT` | Timeout de conexão configurável | -| Logs detalhados em arquivo | `logs/mysql.log` com timestamp | -| Banner informativo | Data/hora de build | -| Pool de conexões | `mysql::Pool` para threading seguro | -| Queries 100% non-blocking | Sem bloqueio do servidor | -| Limpeza automática de ORM | ORMs destruídos quando AMX descarrega | +| Zero external runtime dependencies | No `libmysqlclient`, no OpenSSL — `mysql` crate with the `default-rust` feature (rustls bundled) | +| `MYSQL_OPT_CONNECT_TIMEOUT` | Configurable TCP connect timeout | +| `MYSQL_SAMP_VERSION` in the include | Version constant available to Pawn for introspection | +| Detailed file logs | `logs/mysql.log` with timestamp; I/O failure reported once via `samp::log::error!`, then suppressed | +| Build banner | Date/time stamped by `build.rs` via `BUILD_DATE` / `BUILD_TIME` / `BUILD_YEAR` | +| Connection pool | `mysql::Pool` (`Clone + Send + Sync`) for safe multi-threaded access | +| Fully non-blocking queries | Both `mysql_query` (FIFO) and `mysql_pquery` (parallel) run on worker threads | +| ORM auto-cleanup | `OrmManager::destroy_by_amx` frees instances when their AMX is unloaded | +| Universal SA-MP + Open Multiplayer binary | The same `.so` / `.dll` runs natively (component) or in legacy mode (`legacy_plugins`) | +| Unified `on_tick` | Dispatches callbacks via `ProcessTick` (SA-MP) and `ITimersComponent` (Open Multiplayer native), no Pawn `SetTimer` required | +| `mysql_format` safe truncation | Truncates at the destination buffer boundary respecting UTF-8 char boundaries; warns once per call | +| Strict integer conversions | Every cross-width / sign-changing conversion goes through `TryFrom` / `From`; no silent wrap from `as` | +| 113 unit tests | Cover the entire pure surface (parser, renderer, escape, cache, ORM, query reordering) | + +## Totals + +| Category | R41-4 | mysql_samp | +|---|---|---| +| Pawn natives | — | **55** | +| Pawn forwards | — | **1** | +| Plugin error codes (`MYSQL_ERROR_*`) | — | 9 (`MYSQL_OK` + 8) | +| Connection options (`MYSQL_OPT_*`) | many | 5 (3 wired + 2 no-op SSL) | +| Log levels (`MYSQL_LOG_*`) | bitflags | 5 sequential | +| ORM error codes (`ORM_*`) | 3 | 2 | diff --git a/Cargo.lock b/Cargo.lock index bd5207a..75e296e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,9 +520,9 @@ dependencies = [ [[package]] name = "mysql" -version = "27.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090068bc0a24762663338da78414148333d1ee246a7293c10a517ddf1548b291" +checksum = "4a732193888328fc060ab901c0ed1355521267a51ffbfd9a0b3786434c6b8e7f" dependencies = [ "bufstream", "bytes", @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "mysql_common" -version = "0.36.2" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e7f6a15265801679ddf1ddb38fd74b4555a677a78378d4561fbe5f0b1d4d72" +checksum = "bffc2127d4035fa5a614935c663a15a4468e64e798473e0cc21c8df40a607588" dependencies = [ "base64", "bitflags", @@ -588,10 +588,9 @@ dependencies = [ [[package]] name = "mysql_samp" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", - "log", "mysql", "samp", ] @@ -756,8 +755,8 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "samp" -version = "2.1.0" -source = "git+https://github.com/NullSablex/rust-samp?rev=2f04c37d5383fde4e255a184aa90ec1ee829f8ed#2f04c37d5383fde4e255a184aa90ec1ee829f8ed" +version = "3.0.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ "fern", "log", @@ -767,8 +766,8 @@ dependencies = [ [[package]] name = "samp-codegen" -version = "1.1.0" -source = "git+https://github.com/NullSablex/rust-samp?rev=2f04c37d5383fde4e255a184aa90ec1ee829f8ed#2f04c37d5383fde4e255a184aa90ec1ee829f8ed" +version = "1.3.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ "proc-macro2", "quote", @@ -777,8 +776,8 @@ dependencies = [ [[package]] name = "samp-sdk" -version = "2.1.0" -source = "git+https://github.com/NullSablex/rust-samp?rev=2f04c37d5383fde4e255a184aa90ec1ee829f8ed#2f04c37d5383fde4e255a184aa90ec1ee829f8ed" +version = "3.0.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ "bitflags", ] diff --git a/Cargo.toml b/Cargo.toml index 30ecc23..9df5dd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "mysql_samp" -version = "1.0.0" +version = "1.1.0" edition = "2024" -authors = ["NullSablex"] +authors = ["NullSablex "] repository = "https://github.com/NullSablex/mysql_samp" license = "GPL-3.0" @@ -10,7 +10,9 @@ license = "GPL-3.0" crate-type = ["cdylib", "lib"] [dependencies] -samp = { git = "https://github.com/NullSablex/rust-samp", rev = "2f04c37d5383fde4e255a184aa90ec1ee829f8ed" } -mysql = { version = "27.0.0", default-features = false, features = ["default-rust"] } -chrono = "0.4.44" -log = "0.4" +samp = { git = "https://github.com/NullSablex/rust-samp.git", tag = "v3.0.0" } +mysql = { version = "28.0.0", default-features = false, features = ["default-rust"] } +chrono = "0.4" + +[package.metadata.samp] +uid = "0x4e10002241c700f4" diff --git a/README.md b/README.md index 3608cb4..1e01622 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,54 @@ # mysql_samp -> Plugin MySQL para SA:MP escrito em Rust — por [NullSablex](https://github.com/NullSablex) +> MySQL plugin for SA-MP written in Rust — by [NullSablex](https://github.com/NullSablex) ![License](https://img.shields.io/badge/license-GPL--3.0-blue) -![SA:MP](https://img.shields.io/badge/SA:MP-0.3.7+-orange) -![open.mp](https://img.shields.io/badge/open.mp-compatível-orange) +![SA-MP](https://img.shields.io/badge/SA--MP-0.3.7+-orange) +![Open Multiplayer](https://img.shields.io/badge/Open%20Multiplayer-native%20%26%20legacy-orange) ![Build](https://img.shields.io/badge/build-Linux%20%7C%20Windows-green) ![Architecture](https://img.shields.io/badge/arch-x86%20(32--bit)-lightgrey) -[![Release](https://img.shields.io/github/v/release/NullSablex/MySQL-SAMP?label=download)](https://github.com/NullSablex/mysql_samp/releases/latest) - -> [!WARNING] -> Este projeto está em fase inicial de desenvolvimento. A API pode sofrer alterações entre versões. - -## Visão geral - -**mysql_samp** é um plugin MySQL moderno para SA:MP (San Andreas Multiplayer) e [open.mp](https://open.mp) construído inteiramente em Rust. Fornece uma API completa para conectividade com banco de dados, queries non-blocking, sistema de cache e ORM, sem nenhuma dependência externa em runtime. - -### Destaques - -- **Zero dependências externas** — sem `libmysqlclient`, sem OpenSSL. O protocolo MySQL e o TLS (via rustls) são compilados diretamente no binário. -- **Todas as queries são non-blocking** — `mysql_query` executa em threads separadas com ordenação FIFO. Nunca trava o servidor. -- **Pool de conexões** — reutilização automática de conexões via `mysql::Pool`, com thread safety nativa. -- **ORM integrado** — mapeamento de variáveis Pawn para colunas do banco com operações CRUD automáticas. -- **Sistema de cache** — acesso aos resultados via stack automático ou persistência manual com `cache_save`. -- **Seguro por padrão** — escape automático de strings, UTF-8 forçado, proteção contra SQL injection e memory exhaustion. -- **Deploy simples** — copie o `.so` ou `.dll` e funciona. Sem bibliotecas extras para instalar. - -## Instalação - -1. Baixe a versão mais recente para sua plataforma: - - `mysql_samp.so` (Linux) - - `mysql_samp.dll` (Windows) -2. Coloque o arquivo no diretório `plugins/` do seu servidor. -3. Copie `mysql_samp.inc` para a pasta de includes do compilador: - - **Windows:** `pawno/include/` ou `qawno/include/` - - **Linux:** `include/` (na raiz do servidor) -4. Adicione ao `server.cfg` (ou `config.json` no open.mp): - ``` - plugins mysql_samp.so - ``` - No Windows: - ``` - plugins mysql_samp.dll - ``` +[![Release](https://img.shields.io/github/v/release/NullSablex/mysql_samp?label=download)](https://github.com/NullSablex/mysql_samp/releases/latest) + +## Overview + +**mysql_samp** is a modern MySQL plugin for SA-MP (San Andreas Multiplayer) and [Open Multiplayer](https://open.mp) (open.mp), written entirely in Rust. It provides a complete API for database connectivity, non-blocking queries, a cache system and an ORM, with zero external runtime dependencies. + +The same binary loads on SA-MP and on Open Multiplayer — natively as a component (recommended) or via legacy mode. + +### Highlights + +- **Zero external dependencies** — no `libmysqlclient`, no OpenSSL. The MySQL protocol and TLS (via rustls) are compiled directly into the binary. +- **All queries are non-blocking** — `mysql_query` runs on background threads with FIFO ordering. The server never stalls. +- **Connection pool** — automatic reuse through `mysql::Pool`, thread-safe by design. +- **Built-in ORM** — maps Pawn variables to columns with CRUD helpers. +- **Cache system** — results accessible through an automatic stack or persisted manually with `cache_save`. +- **Safe by default** — string escaping, forced UTF-8, protection against SQL injection and memory exhaustion. +- **Universal binary** — built on top of [rust-samp](https://github.com/NullSablex/rust-samp) v3.0.0; one `.so`/`.dll` runs on SA-MP and on Open Multiplayer (native component or legacy). +- **Simple deploy** — drop the `.so` or `.dll` in and you are done. No system libraries to install. + +## Installation + +1. Download the latest release for your platform: + - `mysql_samp.so` (Linux i686) + - `mysql_samp.dll` (Windows i686, MSVC ABI) + - `mysql_samp.inc` (Pawn include, shared between SA-MP and Open Multiplayer) +2. Place the binary in the server's `plugins/` directory. +3. Copy `mysql_samp.inc` to your compiler's include folder: + - **Windows:** `pawno/include/` or `qawno/include/` + - **Linux:** `include/` (at the server root) +4. Register the plugin: + - **SA-MP** — add to `server.cfg`: + ``` + plugins mysql_samp.so + ``` + (or `mysql_samp.dll` on Windows) + - **Open Multiplayer (native, recommended)** — list the binary under `components` in `config.json`. It will be loaded via `ComponentEntryPoint`, with access to `ICore`, `ITimersComponent` and the other native APIs. + - **Open Multiplayer (legacy)** — still supported. Register the binary under `legacy_plugins` in `config.json` if you prefer the SA-MP compatibility path. Same binary, no extra build flags. > [!IMPORTANT] -> Não é necessário instalar `libmysqlclient` ou qualquer outra biblioteca. O plugin é auto-contido. +> No `libmysqlclient` or other system library is required. The plugin is self-contained. -## Início rápido +## Quick start ```pawn #include @@ -56,13 +57,13 @@ new gMysql; public OnGameModeInit() { - gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp_db"); + gMysql = mysql_connect("127.0.0.1", "root", "password", "samp_db"); if (mysql_errno()) { return 1; } - // Query non-blocking com callback + // Non-blocking query with callback mysql_query(gMysql, "SELECT * FROM players LIMIT 10", "OnPlayersLoaded"); return 1; } @@ -70,7 +71,7 @@ public OnGameModeInit() { forward OnPlayersLoaded(); public OnPlayersLoaded() { new rows = cache_get_row_count(); - printf("Jogadores encontrados: %d", rows); + printf("Players found: %d", rows); new name[MAX_PLAYER_NAME]; for (new i = 0; i < rows; i++) { @@ -85,48 +86,58 @@ public OnGameModeExit() { } ``` -## Documentacao +## Documentation -A documentacao completa do plugin esta em [docs/](docs/): +The full plugin documentation lives in [docs/](docs/): -| Documento | Conteudo | +| Document | Contents | |---|---| -| [Instalacao e configuracao](docs/instalacao.md) | Setup, server.cfg, requisitos | -| [Conexao](docs/conexao.md) | mysql_connect, mysql_close, options, charset, SSL | +| [Installation and setup](docs/installation.md) | Setup, server.cfg / config.json, requirements | +| [Connection](docs/connection.md) | mysql_connect, mysql_close, mysql_status, charset | +| [Options](docs/options.md) | All `MYSQL_OPT_*` values, defaults, SSL caveat | | [Queries](docs/queries.md) | mysql_query, mysql_pquery, mysql_format, mysql_escape_string | -| [Cache](docs/cache.md) | Todas as funcoes cache_*, save/restore, ciclo de vida | -| [ORM](docs/orm.md) | Mapeamento objeto-relacional, CRUD, bindings | -| [Tratamento de erros](docs/erros.md) | mysql_errno, mysql_error, OnQueryError, codigos | -| [Referencia da API](docs/api.md) | Tabela completa de todas as natives e forwards | -| [Seguranca](docs/seguranca.md) | Escape, UTF-8, limites, boas praticas | -| [Migracao do R41-4](docs/migracao.md) | Diferencas e como migrar do mysql R41-4 | +| [Cache](docs/cache.md) | All cache_* functions, save/restore, lifecycle | +| [ORM](docs/orm.md) | Object-relational mapping, CRUD, bindings | +| [Errors](docs/errors.md) | mysql_errno, mysql_error, OnQueryError, error codes | +| [Security](docs/security.md) | Escaping, UTF-8, resource limits, best practices | +| [API reference](docs/api-reference.md) | Full table of every native and forward | +| [Migration from R41-4](docs/migration.md) | Differences and migration steps from mysql R41-4 | -## Compilando o codigo-fonte +## Building from source -### Requisitos +### Requirements -- Toolchain Rust com targets: `i686-unknown-linux-gnu`, `i686-pc-windows-gnu` -- Nenhuma biblioteca do sistema necessaria (build 100% Rust) +- Rust stable toolchain with the targets `i686-unknown-linux-gnu` and `i686-pc-windows-msvc` +- `cargo-xwin` for cross-compiling the Windows `.dll` from Linux (installed automatically by the script) +- No system libraries — the build is 100% Rust -### Build de desenvolvimento +### Development build ```bash -cargo build +cargo build --target i686-unknown-linux-gnu ``` -### Build de release (Linux + Windows) +### Release build (Linux + Windows) + +From Linux: + +```bash +./scripts/build-linux.sh +``` + +From Windows (Git Bash): ```bash -bash scripts/build.sh +./scripts/build-windows.sh ``` -Os arquivos sao gerados em `dist/` com checksums SHA-256. +Both scripts produce `dist/mysql_samp.so` and `dist/mysql_samp.dll`, each with full SA-MP + Open Multiplayer native support. > [!CAUTION] -> Este plugin e distribuido sob a GPL v3. Qualquer trabalho derivado deve manter o codigo-fonte aberto sob a mesma licenca. +> This plugin is distributed under the GPL v3. Any derivative work must keep the source code open under the same license. -## Licenca +## License Copyright (c) 2026 NullSablex -Este projeto esta licenciado sob a [GNU General Public License v3.0](LICENSE). +This project is licensed under the [GNU General Public License v3.0](LICENSE). diff --git a/build.rs b/build.rs index cdb9cd2..f3e268c 100644 --- a/build.rs +++ b/build.rs @@ -26,4 +26,30 @@ fn main() { println!("cargo:rustc-env=BUILD_DATE={date}"); println!("cargo:rustc-env=BUILD_TIME={time}"); println!("cargo:rustc-env=BUILD_YEAR={year}"); + + generate_inc(); +} + +fn generate_inc() { + use std::fs; + + let template_path = "include/mysql_samp.inc.in"; + let output_path = "include/mysql_samp.inc"; + + // No `cargo:rerun-if-changed` directives: build.rs runs on every build + // so the .inc tracks the current Cargo version, build date and template + // without manual intervention. The write below is idempotent — it only + // touches disk when the rendered output actually differs from the file + // on disk, so the always-run policy does not churn timestamps. + + let template = fs::read_to_string(template_path) + .unwrap_or_else(|e| panic!("failed to read {template_path}: {e}")); + + let version = env!("CARGO_PKG_VERSION"); + let rendered = template.replace("{{VERSION}}", version); + + if fs::read_to_string(output_path).ok().as_deref() != Some(rendered.as_str()) { + fs::write(output_path, &rendered) + .unwrap_or_else(|e| panic!("failed to write {output_path}: {e}")); + } } diff --git a/changelog/v0.x.md b/changelog/v0.x.md new file mode 100644 index 0000000..4d92983 --- /dev/null +++ b/changelog/v0.x.md @@ -0,0 +1,14 @@ +# Changelog — v0.x + +## [0.1.0] — 2026/02/22 + +Initial release. + +### Added + +- Connection: `mysql_connect`, `mysql_close`, `mysql_status`. +- Options: `mysql_options_new`, `mysql_options_set_int`, `mysql_options_set_str`. +- Errors: `mysql_errno`. +- Unix socket support (auto-detected when `host` starts with `/`). +- TLS via rustls — no external runtime dependencies. +- Logger with banner; detailed log file at `logs/mysql.log`. diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..4006389 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +mysql-samp.nullsablex.com \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..f1d3367 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,202 @@ +# API reference + +Full table of every native and forward exposed by `mysql_samp`. The plugin registers **55 natives** and **1 forward**. + +Source of truth: [`include/mysql_samp.inc.in`](https://github.com/NullSablex/mysql_samp/blob/master/include/mysql_samp.inc.in) and [`src/lib.rs`](https://github.com/NullSablex/mysql_samp/blob/master/src/lib.rs). + +## Constants + +```pawn +#define MYSQL_SAMP_VERSION "1.1.0" +``` + +The literal above is regenerated by `build.rs` from `CARGO_PKG_VERSION` on every build, so it always tracks the actual plugin version. + +### Connection options + +| Constant | Value | +|---|---| +| `MYSQL_OPT_PORT` | 0 | +| `MYSQL_OPT_SSL` | 1 | +| `MYSQL_OPT_SSL_CA` | 2 | +| `MYSQL_OPT_CONNECT_TIMEOUT` | 3 | +| `MYSQL_OPT_AUTO_RECONNECT` | 4 | + +### Plugin error codes + +| Constant | Value | +|---|---| +| `MYSQL_OK` | 0 | +| `MYSQL_ERROR_CONNECTION_FAILED` | 1 | +| `MYSQL_ERROR_INVALID_OPTIONS` | 2 | +| `MYSQL_ERROR_INVALID_CONNECTION` | 3 | +| `MYSQL_ERROR_PING_FAILED` | 4 | +| `MYSQL_ERROR_QUERY_FAILED` | 5 | +| `MYSQL_ERROR_NO_CACHE_ACTIVE` | 6 | +| `MYSQL_ERROR_INVALID_ORM` | 7 | +| `MYSQL_ERROR_ORM_KEY_NOT_SET` | 8 | + +### Log levels + +| Constant | Value | +|---|---| +| `MYSQL_LOG_NONE` | 0 | +| `MYSQL_LOG_ERROR` | 1 | +| `MYSQL_LOG_WARNING` | 2 | +| `MYSQL_LOG_INFO` | 3 | +| `MYSQL_LOG_ALL` | 4 | + +### ORM error codes + +| Constant | Value | +|---|---| +| `ORM_OK` | 0 | +| `ORM_NO_DATA` | 1 | + +## Natives + +### Options (3) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_options_new()` | `int` | New options handle (`>= 1`) | +| `mysql_options_set_int(handle, option, value)` | `bool` | `false` if handle invalid, option unknown, or value out of range | +| `mysql_options_set_str(handle, option, const value[])` | `bool` | `false` if handle invalid or option not a string option | + +### Connection (3) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_connect(const host[], const user[], const password[], const database[], options = 0)` | `int` | Connection id (`>= 1`) or `0` on failure | +| `mysql_close(connId)` | `bool` | `false` if `connId` does not exist | +| `mysql_status(connId, dest[], max_len = sizeof(dest))` | `bool` | Writes a subset of `SHOW GLOBAL STATUS` into `dest` | + +### Error (2) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_errno(connId = 0)` | `int` | One of `MYSQL_*` codes. `0` reads global state | +| `mysql_error(connId, dest[], max_len = sizeof(dest))` | `bool` | Always `true` (writes the stored message; empty if no error) | + +### Charset (2) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_set_charset(connId, const charset[])` | `bool` | Runs `SET NAMES ''` | +| `mysql_get_charset(connId, dest[], max_len = sizeof(dest))` | `bool` | Reads `@@character_set_connection` | + +### Utility (2) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_unprocessed_queries()` | `int` | In-flight + pending-ordered count | +| `mysql_log(log_level)` | `bool` | Atomic; always `true` | + +### Queries (5) + +| Native | Returns | Notes | +|---|---|---| +| `mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | FIFO-ordered threaded query | +| `mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | Parallel threaded query | +| `mysql_tick()` | `bool` | Drains the dispatch queue manually (back-compat only) | +| `mysql_escape_string(const src[], dest[], max_len = sizeof(dest))` | `bool` | Pure escape, no connection | +| `mysql_format(connId, dest[], max_len, const format[], {Float,_}:...)` | `int` | Bytes written into `dest` (truncated to `max_len - 1` at a UTF-8 boundary) | + +#### `mysql_format` specifiers + +| Specifier | Type | Behavior | +|---|---|---| +| `%d`, `%i` | int | Decimal | +| `%f` | float | `{:.4}` | +| `%s`, `%e` | string | Escaped | +| `%r` | string | Raw, **not** escaped | +| `%%` | literal | `%` | + +#### Callback format characters + +| Char | Pawn type | +|---|---| +| `d`, `i` | int | +| `f` | float | +| `s` | string | + +Anything else is counted as an unknown specifier and logged. + +### Cache — dimensions and values (12) + +| Native | Returns | Notes | +|---|---|---| +| `cache_get_row_count()` | `int` | `-1` if no active cache | +| `cache_get_field_count()` | `int` | `-1` if no active cache | +| `cache_get_field_name(field_idx, dest[], max_len = sizeof(dest))` | `bool` | `false` if `field_idx` is negative or out of range | +| `cache_get_field_type(field_idx)` | `int` | Raw `mysql` `ColumnType` byte, or `-1` | +| `cache_get_value_index(row, col, dest[], max_len = sizeof(dest))` | `bool` | String cell by index | +| `cache_get_value_index_int(row, col)` | `int` | Parsed int, `0` on miss | +| `cache_get_value_index_float(row, col)` | `Float` | Parsed float, `0.0` on miss | +| `cache_get_value_name(row, const field_name[], dest[], max_len = sizeof(dest))` | `bool` | Column name lookup is case-insensitive | +| `cache_get_value_name_int(row, const field_name[])` | `int` | Parsed int, `0` on miss | +| `cache_get_value_name_float(row, const field_name[])` | `Float` | Parsed float, `0.0` on miss | +| `cache_is_value_index_null(row, col)` | `bool` | `true` for NULL **and** for out-of-range | +| `cache_is_value_name_null(row, const field_name[])` | `bool` | `true` for NULL **and** for unknown column | + +### Cache — write-result and query metadata (5) + +| Native | Returns | Notes | +|---|---|---| +| `cache_affected_rows()` | `int` | `-1` if no active cache | +| `cache_insert_id()` | `int` | `-1` if no active cache | +| `cache_warning_count()` | `int` | `-1` if no active cache | +| `cache_get_query_exec_time()` | `int` | Milliseconds, `-1` if no active cache | +| `cache_get_query_string(dest[], max_len = sizeof(dest))` | `bool` | The exact SQL sent to the server | + +### Cache — persistence (6) + +| Native | Returns | Notes | +|---|---|---| +| `cache_save()` | `int` | Saved id (`>= 1`) or `0` if no active cache or limit (1024) hit | +| `cache_delete(cache_id)` | `bool` | Also clears the manual override if the deleted id was active | +| `cache_set_active(cache_id)` | `bool` | `false` if `cache_id` does not exist | +| `cache_unset_active()` | `bool` | `false` if there is no manual override | +| `cache_is_any_active()` | `bool` | Stack top **or** manual override | +| `cache_is_valid(cache_id)` | `bool` | Saved entry exists | + +### ORM (15) + +| Native | Returns | Notes | +|---|---|---| +| `orm_create(const table[], connId)` | `int` | ORM id (`>= 1`) or `0` if `connId` invalid | +| `orm_destroy(orm_id)` | `bool` | `false` if `orm_id` does not exist | +| `orm_errno(orm_id)` | `int` | `ORM_OK` or `ORM_NO_DATA`. `-1` if `orm_id` invalid. Only updated by `orm_apply_cache` | +| `orm_select(orm_id, const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | Threaded SELECT | +| `orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | Threaded UPDATE | +| `orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | Threaded INSERT | +| `orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | Threaded DELETE | +| `orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...)` | `bool` | INSERT if key is empty (`0`, `0.0`, `""`), UPDATE otherwise | +| `orm_apply_cache(orm_id, row = 0)` | `bool` | Sets `orm_errno` to `ORM_NO_DATA` if `row` is invalid | +| `orm_addvar_int(orm_id, &var, const column_name[])` | `bool` | Stores the AMX address of `var` | +| `orm_addvar_float(orm_id, &Float:var, const column_name[])` | `bool` | Stores the AMX address of `var` | +| `orm_addvar_string(orm_id, var[], var_max_len, const column_name[])` | `bool` | `1 <= var_max_len <= 4096` | +| `orm_delvar(orm_id, const column_name[])` | `bool` | `false` if no binding matched | +| `orm_clear_vars(orm_id)` | `bool` | Always `true` if `orm_id` exists | +| `orm_setkey(orm_id, const column_name[])` | `bool` | Always `true` if `orm_id` exists | + +## Forwards (1) + +| Forward | Fired when | +|---|---| +| `OnQueryError(errorid, const error[], const callback[], const query[], connId)` | A threaded query (`mysql_query` / `mysql_pquery` and the ORM CRUD natives that wrap them) failed | + +## Counts + +| Category | Count | +|---|---| +| Options | 3 | +| Connection | 3 | +| Error | 2 | +| Charset | 2 | +| Utility | 2 | +| Queries | 5 | +| Cache | 23 (12 + 5 + 6) | +| ORM | 15 | +| **Total natives** | **55** | +| Forwards | 1 | diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index c220fe4..0000000 --- a/docs/api.md +++ /dev/null @@ -1,158 +0,0 @@ -# Referencia da API - -Tabela completa de todas as natives e forwards do mysql_samp. - -## Conexao - -| Native | Retorno | Descricao | -|---|---|---| -| `mysql_connect(host[], user[], pass[], db[], options = 0)` | int | Conecta ao MySQL. Retorna connId ou 0 | -| `mysql_close(connId)` | bool | Fecha conexao | -| `mysql_status(connId, dest[], max_len)` | bool | Metricas do servidor | -| `mysql_set_charset(connId, charset[])` | bool | Define charset (`SET NAMES`) | -| `mysql_get_charset(connId, dest[], max_len)` | bool | Obtem charset atual | - -## Options - -| Native | Retorno | Descricao | -|---|---|---| -| `mysql_options_new()` | int | Cria handle de options | -| `mysql_options_set_int(handle, option, value)` | bool | Define opcao numerica | -| `mysql_options_set_str(handle, option, value[])` | bool | Define opcao string | - -### Opcoes disponiveis - -| Constante | Tipo | Padrao | Descricao | -|---|---|---|---| -| `MYSQL_OPT_PORT` | int | 3306 | Porta TCP | -| `MYSQL_OPT_SSL` | int | false | Ativar TLS | -| `MYSQL_OPT_SSL_CA` | string | — | Certificado CA | -| `MYSQL_OPT_CONNECT_TIMEOUT` | int | — | Timeout (segundos) | - -## Erro - -| Native | Retorno | Descricao | -|---|---|---| -| `mysql_errno(connId = 0)` | int | Codigo do ultimo erro | -| `mysql_error(connId, dest[], max_len)` | bool | Mensagem do ultimo erro | - -## Queries - -| Native | Retorno | Descricao | -|---|---|---| -| `mysql_query(connId, query[], callback[], format[], ...)` | bool | Query FIFO com callback | -| `mysql_pquery(connId, query[], callback[], format[], ...)` | bool | Query paralela | -| `mysql_escape_string(src[], dest[], max_len)` | bool | Escape de string SQL | -| `mysql_format(connId, dest[], max_len, format[], ...)` | int | Formatacao printf-like | - -### Especificadores de mysql_format - -| Spec | Tipo | Descricao | -|---|---|---| -| `%d` / `%i` | int | Inteiro | -| `%f` | float | Decimal (4 casas) | -| `%s` / `%e` | string | String com escape automatico | -| `%r` | string | String raw (sem escape) | -| `%%` | — | Literal `%` | - -## Cache — Leitura - -| Native | Retorno | Descricao | -|---|---|---| -| `cache_get_row_count()` | int | Numero de linhas (-1 se sem cache) | -| `cache_get_field_count()` | int | Numero de colunas (-1 se sem cache) | -| `cache_get_field_name(idx, dest[], max_len)` | bool | Nome da coluna | -| `cache_get_field_type(idx)` | int | Tipo MySQL da coluna | -| `cache_get_value_index(row, col, dest[], max_len)` | bool | Valor string por indice | -| `cache_get_value_index_int(row, col)` | int | Valor int por indice | -| `cache_get_value_index_float(row, col)` | float | Valor float por indice | -| `cache_get_value_name(row, name[], dest[], max_len)` | bool | Valor string por nome | -| `cache_get_value_name_int(row, name[])` | int | Valor int por nome | -| `cache_get_value_name_float(row, name[])` | float | Valor float por nome | -| `cache_is_value_index_null(row, col)` | bool | NULL por indice | -| `cache_is_value_name_null(row, name[])` | bool | NULL por nome | - -## Cache — Metadados - -| Native | Retorno | Descricao | -|---|---|---| -| `cache_affected_rows()` | int | Linhas afetadas | -| `cache_insert_id()` | int | Last insert ID | -| `cache_warning_count()` | int | Warnings do MySQL | -| `cache_get_query_exec_time()` | int | Tempo de execucao (ms) | -| `cache_get_query_string(dest[], max_len)` | bool | Query original | - -## Cache — Persistencia - -| Native | Retorno | Descricao | -|---|---|---| -| `cache_save()` | int | Salva cache, retorna ID | -| `cache_delete(cache_id)` | bool | Remove cache salvo | -| `cache_set_active(cache_id)` | bool | Ativa cache salvo | -| `cache_unset_active()` | bool | Desativa cache manual | -| `cache_is_any_active()` | bool | Algum cache esta ativo? | -| `cache_is_valid(cache_id)` | bool | Cache salvo existe? | - -## ORM - -| Native | Retorno | Descricao | -|---|---|---| -| `orm_create(table[], connId)` | int | Cria instancia ORM | -| `orm_destroy(orm_id)` | bool | Destroi instancia | -| `orm_errno(orm_id)` | int | Ultimo erro ORM | -| `orm_select(orm_id, callback[], format[], ...)` | bool | SELECT non-blocking | -| `orm_update(orm_id, callback[], format[], ...)` | bool | UPDATE non-blocking | -| `orm_insert(orm_id, callback[], format[], ...)` | bool | INSERT non-blocking | -| `orm_delete(orm_id, callback[], format[], ...)` | bool | DELETE non-blocking | -| `orm_save(orm_id, callback[], format[], ...)` | bool | INSERT ou UPDATE auto | -| `orm_apply_cache(orm_id, row = 0)` | bool | Aplica cache nas vars | -| `orm_addvar_int(orm_id, &var, column[])` | bool | Bind variavel int | -| `orm_addvar_float(orm_id, &Float:var, column[])` | bool | Bind variavel float | -| `orm_addvar_string(orm_id, var[], max_len, column[])` | bool | Bind variavel string | -| `orm_delvar(orm_id, column[])` | bool | Remove binding | -| `orm_clear_vars(orm_id)` | bool | Remove todos os bindings | -| `orm_setkey(orm_id, column[])` | bool | Define chave primaria | - -### Codigos ORM - -| Constante | Valor | Descricao | -|---|---|---| -| `ORM_OK` | 0 | Sem erro | -| `ORM_NO_DATA` | 1 | SELECT sem resultados | - -## Utilidades - -| Native | Retorno | Descricao | -|---|---|---| -| `mysql_unprocessed_queries()` | int | Queries pendentes | -| `mysql_log(log_level)` | bool | Configura nivel de log | - -### Niveis de log - -| Constante | Valor | Descricao | -|---|---|---| -| `MYSQL_LOG_NONE` | 0 | Desativa logs | -| `MYSQL_LOG_ERROR` | 1 | Apenas erros | -| `MYSQL_LOG_WARNING` | 2 | Erros + warnings | -| `MYSQL_LOG_INFO` | 3 | + info | -| `MYSQL_LOG_ALL` | 4 | Tudo (padrao) | - -## Forwards - -| Forward | Descricao | -|---|---| -| `OnQueryError(errorid, error[], callback[], query[], connId)` | Query threaded falhou | - -## Contagem total - -| Categoria | Quantidade | -|---|---| -| Conexao | 5 | -| Options | 3 | -| Erro | 2 | -| Queries | 4 | -| Cache | 19 | -| ORM | 15 | -| Utilidades | 2 | -| Forwards | 1 | -| **Total** | **51** | diff --git a/docs/benchmark.md b/docs/benchmark.md index 0b964e1..f0cd8c9 100644 --- a/docs/benchmark.md +++ b/docs/benchmark.md @@ -1,197 +1,187 @@ -# Benchmark: mysql_samp vs MySQL R41-4 +# Benchmark — mysql_samp vs MySQL R41-4 -Comparação técnica detalhada entre o **mysql_samp** (Rust, v0.2.0) e o **MySQL R41-4** (C++, pBlueG/maddinat0r). +A technical comparison between **mysql_samp** (Rust) and **MySQL R41-4** (C++, BlueG / maddinat0r). Numbers below are honest — they include the case where R41-4 wins. ---- +## How to run the benchmarks -## Como executar os benchmarks +Ready-made files live under `benchmark/`: -Os arquivos prontos estão em `benchmark/`: - -| Arquivo | Descrição | +| File | What it does | |---|---| -| `benchmark/setup.sql` | Cria as tabelas e insere 100 registros de teste | -| `benchmark/bench_mysql_samp.pwn` | Gamemode de benchmark para o mysql_samp | -| `benchmark/bench_r41.pwn` | Gamemode de benchmark para o R41-4 | +| `benchmark/setup.sql` | Creates the tables and inserts 100 test rows | +| `benchmark/bench_mysql_samp.pwn` | Benchmark gamemode for mysql_samp | +| `benchmark/bench_r41.pwn` | Benchmark gamemode for R41-4 | + +### Steps -### Passo a passo +**1. Prepare the database:** -**1. Configure o banco de dados:** ```bash -mysql -u root -p nome_do_banco < benchmark/setup.sql +mysql -u root -p your_database < benchmark/setup.sql ``` -**2. Edite as credenciais** em `bench_mysql_samp.pwn` e `bench_r41.pwn`: +**2. Set the credentials** in `bench_mysql_samp.pwn` and `bench_r41.pwn`: + ```pawn #define DB_HOST "127.0.0.1" #define DB_USER "root" -#define DB_PASS "senha" +#define DB_PASS "password" #define DB_NAME "benchmark" ``` -**3. Compile e execute cada GM** separadamente no seu servidor, com o plugin correspondente carregado. O resultado aparece no console. +**3. Compile and run each gamemode** separately on your server with the corresponding plugin loaded. The result prints to the console. -**4. Compare os números** das 4 etapas entre os dois plugins. +**4. Compare the four-stage output** between the two plugins. -### O que é medido +### What is measured -| Etapa | Tipo | Rounds | +| Stage | Type | Rounds | |---|---|---| -| 1 | SELECT sequencial (FIFO) | 500 queries | -| 2 | SELECT paralelo | 500 queries | -| 3 | INSERT paralelo | 200 queries | -| 4 | `mysql_format` com escape (puro CPU) | 50.000 iterações | - ---- +| 1 | SELECT, sequential (FIFO) | 500 queries | +| 2 | SELECT, parallel | 500 queries | +| 3 | INSERT, parallel | 200 queries | +| 4 | `mysql_format` with escape (pure CPU) | 50 000 iterations | -## Resultados reais (MySQL local, mesma máquina) +## Results — local MySQL, same machine -Ambiente: SA-MP 0.3.7-R2, MySQL local (loopback — `127.0.0.1`), Linux x86. Ambos os plugins testados na mesma máquina que hospeda o MySQL, sem latência de rede. +Environment: SA-MP 0.3.7-R2, MySQL local (loopback — `127.0.0.1`), Linux x86. Both plugins tested on the machine that hosts MySQL, no network latency. -> **Nota sobre conexão remota:** com MySQL em servidor separado (RTT ≥ 1ms), cada query levaria mais de um tick para completar. Isso eliminaria a vantagem do R41-4 nos SELECTs, pois seu despacho via `process_tick` fica limitado a uma query por tick. Os resultados abaixo representam o **melhor cenário possível para o R41-4**. +> **Note on remote MySQL.** With MySQL on a separate host (RTT ≥ 1 ms), each query takes longer than a server tick. That removes the R41-4 advantage on SELECTs because its `process_tick`-driven dispatch is then bottlenecked to one query per tick. The results below represent the **best-case scenario for R41-4**. ### R41-4 -| Etapa | Total | Média | Throughput | +| Stage | Total | Average | Throughput | |---|---|---|---| -| SELECT FIFO — `mysql_tquery` (500x)¹ | 93 ms | 0,186 ms/q | 5.376 q/s | -| SELECT paralelo — `mysql_pquery` (500x) | 51 ms | 0,101 ms/q | 9.804 q/s | -| INSERT paralelo — `mysql_pquery` (200x) | 607 ms | 3,035 ms/q | 329 q/s | -| `mysql_format` com escape (50.000x) | 64 ms | 0,0012 ms | 781.250/s | +| SELECT FIFO — `mysql_tquery` (500x)¹ | 93 ms | 0.186 ms/query | 5 376 q/s | +| SELECT parallel — `mysql_pquery` (500x) | 51 ms | 0.101 ms/query | 9 804 q/s | +| INSERT parallel — `mysql_pquery` (200x) | 607 ms | 3.035 ms/query | 329 q/s | +| `mysql_format` with escape (50 000x) | 64 ms | 0.0012 ms | 781 250 ops/s | ### mysql_samp -| Etapa | Total | Média | Throughput | +| Stage | Total | Average | Throughput | |---|---|---|---| -| SELECT FIFO — `mysql_query` (500x)¹ | 155 ms | 0,310 ms/q | 3.226 q/s | -| SELECT paralelo — `mysql_pquery` (500x) | 94 ms | 0,187 ms/q | 5.319 q/s | -| INSERT paralelo — `mysql_pquery` (200x) | 45 ms | 0,224 ms/q | 4.444 q/s | -| `mysql_format` com escape (50.000x) | 135 ms | 0,0027 ms | 370.370/s | +| SELECT FIFO — `mysql_query` (500x)¹ | 155 ms | 0.310 ms/query | 3 226 q/s | +| SELECT parallel — `mysql_pquery` (500x) | 94 ms | 0.187 ms/query | 5 319 q/s | +| INSERT parallel — `mysql_pquery` (200x) | 45 ms | 0.224 ms/query | 4 444 q/s | +| `mysql_format` with escape (50 000x) | 135 ms | 0.0027 ms | 370 370 ops/s | -### Comparação direta +### Head to head -| Etapa | R41-4 | mysql_samp | Vencedor | +| Stage | R41-4 | mysql_samp | Winner | |---|---|---|---| -| SELECT FIFO (500x)¹ | **0,186 ms/q — 5.376 q/s** | 0,310 ms/q — 3.226 q/s | R41-4 1,7x | -| SELECT paralelo (500x) | **0,101 ms/q — 9.804 q/s** | 0,187 ms/q — 5.319 q/s | R41-4 1,85x | -| INSERT paralelo (200x) | 3,035 ms/q — 329 q/s | **0,224 ms/q — 4.444 q/s** | **mysql_samp 13,5x** | -| mysql_format (50.000x) | **0,0012 ms — 781k/s** | 0,0027 ms — 370k/s | R41-4 2,1x | +| SELECT FIFO (500x)¹ | **0.186 ms/q — 5 376 q/s** | 0.310 ms/q — 3 226 q/s | R41-4, 1.7× | +| SELECT parallel (500x) | **0.101 ms/q — 9 804 q/s** | 0.187 ms/q — 5 319 q/s | R41-4, 1.85× | +| INSERT parallel (200x) | 3.035 ms/q — 329 q/s | **0.224 ms/q — 4 444 q/s** | **mysql_samp, 13.5×** | +| `mysql_format` (50 000x) | **0.0012 ms — 781k/s** | 0.0027 ms — 370k/s | R41-4, 2.1× | -¹ A Etapa 1 não é uma comparação direta — ver nota abaixo. +¹ Stage 1 is not an apples-to-apples comparison. See the note below. -### Nota sobre a Etapa 1: não é apples-to-apples +### Note on Stage 1: not apples-to-apples -O R41-4 não tem equivalente direto ao `mysql_query` do mysql_samp. O mais próximo é `mysql_tquery`, que foi o usado. A diferença é arquitetural: +R41-4 has no direct equivalent of mysql_samp's `mysql_query`. The closest one is `mysql_tquery`, which is what the benchmark uses. The difference is architectural: -| Plugin | Native FIFO | Comportamento | +| Plugin | FIFO native | Behavior | |---|---|---| -| R41-4 | `mysql_tquery` | Pool de threads; callbacks despachados via `process_tick` — vários por tick quando o MySQL responde rápido (loopback) | -| mysql_samp | `mysql_query` | Uma thread por query; resultados reordenados via canal `mpsc`, callbacks entregues em ordem sem depender do tick | +| R41-4 | `mysql_tquery` | Thread pool; callbacks dispatched via `process_tick` — several per tick when MySQL responds fast enough (loopback) | +| mysql_samp | `mysql_query` | One thread per query; results reordered through an `mpsc` channel, callbacks delivered in submission order regardless of tick | -Em semântica do gamemode os dois são equivalentes (N queries, callbacks em ordem de submissão). O throughput do R41-4 na Etapa 1 depende da velocidade do MySQL: em loopback vence; em MySQL remoto perde, pois o despacho via tick passa a ser o gargalo. +The gamemode-visible semantics are equivalent (N queries, callbacks in submission order). R41-4's Stage 1 throughput depends on MySQL latency: in loopback it wins; with remote MySQL it loses, because the per-tick dispatch becomes the bottleneck. ---- +## Executive summary -## Resumo executivo - -| Critério | mysql_samp | R41-4 | +| Criterion | mysql_samp | R41-4 | |---|---|---| -| SELECT FIFO (500x)¹ | 0,310 ms/q — 3.226 q/s | **0,186 ms/q — 5.376 q/s** | -| SELECT paralelo (500x) | 0,187 ms/q — 5.319 q/s | **0,101 ms/q — 9.804 q/s** | -| INSERT paralelo (200x) | **0,224 ms/q — 4.444 q/s** | 3,035 ms/q — 329 q/s | -| mysql_format (50.000x) | 0,0027 ms — 370k/s | **0,0012 ms — 781k/s** | -| Segurança de memória | **Zero falhas garantidas** pelo compilador | Segfaults documentados (issues #291, #310+) | -| SQL injection via `%s` | **Impossível** (`%s` escapa por padrão) | Possível (`%s` é raw no R41-4) | -| Vazamento de memória | **Impossível** (cache gerenciado automaticamente) | Possível sem `cache_delete()` | -| Dependências runtime | **Nenhuma** | MySQL C Connector + Boost | -| Issues em aberto | Novo (em desenvolvimento ativo) | **50+ issues abertos** no GitHub | -| Query síncrona bloqueante | **Removida** (nunca bloqueia o server tick) | Existe (`mysql_query` bloqueia) | - ---- +| SELECT FIFO (500x)¹ | 0.310 ms/q — 3 226 q/s | **0.186 ms/q — 5 376 q/s** | +| SELECT parallel (500x) | 0.187 ms/q — 5 319 q/s | **0.101 ms/q — 9 804 q/s** | +| INSERT parallel (200x) | **0.224 ms/q — 4 444 q/s** | 3.035 ms/q — 329 q/s | +| `mysql_format` (50 000x) | 0.0027 ms — 370k/s | **0.0012 ms — 781k/s** | +| Memory safety | **Compiler-enforced** | Documented segfaults (issues #291, #310+) | +| SQL injection via `%s` | **Impossible** (`%s` escapes by default) | Possible (`%s` is raw in R41-4) | +| Memory leak | **Impossible** (cache managed automatically) | Possible without `cache_delete()` | +| Runtime dependencies | **None** | MySQL C Connector + Boost | +| Open issues | New (active development) | **50+ open issues** on GitHub | +| Synchronous blocking query | **Removed** (never blocks the server tick) | Exists (`mysql_query` blocks) | -## 1. Segurança de memória +## 1. Memory safety -### R41-4 (C++ manual) +### R41-4 (C++, manual memory) -O R41-4 usa C++ com ponteiros brutos e gestão manual de memória. Bugs documentados publicamente: +R41-4 is C++ with raw pointers and manual memory management. Bugs publicly documented: -- **Segmentation fault (SIGSEGV)**: múltiplos relatos de crash no shutdown do servidor enquanto queries estão em execução (issues #291, #310, #311) -- **"FREE RESULT MISSING"**: erro documentado indicando resultado não liberado corretamente (issue #291) -- **Crash ao destruir plugin com queries pendentes**: race condition entre o destrutor do plugin e threads ativas +- **Segmentation fault (SIGSEGV):** multiple reports of crashes during server shutdown while queries are in flight (issues #291, #310, #311) +- **"FREE RESULT MISSING":** documented error indicating a result not freed correctly (issue #291) +- **Crash when unloading the plugin with pending queries:** race between the plugin destructor and live threads -O próprio repositório do R41-4 avisa: +The repository itself warns: > "Use `cache_delete()` if you don't need the query's result anymore or you will experience **memory leaks**." ### mysql_samp (Rust) -O Rust garante em **tempo de compilação**: +Rust guarantees at **compile time**: -- **Zero buffer overflows** — verificação de bounds em todos os acessos a arrays -- **Zero use-after-free** — o borrow checker impede acesso a memória liberada -- **Zero data races** — `Send` e `Sync` garantem que apenas um thread acessa dados mutuamente exclusivos -- **Cache auto-gerenciado** — sem necessidade de `cache_delete()` manual; o Rust libera automaticamente +- **No buffer overflows** — every array access is bounds-checked. +- **No use-after-free** — the borrow checker prevents access to freed memory. +- **No data races** — `Send` and `Sync` prove only one thread accesses mutually-exclusive data. +- **Cache auto-managed** — no manual `cache_delete()`; Rust drops the entry when its scope ends. ```rust -// Impossível em Rust — o compilador rejeita em tempo de build: +// Impossible in Rust — the compiler rejects this at build time: let cache = get_cache(); drop(cache); -use_cache(cache); // ERRO: value moved here — não chega a virar binário +use_cache(cache); // ERROR: value moved here — never produces a binary ``` -**Resultado prático:** o mysql_samp nunca vai derrubar seu servidor por falha de memória. O R41-4 pode. - ---- +**Practical effect:** mysql_samp cannot crash the server because of a memory bug. R41-4 can. -## 2. Segurança SQL: injeção via `%s` +## 2. SQL safety: injection via `%s` ### R41-4 -No R41-4, o especificador `%s` insere a string **sem escape**. Código de um gamemode real: +In R41-4, `%s` inserts the string **without escaping**. Code from a real gamemode: ```pawn -// R41-4 — VULNERÁVEL a SQL injection +// R41-4 — VULNERABLE to SQL injection new query[256]; -mysql_format(gMysql, query, sizeof(query), +mysql_format(g_mysql, query, sizeof(query), "SELECT * FROM accounts WHERE name = '%s'", inputName); -// Se inputName = "' OR 1=1 -- " -// Query gerada: SELECT * FROM accounts WHERE name = '' OR 1=1 -- ' -// Resultado: retorna TODAS as contas +// If inputName = "' OR 1=1 -- " +// Generated query: SELECT * FROM accounts WHERE name = '' OR 1=1 -- ' +// Result: returns EVERY account. ``` -Para escapar no R41-4 era necessário usar `%e` explicitamente — algo que a maioria dos gamemodes nunca fez. +To escape in R41-4 you had to remember to use `%e` — something most gamemodes never did. ### mysql_samp ```pawn -// mysql_samp — SEGURO por padrão +// mysql_samp — SAFE by default new query[256]; -mysql_format(gMysql, query, sizeof(query), +mysql_format(g_mysql, query, sizeof(query), "SELECT * FROM accounts WHERE name = '%s'", inputName); -// Se inputName = "' OR 1=1 -- " -// Query gerada: SELECT * FROM accounts WHERE name = '\' OR 1=1 -- ' -// Resultado: nenhuma linha (string escapada corretamente) +// If inputName = "' OR 1=1 -- " +// Generated query: SELECT * FROM accounts WHERE name = '\' OR 1=1 -- ' +// Result: no rows (string escaped correctly). ``` -`%s` sempre escapa. Para inserir valores raw confiáveis (nomes de tabela, SQL dinâmico), use `%r` explicitamente. +`%s` always escapes. To insert trusted raw values (table names, fixed SQL fragments) use `%r` explicitly. -**Resultado prático:** gamemodes migrados do R41-4 ficam automaticamente protegidos. Gamemodes novos não precisam pensar em escaping. +**Practical effect:** gamemodes migrated from R41-4 become safe automatically. New gamemodes do not need to think about escaping. ---- - -## 3. Dependências de runtime +## 3. Runtime dependencies ### R41-4 -Para funcionar, o servidor precisa ter instalado: +The server has to provide: -| Biblioteca | Versão | Observação | +| Library | Version | Notes | |---|---|---| -| `libmysqlclient` | 5.5+ / 6.1 | Sistema ou bundled | -| `Boost` | 1.57+ | Compilado no plugin | -| `libz.so.1` | Sistema | Reportado em issue #292 | +| `libmysqlclient` | 5.5+ / 6.1 | System or bundled | +| `Boost` | 1.57+ | Compiled into the plugin | +| `libz.so.1` | system | Reported in issue #292 | -Erros típicos de configuração: +Typical configuration errors: ``` error while loading shared libraries: libmysqlclient.so.18: cannot open shared object file @@ -200,156 +190,145 @@ libz.so.1: cannot open shared object file: No such file or directory ### mysql_samp -**Zero dependências externas.** O binário é completamente autocontido: - -- TLS/SSL via **rustls** (puro Rust, embutido no binário) -- Driver MySQL via crate `mysql` com feature `default-rust` (sem libmysqlclient) -- Sem Boost, sem OpenSSL, sem libz +**No external runtime dependencies.** The binary is fully self-contained: -Basta copiar o `.so` ou `.dll` para a pasta `plugins/`. Funciona imediatamente em qualquer distribuição Linux. +- TLS/SSL via **rustls** (pure Rust, embedded in the binary). Note: the `MYSQL_OPT_SSL` / `MYSQL_OPT_SSL_CA` options exist but are not yet wired through to the connection layer — see [Options → SSL caveat](options.md#ssl). +- MySQL driver via the `mysql` crate with the `default-rust` feature (no `libmysqlclient`). +- No Boost, no OpenSSL, no libz. ---- +Drop the `.so` or `.dll` into `plugins/` and it works on any Linux distribution. -## 4. Modelo de threading +## 4. Threading model ### R41-4 ``` -GameMode ──► mysql_query() ──► SÍNCRONO — bloqueia o server tick até query retornar - ──► mysql_tquery() ──► 1 worker thread por conexão (FIFO) - ──► mysql_pquery() ──► pool de conexões paralelas (sem ordem) +GameMode ──► mysql_query() ──► SYNCHRONOUS — blocks the server tick until the query returns + ──► mysql_tquery() ──► 1 worker thread per connection (FIFO) + ──► mysql_pquery() ──► pool of parallel connections (no order) -Fila: Boost lockfree::spsc_queue (capacidade < 65.536 entradas) -Sincronização: std::mutex em cada chamada MySQL C API +Queue: Boost lockfree::spsc_queue (capacity < 65 536 entries) +Sync: std::mutex around every MySQL C API call ``` -**Problema:** `mysql_query()` síncrono bloqueia o server tick. Se a query demorar 100 ms, o servidor fica frozen por 100 ms — nenhum jogador recebe pacotes nesse intervalo. +**Problem:** synchronous `mysql_query()` blocks the server tick. A 100 ms query freezes the server for 100 ms — no player receives packets during that window. ### mysql_samp ``` -GameMode ──► mysql_query() ──► fila FIFO via mpsc channel (NUNCA bloqueia) - ──► mysql_pquery() ──► pool paralelo via mpsc channel +GameMode ──► mysql_query() ──► FIFO queue over an mpsc channel (NEVER blocks) + ──► mysql_pquery() ──► parallel pool over an mpsc channel -Sincronização: garantida pelo sistema de tipos do Rust (sem mutexes manuais) +Sync: enforced by Rust's type system (no manual mutex) ``` -**Não existe query síncrona.** O mysql_samp eliminou `mysql_query()` bloqueante por design — impossível travar o servidor por acidente. - ---- +**There is no synchronous query.** mysql_samp removed the blocking `mysql_query` by design — you cannot freeze the server by mistake. -## 5. Issues críticos do R41-4 sem correção +## 5. Critical R41-4 issues with no upstream fix -Baseado nos issues públicos de https://github.com/pBlueG/SA-MP-MySQL: +Based on public issues at https://github.com/pBlueG/SA-MP-MySQL: -| Issue | Descrição | Status | +| Issue | Description | Status | |---|---|---| -| #291 | "FREE RESULT MISSING" — resultado não liberado corretamente | Aberto | -| #288 | SSL não funciona no Linux | Aberto | -| #277 | `mysql_tquery` não executa UPDATE em certas condições | Aberto | -| #292 | `libz.so.1: cannot open shared object file` | Aberto | -| Múltiplos | Segmentation fault (SIGSEGV) no shutdown com queries pendentes | Abertos | -| Múltiplos | Incompatibilidade com sampgdk e outros plugins | Abertos | +| #291 | "FREE RESULT MISSING" — result not freed correctly | Open | +| #288 | SSL does not work on Linux | Open | +| #277 | `mysql_tquery` skips UPDATE under certain conditions | Open | +| #292 | `libz.so.1: cannot open shared object file` | Open | +| Multiple | Segmentation fault (SIGSEGV) on shutdown with pending queries | Open | +| Multiple | Incompatibility with sampgdk and other plugins | Open | -O mysql_samp não apresenta nenhum desses problemas por design: +mysql_samp does not have any of these by design: -- **SIGSEGV no shutdown**: impossível em Rust — o borrow checker garante que threads não acessam dados já liberados -- **SSL no Linux**: rustls não depende de libssl do sistema — funciona em qualquer distribuição -- **libz / dependências ausentes**: não existe — sem dependências externas -- **FREE RESULT MISSING**: impossível — a memória é liberada automaticamente pelo Rust +- **SIGSEGV on shutdown:** impossible in Rust — the borrow checker prevents access to freed data after threads end. +- **SSL on Linux:** rustls does not depend on the system `libssl` — it works on any distribution. (The plugin needs to wire the options through; see [Options → SSL caveat](options.md#ssl).) +- **Missing `libz` / dependencies:** none exist — no external dependencies. +- **FREE RESULT MISSING:** impossible — memory is freed automatically by Rust. ---- +## 6. API ergonomics -## 6. API: ergonomia e modernidade - -### Valores de retorno direto vs by-ref +### Return value vs by-ref ```pawn -// R41-4 — by-ref (verboso) +// R41-4 — by-ref (verbose) new rows; cache_get_row_count(rows); new score; cache_get_value_name_int(0, "score", score); -// mysql_samp — retorno direto (limpo) +// mysql_samp — return value (clean) new rows = cache_get_row_count(); new score = cache_get_value_name_int(0, "score"); ``` -### Gestão de cache +### Cache management ```pawn -// R41-4 — DEVE chamar cache_delete ou há vazamento de memória +// R41-4 — MUST call cache_delete or leak public OnPlayerData(playerid) { - // ... lê dados ... - cache_delete(cache_save()); // obrigatório + // ... read data ... + cache_delete(cache_save()); // required } -// mysql_samp — sem cache_delete necessário -// O cache ativo é liberado automaticamente após o callback +// mysql_samp — no cache_delete needed +// The active cache is released automatically after the callback returns. public OnPlayerData(playerid) { - // ... lê dados ... - // nada a fazer + // ... read data ... + // nothing to do } ``` -### Tags Pawn +### Pawn tags ```pawn -// R41-4 — tags customizadas (podem causar warnings) -new MySQL:gMysql = mysql_connect(...); -new Cache:cache = cache_save(); -new ORM:orm = orm_create("table", gMysql); - -// mysql_samp — sem tags -new gMysql = mysql_connect(...); -new cache = cache_save(); -new orm = orm_create("table", gMysql); +// R41-4 — custom tags (can trigger warnings) +new MySQL:g_mysql = mysql_connect(...); +new Cache:cache = cache_save(); +new ORM:orm = orm_create("table", g_mysql); + +// mysql_samp — no tags +new g_mysql = mysql_connect(...); +new cache = cache_save(); +new orm = orm_create("table", g_mysql); ``` ---- - -## 7. Comparação completa de features +## 7. Full feature matrix | Feature | mysql_samp | R41-4 | |---|---|---| -| Queries FIFO (threaded) | `mysql_query` | `mysql_tquery` | -| Queries paralelas | `mysql_pquery` | `mysql_pquery` | -| Queries síncronas bloqueantes | **Removida por segurança** | `mysql_query` (legado) | -| ORM | Sim (`orm_*`) | Sim (`orm_*`) | -| Cache salvo | `cache_save()` / `cache_set_active()` | Igual | -| `mysql_format %s` | Escapa automaticamente | Raw (inseguro) | -| `mysql_format %e` | Alias de `%s` (escape) | Igual ao mysql_samp | -| `mysql_format %r` | Raw (sem escape) | **Não existe** | -| Tags Pawn | **Sem tags** | `MySQL:`, `Cache:`, `ORM:` | -| Dependências runtime | **Nenhuma** | libssl + libmysqlclient + Boost | -| open.mp | Compatível | Compatível | -| SSL/TLS | rustls (embutido) | Via MySQL C Connector | -| Multi-result sets | Não suportado | `cache_set_result()` | -| Segfault possível | **Não** | **Sim** (documentado) | -| Vazamento de cache | **Impossível** | Possível sem `cache_delete()` | -| Issues conhecidos críticos | Nenhum em produção | 50+ abertos no GitHub | - ---- +| Threaded FIFO query | `mysql_query` | `mysql_tquery` | +| Parallel query | `mysql_pquery` | `mysql_pquery` | +| Blocking sync query | **Removed for safety** | `mysql_query` (legacy) | +| ORM | Yes (`orm_*`) | Yes (`orm_*`) | +| Saved cache | `cache_save()` / `cache_set_active()` | Same | +| `mysql_format %s` | Escapes automatically | Raw (unsafe) | +| `mysql_format %e` | Alias for `%s` (escape) | Same as mysql_samp | +| `mysql_format %r` | Raw (no escape) | **Does not exist** | +| Pawn tags | **No tags** | `MySQL:`, `Cache:`, `ORM:` | +| Runtime dependencies | **None** | libssl + libmysqlclient + Boost | +| Open Multiplayer | Native + legacy | Compatible (legacy) | +| SSL/TLS | rustls (binary) — options not yet wired through | Via MySQL C Connector | +| Multi-result sets | Not supported | `cache_set_result()` | +| Possible segfault | **No** | **Yes** (documented) | +| Possible cache leak | **No** | Yes without `cache_delete()` | +| Critical known issues | None in production | 50+ open on GitHub | -## 8. Conclusão +## 8. Conclusion -Os resultados medidos em loopback (melhor cenário para o R41-4) mostram um quadro honesto: +The loopback numbers (best case for R41-4) show an honest picture: -**O R41-4 tem throughput de SELECT mais alto em loopback** — 1,7–1,85x em SELECT FIFO e paralelo, e 2,1x em `mysql_format`. Isso acontece porque, com MySQL respondendo em < 0,1ms (abaixo de um tick), o R41-4 consegue despachar múltiplos callbacks por tick e aproveita totalmente o cache do banco. +**R41-4 has higher SELECT throughput in loopback** — 1.7–1.85× on FIFO and parallel SELECT, and 2.1× on `mysql_format`. With MySQL answering in < 0.1 ms (below one tick), R41-4 dispatches several callbacks per tick and exploits the server cache fully. -**O mysql_samp tem 13,5x mais throughput em INSERT** — escritas invalidam cache e forçam I/O real, tornando o gargalo de callback-via-tick do R41-4 irrelevante e expondo a diferença arquitetural. Gamemodes reais fazem muito mais escrita (save de jogador, logs, eventos) do que leitura repetida da mesma row. +**mysql_samp has 13.5× the INSERT throughput** — writes invalidate cache and force real I/O, making R41-4's per-tick callback bottleneck irrelevant and exposing the architectural difference. Real gamemodes write much more often (player save, logs, events) than they re-read the same row. -**O loopback é o melhor cenário possível para o R41-4.** Com MySQL remoto (RTT ≥ 1ms, o que inclui a maioria dos ambientes de produção), cada query passa a durar mais que um tick e a vantagem de SELECT do R41-4 desaparece — o mysql_samp passa a vencer em todas as etapas. +**Loopback is the best case for R41-4.** With remote MySQL (RTT ≥ 1 ms, the typical production setup), every query lasts longer than a tick and R41-4's SELECT advantage disappears — mysql_samp wins every stage. -**As vantagens do mysql_samp são estruturais, independentes de benchmark:** +**mysql_samp's advantages are structural, independent of any benchmark:** -1. **Segurança de memória garantida pelo compilador** — sem possibilidade de segfault, use-after-free ou data race -2. **Segurança SQL por padrão** — `%s` escapa sem que o desenvolvedor precise lembrar -3. **Zero dependências** — funciona em qualquer servidor sem instalar libmysqlclient, Boost ou libssl -4. **Sem query síncrona bloqueante** — o R41-4 tem `mysql_query` que congela o server tick; o mysql_samp não +1. **Compiler-enforced memory safety** — no segfaults, no use-after-free, no data races. +2. **Safe SQL by default** — `%s` escapes without the developer having to remember. +3. **Zero dependencies** — works on any server without installing `libmysqlclient`, Boost or `libssl`. +4. **No blocking sync query** — R41-4 has `mysql_query` that freezes the tick; mysql_samp does not. -Para novos projetos, o mysql_samp é a escolha tecnicamente superior. -Para projetos existentes que precisam de compatibilidade com código legado do R41-4, consulte [migracao.md](migracao.md). +For new projects, mysql_samp is the technically superior choice. For existing projects that need compatibility with legacy R41-4 code, see [migration.md](migration.md). diff --git a/docs/cache.md b/docs/cache.md index 4f701c5..5b23a70 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -1,40 +1,46 @@ # Cache -O sistema de cache armazena os resultados das queries. Quando um callback de query e chamado, o cache da query fica automaticamente disponivel para leitura. +The cache holds the result of a single query so that the `cache_*` natives can read it. There are two ways a cache becomes "active": -## Ciclo de vida do cache - -1. Uma query e executada na thread -2. O resultado e armazenado em um `CacheEntry` -3. No `process_tick`, o cache e empilhado (**push**) -4. O callback e chamado — dentro dele, as funcoes `cache_*` acessam o resultado -5. Apos o callback retornar, o cache e desempilhado (**pop**) +1. **Automatic stack.** Before a query callback fires, the plugin pushes the result onto an internal stack. When the callback returns, the entry is popped. Inside the callback, every `cache_*` native reads from the top of the stack. +2. **Manual activation.** `cache_save()` clones the active entry into a persistent slot identified by an id. Later, `cache_set_active(id)` makes the saved entry override the stack top until `cache_unset_active()` is called. ``` -mysql_query → [thread] → resultado → push cache → callback() → pop cache +mysql_query → worker thread → result → push → callback() → pop ``` -> Fora de um callback de query, as funcoes `cache_*` retornam valores de erro (-1, false, 0.0) a menos que voce use `cache_set_active` com um cache salvo. +Outside a callback (and without `cache_set_active`), the cache natives return their "no active cache" sentinels: + +| Native shape | Sentinel | +|---|---| +| `cache_get_*` returning `int` | `-1` | +| `cache_get_*_int` | `0` | +| `cache_get_*_float` | `0.0` | +| `cache_get_*` writing to `dest` | `false`, `dest` untouched | +| `cache_is_value_*_null` | `true` (treat absence as NULL) | -## Leitura de resultados +## Reading rows -### Dimensoes +### Dimensions ```pawn -native cache_get_row_count(); // Numero de linhas (-1 se sem cache) -native cache_get_field_count(); // Numero de colunas (-1 se sem cache) +native cache_get_row_count(); +native cache_get_field_count(); ``` +Both return `-1` when no cache is active. + ```pawn forward OnQueryDone(); -public OnQueryDone() { - new rows = cache_get_row_count(); +public OnQueryDone() +{ + new rows = cache_get_row_count(); new fields = cache_get_field_count(); - printf("Resultado: %d rows x %d fields", rows, fields); + printf("result: %d rows x %d fields", rows, fields); } ``` -### Valor por indice (row, col) +### By column index ```pawn native bool:cache_get_value_index(row, col, dest[], max_len = sizeof(dest)); @@ -42,20 +48,24 @@ native cache_get_value_index_int(row, col); native Float:cache_get_value_index_float(row, col); ``` +`row` and `col` are zero-based and must be non-negative. Negative values are rejected without touching `dest`. Out-of-range or NULL cells return `false` / `0` / `0.0`. + ```pawn forward OnResult(); -public OnResult() { +public OnResult() +{ new rows = cache_get_row_count(); - for (new i = 0; i < rows; i++) { + for (new i = 0; i < rows; i++) + { new id = cache_get_value_index_int(i, 0); new name[64]; cache_get_value_index(i, 1, name); - printf("ID: %d, Nome: %s", id, name); + printf("id=%d name=%s", id, name); } } ``` -### Valor por nome de coluna (row, field_name) +### By column name ```pawn native bool:cache_get_value_name(row, const field_name[], dest[], max_len = sizeof(dest)); @@ -63,169 +73,155 @@ native cache_get_value_name_int(row, const field_name[]); native Float:cache_get_value_name_float(row, const field_name[]); ``` +The column lookup is **case-insensitive**: `"name"`, `"Name"` and `"NAME"` all match the same column. + ```pawn forward OnPlayerData(); -public OnPlayerData() { - if (cache_get_row_count() > 0) { - new name[MAX_PLAYER_NAME]; - cache_get_value_name(0, "username", name); +public OnPlayerData() +{ + if (cache_get_row_count() <= 0) return; - new level = cache_get_value_name_int(0, "level"); - new Float:score = cache_get_value_name_float(0, "score"); + new name[MAX_PLAYER_NAME]; + cache_get_value_name(0, "username", name); - printf("%s - Level %d - Score %.2f", name, level, score); - } + new level = cache_get_value_name_int(0, "level"); + new Float:score = cache_get_value_name_float(0, "score"); + + printf("%s — level %d — score %.2f", name, level, score); } ``` -> A busca por nome e case-insensitive: `"Username"`, `"username"` e `"USERNAME"` funcionam igualmente. - -### Verificacao de NULL +### NULL checks ```pawn native bool:cache_is_value_index_null(row, col); native bool:cache_is_value_name_null(row, const field_name[]); ``` +Returns `true` when the cell is SQL `NULL`, **also** when the row/column is out of bounds or no cache is active. The "no cache" → `true` mapping means downstream code that just wants to skip empty cells does not need an extra guard, but if you need to distinguish "missing row" from "NULL cell" check `cache_get_row_count()` first. + ```pawn -if (!cache_is_value_name_null(0, "email")) { +if (!cache_is_value_name_null(0, "email")) +{ new email[128]; cache_get_value_name(0, "email", email); - printf("Email: %s", email); -} else { - printf("Email nao definido"); + printf("email: %s", email); } ``` -## Metadados +## Metadata -### Colunas +### Columns ```pawn native bool:cache_get_field_name(field_idx, dest[], max_len = sizeof(dest)); -native cache_get_field_type(field_idx); // Tipo MySQL (ver mysql_com.h) -native cache_get_field_count(); +native cache_get_field_type(field_idx); ``` +`cache_get_field_type` returns the raw MySQL column-type byte (`mysql::consts::ColumnType` as a `u8`). For example: `3` = `MYSQL_TYPE_LONG`, `253` = `MYSQL_TYPE_VAR_STRING`. Returns `-1` for out-of-range indices. + ```pawn new fields = cache_get_field_count(); -for (new i = 0; i < fields; i++) { +for (new i = 0; i < fields; i++) +{ new name[64]; cache_get_field_name(i, name); - printf("Coluna %d: %s (tipo %d)", i, name, cache_get_field_type(i)); + printf("col %d: %s (type=%d)", i, name, cache_get_field_type(i)); } ``` -### Resultados de escrita (INSERT/UPDATE/DELETE) +### Write-result metadata ```pawn -native cache_affected_rows(); // Linhas afetadas -native cache_insert_id(); // Last insert ID (auto_increment) -native cache_warning_count(); // Warnings do MySQL +native cache_affected_rows(); // -1 if no cache, otherwise rows affected +native cache_insert_id(); // -1 if no cache, otherwise last AUTO_INCREMENT +native cache_warning_count(); // -1 if no cache, otherwise warnings from the server ``` ```pawn forward OnPlayerInserted(); -public OnPlayerInserted() { - new id = cache_insert_id(); - printf("Novo jogador inserido com ID: %d", id); +public OnPlayerInserted() +{ + printf("new player id: %d", cache_insert_id()); } forward OnPlayersDeleted(); -public OnPlayersDeleted() { - printf("Jogadores removidos: %d", cache_affected_rows()); +public OnPlayersDeleted() +{ + printf("rows deleted: %d", cache_affected_rows()); } ``` -### Debug +### Query metadata ```pawn -native cache_get_query_exec_time(); // Tempo de execucao em ms -native bool:cache_get_query_string(dest[], max_len = sizeof(dest)); +native cache_get_query_exec_time(); // milliseconds, -1 if no cache +native bool:cache_get_query_string(dest[], max_len = sizeof(dest)); // the SQL text actually executed ``` +The exec time is computed by the worker thread, so it includes the round-trip to the MySQL server but not the dispatch delay back to the callback. + ```pawn -printf("Query executada em %d ms", cache_get_query_exec_time()); +printf("query took %d ms", cache_get_query_exec_time()); new query[512]; cache_get_query_string(query); -printf("Query: %s", query); +printf("query: %s", query); ``` -## Cache persistente (save/restore) - -Por padrao, o cache e destruido apos o callback. Use `cache_save` para preservar o resultado. +## Persistent caches -### cache_save - -```pawn -native cache_save(); // Retorna cache_id (>= 1) ou 0 se falhar -``` - -### cache_delete +By default the cache is dropped when the callback returns. To keep a result around, save it: ```pawn +native cache_save(); // returns the saved id (>= 1) or 0 on failure native bool:cache_delete(cache_id); -``` - -### cache_set_active / cache_unset_active - -Ativa manualmente um cache salvo. Enquanto ativo, todas as funcoes `cache_*` leem deste cache. - -```pawn native bool:cache_set_active(cache_id); native bool:cache_unset_active(); +native bool:cache_is_any_active(); +native bool:cache_is_valid(cache_id); ``` -### Exemplo completo +`cache_save()` returns `0` when no cache is active or when the saved-cache limit (1024) is reached. ```pawn -new gSavedCache; +new g_saved; forward OnDataLoaded(); -public OnDataLoaded() { - // Salva o cache para uso posterior - gSavedCache = cache_save(); - printf("Cache salvo com ID: %d", gSavedCache); +public OnDataLoaded() +{ + g_saved = cache_save(); + printf("cache saved id=%d", g_saved); } -// Em outro momento... -stock UseSavedData() { - if (!cache_is_valid(gSavedCache)) { - printf("Cache expirado!"); +stock UseSavedData() +{ + if (!cache_is_valid(g_saved)) + { + printf("cache expired"); return; } - cache_set_active(gSavedCache); - + cache_set_active(g_saved); new rows = cache_get_row_count(); - printf("Dados salvos: %d rows", rows); - - // ... ler dados ... - + printf("saved cache has %d rows", rows); cache_unset_active(); } -// Quando nao precisar mais -stock CleanupCache() { - cache_delete(gSavedCache); - gSavedCache = 0; +stock DropSavedData() +{ + cache_delete(g_saved); + g_saved = 0; } ``` -### Verificacao de estado +`cache_delete` also clears the manual override if the deleted id is currently active. `cache_unset_active` returns `false` if there is no manual override to clear (the stack-top behavior continues normally). -```pawn -native bool:cache_is_any_active(); // Algum cache esta ativo? -native bool:cache_is_valid(cache_id); // O cache salvo ainda existe? -``` +## Limits -## Limites - -| Limite | Valor | -|---|---| -| Caches salvos simultaneos | 1024 | -| Rows por resultado | 100.000 | +| Resource | Limit | What happens when hit | +|---|---|---| +| Saved caches | 1 024 | `cache_save()` returns `0`, warning logged | +| Rows per query result | 100 000 | Extra rows are drained from the protocol but discarded; one warning is logged | -Se o limite de caches salvos for atingido, `cache_save` retorna `0` e um warning e logado. -Resultados com mais de 100.000 rows sao truncados automaticamente com warning. +The 100k-row limit prevents a single runaway `SELECT *` from blowing the server's memory. diff --git a/docs/conexao.md b/docs/conexao.md deleted file mode 100644 index 82e5568..0000000 --- a/docs/conexao.md +++ /dev/null @@ -1,184 +0,0 @@ -# Conexao - -## mysql_connect - -Estabelece uma conexao com o servidor MySQL. - -```pawn -native mysql_connect(const host[], const user[], const password[], const database[], options = 0); -``` - -| Parametro | Tipo | Descricao | -|---|---|---| -| `host` | string | IP, hostname ou caminho do socket Unix | -| `user` | string | Usuario do banco | -| `password` | string | Senha do banco | -| `database` | string | Nome do banco de dados | -| `options` | int | Handle de options (opcional, 0 = padrao) | - -**Retorno:** ID da conexao (>= 1) em caso de sucesso, ou `0` em caso de falha. - -### Conexao basica (porta 3306) - -```pawn -new gMysql; - -public OnGameModeInit() { - gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp_db"); - - if (mysql_errno()) { - printf("Falha ao conectar: erro %d", mysql_errno()); - return 1; - } - - printf("Conectado! ID: %d", gMysql); - return 1; -} -``` - -### Conexao via Unix socket - -Se o `host` comecar com `/`, a conexao e feita via socket Unix: - -```pawn -gMysql = mysql_connect("/var/run/mysqld/mysqld.sock", "root", "", "samp_db"); -``` - -### Multiplas conexoes - -Voce pode manter varias conexoes simultaneas: - -```pawn -new gMain, gLogs; - -public OnGameModeInit() { - gMain = mysql_connect("127.0.0.1", "root", "senha", "samp_main"); - gLogs = mysql_connect("127.0.0.1", "root", "senha", "samp_logs"); - return 1; -} -``` - -## mysql_close - -Fecha uma conexao existente e libera os recursos do pool. - -```pawn -native bool:mysql_close(connId); -``` - -**Retorno:** `true` se a conexao foi fechada, `false` se nao encontrada. - -```pawn -public OnGameModeExit() { - mysql_close(gMysql); - return 1; -} -``` - -## mysql_status - -Retorna metricas de status do servidor MySQL. - -```pawn -native bool:mysql_status(connId, dest[], max_len = sizeof(dest)); -``` - -**Retorno:** `true` se obteve o status, `false` em caso de falha. - -O resultado e uma string com metricas como `Uptime`, `Threads_connected`, `Questions`, `Slow_queries`, `Opens`, `Flush_tables`, `Open_tables`, `Queries_per_second_avg`. - -```pawn -new status[256]; -mysql_status(gMysql, status); -printf("Status: %s", status); -// Output: Uptime: 12345 Threads_connected: 3 Questions: 567 ... -``` - -## Options - -Options sao **totalmente opcionais**. Use apenas quando precisar de configuracoes diferentes do padrao. - -### mysql_options_new - -Cria um handle de options com valores padrao. - -```pawn -native mysql_options_new(); -``` - -**Retorno:** handle de options (>= 1). - -### mysql_options_set_int - -Define uma opcao numerica. - -```pawn -native bool:mysql_options_set_int(handle, option, value); -``` - -### mysql_options_set_str - -Define uma opcao de texto. - -```pawn -native bool:mysql_options_set_str(handle, option, const value[]); -``` - -### Opcoes disponiveis - -| Opcao | Tipo | Padrao | Descricao | -|---|---|---|---| -| `MYSQL_OPT_PORT` | int | 3306 | Porta TCP | -| `MYSQL_OPT_SSL` | int | false | Ativar TLS (via rustls) | -| `MYSQL_OPT_SSL_CA` | string | — | Caminho do certificado CA | -| `MYSQL_OPT_CONNECT_TIMEOUT` | int | — | Timeout de conexao em segundos | - -### Exemplo com options - -```pawn -new opts = mysql_options_new(); -mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); -mysql_options_set_int(opts, MYSQL_OPT_SSL, true); -mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 10); - -gMysql = mysql_connect("db.example.com", "user", "pass", "samp_db", opts); -``` - -## Charset - -O plugin forca `utf8mb4` como charset padrao em todas as conexoes. Voce pode alterar se necessario. - -### mysql_set_charset - -```pawn -native bool:mysql_set_charset(connId, const charset[]); -``` - -Executa `SET NAMES` com o charset especificado. - -```pawn -mysql_set_charset(gMysql, "latin1"); -``` - -### mysql_get_charset - -```pawn -native bool:mysql_get_charset(connId, dest[], max_len = sizeof(dest)); -``` - -Retorna o charset atual da conexao. - -```pawn -new charset[32]; -mysql_get_charset(gMysql, charset); -printf("Charset: %s", charset); // utf8mb4 -``` - -## Pool de conexoes - -Internamente, cada `mysql_connect` cria um `mysql::Pool`. Isso significa que: - -- Conexoes sao reutilizadas automaticamente entre queries -- Threads de query obtem conexoes do pool sem conflito -- O pool e thread-safe (Send+Sync+Clone) -- Nao e necessario gerenciar conexoes manualmente diff --git a/docs/connection.md b/docs/connection.md new file mode 100644 index 0000000..bb1dfd5 --- /dev/null +++ b/docs/connection.md @@ -0,0 +1,158 @@ +# Connection + +Each `mysql_connect` call creates a [`mysql::Pool`](https://docs.rs/mysql/latest/mysql/struct.Pool.html) that worker threads share. The plugin always issues `SET NAMES utf8mb4` on every new connection, so all string round-trips are UTF-8 safe by default. + +## mysql_connect + +```pawn +native mysql_connect(const host[], const user[], const password[], const database[], options = 0); +``` + +| Parameter | Type | Description | +|---|---|---| +| `host` | string | IPv4/hostname **or** absolute path to a Unix socket (path starts with `/`) | +| `user` | string | MySQL user | +| `password` | string | MySQL password (may be empty) | +| `database` | string | Default schema for the connection | +| `options` | int | Options handle from `mysql_options_new`. `0` means "use defaults" | + +**Returns:** the connection id (`>= 1`) on success, `0` on failure. On failure the global error state is populated and `mysql_errno(0)` / `mysql_error(0, …)` describe the cause. + +### TCP example + +```pawn +new g_mysql; + +public OnGameModeInit() +{ + g_mysql = mysql_connect("127.0.0.1", "root", "password", "samp_db"); + if (g_mysql == 0) + { + new err[256]; + mysql_error(0, err); + printf("[MySQL] connect failed: %s", err); + return 1; + } + printf("[MySQL] connected (id=%d)", g_mysql); + return 1; +} +``` + +### Unix socket + +If `host` starts with `/`, the plugin connects via the local socket and the configured port is ignored. + +```pawn +g_mysql = mysql_connect("/var/run/mysqld/mysqld.sock", "root", "", "samp_db"); +``` + +### Multiple connections + +A gamemode can hold any number of connections in parallel; each one has its own pool and its own error slot. + +```pawn +new g_main, g_logs; + +public OnGameModeInit() +{ + g_main = mysql_connect("127.0.0.1", "root", "pass", "samp_main"); + g_logs = mysql_connect("127.0.0.1", "root", "pass", "samp_logs"); + return 1; +} +``` + +### Customizing the connection + +To use a non-default port, TLS or timeout, create an options handle first. See [Options](options.md). + +```pawn +new opts = mysql_options_new(); +mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); +mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 10); + +g_mysql = mysql_connect("db.example.com", "user", "pass", "samp_db", opts); +``` + +## mysql_close + +```pawn +native bool:mysql_close(connId); +``` + +Removes the connection entry, dropping the underlying `Pool`. Returns `true` if the connection existed, `false` otherwise. Any in-flight queries continue running to completion on their worker threads, but their results will be discarded when the dispatcher cannot find the pool anymore. + +```pawn +public OnGameModeExit() +{ + mysql_close(g_mysql); + return 1; +} +``` + +## mysql_status + +```pawn +native bool:mysql_status(connId, dest[], max_len = sizeof(dest)); +``` + +Runs `SHOW GLOBAL STATUS` and formats a fixed subset of metrics into `dest`, separated by two spaces. Returns `true` on success, `false` if the query failed or the connection id is invalid. + +Reported metrics: `Uptime`, `Threads_connected`, `Questions`, `Slow_queries`, `Opens`, `Flush_tables`, `Open_tables`, `Queries_per_second_avg`. + +```pawn +new status[256]; +if (mysql_status(g_mysql, status)) +{ + printf("Status: %s", status); + // Status: Uptime: 12345 Threads_connected: 3 Questions: 567 ... +} +``` + +## Charset + +The plugin forces `utf8mb4` on every fresh connection via the pool's `init` block. You can switch to another charset at runtime, but consider the security note in [Security](security.md#multi-byte-charsets) before doing so. + +### mysql_set_charset + +```pawn +native bool:mysql_set_charset(connId, const charset[]); +``` + +Runs `SET NAMES ''` on the next available pool connection. Returns `true` if the statement succeeded, `false` if the connection is invalid or the server rejected the charset. + +```pawn +mysql_set_charset(g_mysql, "utf8mb4"); +``` + +### mysql_get_charset + +```pawn +native bool:mysql_get_charset(connId, dest[], max_len = sizeof(dest)); +``` + +Runs `SELECT @@character_set_connection`. Returns `true` if the query succeeded and `dest` was populated. + +```pawn +new charset[32]; +mysql_get_charset(g_mysql, charset); +printf("charset = %s", charset); // utf8mb4 +``` + +## Connection pool + +`mysql::Pool` is `Clone + Send + Sync`. The plugin gives each worker thread its own `Pool` clone so they fetch connections without explicit locking. Highlights: + +- Connections are created lazily on the first `get_conn()`. +- The plugin does not expose the pool size — the `mysql` crate manages it internally. +- A query that fails with a connection-lost error (the mysql crate maps these to error code `0`) is retried once by `attempt_query` when `MYSQL_OPT_AUTO_RECONNECT` is enabled (the default). + +## What happens on failure + +When `mysql_connect` cannot build the pool or cannot open the validation connection: + +1. A short message goes to the console: `[MySQL] Connection failed (error 1). See logs/mysql.log for details.` +2. The detailed reason (including the raw `mysql` crate error) goes to `logs/mysql.log`. +3. The plugin global error state is set: `mysql_errno(0)` returns `MYSQL_ERROR_CONNECTION_FAILED`, `mysql_error(0, …)` returns the full message. +4. The native returns `0`. + +The Pawn side **must** check `mysql_errno()` after `mysql_connect` — a failed call cannot be detected from the return value alone if the gamemode does not store it. diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..47be800 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,137 @@ +# Error handling + +The plugin reports failures through three channels: + +1. **Plugin-level errno** — `mysql_errno(connId)` / `mysql_error(connId, dest)`. Each connection has its own error slot; `connId == 0` reads the global state (set by connect failures, ORM build failures, etc.). +2. **Threaded-query forward** — `OnQueryError` is fired on every loaded AMX when a `mysql_query` / `mysql_pquery` fails. +3. **Console + `logs/mysql.log`** — short message in the console, full details in the file. + +## mysql_errno + +```pawn +native mysql_errno(connId = 0); +``` + +Returns one of the plugin error codes below. `connId == 0` returns the **global error**, useful for `mysql_connect` failures. + +| Constant | Value | Set when | +|---|---|---| +| `MYSQL_OK` | 0 | No error since the last reset | +| `MYSQL_ERROR_CONNECTION_FAILED` | 1 | `mysql_connect` failed (global) | +| `MYSQL_ERROR_INVALID_OPTIONS` | 2 | `mysql_connect` received an unknown options handle (global) | +| `MYSQL_ERROR_INVALID_CONNECTION` | 3 | `mysql_query` / `mysql_pquery` / `orm_create` received an unknown `connId` (global) | +| `MYSQL_ERROR_PING_FAILED` | 4 | `mysql_status` could not fetch server stats (per-connection) | +| `MYSQL_ERROR_QUERY_FAILED` | 5 | A threaded query failed (per-connection); the detailed message is the MySQL server text | +| `MYSQL_ERROR_NO_CACHE_ACTIVE` | 6 | `orm_apply_cache` was called without an active cache (global) | +| `MYSQL_ERROR_INVALID_ORM` | 7 | An ORM native received an unknown `orm_id`, or ORM insert/save could not build a query (global) | +| `MYSQL_ERROR_ORM_KEY_NOT_SET` | 8 | ORM select/update/delete could not build a query because the key column is not set or no variables are bound (global) | + +```pawn +g_mysql = mysql_connect("127.0.0.1", "root", "pw", "samp_db"); +if (mysql_errno() != MYSQL_OK) +{ + new msg[256]; + mysql_error(0, msg); + printf("connect failed (%d): %s", mysql_errno(), msg); + return 1; +} +``` + +## mysql_error + +```pawn +native bool:mysql_error(connId, dest[], max_len = sizeof(dest)); +``` + +Writes the text of the last error into `dest`. Returns `true` on success. + +When `connId == 0`, reads the global error message. When `connId` is unknown, also falls back to the global error. + +## OnQueryError + +```pawn +forward OnQueryError(errorid, const error[], const callback[], const query[], connId); +``` + +Fired on every loaded AMX when a threaded query fails. Parameters: + +| Parameter | Type | Description | +|---|---|---| +| `errorid` | int | MySQL server error code (1062, 1045, 1064, …) or `0` for transport-level errors (TCP drop, IO error) | +| `error` | string | Full error text from the `mysql` crate | +| `callback` | string | Name of the public the query asked for (empty if fire-and-forget) | +| `query` | string | Exact SQL that was sent to the server | +| `connId` | int | Id of the connection that produced the error | + +```pawn +public OnQueryError(errorid, const error[], const callback[], const query[], connId) +{ + printf("[mysql %d] %s", errorid, error); + printf(" callback: %s", callback); + printf(" query: %s", query); + printf(" conn: %d", connId); + return 1; +} +``` + +The plugin also sets the per-connection errno to `MYSQL_ERROR_QUERY_FAILED` after firing the forward — so `mysql_errno(connId)` / `mysql_error(connId, ...)` can be read afterwards if you want the message in a different context. + +### Common MySQL error codes + +| Code | Cause | +|---|---| +| 1045 | Access denied (wrong user/password) | +| 1049 | Unknown database | +| 1062 | Duplicate entry (UNIQUE / PRIMARY KEY violation) | +| 1064 | SQL syntax error | +| 1146 | Table does not exist | +| 1451 | Foreign-key constraint blocked the operation | +| 2002 | Could not connect to the server (host unreachable) | +| 2006 | "MySQL server has gone away" | +| 0 | Transport/IO error — the `mysql` crate uses `0` for non-MySQL failures such as TCP drops. When `MYSQL_OPT_AUTO_RECONNECT` is on, the plugin retries the query once before reporting it. | + +## Logs + +### Console + +The console gets only generic messages, never query text or credentials: + +``` +[MySQL] Connection failed (error 1). See logs/mysql.log for details. +[MySQL] Query failed on connection 1 (error 1064). See logs/mysql.log for details. +[MySQL] Query result truncated to 100000 rows. +``` + +### logs/mysql.log + +Detailed entries with timestamp: + +``` +[2026-05-18 14:30:15] [ERROR] Pool creation failed: Access denied for user 'root'@'localhost' +[2026-05-18 14:30:20] [ERROR] Query error: You have an error in your SQL syntax; check the manual ... +[2026-05-18 14:30:30] [WARNING] cache_save failed: maximum saved caches reached (1024). +``` + +If the directory or the file is not writable, the plugin emits **one** console error of the form `[MySQL] Failed to write logs/mysql.log: . Further file-write errors will be suppressed.` and then stops trying. This avoids flooding the console when the disk is full or permissions are wrong. + +### Log level + +`mysql_log` adjusts the minimum severity that the plugin emits: + +```pawn +mysql_log(MYSQL_LOG_NONE); // 0 — nothing +mysql_log(MYSQL_LOG_ERROR); // 1 — errors only +mysql_log(MYSQL_LOG_WARNING); // 2 — errors + warnings +mysql_log(MYSQL_LOG_INFO); // 3 — errors + warnings + info +mysql_log(MYSQL_LOG_ALL); // 4 — everything (default) +``` + +The setting is atomic and takes effect immediately. + +## Practical checklist + +1. **Always check `mysql_errno()` after `mysql_connect`** — a failed connect returns `0`. Without a check, the gamemode happily issues queries against connection `0`, which is invalid. +2. **Implement `OnQueryError`** — a missing implementation is not a compile error, but you will be flying blind when a query fails in production. +3. **Read `logs/mysql.log` for the long error text** — the console does not have the SQL or the original server message. +4. **Use `mysql_format` with `%s`** — it escapes by default and removes a whole class of SQL injection bugs. +5. **Guard cache reads with `cache_get_row_count()`** — avoids spurious `-1` / `0` / empty-string readings when the result is empty. diff --git a/docs/erros.md b/docs/erros.md deleted file mode 100644 index 8bb8693..0000000 --- a/docs/erros.md +++ /dev/null @@ -1,132 +0,0 @@ -# Tratamento de erros - -## mysql_errno - -Retorna o codigo do ultimo erro de uma conexao (ou erro global). - -```pawn -native mysql_errno(connId = 0); -``` - -| Parametro | Descricao | -|---|---| -| `connId` | ID da conexao, ou `0` para o ultimo erro global | - -### Codigos de erro do plugin - -| Constante | Valor | Descricao | -|---|---|---| -| `MYSQL_OK` | 0 | Sem erro | -| `MYSQL_ERROR_CONNECTION_FAILED` | 1 | Falha na conexao | -| `MYSQL_ERROR_INVALID_OPTIONS` | 2 | Handle de options invalido | -| `MYSQL_ERROR_INVALID_CONNECTION` | 3 | Conexao invalida | -| `MYSQL_ERROR_PING_FAILED` | 4 | Ping falhou | -| `MYSQL_ERROR_QUERY_FAILED` | 5 | Falha na query | -| `MYSQL_ERROR_NO_CACHE_ACTIVE` | 6 | Nenhum cache ativo | -| `MYSQL_ERROR_INVALID_ORM` | 7 | ORM invalido | -| `MYSQL_ERROR_ORM_KEY_NOT_SET` | 8 | Chave ORM nao definida | - -```pawn -gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp_db"); - -if (mysql_errno()) { - printf("Erro de conexao: %d", mysql_errno()); - return 1; -} -``` - -## mysql_error - -Retorna a mensagem de erro em texto. - -```pawn -native bool:mysql_error(connId, dest[], max_len = sizeof(dest)); -``` - -```pawn -if (mysql_errno()) { - new msg[256]; - mysql_error(0, msg); - printf("Erro: %s", msg); -} -``` - -## OnQueryError - -Forward chamado automaticamente quando uma query threaded falha. E disparado em **todos** os AMX carregados. - -```pawn -forward OnQueryError(errorid, const error[], const callback[], const query[], connId); -``` - -| Parametro | Tipo | Descricao | -|---|---|---| -| `errorid` | int | Codigo de erro MySQL (1062, 1045, etc.) ou 0 | -| `error` | string | Mensagem de erro completa | -| `callback` | string | Nome do callback que seria chamado | -| `query` | string | Query SQL que falhou | -| `connId` | int | ID da conexao | - -```pawn -public OnQueryError(errorid, const error[], const callback[], const query[], connId) { - printf("[MySQL Error %d] %s", errorid, error); - printf(" Callback: %s", callback); - printf(" Query: %s", query); - printf(" Connection: %d", connId); - return 1; -} -``` - -### Erros comuns do MySQL - -| Codigo | Descricao | -|---|---| -| 1045 | Acesso negado (usuario/senha errados) | -| 1049 | Banco de dados desconhecido | -| 1062 | Entrada duplicada (UNIQUE constraint) | -| 1064 | Erro de sintaxe SQL | -| 1146 | Tabela nao existe | -| 1451 | Foreign key constraint falhou | -| 2002 | Nao foi possivel conectar ao servidor | -| 2006 | MySQL server has gone away | - -## Logs - -### Console - -O console do servidor exibe apenas mensagens genericas com codigos de erro: - -``` -[MySQL] Connection failed (error 1). See logs/mysql.log for details. -[MySQL] Query failed on connection 1 (error 1064). See logs/mysql.log for details. -``` - -> Nunca exibe queries, senhas ou dados sensiveis no console. - -### logs/mysql.log - -O arquivo de log contem detalhes completos com timestamp: - -``` -[2026-02-23 14:30:15] [ERROR] Pool creation failed: Access denied for user 'root'@'localhost' -[2026-02-23 14:30:20] [ERROR] Query error: You have an error in your SQL syntax; ... -[2026-02-23 14:30:30] [WARNING] cache_save failed: maximum saved caches reached (1024). -``` - -### Niveis de log - -```pawn -mysql_log(MYSQL_LOG_NONE); // Desativa tudo -mysql_log(MYSQL_LOG_ERROR); // Apenas erros -mysql_log(MYSQL_LOG_WARNING); // Erros + warnings -mysql_log(MYSQL_LOG_INFO); // Erros + warnings + info -mysql_log(MYSQL_LOG_ALL); // Tudo (padrao) -``` - -## Boas praticas - -1. **Sempre verifique `mysql_errno` apos `mysql_connect`** — uma conexao falhada retorna `0` -2. **Implemente `OnQueryError`** — captura erros de queries que voce nao previa -3. **Consulte `logs/mysql.log` para debug** — os detalhes estao la, nao no console -4. **Use `mysql_format` com `%s`** — previne SQL injection automaticamente -5. **Verifique `cache_get_row_count` antes de ler dados** — evita ler cache vazio diff --git a/docs/exemplos-migracao.md b/docs/exemplos-migracao.md deleted file mode 100644 index dbec858..0000000 --- a/docs/exemplos-migracao.md +++ /dev/null @@ -1,752 +0,0 @@ -# Exemplos de migracao: antes e depois - -Exemplos reais de codigo Pawn mostrando o **antes** (R41-4) e o **depois** (mysql_samp). - ---- - -## 1. Conexao basica - -### R41-4 -```pawn -new MySQL:gMysql; - -public OnGameModeInit() -{ - gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp"); - - if (gMysql == MYSQL_INVALID_HANDLE) - { - printf("Falha na conexao MySQL."); - return 0; - } - return 1; -} -``` - -### mysql_samp -```pawn -new gMysql; - -public OnGameModeInit() -{ - gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp"); - - if (gMysql == 0) - { - printf("Falha na conexao MySQL."); - return 0; - } - return 1; -} -``` - -> **Mudancas:** sem tag `MySQL:`, sem `MYSQL_INVALID_HANDLE` (usar `0`). - ---- - -## 2. Conexao com porta customizada - -### R41-4 -```pawn -new MySQLOpt:opt = mysql_init_options(); -mysql_set_option(opt, SERVER_PORT, 3307); -new MySQL:gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp", opt); -``` - -### mysql_samp -```pawn -new opt = mysql_options_new(); -mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); -new gMysql = mysql_connect("127.0.0.1", "root", "senha", "samp", opt); -``` - -> **Mudancas:** sem tags, `mysql_init_options` → `mysql_options_new`, `mysql_set_option` → `mysql_options_set_int`, `SERVER_PORT` → `MYSQL_OPT_PORT`. - ---- - -## 3. Login — verificar se jogador existe - -### R41-4 -```pawn -public OnPlayerConnect(playerid) -{ - new query[128], name[MAX_PLAYER_NAME]; - GetPlayerName(playerid, name, sizeof(name)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM jogadores WHERE nome = '%e'", name); - mysql_tquery(gMysql, query, "OnCheckAccount", "i", playerid); - return 1; -} - -forward OnCheckAccount(playerid); -public OnCheckAccount(playerid) -{ - if (cache_num_rows() > 0) - { - ShowPlayerDialog(playerid, DIALOG_LOGIN, ...); - } - else - { - ShowPlayerDialog(playerid, DIALOG_REGISTER, ...); - } - return 1; -} -``` - -### mysql_samp -```pawn -public OnPlayerConnect(playerid) -{ - new query[128], name[MAX_PLAYER_NAME]; - GetPlayerName(playerid, name, sizeof(name)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM jogadores WHERE nome = '%s'", name); - mysql_query(gMysql, query, "OnCheckAccount", "d", playerid); - return 1; -} - -forward OnCheckAccount(playerid); -public OnCheckAccount(playerid) -{ - if (cache_get_row_count() > 0) - { - ShowPlayerDialog(playerid, DIALOG_LOGIN, ...); - } - else - { - ShowPlayerDialog(playerid, DIALOG_REGISTER, ...); - } - return 1; -} -``` - -> **Mudancas:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `"i"` → `"d"` (opcional), `cache_num_rows()` → `cache_get_row_count()`. - ---- - -## 4. Login — carregar dados do jogador - -### R41-4 -```pawn -forward OnPlayerLogin(playerid); -public OnPlayerLogin(playerid) -{ - if (cache_num_rows() > 0) - { - cache_get_value_name(0, "hash", Player[playerid][Hash], 65); - - cache_get_value_name_int(0, "score", Player[playerid][Score]); - cache_get_value_name_int(0, "money", Player[playerid][Money]); - cache_get_value_name_float(0, "pos_x", Player[playerid][PosX]); - cache_get_value_name_float(0, "pos_y", Player[playerid][PosY]); - cache_get_value_name_float(0, "pos_z", Player[playerid][PosZ]); - cache_get_value_name_int(0, "skin", Player[playerid][Skin]); - } - return 1; -} -``` - -### mysql_samp -```pawn -forward OnPlayerLogin(playerid); -public OnPlayerLogin(playerid) -{ - if (cache_get_row_count() > 0) - { - cache_get_value_name(0, "hash", Player[playerid][Hash]); - - Player[playerid][Score] = cache_get_value_name_int(0, "score"); - Player[playerid][Money] = cache_get_value_name_int(0, "money"); - Player[playerid][PosX] = cache_get_value_name_float(0, "pos_x"); - Player[playerid][PosY] = cache_get_value_name_float(0, "pos_y"); - Player[playerid][PosZ] = cache_get_value_name_float(0, "pos_z"); - Player[playerid][Skin] = cache_get_value_name_int(0, "skin"); - } - return 1; -} -``` - -> **Mudancas:** `cache_num_rows()` → `cache_get_row_count()`. `cache_get_value_name_int` mudou de by-ref (3 params) para retorno direto (2 params). `cache_get_value_name_float` idem. `cache_get_value_name` para strings manteve a mesma assinatura (usa `sizeof` automatico). - ---- - -## 5. Registro — inserir jogador - -### R41-4 -```pawn -public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) -{ - if (dialogid == DIALOG_REGISTER && response) - { - new query[256], name[MAX_PLAYER_NAME], hash[65]; - GetPlayerName(playerid, name, sizeof(name)); - SHA256_PassHash(inputtext, SALT, hash, sizeof(hash)); - - mysql_format(gMysql, query, sizeof(query), - "INSERT INTO jogadores (nome, hash) VALUES ('%e', '%e')", - name, hash); - mysql_tquery(gMysql, query, "OnPlayerRegister", "i", playerid); - } - return 1; -} - -forward OnPlayerRegister(playerid); -public OnPlayerRegister(playerid) -{ - Player[playerid][ID] = cache_insert_id(); - SendClientMessage(playerid, -1, "Registrado com sucesso!"); - return 1; -} -``` - -### mysql_samp -```pawn -public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) -{ - if (dialogid == DIALOG_REGISTER && response) - { - new query[256], name[MAX_PLAYER_NAME], hash[65]; - GetPlayerName(playerid, name, sizeof(name)); - SHA256_PassHash(inputtext, SALT, hash, sizeof(hash)); - - mysql_format(gMysql, query, sizeof(query), - "INSERT INTO jogadores (nome, hash) VALUES ('%s', '%s')", - name, hash); - mysql_query(gMysql, query, "OnPlayerRegister", "d", playerid); - } - return 1; -} - -forward OnPlayerRegister(playerid); -public OnPlayerRegister(playerid) -{ - Player[playerid][ID] = cache_insert_id(); - SendClientMessage(playerid, -1, "Registrado com sucesso!"); - return 1; -} -``` - -> **Mudancas:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `"i"` → `"d"` (opcional). - ---- - -## 6. Salvar dados do jogador - -### R41-4 -```pawn -stock SalvarJogador(playerid) -{ - new query[512]; - mysql_format(gMysql, query, sizeof(query), - "UPDATE jogadores SET score = %d, money = %d, pos_x = %f, pos_y = %f, pos_z = %f WHERE id = %d", - GetPlayerScore(playerid), - GetPlayerMoney(playerid), - Player[playerid][PosX], - Player[playerid][PosY], - Player[playerid][PosZ], - Player[playerid][ID]); - mysql_tquery(gMysql, query); - return 1; -} -``` - -### mysql_samp -```pawn -stock SalvarJogador(playerid) -{ - new query[512]; - mysql_format(gMysql, query, sizeof(query), - "UPDATE jogadores SET score = %d, money = %d, pos_x = %f, pos_y = %f, pos_z = %f WHERE id = %d", - GetPlayerScore(playerid), - GetPlayerMoney(playerid), - Player[playerid][PosX], - Player[playerid][PosY], - Player[playerid][PosZ], - Player[playerid][ID]); - mysql_query(gMysql, query); - return 1; -} -``` - -> **Mudanca minima:** apenas `mysql_tquery` → `mysql_query`. Sem callback, sem formato — fire-and-forget. - ---- - -## 7. Sistema VIP — query com callback e parametros - -### R41-4 -```pawn -stock CheckarVIP(playerid) -{ - new query[128], name[MAX_PLAYER_NAME]; - GetPlayerName(playerid, name, sizeof(name)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT vip_level, vip_expira FROM jogadores WHERE nome = '%e'", name); - mysql_tquery(gMysql, query, "OnVIPCheck", "i", playerid); -} - -forward OnVIPCheck(playerid); -public OnVIPCheck(playerid) -{ - if (cache_num_rows() > 0) - { - new level, expira[20]; - cache_get_value_name_int(0, "vip_level", level); - cache_get_value_name(0, "vip_expira", expira, sizeof(expira)); - - Player[playerid][VIPLevel] = level; - } - return 1; -} -``` - -### mysql_samp -```pawn -stock CheckarVIP(playerid) -{ - new query[128], name[MAX_PLAYER_NAME]; - GetPlayerName(playerid, name, sizeof(name)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT vip_level, vip_expira FROM jogadores WHERE nome = '%s'", name); - mysql_query(gMysql, query, "OnVIPCheck", "d", playerid); -} - -forward OnVIPCheck(playerid); -public OnVIPCheck(playerid) -{ - if (cache_get_row_count() > 0) - { - new expira[20]; - Player[playerid][VIPLevel] = cache_get_value_name_int(0, "vip_level"); - cache_get_value_name(0, "vip_expira", expira); - } - return 1; -} -``` - -> **Mudancas:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `cache_num_rows` → `cache_get_row_count`, `cache_get_value_name_int` mudou de by-ref para retorno direto, `cache_get_value_name` sem `sizeof` explicito (usa padrao). - ---- - -## 8. Sistema de ban - -### R41-4 -```pawn -stock BanirJogador(playerid, adminid, const razao[]) -{ - new query[256], nome[MAX_PLAYER_NAME], admin[MAX_PLAYER_NAME]; - GetPlayerName(playerid, nome, sizeof(nome)); - GetPlayerName(adminid, admin, sizeof(admin)); - - mysql_format(gMysql, query, sizeof(query), - "INSERT INTO bans (nome, admin, razao, ip) VALUES ('%e', '%e', '%e', '%e')", - nome, admin, razao, Player[playerid][IP]); - mysql_tquery(gMysql, query); - Kick(playerid); -} - -stock VerificarBan(playerid) -{ - new query[128], nome[MAX_PLAYER_NAME]; - GetPlayerName(playerid, nome, sizeof(nome)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT razao FROM bans WHERE nome = '%e'", nome); - mysql_tquery(gMysql, query, "OnBanCheck", "i", playerid); -} - -forward OnBanCheck(playerid); -public OnBanCheck(playerid) -{ - if (cache_num_rows() > 0) - { - new razao[128]; - cache_get_value_name(0, "razao", razao, sizeof(razao)); - SendClientMessage(playerid, -1, razao); - Kick(playerid); - } - return 1; -} -``` - -### mysql_samp -```pawn -stock BanirJogador(playerid, adminid, const razao[]) -{ - new query[256], nome[MAX_PLAYER_NAME], admin[MAX_PLAYER_NAME]; - GetPlayerName(playerid, nome, sizeof(nome)); - GetPlayerName(adminid, admin, sizeof(admin)); - - mysql_format(gMysql, query, sizeof(query), - "INSERT INTO bans (nome, admin, razao, ip) VALUES ('%s', '%s', '%s', '%s')", - nome, admin, razao, Player[playerid][IP]); - mysql_query(gMysql, query); - Kick(playerid); -} - -stock VerificarBan(playerid) -{ - new query[128], nome[MAX_PLAYER_NAME]; - GetPlayerName(playerid, nome, sizeof(nome)); - - mysql_format(gMysql, query, sizeof(query), - "SELECT razao FROM bans WHERE nome = '%s'", nome); - mysql_query(gMysql, query, "OnBanCheck", "d", playerid); -} - -forward OnBanCheck(playerid); -public OnBanCheck(playerid) -{ - if (cache_get_row_count() > 0) - { - new razao[128]; - cache_get_value_name(0, "razao", razao); - SendClientMessage(playerid, -1, razao); - Kick(playerid); - } - return 1; -} -``` - ---- - -## 9. Carregar veiculos do banco - -### R41-4 -```pawn -stock CarregarVeiculos() -{ - mysql_tquery(gMysql, "SELECT * FROM veiculos", "OnVeiculosCarregados"); -} - -forward OnVeiculosCarregados(); -public OnVeiculosCarregados() -{ - new rows = cache_num_rows(); - - for (new i = 0; i < rows; i++) - { - new modelo, Float:x, Float:y, Float:z, Float:a, cor1, cor2; - - cache_get_value_name_int(i, "modelo", modelo); - cache_get_value_name_float(i, "pos_x", x); - cache_get_value_name_float(i, "pos_y", y); - cache_get_value_name_float(i, "pos_z", z); - cache_get_value_name_float(i, "angulo", a); - cache_get_value_name_int(i, "cor1", cor1); - cache_get_value_name_int(i, "cor2", cor2); - - CreateVehicle(modelo, x, y, z, a, cor1, cor2, -1); - } - printf("Veiculos carregados: %d", rows); - return 1; -} -``` - -### mysql_samp -```pawn -stock CarregarVeiculos() -{ - mysql_query(gMysql, "SELECT * FROM veiculos", "OnVeiculosCarregados"); -} - -forward OnVeiculosCarregados(); -public OnVeiculosCarregados() -{ - new rows = cache_get_row_count(); - - for (new i = 0; i < rows; i++) - { - new modelo = cache_get_value_name_int(i, "modelo"); - new Float:x = cache_get_value_name_float(i, "pos_x"); - new Float:y = cache_get_value_name_float(i, "pos_y"); - new Float:z = cache_get_value_name_float(i, "pos_z"); - new Float:a = cache_get_value_name_float(i, "angulo"); - new cor1 = cache_get_value_name_int(i, "cor1"); - new cor2 = cache_get_value_name_int(i, "cor2"); - - CreateVehicle(modelo, x, y, z, a, cor1, cor2, -1); - } - printf("Veiculos carregados: %d", rows); - return 1; -} -``` - -> **Mudancas:** `mysql_tquery` → `mysql_query`, `cache_num_rows` → `cache_get_row_count`, `cache_get_value_name_int/float` mudou de by-ref (3 params) para retorno direto (2 params). - ---- - -## 10. ORM — sistema de jogador completo - -### R41-4 -```pawn -enum pInfo -{ - ORM:ORM_ID, - ID, - Nome[MAX_PLAYER_NAME], - Hash[65], - Score, - Money, - Float:PosX, - Float:PosY, - Float:PosZ, -}; -new Player[MAX_PLAYERS][pInfo]; - -stock CriarORM(playerid) -{ - new ORM:orm = orm_create("jogadores", gMysql); - Player[playerid][ORM_ID] = orm; - - orm_addvar_int(orm, Player[playerid][ID], "id"); - orm_addvar_string(orm, Player[playerid][Nome], MAX_PLAYER_NAME, "nome"); - orm_addvar_string(orm, Player[playerid][Hash], 65, "hash"); - orm_addvar_int(orm, Player[playerid][Score], "score"); - orm_addvar_int(orm, Player[playerid][Money], "money"); - orm_addvar_float(orm, Player[playerid][PosX], "pos_x"); - orm_addvar_float(orm, Player[playerid][PosY], "pos_y"); - orm_addvar_float(orm, Player[playerid][PosZ], "pos_z"); - - orm_setkey(orm, "id"); - return _:orm; -} - -// Carregar -orm_select(Player[playerid][ORM_ID], "OnPlayerLoad", "i", playerid); - -forward OnPlayerLoad(playerid); -public OnPlayerLoad(playerid) -{ - orm_apply_cache(Player[playerid][ORM_ID], 0); - SetPlayerScore(playerid, Player[playerid][Score]); - GivePlayerMoney(playerid, Player[playerid][Money]); -} - -// Salvar -orm_save(Player[playerid][ORM_ID]); -``` - -### mysql_samp -```pawn -enum pInfo -{ - ORM_ID, - ID, - Nome[MAX_PLAYER_NAME], - Hash[65], - Score, - Money, - Float:PosX, - Float:PosY, - Float:PosZ, -}; -new Player[MAX_PLAYERS][pInfo]; - -stock CriarORM(playerid) -{ - new orm = orm_create("jogadores", gMysql); - Player[playerid][ORM_ID] = orm; - - orm_addvar_int(orm, Player[playerid][ID], "id"); - orm_addvar_string(orm, Player[playerid][Nome], MAX_PLAYER_NAME, "nome"); - orm_addvar_string(orm, Player[playerid][Hash], 65, "hash"); - orm_addvar_int(orm, Player[playerid][Score], "score"); - orm_addvar_int(orm, Player[playerid][Money], "money"); - orm_addvar_float(orm, Player[playerid][PosX], "pos_x"); - orm_addvar_float(orm, Player[playerid][PosY], "pos_y"); - orm_addvar_float(orm, Player[playerid][PosZ], "pos_z"); - - orm_setkey(orm, "id"); - return orm; -} - -// Carregar -orm_select(Player[playerid][ORM_ID], "OnPlayerLoad", "d", playerid); - -forward OnPlayerLoad(playerid); -public OnPlayerLoad(playerid) -{ - orm_apply_cache(Player[playerid][ORM_ID], 0); - SetPlayerScore(playerid, Player[playerid][Score]); - GivePlayerMoney(playerid, Player[playerid][Money]); -} - -// Salvar -orm_save(Player[playerid][ORM_ID]); -``` - -> **Mudancas:** sem tag `ORM:` (usar `int`), `"i"` → `"d"` (opcional). O resto e identico. - ---- - -## 11. Cache salvo — guardar e restaurar - -### R41-4 -```pawn -forward OnQueryA(playerid); -public OnQueryA(playerid) -{ - new Cache:cache = cache_save(); - - mysql_tquery(gMysql, "SELECT ...", "OnQueryB", "ii", playerid, _:cache); - return 1; -} - -forward OnQueryB(playerid, cache_id); -public OnQueryB(playerid, cache_id) -{ - new rows = cache_num_rows(); - // ... - - cache_set_active(Cache:cache_id); - new rowsA = cache_num_rows(); - // ... - cache_unset_active(); - - cache_delete(Cache:cache_id); - return 1; -} -``` - -### mysql_samp -```pawn -forward OnQueryA(playerid); -public OnQueryA(playerid) -{ - new cache = cache_save(); - - mysql_query(gMysql, "SELECT ...", "OnQueryB", "dd", playerid, cache); - return 1; -} - -forward OnQueryB(playerid, cache_id); -public OnQueryB(playerid, cache_id) -{ - new rows = cache_get_row_count(); - // ... - - cache_set_active(cache_id); - new rowsA = cache_get_row_count(); - // ... - cache_unset_active(); - - cache_delete(cache_id); - return 1; -} -``` - -> **Mudancas:** sem `Cache:`, `mysql_tquery` → `mysql_query`, `"ii"` → `"dd"` (opcional — `"ii"` tambem funciona), `cache_num_rows` → `cache_get_row_count`. - ---- - -## 12. Tratamento de erros - -### R41-4 -```pawn -forward OnQueryError(errorid, const error[], const callback[], const query[], MySQL:handle); -public OnQueryError(errorid, const error[], const callback[], const query[], MySQL:handle) -{ - printf("[MySQL] Erro %d na query: %s", errorid, error); - printf("[MySQL] Callback: %s | Query: %s", callback, query); - return 1; -} -``` - -### mysql_samp -```pawn -forward OnQueryError(errorid, const error[], const callback[], const query[], connId); -public OnQueryError(errorid, const error[], const callback[], const query[], connId) -{ - printf("[MySQL] Erro %d na query: %s", errorid, error); - printf("[MySQL] Callback: %s | Query: %s", callback, query); - - // Novidade: consultar erro detalhado da conexao - new errMsg[256]; - mysql_error(connId, errMsg); - printf("[MySQL] Detalhe: %s", errMsg); - return 1; -} -``` - -> **Mudanca:** sem tag `MySQL:` no ultimo parametro. Bonus: `mysql_error(connId, dest)` para detalhes extras. - ---- - -## 13. Escape de strings - -### R41-4 -```pawn -new escaped[128]; -mysql_escape_string(input, escaped, sizeof(escaped), gMysql); -``` - -### mysql_samp -```pawn -new escaped[128]; -mysql_escape_string(input, escaped); -``` - -> **Mudancas:** sem handle MySQL como ultimo parametro. No mysql_samp o escape e sempre UTF-8, nao depende de conexao. - ---- - -## 14. NULL check - -### R41-4 -```pawn -new bool:is_null; -cache_is_value_name_null(0, "email", is_null); -if (is_null) -{ - // campo e NULL -} -``` - -### mysql_samp -```pawn -if (cache_is_value_name_null(0, "email")) -{ - // campo e NULL -} -``` - -> **Mudanca:** retorno direto em vez de by-ref. Mais limpo. - ---- - -## Checklist rapido de busca e substituicao - -Para migrar mecanicamente, busque e substitua no seu gamemode: - -| Buscar | Substituir por | -|---|---| -| `#include ` | `#include ` | -| `mysql_tquery(` | `mysql_query(` | -| `cache_num_rows(` | `cache_get_row_count(` | -| `cache_num_fields(` | `cache_get_field_count(` | -| `mysql_init_options(` | `mysql_options_new(` | -| `mysql_stat(` | `mysql_status(` | -| `new MySQL:` | `new ` | -| `new Cache:` | `new ` | -| `new ORM:` | `new ` | -| `new MySQLOpt:` | `new ` | -| `Cache:` (em chamadas) | *(remover)* | -| `ORM:` (em chamadas) | *(remover)* | -| `MySQL:` (em chamadas) | *(remover)* | -| `MySQLOpt:` (em chamadas) | *(remover)* | -| `MYSQL_INVALID_HANDLE` | `0` | -| `MYSQL_DEFAULT_HANDLE` | *(remover — usar variavel diretamente)* | - -> **Requer revisao manual:** -> - `cache_get_value_*_int` e `_float`: mudar de 3 params (by-ref) para 2 params (retorno direto) -> - `cache_is_value_*_null`: mudar de 3 params (by-ref) para 2 params (retorno direto) -> - `cache_get_row_count` / `cache_get_field_count`: mudar de by-ref para retorno direto -> - `mysql_escape_string`: remover `MySQL:handle` (ultimo param) -> - `mysql_error`: inverter ordem — `mysql_error(connId, dest)` em vez de `mysql_error(dest, max_len, handle)` -> - `mysql_set_option` → `mysql_options_set_int` / `mysql_options_set_str` -> - `%s` → `%r` onde o `%s` antigo era intencional (raw). `%e` → `%s` ou manter `%e` diff --git a/docs/index.md b/docs/index.md index 94c98e4..74de98c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,51 +1,53 @@ -# Documentação — mysql_samp +# mysql_samp -Plugin MySQL para SA:MP e open.mp escrito em Rust. Zero dependências externas, queries non-blocking, cache e ORM integrados. +MySQL plugin for SA-MP and Open Multiplayer, written entirely in Rust. Non-blocking queries with FIFO ordering, a result cache, an ORM, zero external runtime dependencies. ---- +The same `.so` / `.dll` runs on SA-MP and on Open Multiplayer — natively as a component (recommended) or via legacy mode. See [Installation](installation.md) for both registration paths. -## Por onde começar? +## Where to start -| | | +| Goal | Path | |---|---| -| **Novo por aqui** | [Instalação](instalacao.md) → [Conexão](conexao.md) → [Queries](queries.md) | -| **Migrando do R41-4** | [Guia de migração](migracao.md) → [O que mudou](mudancas.md) → [Exemplos](exemplos-migracao.md) | -| **Referência rápida** | [API completa](api.md) | -| **Desempenho** | [Benchmark](benchmark.md) | +| First time here | [Installation](installation.md) → [Connection](connection.md) → [Queries](queries.md) | +| Coming from MySQL R41-4 | [Migration guide](migration.md) → [Migration changes](migration-changes.md) → [Migration examples](migration-examples.md) | +| Quick lookup | [API reference](api-reference.md) | +| Performance numbers | [Benchmark](benchmark.md) | ---- +## Minimal example -## Exemplo mínimo - -Conexão, query assíncrona e leitura do resultado no callback: +Connect, fire a threaded query, read the result inside the callback: ```pawn +#include #include -new MySQL:g_mysql; +new g_mysql; public OnGameModeInit() { - g_mysql = mysql_connect("127.0.0.1", "root", "senha", "banco"); + g_mysql = mysql_connect("127.0.0.1", "root", "password", "samp_db"); + + if (mysql_errno() != MYSQL_OK) + { + printf("[MySQL] connect failed: errno=%d", mysql_errno()); + return 1; + } - // Query FIFO (non-blocking) — executa em thread separada - mysql_query(g_mysql, "SELECT id, nome FROM jogadores LIMIT 5", "OnJogadoresCarregados", ""); + // Non-blocking, FIFO-ordered query. Callback receives playerid via "d" format. + mysql_query(g_mysql, "SELECT id, name FROM players LIMIT 5", "OnPlayersLoaded", "d", 0); return 1; } -forward OnJogadoresCarregados(errorid, error[]); -public OnJogadoresCarregados(errorid, error[]) +forward OnPlayersLoaded(playerid); +public OnPlayersLoaded(playerid) { - if (errorid != 0) { - printf("[MySQL] Erro %d: %s", errorid, error); - return; - } - - while (cache_next_row()) { - new id, nome[MAX_PLAYER_NAME]; - cache_get_value_name_int("id", id); - cache_get_value_name("nome", nome); - printf("Jogador #%d: %s", id, nome); + new rows = cache_get_row_count(); + for (new i = 0; i < rows; i++) + { + new id = cache_get_value_name_int(i, "id"); + new name[MAX_PLAYER_NAME]; + cache_get_value_name(i, "name", name); + printf("Player #%d: %s", id, name); } } @@ -56,15 +58,24 @@ public OnGameModeExit() } ``` ---- - -## Tópicos +## Topics -| Tópico | Descrição | +| Topic | Contents | |---|---| -| [Queries](queries.md) | `mysql_query`, `mysql_pquery`, `mysql_format`, escape de strings | -| [Cache](cache.md) | Leitura de resultados, navegação entre linhas, cache salvo | -| [ORM](orm.md) | Mapeamento de variáveis Pawn para colunas, CRUD automático | -| [Options](options.md) | Configuração de porta, charset, timeout e outros | -| [Segurança](seguranca.md) | Proteção contra SQL injection, limites e boas práticas | -| [Erros](erros.md) | `mysql_errno`, `OnQueryError`, códigos de erro do MySQL | +| [Installation](installation.md) | Download, register on SA-MP and Open Multiplayer, log files | +| [Connection](connection.md) | `mysql_connect`, `mysql_close`, `mysql_status`, charset | +| [Options](options.md) | All `MYSQL_OPT_*` values, defaults, SSL caveat | +| [Queries](queries.md) | `mysql_query`, `mysql_pquery`, `mysql_format`, `mysql_escape_string` | +| [Cache](cache.md) | `cache_*` natives, active stack, persistent caches | +| [ORM](orm.md) | Bind Pawn variables to columns, CRUD without writing SQL | +| [Errors](errors.md) | `mysql_errno`, `mysql_error`, `OnQueryError`, MySQL error codes | +| [Security](security.md) | SQL injection, escaping rules, resource limits | +| [API reference](api-reference.md) | One-line table of every native and forward (55 total) | + +## Plugin facts + +- **rust-samp**: built on top of [rust-samp v3.0.0](https://github.com/NullSablex/rust-samp). +- **MySQL crate**: `mysql` 28.0 with the `default-rust` feature (pure-Rust driver, no `libmysqlclient`). +- **TLS**: rustls is compiled into the binary, but the SSL options (`MYSQL_OPT_SSL`, `MYSQL_OPT_SSL_CA`) are currently **not wired through** to the connection layer — see the caveat in [Options](options.md#ssl). +- **Tick dispatch**: the unified `on_tick` from rust-samp v3 fires on both SA-MP (via `ProcessTick`) and Open Multiplayer native mode (via `ITimersComponent`). No Pawn timer is required. +- **Threading**: each `mysql_query` spawns a worker thread that pulls a connection from a `mysql::Pool`; results travel back over an `mpsc` channel and are dispatched on the next tick. diff --git a/docs/instalacao.md b/docs/instalacao.md deleted file mode 100644 index 995f4d0..0000000 --- a/docs/instalacao.md +++ /dev/null @@ -1,104 +0,0 @@ -# Instalacao e configuracao - -## Requisitos - -- Servidor SA:MP 0.3.7+ ou [open.mp](https://open.mp) -- MySQL/MariaDB 5.7+ (recomendado 8.0+) -- Nenhuma biblioteca externa necessaria — o plugin e auto-contido - -## Download - -Baixe a versao mais recente em [Releases](https://github.com/NullSablex/mysql_samp/releases/latest): - -| Plataforma | Arquivo | -|---|---| -| Linux (32-bit) | `mysql_samp.so` | -| Windows (32-bit) | `mysql_samp.dll` | - -## Instalacao - -1. Copie o binario para o diretorio `plugins/` do seu servidor -2. Copie `mysql_samp.inc` para a pasta de includes do seu compilador: - - **Windows:** `pawno/include/` ou `qawno/include/` - - **Linux:** `include/` (na raiz do servidor) -3. Edite o `server.cfg` (ou `config.json` no open.mp): - -**Linux:** -``` -plugins mysql_samp.so -``` - -**Windows:** -``` -plugins mysql_samp.dll -``` - -> O plugin nao depende de `libmysqlclient`, `libssl` ou qualquer outra biblioteca do sistema. Tudo e compilado estaticamente no binario. - -## Estrutura de arquivos - -**Linux:** -``` -servidor/ -├── gamemodes/ -│ └── seu_gamemode.amx -├── include/ -│ └── mysql_samp.inc -├── plugins/ -│ └── mysql_samp.so -├── logs/ -│ └── mysql.log ← criado automaticamente pelo plugin -└── server.cfg -``` - -**Windows:** -``` -servidor/ -├── gamemodes/ -│ └── seu_gamemode.amx -├── pawno/ -│ └── include/ -│ └── mysql_samp.inc -├── plugins/ -│ └── mysql_samp.dll -├── logs/ -│ └── mysql.log ← criado automaticamente pelo plugin -└── server.cfg -``` - -## Verificando a instalacao - -Ao iniciar o servidor, o banner do plugin aparece no console: - -``` - | mysql_samp 0.2.0 | 2026 - |------------------------------- - | Author and maintainer: NullSablex - | - | Compiled: Feb 23 2026 at 14:30:00 - |------------------------------- - | Repository: https://github.com/NullSablex/mysql_samp -``` - -Se o banner nao aparecer, verifique: -- O arquivo esta na pasta `plugins/` correta -- A plataforma esta correta (32-bit) -- O `server.cfg` tem o nome exato do arquivo - -## Logs - -O plugin cria automaticamente o diretorio `logs/` e o arquivo `logs/mysql.log`. Este arquivo contem detalhes de erros com timestamp, uteis para debug. - -O console do servidor exibe apenas mensagens genericas (sem dados sensiveis como queries ou senhas). - -### Niveis de log - -Voce pode configurar o nivel de log em runtime: - -```pawn -mysql_log(MYSQL_LOG_NONE); // 0 - desativa todos os logs -mysql_log(MYSQL_LOG_ERROR); // 1 - apenas erros -mysql_log(MYSQL_LOG_WARNING); // 2 - erros + warnings -mysql_log(MYSQL_LOG_INFO); // 3 - erros + warnings + info -mysql_log(MYSQL_LOG_ALL); // 4 - tudo (padrao) -``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..0c8a9b3 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,135 @@ +# Installation + +## Requirements + +- SA-MP 0.3.7+ **or** Open Multiplayer +- MySQL 5.7+ / MariaDB 10.3+ (8.0 / 10.6+ recommended) +- 32-bit binary host: both servers load `i686` plugins +- No system libraries — the plugin is statically self-contained + +## Download + +Grab the latest release from [Releases](https://github.com/NullSablex/mysql_samp/releases/latest): + +| File | Platform | +|---|---| +| `mysql_samp.so` | Linux i686 (`i686-unknown-linux-gnu`) | +| `mysql_samp.dll` | Windows i686 (`i686-pc-windows-msvc`) | +| `mysql_samp.inc` | Pawn include — shared between SA-MP and Open Multiplayer | + +The same `.so` / `.dll` runs on SA-MP and on Open Multiplayer. + +## Register the plugin + +### SA-MP + +Drop the binary into `plugins/` and add the file name to `server.cfg`: + +``` +plugins mysql_samp.so +``` + +(`mysql_samp.dll` on Windows.) + +### Open Multiplayer — native mode (recommended) + +Drop the binary into `components/` (open.mp folder) and register it under `components` in `config.json`. The plugin is loaded via `ComponentEntryPoint` and gets access to `ICore`, `ITimersComponent` and the other native APIs. + +### Open Multiplayer — legacy mode + +The same binary still ships the SA-MP plugin ABI (`Supports`, `Load`, `Unload`, `AmxLoad`, `AmxUnload`, `ProcessTick`). Drop it into `plugins/` and register it under `legacy_plugins` in `config.json` if you prefer the SA-MP compatibility path over the native component. No rebuild, no extra flag. + +## Place the include + +| Compiler | Path | +|---|---| +| Pawno (Windows) | `pawno/include/mysql_samp.inc` | +| Qawno (open.mp) | `qawno/include/mysql_samp.inc` | +| Linux | `include/mysql_samp.inc` (at the server root) | + +Then in your gamemode: + +```pawn +#include +``` + +The include exposes the plugin version as a Pawn constant: + +```pawn +printf("mysql_samp version: %s", MYSQL_SAMP_VERSION); +``` + +## Directory layout + +**Linux (SA-MP):** + +``` +server/ +├── gamemodes/ +│ └── your_gm.amx +├── include/ +│ └── mysql_samp.inc +├── plugins/ +│ └── mysql_samp.so +├── logs/ +│ └── mysql.log ← auto-created on first error +└── server.cfg +``` + +**Windows (SA-MP):** + +``` +server/ +├── gamemodes/ +│ └── your_gm.amx +├── pawno/include/ +│ └── mysql_samp.inc +├── plugins/ +│ └── mysql_samp.dll +├── logs/ +│ └── mysql.log ← auto-created on first error +└── server.cfg +``` + +## Verifying the install + +When the server starts, the plugin prints a banner: + +``` + | mysql_samp 1.0.0 | 2026 + |------------------------------- + | Author and maintainer: NullSablex + | + | Compiled: Feb 23 2026 at 14:30:00 + |------------------------------- + | Repository: https://github.com/NullSablex/mysql_samp +``` + +If the banner is missing: + +- Check the binary is in the right folder (`plugins/`, `components/`, or `legacy_plugins` per the registration path). +- Confirm the architecture (the binary is 32-bit; the server must be too — both SA-MP and open.mp run 32-bit on Linux). +- Make sure `server.cfg` / `config.json` references the exact file name. + +## Logs + +The plugin keeps a separate log file for sensitive details: `logs/mysql.log`. The directory is created automatically; if the working directory is not writable the plugin emits one console error and suppresses further file-write attempts. + +| Destination | Contents | +|---|---| +| Server console | Generic messages with error codes — `[MySQL] Query failed on connection 1 (error 1064). See logs/mysql.log for details.` | +| `logs/mysql.log` | Full details with timestamp — full error messages, including raw text reported by the MySQL server | + +The console never prints query text, credentials or row data. + +### Log level + +`mysql_log(level)` adjusts the minimum severity that reaches both sinks at runtime. The default is `MYSQL_LOG_ALL`. + +```pawn +mysql_log(MYSQL_LOG_NONE); // 0 — disable every category +mysql_log(MYSQL_LOG_ERROR); // 1 — errors only +mysql_log(MYSQL_LOG_WARNING); // 2 — errors + warnings +mysql_log(MYSQL_LOG_INFO); // 3 — errors + warnings + info +mysql_log(MYSQL_LOG_ALL); // 4 — everything (default) +``` diff --git a/docs/migracao.md b/docs/migracao.md deleted file mode 100644 index 16aad78..0000000 --- a/docs/migracao.md +++ /dev/null @@ -1,296 +0,0 @@ -# Migracao do MySQL R41-4 - -Este guia ajuda na migracao de servidores que usam o plugin **MySQL R41-4** (BlueG/maddinat0r) para o **mysql_samp**. - -Referencia completa do R41-4: `reference/a_mysql.inc` - -## Diferencas principais - -### Queries - -| R41-4 | mysql_samp | Notas | -|---|---|---| -| `mysql_tquery(MySQL:handle, query, cb, fmt, ...)` | `mysql_query(connId, query, cb, fmt, ...)` | Threaded FIFO (mesmo comportamento) | -| `mysql_pquery(MySQL:handle, query, cb, fmt, ...)` | `mysql_pquery(connId, query, cb, fmt, ...)` | Paralela sem ordem (igual) | -| `Cache:mysql_query(MySQL:handle, query, use_cache)` | *(removido)* | Era sincrona — nao existe mais | -| `mysql_tquery_file(...)` | *(removido)* | Nao suportado | -| `mysql_query_file(...)` | *(removido)* | Nao suportado | - -> No R41-4, `mysql_query` era sincrona e travava o servidor. No mysql_samp, `mysql_query` e sempre threaded (equivale ao antigo `mysql_tquery`). - -### mysql_format: %s agora escapa - -**Mudanca critica:** No R41-4, `%s` inseria a string raw (sem escape). No mysql_samp, `%s` **escapa automaticamente**. - -| Especificador | R41-4 | mysql_samp | -|---|---|---| -| `%d` / `%i` | Inteiro | Inteiro (igual) | -| `%f` | Float | Float (igual) | -| `%s` | String **raw** (sem escape) | String **escaped** (com escape) | -| `%e` | String escaped | String escaped (alias de %s) | -| `%r` | *(nao existe)* | String raw (sem escape) | - -**Migracao:** -- Se voce usava `%e` → mantenha `%e` ou troque por `%s` (ambos escapam) -- Se voce usava `%s` para inserir dados do usuario → nao precisa mudar (agora e seguro automaticamente) -- Se voce usava `%s` para inserir valores confiaveis (nomes de tabela, SQL dinamico) → troque por `%r` - -```pawn -// R41-4 -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM %s WHERE name = '%e'", tableName, playerName); - -// mysql_samp -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM %r WHERE name = '%s'", tableName, playerName); -``` - -### mysql_escape_string - -| R41-4 | mysql_samp | -|---|---| -| `mysql_escape_string(src, dest, max_len, MySQL:handle)` | `mysql_escape_string(src, dest, max_len)` | - -No mysql_samp, o escape e uma funcao pura — nao requer conexao. O charset e sempre UTF-8 (forcado pelo plugin). - -### Cache — mudanca de assinatura - -**Mudanca mais importante:** no R41-4, natives de cache int/float/null escrevem por referencia (`&destination`). No mysql_samp, elas **retornam o valor diretamente**. - -| R41-4 | mysql_samp | Mudanca | -|---|---|---| -| `cache_get_row_count(&dest)` | `cache_get_row_count()` retorna `int` | By-ref → retorno | -| `cache_get_field_count(&dest)` | `cache_get_field_count()` retorna `int` | By-ref → retorno | -| `cache_get_result_count(&dest)` | *(removido)* | Multi-result sets | -| `cache_get_value_index(row, col, dest[], max_len)` | `cache_get_value_index(row, col, dest[], max_len)` | Igual | -| `cache_get_value_index_int(row, col, &dest)` | `cache_get_value_index_int(row, col)` retorna `int` | By-ref → retorno | -| `cache_get_value_index_float(row, col, &Float:dest)` | `cache_get_value_index_float(row, col)` retorna `Float` | By-ref → retorno | -| `cache_get_value_name(row, name, dest[], max_len)` | `cache_get_value_name(row, name, dest[], max_len)` | Igual | -| `cache_get_value_name_int(row, name, &dest)` | `cache_get_value_name_int(row, name)` retorna `int` | By-ref → retorno | -| `cache_get_value_name_float(row, name, &Float:dest)` | `cache_get_value_name_float(row, name)` retorna `Float` | By-ref → retorno | -| `cache_is_value_index_null(row, col, &bool:dest)` | `cache_is_value_index_null(row, col)` retorna `bool` | By-ref → retorno | -| `cache_is_value_name_null(row, name, &bool:dest)` | `cache_is_value_name_null(row, name)` retorna `bool` | By-ref → retorno | -| `cache_set_result(idx)` | *(removido)* | Multi-result sets | -| `cache_save()` | `cache_save()` | Igual (sem tag `Cache:`) | -| `cache_delete(Cache:id)` | `cache_delete(id)` | Sem tag | -| `cache_set_active(Cache:id)` | `cache_set_active(id)` | Sem tag | -| `cache_unset_active()` | `cache_unset_active()` | Igual | -| `cache_affected_rows()` | `cache_affected_rows()` | Igual | -| `cache_insert_id()` | `cache_insert_id()` | Igual | -| `cache_warning_count()` | `cache_warning_count()` | Igual | -| `cache_get_query_exec_time(unit)` | `cache_get_query_exec_time()` | Sem param (sempre ms) | -| `cache_get_query_string(dest, max_len)` | `cache_get_query_string(dest, max_len)` | Igual | -| `cache_is_any_active()` | `cache_is_any_active()` | Igual | -| `cache_is_valid(Cache:id)` | `cache_is_valid(id)` | Sem tag | - -**Stock wrappers do R41-4:** -- `cache_num_rows()` → usar `cache_get_row_count()` diretamente -- `cache_num_fields()` → usar `cache_get_field_count()` diretamente - -### ORM - -| R41-4 | mysql_samp | Diferenca | -|---|---|---| -| `ORM:orm_create(table, MySQL:handle)` | `orm_create(table, connId)` | Sem tags | -| `orm_destroy(ORM:id)` | `orm_destroy(orm_id)` retorna `bool` | Retorno adicionado | -| `E_ORM_ERROR:orm_errno(ORM:id)` | `orm_errno(orm_id)` retorna `int` | Sem tag | -| `orm_select(ORM:id, cb, fmt, ...)` | `orm_select(orm_id, cb, fmt, ...)` | Sem tag | -| `orm_update(ORM:id, cb, fmt, ...)` | `orm_update(orm_id, cb, fmt, ...)` | Sem tag | -| `orm_insert(ORM:id, cb, fmt, ...)` | `orm_insert(orm_id, cb, fmt, ...)` | Sem tag | -| `orm_delete(ORM:id, cb, fmt, ...)` | `orm_delete(orm_id, cb, fmt, ...)` | Sem tag | -| `orm_save(ORM:id, cb, fmt, ...)` | `orm_save(orm_id, cb, fmt, ...)` | Sem tag | -| `orm_load(...)` | *(removido)* | Era alias de orm_select | -| `orm_apply_cache(ORM:id, row, result_idx)` | `orm_apply_cache(orm_id, row)` | Sem result_idx | -| `orm_addvar_int(ORM:id, &var, col)` | `orm_addvar_int(orm_id, &var, col)` | Sem tag | -| `orm_addvar_float(ORM:id, &Float:var, col)` | `orm_addvar_float(orm_id, &Float:var, col)` | Sem tag | -| `orm_addvar_string(ORM:id, var[], max, col)` | `orm_addvar_string(orm_id, var[], max, col)` | max limitado a 4096 | -| `orm_delvar(ORM:id, col)` | `orm_delvar(orm_id, col)` | Sem tag | -| `orm_clear_vars(ORM:id)` | `orm_clear_vars(orm_id)` | Sem tag | -| `orm_setkey(ORM:id, col)` | `orm_setkey(orm_id, col)` | Sem tag | - -**Enum de erro diferente:** - -| R41-4 | mysql_samp | -|---|---| -| `ERROR_INVALID = 0` | *(nao existe)* | -| `ERROR_OK = 1` | `ORM_OK = 0` | -| `ERROR_NO_DATA = 2` | `ORM_NO_DATA = 1` | - -### Conexao - -| R41-4 | mysql_samp | -|---|---| -| `MySQL:mysql_connect(host, user, pass, db, MySQLOpt:opt)` | `mysql_connect(host, user, pass, db, options)` | -| `MySQLOpt:mysql_init_options()` | `mysql_options_new()` | -| `mysql_set_option(MySQLOpt:opt, E_MYSQL_OPTION:type, ...)` | `mysql_options_set_int(opt, type, val)` / `mysql_options_set_str(opt, type, val[])` | -| `mysql_close(MySQL:handle)` | `mysql_close(connId)` | -| `mysql_stat(dest, max_len, MySQL:handle)` | `mysql_status(connId, dest, max_len)` | -| `mysql_connect_file(file)` | *(removido)* | -| `mysql_global_options(type, val)` | *(removido)* | - -**Diferenca de estilo:** no R41-4, o `MySQL:handle` e o ultimo parametro (opcional, default `MySQL:1`). No mysql_samp, o `connId` e o primeiro parametro. - -### Erro - -| R41-4 | mysql_samp | Diferenca | -|---|---|---| -| `mysql_errno(MySQL:handle)` | `mysql_errno(connId)` | Sem tag, connId primeiro | -| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | connId primeiro (era ultimo) | - -### Charset - -| R41-4 | mysql_samp | Diferenca | -|---|---|---| -| `mysql_set_charset(charset, MySQL:handle)` | `mysql_set_charset(connId, charset)` | connId primeiro | -| `mysql_get_charset(dest, max_len, MySQL:handle)` | `mysql_get_charset(connId, dest, max_len)` | connId primeiro | - -### Log - -| R41-4 | mysql_samp | -|---|---| -| `mysql_log(E_LOGLEVEL:level)` — bitflags (DEBUG=1, INFO=2, WARNING=4, ERROR=8) | `mysql_log(level)` — sequencial (NONE=0, ERROR=1, WARNING=2, INFO=3, ALL=4) | - -### Formato de callback - -O mysql_samp aceita tanto `"i"` quanto `"d"` para inteiros. Se voce ja usa `"i"`, nao precisa mudar. - -| R41-4 | mysql_samp | -|---|---| -| `"i"` | `"d"` ou `"i"` (ambos funcionam) | -| `"f"` | `"f"` (igual) | -| `"s"` | `"s"` (igual) | - -### Forward - -| R41-4 | mysql_samp | -|---|---| -| `OnQueryError(errorid, error[], callback[], query[], MySQL:handle)` | `OnQueryError(errorid, error[], callback[], query[], connId)` | - -## Funcoes removidas - -| R41-4 | Razao da remocao | -|---|---| -| `Cache:mysql_query(handle, query, use_cache)` | Bloqueia o servidor — inaceitavel | -| `mysql_tquery_file(...)` / `mysql_query_file(...)` | Nao suportado | -| `mysql_connect_file(file)` | Nao suportado | -| `mysql_global_options(type, val)` | Nao aplicavel | -| `cache_get_result_count(&dest)` | Multi-result sets desnecessario para SA:MP | -| `cache_set_result(idx)` | Multi-result sets desnecessario para SA:MP | -| `orm_load(...)` | Era alias de `orm_select` | - -## Passo a passo da migracao - -### 1. Substitua o include - -```pawn -// Antes -#include - -// Depois -#include -``` - -### 2. Renomeie as queries - -```pawn -// Antes -mysql_tquery(gMysql, query, "OnData", "i", playerid); - -// Depois -mysql_query(gMysql, query, "OnData", "d", playerid); -// ou (ambos funcionam) -mysql_query(gMysql, query, "OnData", "i", playerid); -``` - -### 3. Atualize mysql_format - -```pawn -// Antes (R41-4: %s = raw, %e = escaped) -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM %s WHERE name = '%e'", table, name); - -// Depois (mysql_samp: %r = raw, %s = escaped) -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM %r WHERE name = '%s'", table, name); -``` - -### 4. Atualize mysql_connect com options - -```pawn -// Antes (R41-4: porta via mysql_set_option) -new MySQLOpt:opt = mysql_init_options(); -mysql_set_option(opt, SERVER_PORT, 3307); -new MySQL:gMysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); - -// Depois (mysql_samp: porta via mysql_options_set_int) -new opt = mysql_options_new(); -mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); -new gMysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); - -// Ou, se usa porta 3306 (padrao): -new gMysql = mysql_connect("127.0.0.1", "root", "pass", "db"); -``` - -### 5. Atualize mysql_escape_string - -```pawn -// Antes (R41-4: handle como ultimo param) -mysql_escape_string(input, escaped, sizeof(escaped), gMysql); - -// Depois (mysql_samp: sem handle) -mysql_escape_string(input, escaped); -``` - -### 6. Adapte cache int/float de by-ref para retorno direto - -```pawn -// Antes (R41-4: by-ref) -new score; -cache_get_value_name_int(0, "score", score); - -// Depois (mysql_samp: retorno direto) -new score = cache_get_value_name_int(0, "score"); -``` - -### 7. Adapte cache row_count / field_count - -```pawn -// Antes (R41-4) -new rows = cache_num_rows(); -// ou -new rows; -cache_get_row_count(rows); - -// Depois (mysql_samp) -new rows = cache_get_row_count(); -``` - -### 8. Remova todas as tags - -```pawn -// Antes -new MySQL:gMysql; -new Cache:cache = cache_save(); -new ORM:orm = orm_create("table", gMysql); - -// Depois -new gMysql; -new cache = cache_save(); -new orm = orm_create("table", gMysql); -``` - -### 9. Atualize mysql_error (ordem dos params) - -```pawn -// Antes (R41-4: handle ultimo) -new errMsg[256]; -mysql_error(errMsg, sizeof(errMsg), gMysql); - -// Depois (mysql_samp: connId primeiro) -new errMsg[256]; -mysql_error(gMysql, errMsg); -``` - -### 10. Compile e teste - -Compile seu gamemode com o novo include e teste todas as funcionalidades. Verifique `logs/mysql.log` para erros. diff --git a/docs/migration-changes.md b/docs/migration-changes.md new file mode 100644 index 0000000..2564187 --- /dev/null +++ b/docs/migration-changes.md @@ -0,0 +1,287 @@ +# What changed (R41-4 → mysql_samp) + +Flat reference of every difference between MySQL R41-4 and mysql_samp. See [migration.md](migration.md) for the explained guide and [migration-examples.md](migration-examples.md) for side-by-side snippets. + +## Include + +```pawn +// R41-4 +#include + +// mysql_samp +#include +``` + +## Tags removed + +mysql_samp does not use custom Pawn tags. Every handle is a plain `int`. + +| R41-4 | mysql_samp | +|---|---| +| `new MySQL:g_mysql` | `new g_mysql` | +| `new Cache:c = cache_save();` | `new c = cache_save();` | +| `new ORM:o = orm_create(...)` | `new o = orm_create(...)` | +| `cache_delete(Cache:c);` | `cache_delete(c);` | +| `MySQLOpt:o = mysql_init_options()` | `new o = mysql_options_new()` | +| `MYSQL_INVALID_HANDLE` | plain `0` | + +## Callback format + +`"d"` and `"i"` are both accepted as int. If you already use `"i"`, no change is needed. + +| R41-4 | mysql_samp | Meaning | +|---|---|---| +| `"i"` | `"d"` or `"i"` | int | +| `"f"` | `"f"` | float | +| `"s"` | `"s"` | string | + +```pawn +// R41-4 +mysql_tquery(g_mysql, query, "OnLoad", "i", playerid); + +// mysql_samp (both work) +mysql_query(g_mysql, query, "OnLoad", "d", playerid); +mysql_query(g_mysql, query, "OnLoad", "i", playerid); +``` + +## mysql_format specifiers + +| Specifier | R41-4 | mysql_samp | +|---|---|---| +| `%d`, `%i` | int | int | +| `%f` | float | float (4 decimals — `{:.4}`) | +| `%s` | **raw** (no escape) | **escaped** | +| `%e` | escaped | escaped (alias of `%s`) | +| `%r` | *does not exist* | **raw** (no escape) | +| `%%` | literal | literal | + +**Rule of thumb:** where you used `%e`, use `%s` (both escape). Where you used `%s` to inject a table or column name, switch to `%r`. + +## Queries + +| R41-4 | mysql_samp | Behavior | +|---|---|---| +| `Cache:mysql_query(handle, query, use_cache)` | *removed* | The synchronous version blocked the server tick | +| `mysql_tquery(handle, query, cb, fmt, ...)` | `mysql_query(connId, query, cb, fmt, ...)` | Threaded, FIFO ordering (same as before) | +| `mysql_pquery(handle, query, cb, fmt, ...)` | `mysql_pquery(connId, query, cb, fmt, ...)` | Threaded, no order (same as before) | + +Every query is now non-blocking. The cache is only valid inside the callback (or after `cache_set_active` on a saved id). + +## Connection + +| R41-4 | mysql_samp | +|---|---| +| `MySQL:mysql_connect(host, user, pass, db, MySQLOpt:opt)` | `mysql_connect(host, user, pass, db, options)` | +| `MySQLOpt:mysql_init_options()` | `mysql_options_new()` | +| `mysql_set_option(opt, type, ...)` | `mysql_options_set_int(opt, type, val)` / `mysql_options_set_str(opt, type, val[])` | +| `mysql_escape_string(src, dest, max_len, MySQL:handle)` | `mysql_escape_string(src, dest, max_len)` | +| `mysql_stat(dest, max_len, MySQL:handle)` | `mysql_status(connId, dest, max_len)` | +| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | +| `mysql_errno(MySQL:handle)` | `mysql_errno(connId)` | +| `mysql_set_charset(charset, MySQL:handle)` | `mysql_set_charset(connId, charset)` | +| `mysql_get_charset(dest, max_len, MySQL:handle)` | `mysql_get_charset(connId, dest, max_len)` | +| `mysql_close(MySQL:handle)` | `mysql_close(connId)` | +| `mysql_unprocessed_queries(MySQL:handle)` | `mysql_unprocessed_queries()` | +| *does not exist* | `mysql_log(level)` | + +**Argument order changed.** In R41-4 the handle is the last (optional) parameter on most calls. In mysql_samp the `connId` is always the **first** parameter. + +**Port option name changed.** In R41-4 you wrote `mysql_set_option(opt, SERVER_PORT, 3307)`. In mysql_samp it is `mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307)`. + +```pawn +// R41-4 +new MySQLOpt:opt = mysql_init_options(); +mysql_set_option(opt, SERVER_PORT, 3307); +new MySQL:g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); + +// mysql_samp +new opt = mysql_options_new(); +mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); +new g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); + +// mysql_samp (default port 3306 — no options needed) +new g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db"); +``` + +## Cache — by-ref → return value + +The biggest single change. R41-4 wrote the result through a pointer; mysql_samp returns the value directly. + +### Row/field counts + +| R41-4 | mysql_samp | +|---|---| +| `cache_get_row_count(&dest)` | `cache_get_row_count()` → `int` | +| `cache_get_field_count(&dest)` | `cache_get_field_count()` → `int` | +| `cache_num_rows()` (stock wrapper) | `cache_get_row_count()` | +| `cache_num_fields()` (stock wrapper) | `cache_get_field_count()` | + +```pawn +// R41-4 +new rows; +cache_get_row_count(rows); +// or +new rows2 = cache_num_rows(); + +// mysql_samp +new rows = cache_get_row_count(); +``` + +### Values by index + +| R41-4 | mysql_samp | +|---|---| +| `cache_get_value_index(row, col, dest[], max_len)` | unchanged | +| `cache_get_value_index_int(row, col, &dest)` | `cache_get_value_index_int(row, col)` → `int` | +| `cache_get_value_index_float(row, col, &Float:dest)` | `cache_get_value_index_float(row, col)` → `Float` | + +```pawn +// R41-4 +new score; +cache_get_value_index_int(0, 2, score); + +// mysql_samp +new score = cache_get_value_index_int(0, 2); +``` + +### Values by name + +| R41-4 | mysql_samp | +|---|---| +| `cache_get_value_name(row, col_name, dest[], max_len)` | unchanged | +| `cache_get_value_name_int(row, col_name, &dest)` | `cache_get_value_name_int(row, col_name)` → `int` | +| `cache_get_value_name_float(row, col_name, &Float:dest)` | `cache_get_value_name_float(row, col_name)` → `Float` | + +Column name lookup is case-insensitive in both plugins. + +```pawn +// R41-4 +new score; +cache_get_value_name_int(0, "score", score); +new Float:pos_x; +cache_get_value_name_float(0, "pos_x", pos_x); + +// mysql_samp +new score = cache_get_value_name_int(0, "score"); +new Float:pos_x = cache_get_value_name_float(0, "pos_x"); +``` + +### NULL checks + +| R41-4 | mysql_samp | +|---|---| +| `cache_is_value_index_null(row, col, &bool:dest)` | `cache_is_value_index_null(row, col)` → `bool` | +| `cache_is_value_name_null(row, col_name, &bool:dest)` | `cache_is_value_name_null(row, col_name)` → `bool` | + +```pawn +// R41-4 +new bool:is_null; +cache_is_value_name_null(0, "email", is_null); + +// mysql_samp +new bool:is_null = cache_is_value_name_null(0, "email"); +``` + +## Cache — natives with unchanged signatures + +- `cache_get_value_index(row, col, dest[], max_len)` +- `cache_get_value_name(row, col_name, dest[], max_len)` +- `cache_get_field_name(idx, dest[], max_len)` +- `cache_affected_rows()` +- `cache_insert_id()` +- `cache_warning_count()` +- `cache_save()` +- `cache_delete(cache_id)` (without the `Cache:` tag) +- `cache_set_active(cache_id)` (without the `Cache:` tag) +- `cache_unset_active()` +- `cache_is_any_active()` +- `cache_is_valid(cache_id)` (without the `Cache:` tag) +- `cache_get_query_string(dest[], max_len)` + +## Cache — new natives + +| Native | Description | +|---|---| +| `cache_get_field_type(idx)` | Raw MySQL `ColumnType` byte for the given column index | +| `cache_get_query_exec_time()` | Server-side execution time in milliseconds | + +## Cache — removed natives + +| R41-4 | Why | +|---|---| +| `cache_get_result_count(&dest)` | Multi-result sets — unused by SA-MP gamemodes | +| `cache_set_result(idx)` | Multi-result sets | +| `cache_get_query_exec_time(unit)` | Replaced by the no-argument version (always ms) | + +## ORM + +The API is almost identical, with these differences: + +| R41-4 | mysql_samp | Difference | +|---|---|---| +| `ORM:orm_create(table, MySQL:handle)` | `orm_create(table, connId)` | No tags | +| `orm_destroy(ORM:id)` | `orm_destroy(orm_id)` → `bool` | Now returns `bool` | +| `E_ORM_ERROR:orm_errno(ORM:id)` | `orm_errno(orm_id)` → `int` | No enum tag | +| `orm_apply_cache(ORM:id, row, result_idx)` | `orm_apply_cache(orm_id, row)` | No `result_idx` (single result set) | +| `orm_load(...)` | *removed* | Was an alias of `orm_select` | +| `orm_addvar_string(orm, var, max_len, col)` | unchanged | `max_len` now capped at 4096 | + +ORM error enum: + +| R41-4 | mysql_samp | +|---|---| +| `ERROR_INVALID = 0` | *does not exist* | +| `ERROR_OK = 1` | `ORM_OK = 0` | +| `ERROR_NO_DATA = 2` | `ORM_NO_DATA = 1` | + +The rest of the ORM natives (`orm_select`, `orm_update`, `orm_insert`, `orm_delete`, `orm_save`, `orm_addvar_*`, `orm_delvar`, `orm_clear_vars`, `orm_setkey`) keep the same signatures, only without the `ORM:` tag. + +## Error + +| R41-4 | mysql_samp | Difference | +|---|---|---| +| `mysql_errno(MySQL:handle)` | `mysql_errno(connId = 0)` | No tag, `connId` first | +| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | `connId` first (was last) | + +## Log + +| R41-4 | mysql_samp | +|---|---| +| `mysql_log(E_LOGLEVEL:level)` — bitflags (DEBUG=1, INFO=2, WARNING=4, ERROR=8) | `mysql_log(level)` — sequential (NONE=0, ERROR=1, WARNING=2, INFO=3, ALL=4) | + +```pawn +// R41-4 +mysql_log(ERROR | WARNING); + +// mysql_samp +mysql_log(MYSQL_LOG_WARNING); // includes ERROR +``` + +## Forward + +| R41-4 | mysql_samp | +|---|---| +| `OnQueryError(errorid, error[], callback[], query[], MySQL:handle)` | `OnQueryError(errorid, error[], callback[], query[], connId)` | + +Same parameters, no `MySQL:` tag on the last one. + +## Mandatory checklist + +1. Replace `#include ` with `#include `. +2. Replace `mysql_tquery` with `mysql_query`. +3. Replace `%s` (raw) with `%r` in `mysql_format` wherever the old `%s` was intentionally unescaped. +4. Replace `mysql_escape_string(..., MySQL:handle)` with `mysql_escape_string(src, dest, max_len)` (no handle). +5. Replace `cache_num_rows()` with `cache_get_row_count()`. +6. Convert `cache_get_value_*_int` / `cache_get_value_*_float` from by-ref to return value. +7. Convert `cache_is_value_*_null` from by-ref to return value. +8. Convert `cache_get_row_count` / `cache_get_field_count` from by-ref to return value. +9. Strip every tag (`MySQL:`, `Cache:`, `ORM:`, `MySQLOpt:`, `E_ORM_ERROR:`). +10. Replace `mysql_init_options()` with `mysql_options_new()`. +11. Replace `mysql_set_option(opt, type, ...)` with `mysql_options_set_int` / `mysql_options_set_str`. +12. Reverse the parameter order of `mysql_error` (connId is now first). +13. Replace `mysql_stat` with `mysql_status` (connId first). +14. Move any code that read the cache after a synchronous `mysql_query` into a proper callback. + +**Optional:** + +- `"i"` → `"d"` in callback formats (both work in mysql_samp). diff --git a/docs/migration-examples.md b/docs/migration-examples.md new file mode 100644 index 0000000..5a9e712 --- /dev/null +++ b/docs/migration-examples.md @@ -0,0 +1,772 @@ +# Migration examples: before and after + +Real Pawn snippets showing the **before** (R41-4) and the **after** (mysql_samp). For the explained guide and the full reference of differences, see [migration.md](migration.md) and [migration-changes.md](migration-changes.md). + +--- + +## 1. Basic connection + +### R41-4 + +```pawn +new MySQL:g_mysql; + +public OnGameModeInit() +{ + g_mysql = mysql_connect("127.0.0.1", "root", "password", "samp"); + if (g_mysql == MYSQL_INVALID_HANDLE) + { + printf("MySQL connection failed."); + return 0; + } + return 1; +} +``` + +### mysql_samp + +```pawn +new g_mysql; + +public OnGameModeInit() +{ + g_mysql = mysql_connect("127.0.0.1", "root", "password", "samp"); + if (g_mysql == 0) + { + printf("MySQL connection failed."); + return 0; + } + return 1; +} +``` + +> **Changes:** no `MySQL:` tag, no `MYSQL_INVALID_HANDLE` (use plain `0`). + +--- + +## 2. Connect with a custom port + +### R41-4 + +```pawn +new MySQLOpt:opt = mysql_init_options(); +mysql_set_option(opt, SERVER_PORT, 3307); +new MySQL:g_mysql = mysql_connect("127.0.0.1", "root", "pass", "samp", opt); +``` + +### mysql_samp + +```pawn +new opt = mysql_options_new(); +mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); +new g_mysql = mysql_connect("127.0.0.1", "root", "pass", "samp", opt); +``` + +> **Changes:** no tags, `mysql_init_options` → `mysql_options_new`, `mysql_set_option` → `mysql_options_set_int`, `SERVER_PORT` → `MYSQL_OPT_PORT`. + +--- + +## 3. Check whether a player exists + +### R41-4 + +```pawn +public OnPlayerConnect(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM players WHERE name = '%e'", name); + mysql_tquery(g_mysql, query, "OnCheckAccount", "i", playerid); + return 1; +} + +forward OnCheckAccount(playerid); +public OnCheckAccount(playerid) +{ + if (cache_num_rows() > 0) + { + ShowPlayerDialog(playerid, DIALOG_LOGIN, ...); + } + else + { + ShowPlayerDialog(playerid, DIALOG_REGISTER, ...); + } + return 1; +} +``` + +### mysql_samp + +```pawn +public OnPlayerConnect(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM players WHERE name = '%s'", name); + mysql_query(g_mysql, query, "OnCheckAccount", "d", playerid); + return 1; +} + +forward OnCheckAccount(playerid); +public OnCheckAccount(playerid) +{ + if (cache_get_row_count() > 0) + { + ShowPlayerDialog(playerid, DIALOG_LOGIN, ...); + } + else + { + ShowPlayerDialog(playerid, DIALOG_REGISTER, ...); + } + return 1; +} +``` + +> **Changes:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `"i"` → `"d"` (optional), `cache_num_rows()` → `cache_get_row_count()`. + +--- + +## 4. Load player data + +### R41-4 + +```pawn +forward OnPlayerLogin(playerid); +public OnPlayerLogin(playerid) +{ + if (cache_num_rows() > 0) + { + cache_get_value_name(0, "hash", Player[playerid][Hash], 65); + + cache_get_value_name_int(0, "score", Player[playerid][Score]); + cache_get_value_name_int(0, "money", Player[playerid][Money]); + cache_get_value_name_float(0, "pos_x", Player[playerid][PosX]); + cache_get_value_name_float(0, "pos_y", Player[playerid][PosY]); + cache_get_value_name_float(0, "pos_z", Player[playerid][PosZ]); + cache_get_value_name_int(0, "skin", Player[playerid][Skin]); + } + return 1; +} +``` + +### mysql_samp + +```pawn +forward OnPlayerLogin(playerid); +public OnPlayerLogin(playerid) +{ + if (cache_get_row_count() > 0) + { + cache_get_value_name(0, "hash", Player[playerid][Hash]); + + Player[playerid][Score] = cache_get_value_name_int(0, "score"); + Player[playerid][Money] = cache_get_value_name_int(0, "money"); + Player[playerid][PosX] = cache_get_value_name_float(0, "pos_x"); + Player[playerid][PosY] = cache_get_value_name_float(0, "pos_y"); + Player[playerid][PosZ] = cache_get_value_name_float(0, "pos_z"); + Player[playerid][Skin] = cache_get_value_name_int(0, "skin"); + } + return 1; +} +``` + +> **Changes:** `cache_num_rows()` → `cache_get_row_count()`. `cache_get_value_name_int` / `_float` switched from by-ref (3 params) to return value (2 params). `cache_get_value_name` for strings keeps the same shape (using the default `sizeof` argument). + +--- + +## 5. Register — insert a new player + +### R41-4 + +```pawn +public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) +{ + if (dialogid == DIALOG_REGISTER && response) + { + new query[256], name[MAX_PLAYER_NAME], hash[65]; + GetPlayerName(playerid, name, sizeof(name)); + SHA256_PassHash(inputtext, SALT, hash, sizeof(hash)); + + mysql_format(g_mysql, query, sizeof(query), + "INSERT INTO players (name, hash) VALUES ('%e', '%e')", + name, hash); + mysql_tquery(g_mysql, query, "OnPlayerRegister", "i", playerid); + } + return 1; +} + +forward OnPlayerRegister(playerid); +public OnPlayerRegister(playerid) +{ + Player[playerid][ID] = cache_insert_id(); + SendClientMessage(playerid, -1, "Account created."); + return 1; +} +``` + +### mysql_samp + +```pawn +public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) +{ + if (dialogid == DIALOG_REGISTER && response) + { + new query[256], name[MAX_PLAYER_NAME], hash[65]; + GetPlayerName(playerid, name, sizeof(name)); + SHA256_PassHash(inputtext, SALT, hash, sizeof(hash)); + + mysql_format(g_mysql, query, sizeof(query), + "INSERT INTO players (name, hash) VALUES ('%s', '%s')", + name, hash); + mysql_query(g_mysql, query, "OnPlayerRegister", "d", playerid); + } + return 1; +} + +forward OnPlayerRegister(playerid); +public OnPlayerRegister(playerid) +{ + Player[playerid][ID] = cache_insert_id(); + SendClientMessage(playerid, -1, "Account created."); + return 1; +} +``` + +> **Changes:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `"i"` → `"d"` (optional). + +--- + +## 6. Save player data + +### R41-4 + +```pawn +stock SavePlayer(playerid) +{ + new query[512]; + mysql_format(g_mysql, query, sizeof(query), + "UPDATE players SET score = %d, money = %d, pos_x = %f, pos_y = %f, pos_z = %f WHERE id = %d", + GetPlayerScore(playerid), + GetPlayerMoney(playerid), + Player[playerid][PosX], + Player[playerid][PosY], + Player[playerid][PosZ], + Player[playerid][ID]); + mysql_tquery(g_mysql, query); + return 1; +} +``` + +### mysql_samp + +```pawn +stock SavePlayer(playerid) +{ + new query[512]; + mysql_format(g_mysql, query, sizeof(query), + "UPDATE players SET score = %d, money = %d, pos_x = %f, pos_y = %f, pos_z = %f WHERE id = %d", + GetPlayerScore(playerid), + GetPlayerMoney(playerid), + Player[playerid][PosX], + Player[playerid][PosY], + Player[playerid][PosZ], + Player[playerid][ID]); + mysql_query(g_mysql, query); + return 1; +} +``` + +> **Minimal change:** `mysql_tquery` → `mysql_query`. No callback, no format — fire-and-forget. + +--- + +## 7. VIP check — query with callback and parameters + +### R41-4 + +```pawn +stock CheckVIP(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT vip_level, vip_expires FROM players WHERE name = '%e'", name); + mysql_tquery(g_mysql, query, "OnVIPCheck", "i", playerid); +} + +forward OnVIPCheck(playerid); +public OnVIPCheck(playerid) +{ + if (cache_num_rows() > 0) + { + new level, expires[20]; + cache_get_value_name_int(0, "vip_level", level); + cache_get_value_name(0, "vip_expires", expires, sizeof(expires)); + + Player[playerid][VIPLevel] = level; + } + return 1; +} +``` + +### mysql_samp + +```pawn +stock CheckVIP(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT vip_level, vip_expires FROM players WHERE name = '%s'", name); + mysql_query(g_mysql, query, "OnVIPCheck", "d", playerid); +} + +forward OnVIPCheck(playerid); +public OnVIPCheck(playerid) +{ + if (cache_get_row_count() > 0) + { + new expires[20]; + Player[playerid][VIPLevel] = cache_get_value_name_int(0, "vip_level"); + cache_get_value_name(0, "vip_expires", expires); + } + return 1; +} +``` + +> **Changes:** `%e` → `%s`, `mysql_tquery` → `mysql_query`, `cache_num_rows` → `cache_get_row_count`, `cache_get_value_name_int` switched to return value, `cache_get_value_name` works with the default `sizeof`. + +--- + +## 8. Ban system + +### R41-4 + +```pawn +stock BanPlayer(playerid, adminid, const reason[]) +{ + new query[256], name[MAX_PLAYER_NAME], admin[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + GetPlayerName(adminid, admin, sizeof(admin)); + + mysql_format(g_mysql, query, sizeof(query), + "INSERT INTO bans (name, admin, reason, ip) VALUES ('%e', '%e', '%e', '%e')", + name, admin, reason, Player[playerid][IP]); + mysql_tquery(g_mysql, query); + Kick(playerid); +} + +stock CheckBan(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT reason FROM bans WHERE name = '%e'", name); + mysql_tquery(g_mysql, query, "OnBanCheck", "i", playerid); +} + +forward OnBanCheck(playerid); +public OnBanCheck(playerid) +{ + if (cache_num_rows() > 0) + { + new reason[128]; + cache_get_value_name(0, "reason", reason, sizeof(reason)); + SendClientMessage(playerid, -1, reason); + Kick(playerid); + } + return 1; +} +``` + +### mysql_samp + +```pawn +stock BanPlayer(playerid, adminid, const reason[]) +{ + new query[256], name[MAX_PLAYER_NAME], admin[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + GetPlayerName(adminid, admin, sizeof(admin)); + + mysql_format(g_mysql, query, sizeof(query), + "INSERT INTO bans (name, admin, reason, ip) VALUES ('%s', '%s', '%s', '%s')", + name, admin, reason, Player[playerid][IP]); + mysql_query(g_mysql, query); + Kick(playerid); +} + +stock CheckBan(playerid) +{ + new query[128], name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + mysql_format(g_mysql, query, sizeof(query), + "SELECT reason FROM bans WHERE name = '%s'", name); + mysql_query(g_mysql, query, "OnBanCheck", "d", playerid); +} + +forward OnBanCheck(playerid); +public OnBanCheck(playerid) +{ + if (cache_get_row_count() > 0) + { + new reason[128]; + cache_get_value_name(0, "reason", reason); + SendClientMessage(playerid, -1, reason); + Kick(playerid); + } + return 1; +} +``` + +--- + +## 9. Load vehicles from the database + +### R41-4 + +```pawn +stock LoadVehicles() +{ + mysql_tquery(g_mysql, "SELECT * FROM vehicles", "OnVehiclesLoaded"); +} + +forward OnVehiclesLoaded(); +public OnVehiclesLoaded() +{ + new rows = cache_num_rows(); + for (new i = 0; i < rows; i++) + { + new model, Float:x, Float:y, Float:z, Float:a, color1, color2; + + cache_get_value_name_int(i, "model", model); + cache_get_value_name_float(i, "pos_x", x); + cache_get_value_name_float(i, "pos_y", y); + cache_get_value_name_float(i, "pos_z", z); + cache_get_value_name_float(i, "angle", a); + cache_get_value_name_int(i, "color1", color1); + cache_get_value_name_int(i, "color2", color2); + + CreateVehicle(model, x, y, z, a, color1, color2, -1); + } + printf("vehicles loaded: %d", rows); + return 1; +} +``` + +### mysql_samp + +```pawn +stock LoadVehicles() +{ + mysql_query(g_mysql, "SELECT * FROM vehicles", "OnVehiclesLoaded"); +} + +forward OnVehiclesLoaded(); +public OnVehiclesLoaded() +{ + new rows = cache_get_row_count(); + for (new i = 0; i < rows; i++) + { + new model = cache_get_value_name_int(i, "model"); + new Float:x = cache_get_value_name_float(i, "pos_x"); + new Float:y = cache_get_value_name_float(i, "pos_y"); + new Float:z = cache_get_value_name_float(i, "pos_z"); + new Float:a = cache_get_value_name_float(i, "angle"); + new color1 = cache_get_value_name_int(i, "color1"); + new color2 = cache_get_value_name_int(i, "color2"); + + CreateVehicle(model, x, y, z, a, color1, color2, -1); + } + printf("vehicles loaded: %d", rows); + return 1; +} +``` + +> **Changes:** `mysql_tquery` → `mysql_query`, `cache_num_rows` → `cache_get_row_count`, by-ref → return value for `_int` and `_float`. + +--- + +## 10. ORM — full player flow + +### R41-4 + +```pawn +enum pInfo +{ + ORM:ORM_ID, + ID, + Name[MAX_PLAYER_NAME], + Hash[65], + Score, + Money, + Float:PosX, + Float:PosY, + Float:PosZ, +}; +new Player[MAX_PLAYERS][pInfo]; + +stock CreateORM(playerid) +{ + new ORM:orm = orm_create("players", g_mysql); + Player[playerid][ORM_ID] = orm; + + orm_addvar_int(orm, Player[playerid][ID], "id"); + orm_addvar_string(orm, Player[playerid][Name], MAX_PLAYER_NAME, "name"); + orm_addvar_string(orm, Player[playerid][Hash], 65, "hash"); + orm_addvar_int(orm, Player[playerid][Score], "score"); + orm_addvar_int(orm, Player[playerid][Money], "money"); + orm_addvar_float(orm, Player[playerid][PosX], "pos_x"); + orm_addvar_float(orm, Player[playerid][PosY], "pos_y"); + orm_addvar_float(orm, Player[playerid][PosZ], "pos_z"); + + orm_setkey(orm, "id"); + return _:orm; +} + +orm_select(Player[playerid][ORM_ID], "OnPlayerLoad", "i", playerid); + +forward OnPlayerLoad(playerid); +public OnPlayerLoad(playerid) +{ + orm_apply_cache(Player[playerid][ORM_ID], 0); + SetPlayerScore(playerid, Player[playerid][Score]); + GivePlayerMoney(playerid, Player[playerid][Money]); +} + +orm_save(Player[playerid][ORM_ID]); +``` + +### mysql_samp + +```pawn +enum pInfo +{ + ORM_ID, + ID, + Name[MAX_PLAYER_NAME], + Hash[65], + Score, + Money, + Float:PosX, + Float:PosY, + Float:PosZ, +}; +new Player[MAX_PLAYERS][pInfo]; + +stock CreateORM(playerid) +{ + new orm = orm_create("players", g_mysql); + Player[playerid][ORM_ID] = orm; + + orm_addvar_int(orm, Player[playerid][ID], "id"); + orm_addvar_string(orm, Player[playerid][Name], MAX_PLAYER_NAME, "name"); + orm_addvar_string(orm, Player[playerid][Hash], 65, "hash"); + orm_addvar_int(orm, Player[playerid][Score], "score"); + orm_addvar_int(orm, Player[playerid][Money], "money"); + orm_addvar_float(orm, Player[playerid][PosX], "pos_x"); + orm_addvar_float(orm, Player[playerid][PosY], "pos_y"); + orm_addvar_float(orm, Player[playerid][PosZ], "pos_z"); + + orm_setkey(orm, "id"); + return orm; +} + +orm_select(Player[playerid][ORM_ID], "OnPlayerLoad", "d", playerid); + +forward OnPlayerLoad(playerid); +public OnPlayerLoad(playerid) +{ + orm_apply_cache(Player[playerid][ORM_ID], 0); + SetPlayerScore(playerid, Player[playerid][Score]); + GivePlayerMoney(playerid, Player[playerid][Money]); +} + +orm_save(Player[playerid][ORM_ID]); +``` + +> **Changes:** no `ORM:` tag (plain `int`), `"i"` → `"d"` (optional). Everything else is identical. + +--- + +## 11. Saved cache — store and reuse + +### R41-4 + +```pawn +forward OnQueryA(playerid); +public OnQueryA(playerid) +{ + new Cache:cache = cache_save(); + + mysql_tquery(g_mysql, "SELECT ...", "OnQueryB", "ii", playerid, _:cache); + return 1; +} + +forward OnQueryB(playerid, cache_id); +public OnQueryB(playerid, cache_id) +{ + new rows = cache_num_rows(); + // ... + + cache_set_active(Cache:cache_id); + new rowsA = cache_num_rows(); + // ... + cache_unset_active(); + + cache_delete(Cache:cache_id); + return 1; +} +``` + +### mysql_samp + +```pawn +forward OnQueryA(playerid); +public OnQueryA(playerid) +{ + new cache = cache_save(); + + mysql_query(g_mysql, "SELECT ...", "OnQueryB", "dd", playerid, cache); + return 1; +} + +forward OnQueryB(playerid, cache_id); +public OnQueryB(playerid, cache_id) +{ + new rows = cache_get_row_count(); + // ... + + cache_set_active(cache_id); + new rowsA = cache_get_row_count(); + // ... + cache_unset_active(); + + cache_delete(cache_id); + return 1; +} +``` + +> **Changes:** no `Cache:` tag, `mysql_tquery` → `mysql_query`, `"ii"` → `"dd"` (optional — `"ii"` works too), `cache_num_rows` → `cache_get_row_count`. + +--- + +## 12. Error handling + +### R41-4 + +```pawn +forward OnQueryError(errorid, const error[], const callback[], const query[], MySQL:handle); +public OnQueryError(errorid, const error[], const callback[], const query[], MySQL:handle) +{ + printf("[MySQL] error %d on query: %s", errorid, error); + printf("[MySQL] callback: %s | query: %s", callback, query); + return 1; +} +``` + +### mysql_samp + +```pawn +forward OnQueryError(errorid, const error[], const callback[], const query[], connId); +public OnQueryError(errorid, const error[], const callback[], const query[], connId) +{ + printf("[MySQL] error %d on query: %s", errorid, error); + printf("[MySQL] callback: %s | query: %s", callback, query); + + // bonus: fetch the connection-scoped error text + new err[256]; + mysql_error(connId, err); + printf("[MySQL] detail: %s", err); + return 1; +} +``` + +> **Change:** no `MySQL:` tag on the last parameter. As a bonus, `mysql_error(connId, dest)` gives you the per-connection error text. + +--- + +## 13. Escape a string + +### R41-4 + +```pawn +new escaped[128]; +mysql_escape_string(input, escaped, sizeof(escaped), g_mysql); +``` + +### mysql_samp + +```pawn +new escaped[128]; +mysql_escape_string(input, escaped); +``` + +> **Changes:** no handle as the last parameter. The escape rules are connection-independent — the plugin always forces UTF-8. + +--- + +## 14. NULL check + +### R41-4 + +```pawn +new bool:is_null; +cache_is_value_name_null(0, "email", is_null); +if (is_null) +{ + // column is NULL +} +``` + +### mysql_samp + +```pawn +if (cache_is_value_name_null(0, "email")) +{ + // column is NULL +} +``` + +> **Change:** return value instead of by-ref. Cleaner. + +--- + +## Quick find-and-replace checklist + +For a mechanical first pass, search and replace in your gamemode: + +| Find | Replace with | +|---|---| +| `#include ` | `#include ` | +| `mysql_tquery(` | `mysql_query(` | +| `cache_num_rows(` | `cache_get_row_count(` | +| `cache_num_fields(` | `cache_get_field_count(` | +| `mysql_init_options(` | `mysql_options_new(` | +| `mysql_stat(` | `mysql_status(` | +| `new MySQL:` | `new ` | +| `new Cache:` | `new ` | +| `new ORM:` | `new ` | +| `new MySQLOpt:` | `new ` | +| `Cache:` (in calls) | *(remove)* | +| `ORM:` (in calls) | *(remove)* | +| `MySQL:` (in calls) | *(remove)* | +| `MySQLOpt:` (in calls) | *(remove)* | +| `MYSQL_INVALID_HANDLE` | `0` | +| `MYSQL_DEFAULT_HANDLE` | *(remove — use the variable directly)* | + +> **Needs manual review:** +> - `cache_get_value_*_int` / `_float`: change from 3-arg by-ref to 2-arg return value. +> - `cache_is_value_*_null`: change from 3-arg by-ref to 2-arg return value. +> - `cache_get_row_count` / `cache_get_field_count`: change from by-ref to return value. +> - `mysql_escape_string`: drop the `MySQL:handle` parameter. +> - `mysql_error`: reverse the order — `mysql_error(connId, dest)` instead of `mysql_error(dest, max_len, handle)`. +> - `mysql_set_option` → `mysql_options_set_int` / `mysql_options_set_str`. +> - `%s` → `%r` where the old `%s` was intentionally raw. `%e` → `%s` (or keep `%e`). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..30a2db7 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,312 @@ +# Migration from MySQL R41-4 + +This page lists every signature and semantic difference between the **MySQL R41-4** plugin (BlueG / maddinat0r) and **mysql_samp**. For side-by-side gamemode snippets see [migration-examples.md](migration-examples.md). For a flat list of "what changed", see [migration-changes.md](migration-changes.md). + +## Include + +```pawn +// R41-4 +#include + +// mysql_samp +#include +``` + +## Pawn tags + +mysql_samp does **not** use custom Pawn tags. Every handle is a plain `int`. + +| R41-4 | mysql_samp | +|---|---| +| `new MySQL:g_mysql` | `new g_mysql` | +| `new Cache:c = cache_save()` | `new c = cache_save()` | +| `new ORM:o = orm_create(...)` | `new o = orm_create(...)` | +| `new MySQLOpt:o = mysql_init_options()` | `new o = mysql_options_new()` | +| `MYSQL_INVALID_HANDLE` | plain `0` | + +Remove every `MySQL:`, `Cache:`, `ORM:`, `MySQLOpt:`, `E_ORM_ERROR:` and `E_MYSQL_OPTION:` tag. + +## Queries + +| R41-4 | mysql_samp | Notes | +|---|---|---| +| `Cache:mysql_query(handle, query, use_cache)` | *removed* | The sync version blocked the server tick — removed by design | +| `mysql_tquery(handle, query, cb, fmt, ...)` | `mysql_query(connId, query, cb, fmt, ...)` | Same behavior (threaded, FIFO) | +| `mysql_pquery(handle, query, cb, fmt, ...)` | `mysql_pquery(connId, query, cb, fmt, ...)` | Same behavior (threaded, no order) | +| `mysql_query_file(file)` / `mysql_tquery_file(file)` | *removed* | Not supported | + +> In R41-4, `mysql_query` was synchronous. In mysql_samp the same name is always threaded — equivalent to the old `mysql_tquery`. + +## mysql_format: `%s` now escapes + +In R41-4, `%s` inserted the raw string; you had to remember to use `%e` for escaping. In mysql_samp, **`%s` escapes by default** — the safe direction is now the default. + +| Specifier | R41-4 | mysql_samp | +|---|---|---| +| `%d`, `%i` | int | int (unchanged) | +| `%f` | float | float (4 decimals — `{:.4}`) | +| `%s` | **raw** (no escape) | **escaped** | +| `%e` | escaped | escaped (alias of `%s`) | +| `%r` | *does not exist* | **raw**, no escape | +| `%%` | literal `%` | literal `%` | + +How to migrate: + +- `%e` → keep as `%e` or change to `%s`. Both escape. +- `%s` on user input → keep as `%s`. Now safe automatically. +- `%s` used to inject trusted constants (table names, fixed fragments) → change to `%r`. + +```pawn +// R41-4 +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM %s WHERE name = '%e'", tableName, playerName); + +// mysql_samp +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM %r WHERE name = '%s'", tableName, playerName); +``` + +## mysql_escape_string + +| R41-4 | mysql_samp | +|---|---| +| `mysql_escape_string(src, dest, max_len, MySQL:handle)` | `mysql_escape_string(src, dest, max_len)` | + +`mysql_samp` does not take a handle: the escape is a pure function. The charset is always UTF-8 (the plugin forces `SET NAMES utf8mb4` on every new connection). + +## Cache — by-ref → return value + +The largest signature change. R41-4 used out-parameters; mysql_samp returns the value directly. + +| R41-4 | mysql_samp | Change | +|---|---|---| +| `cache_get_row_count(&dest)` | `cache_get_row_count()` → `int` | by-ref → return | +| `cache_get_field_count(&dest)` | `cache_get_field_count()` → `int` | by-ref → return | +| `cache_get_result_count(&dest)` | *removed* | Multi-result sets | +| `cache_get_value_index(row, col, dest[], max_len)` | unchanged | — | +| `cache_get_value_index_int(row, col, &dest)` | `cache_get_value_index_int(row, col)` → `int` | by-ref → return | +| `cache_get_value_index_float(row, col, &Float:dest)` | `cache_get_value_index_float(row, col)` → `Float` | by-ref → return | +| `cache_get_value_name(row, name, dest[], max_len)` | unchanged | — | +| `cache_get_value_name_int(row, name, &dest)` | `cache_get_value_name_int(row, name)` → `int` | by-ref → return | +| `cache_get_value_name_float(row, name, &Float:dest)` | `cache_get_value_name_float(row, name)` → `Float` | by-ref → return | +| `cache_is_value_index_null(row, col, &bool:dest)` | `cache_is_value_index_null(row, col)` → `bool` | by-ref → return | +| `cache_is_value_name_null(row, name, &bool:dest)` | `cache_is_value_name_null(row, name)` → `bool` | by-ref → return | +| `cache_set_result(idx)` | *removed* | Multi-result sets | +| `cache_save()` | `cache_save()` | No `Cache:` tag | +| `cache_delete(Cache:id)` | `cache_delete(id)` | No tag | +| `cache_set_active(Cache:id)` | `cache_set_active(id)` | No tag | +| `cache_unset_active()` | unchanged | — | +| `cache_affected_rows()` | unchanged | — | +| `cache_insert_id()` | unchanged | — | +| `cache_warning_count()` | unchanged | — | +| `cache_get_query_exec_time(unit)` | `cache_get_query_exec_time()` | No `unit` parameter — value is always milliseconds | +| `cache_get_query_string(dest, max_len)` | unchanged | — | +| `cache_is_any_active()` | unchanged | — | +| `cache_is_valid(Cache:id)` | `cache_is_valid(id)` | No tag | + +R41-4 stocks `cache_num_rows()` / `cache_num_fields()` map directly to `cache_get_row_count()` / `cache_get_field_count()`. + +## ORM + +| R41-4 | mysql_samp | Difference | +|---|---|---| +| `ORM:orm_create(table, MySQL:handle)` | `orm_create(table, connId)` | No tags | +| `orm_destroy(ORM:id)` | `orm_destroy(orm_id)` → `bool` | Now returns `bool` | +| `E_ORM_ERROR:orm_errno(ORM:id)` | `orm_errno(orm_id)` → `int` | No tag | +| `orm_select` / `orm_update` / `orm_insert` / `orm_delete` / `orm_save` | same names, no `ORM:` tag | — | +| `orm_load(...)` | *removed* | Was an alias of `orm_select` | +| `orm_apply_cache(ORM:id, row, result_idx)` | `orm_apply_cache(orm_id, row)` | No `result_idx` (single result set) | +| `orm_addvar_int` / `orm_addvar_float` / `orm_addvar_string` | same names, no tag | `max_len` now capped at 4096 | +| `orm_delvar(ORM:id, col)` | `orm_delvar(orm_id, col)` | No tag | +| `orm_clear_vars(ORM:id)` | `orm_clear_vars(orm_id)` | No tag | +| `orm_setkey(ORM:id, col)` | `orm_setkey(orm_id, col)` | No tag | + +ORM error enum: + +| R41-4 | mysql_samp | +|---|---| +| `ERROR_INVALID = 0` | *does not exist* | +| `ERROR_OK = 1` | `ORM_OK = 0` | +| `ERROR_NO_DATA = 2` | `ORM_NO_DATA = 1` | + +## Connection + +| R41-4 | mysql_samp | +|---|---| +| `MySQL:mysql_connect(host, user, pass, db, MySQLOpt:opt)` | `mysql_connect(host, user, pass, db, options)` | +| `MySQLOpt:mysql_init_options()` | `mysql_options_new()` | +| `mysql_set_option(opt, type, ...)` | `mysql_options_set_int(opt, type, val)` / `mysql_options_set_str(opt, type, val[])` | +| `mysql_close(MySQL:handle)` | `mysql_close(connId)` | +| `mysql_stat(dest, max_len, MySQL:handle)` | `mysql_status(connId, dest, max_len)` | +| `mysql_connect_file(file)` | *removed* | +| `mysql_global_options(type, val)` | *removed* | + +**Argument order changed:** in R41-4, the `MySQL:handle` is the last (optional) parameter on most calls. In mysql_samp, the `connId` is always the **first** parameter. + +## Error + +| R41-4 | mysql_samp | Difference | +|---|---|---| +| `mysql_errno(MySQL:handle)` | `mysql_errno(connId = 0)` | No tag, `connId` first | +| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | `connId` first (was last) | + +## Charset + +| R41-4 | mysql_samp | +|---|---| +| `mysql_set_charset(charset, MySQL:handle)` | `mysql_set_charset(connId, charset)` | +| `mysql_get_charset(dest, max_len, MySQL:handle)` | `mysql_get_charset(connId, dest, max_len)` | + +`connId` moved to the first position in both. + +## Log + +| R41-4 | mysql_samp | +|---|---| +| `mysql_log(E_LOGLEVEL:level)` — bitflags (`DEBUG = 1`, `INFO = 2`, `WARNING = 4`, `ERROR = 8`) | `mysql_log(level)` — sequential (`NONE = 0`, `ERROR = 1`, `WARNING = 2`, `INFO = 3`, `ALL = 4`) | + +## Callback format + +The mysql_samp callback format accepts both `"d"` and `"i"` as int. No change needed if you already use `"i"`. + +| Char | R41-4 | mysql_samp | +|---|---|---| +| `i` | int | int | +| `d` | *not accepted* | int | +| `f` | float | float | +| `s` | string | string | + +## Forward + +| R41-4 | mysql_samp | +|---|---| +| `OnQueryError(errorid, error[], callback[], query[], MySQL:handle)` | `OnQueryError(errorid, error[], callback[], query[], connId)` | + +Same parameters, no `MySQL:` tag on the last one. + +## Removed natives + +| R41-4 | Why | +|---|---| +| `Cache:mysql_query(handle, query, use_cache)` | The synchronous version blocked the server tick | +| `mysql_tquery_file(...)` / `mysql_query_file(...)` | Not supported | +| `mysql_connect_file(file)` | Not supported | +| `mysql_global_options(type, val)` | Not applicable | +| `cache_get_result_count(&dest)` | Multi-result sets — not used by SA-MP gamemodes | +| `cache_set_result(idx)` | Multi-result sets | +| `orm_load(...)` | Was an alias of `orm_select` | + +## Step-by-step + +### 1. Swap the include + +```pawn +// before +#include + +// after +#include +``` + +### 2. Rename query calls + +```pawn +// before +mysql_tquery(g_mysql, query, "OnData", "i", playerid); + +// after (both forms work) +mysql_query(g_mysql, query, "OnData", "d", playerid); +mysql_query(g_mysql, query, "OnData", "i", playerid); +``` + +### 3. Update mysql_format placeholders + +```pawn +// before (R41-4: %s = raw, %e = escaped) +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM %s WHERE name = '%e'", table, name); + +// after (mysql_samp: %r = raw, %s = escaped) +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM %r WHERE name = '%s'", table, name); +``` + +### 4. Update connect + options + +```pawn +// before +new MySQLOpt:opt = mysql_init_options(); +mysql_set_option(opt, SERVER_PORT, 3307); +new MySQL:g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); + +// after +new opt = mysql_options_new(); +mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); +new g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); + +// or, if port 3306 is fine, drop the options entirely: +new g_mysql = mysql_connect("127.0.0.1", "root", "pass", "db"); +``` + +### 5. Update mysql_escape_string + +```pawn +// before +mysql_escape_string(input, escaped, sizeof(escaped), g_mysql); + +// after +mysql_escape_string(input, escaped); +``` + +### 6. Convert cache reads to return-value form + +```pawn +// before +new score; +cache_get_value_name_int(0, "score", score); + +// after +new score = cache_get_value_name_int(0, "score"); +``` + +### 7. Convert row/field counts + +```pawn +// before +new rows; +cache_get_row_count(rows); // by-ref +new rows2 = cache_num_rows(); // stock wrapper + +// after +new rows = cache_get_row_count(); +``` + +### 8. Strip every Pawn tag + +```pawn +// before +new MySQL:g_mysql; +new Cache:c = cache_save(); +new ORM:o = orm_create("table", g_mysql); + +// after +new g_mysql; +new c = cache_save(); +new o = orm_create("table", g_mysql); +``` + +### 9. Swap `mysql_error` argument order + +```pawn +// before +new err[256]; +mysql_error(err, sizeof(err), g_mysql); + +// after +new err[256]; +mysql_error(g_mysql, err); +``` + +### 10. Compile and test + +Compile the gamemode against the new include and run it. Watch `logs/mysql.log` for errors that the console summarized. diff --git a/docs/mudancas.md b/docs/mudancas.md deleted file mode 100644 index 289457f..0000000 --- a/docs/mudancas.md +++ /dev/null @@ -1,282 +0,0 @@ -# O que mudou na pratica - -Referencia rapida de tudo que muda ao migrar do **MySQL R41-4** para o **mysql_samp**. - -## Include - -```pawn -// R41-4 -#include - -// mysql_samp -#include -``` - -## Tags removidas - -O mysql_samp nao usa tags customizadas. Tudo e `int` simples. - -| R41-4 | mysql_samp | -|---|---| -| `new MySQL:gMysql` | `new gMysql` | -| `new Cache:cache = cache_save();` | `new cache = cache_save();` | -| `new ORM:orm = orm_create(...)` | `new orm = orm_create(...)` | -| `cache_delete(Cache:cache);` | `cache_delete(cache);` | -| `MySQLOpt:opt = mysql_init_options()` | `new opt = mysql_options_new()` | - -## Formato de callback - -| R41-4 | mysql_samp | Significado | -|---|---|---| -| `"i"` | `"d"` ou `"i"` | Inteiro (ambos funcionam no mysql_samp) | -| `"f"` | `"f"` | Float (igual) | -| `"s"` | `"s"` | String (igual) | - -> **Nota:** o mysql_samp aceita tanto `"d"` quanto `"i"` para inteiros. Se voce ja usa `"i"`, nao precisa mudar. - -```pawn -// R41-4 -mysql_tquery(gMysql, query, "OnLoad", "i", playerid); - -// mysql_samp (ambos funcionam) -mysql_query(gMysql, query, "OnLoad", "d", playerid); -mysql_query(gMysql, query, "OnLoad", "i", playerid); -``` - -## mysql_format: especificadores - -| Especificador | R41-4 | mysql_samp | -|---|---|---| -| `%d` / `%i` | Inteiro | Inteiro (igual) | -| `%f` | Float | Float (igual) | -| `%s` | String **raw** (sem escape) | String **escaped** (com escape) | -| `%e` | String escaped | String escaped (alias de %s) | -| `%r` | *(nao existe)* | String raw (sem escape) | - -**Regra pratica:** onde voce usava `%e`, use `%s`. Onde usava `%s` para nomes de tabela/coluna, use `%r`. - -## Queries - -| R41-4 | mysql_samp | Comportamento | -|---|---|---| -| `Cache:mysql_query(handle, query, use_cache)` | *(removido)* | Era sincrona — nao existe mais | -| `mysql_tquery(handle, query, cb, fmt, ...)` | `mysql_query(connId, query, cb, fmt, ...)` | Threaded FIFO (mesmo comportamento) | -| `mysql_pquery(handle, query, cb, fmt, ...)` | `mysql_pquery(connId, query, cb, fmt, ...)` | Paralela sem ordem (igual) | - -**Importante:** toda query e non-blocking. O cache so esta disponivel dentro do callback. - -## Conexao - -| R41-4 | mysql_samp | -|---|---| -| `MySQL:mysql_connect(host, user, pass, db, MySQLOpt:opt)` | `mysql_connect(host, user, pass, db, options)` | -| `MySQLOpt:mysql_init_options()` | `mysql_options_new()` | -| `mysql_set_option(MySQLOpt:opt, type, ...)` | `mysql_options_set_int(opt, type, val)` / `mysql_options_set_str(opt, type, val[])` | -| `mysql_escape_string(src, dest, max_len, MySQL:handle)` | `mysql_escape_string(src, dest, max_len)` | -| `mysql_stat(dest, max_len, MySQL:handle)` | `mysql_status(connId, dest, max_len)` | -| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | -| `mysql_errno(MySQL:handle)` | `mysql_errno(connId)` | -| `mysql_set_charset(charset, MySQL:handle)` | `mysql_set_charset(connId, charset)` | -| `mysql_get_charset(dest, max_len, MySQL:handle)` | `mysql_get_charset(connId, dest, max_len)` | -| `mysql_close(MySQL:handle)` | `mysql_close(connId)` | -| `mysql_unprocessed_queries(MySQL:handle)` | `mysql_unprocessed_queries()` | -| *(nao existe)* | `mysql_log(level)` | - -**Diferenca de estilo:** no R41-4, o handle e o ultimo parametro (opcional, default `MySQL:1`). No mysql_samp, o connId e o primeiro parametro. - -**Porta:** no R41-4, usa-se `mysql_set_option(opt, SERVER_PORT, 3307)`. No mysql_samp, usa-se `mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307)`. - -```pawn -// R41-4 -new MySQLOpt:opt = mysql_init_options(); -mysql_set_option(opt, SERVER_PORT, 3307); -new MySQL:gMysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); - -// mysql_samp -new opt = mysql_options_new(); -mysql_options_set_int(opt, MYSQL_OPT_PORT, 3307); -new gMysql = mysql_connect("127.0.0.1", "root", "pass", "db", opt); - -// mysql_samp (porta padrao 3306 — sem options) -new gMysql = mysql_connect("127.0.0.1", "root", "pass", "db"); -``` - -## Cache — mudanca de assinatura (by-ref → retorno direto) - -**Mudanca mais significativa:** no R41-4, a maioria das natives de cache escrevem o valor por referencia (`&destination`). No mysql_samp, elas **retornam o valor diretamente**. - -### Contagem de linhas/colunas - -| R41-4 | mysql_samp | -|---|---| -| `cache_get_row_count(&dest)` | `cache_get_row_count()` retorna `int` | -| `cache_get_field_count(&dest)` | `cache_get_field_count()` retorna `int` | -| `cache_num_rows()` (stock wrapper) | `cache_get_row_count()` | -| `cache_num_fields()` (stock wrapper) | `cache_get_field_count()` | - -```pawn -// R41-4 -new rows; -cache_get_row_count(rows); -// ou -new rows = cache_num_rows(); - -// mysql_samp -new rows = cache_get_row_count(); -``` - -### Leitura de valores por indice - -| R41-4 | mysql_samp | -|---|---| -| `cache_get_value_index(row, col, dest[], max_len)` | `cache_get_value_index(row, col, dest[], max_len)` (igual) | -| `cache_get_value_index_int(row, col, &dest)` | `cache_get_value_index_int(row, col)` retorna `int` | -| `cache_get_value_index_float(row, col, &Float:dest)` | `cache_get_value_index_float(row, col)` retorna `Float` | - -```pawn -// R41-4 -new score; -cache_get_value_index_int(0, 2, score); - -// mysql_samp -new score = cache_get_value_index_int(0, 2); -``` - -### Leitura de valores por nome - -| R41-4 | mysql_samp | -|---|---| -| `cache_get_value_name(row, col_name, dest[], max_len)` | `cache_get_value_name(row, col_name, dest[], max_len)` (igual) | -| `cache_get_value_name_int(row, col_name, &dest)` | `cache_get_value_name_int(row, col_name)` retorna `int` | -| `cache_get_value_name_float(row, col_name, &Float:dest)` | `cache_get_value_name_float(row, col_name)` retorna `Float` | - -```pawn -// R41-4 -new score; -cache_get_value_name_int(0, "score", score); -new Float:pos_x; -cache_get_value_name_float(0, "pos_x", pos_x); - -// mysql_samp -new score = cache_get_value_name_int(0, "score"); -new Float:pos_x = cache_get_value_name_float(0, "pos_x"); -``` - -### NULL check - -| R41-4 | mysql_samp | -|---|---| -| `cache_is_value_index_null(row, col, &bool:dest)` | `cache_is_value_index_null(row, col)` retorna `bool` | -| `cache_is_value_name_null(row, col_name, &bool:dest)` | `cache_is_value_name_null(row, col_name)` retorna `bool` | - -```pawn -// R41-4 -new bool:is_null; -cache_is_value_name_null(0, "email", is_null); - -// mysql_samp -new bool:is_null = cache_is_value_name_null(0, "email"); -``` - -## Cache — natives que mantiveram assinatura - -- `cache_get_value_index(row, col, dest[], max_len)` — string por indice -- `cache_get_value_name(row, col_name, dest[], max_len)` — string por nome -- `cache_get_field_name(idx, dest[], max_len)` — nome da coluna -- `cache_affected_rows()` — rows afetadas -- `cache_insert_id()` — ultimo ID inserido -- `cache_warning_count()` — numero de warnings -- `cache_save()` — salva cache (retorna ID) -- `cache_delete(cache_id)` — remove cache salvo -- `cache_set_active(cache_id)` — ativa cache salvo -- `cache_unset_active()` — desativa cache manual -- `cache_is_any_active()` — verifica se ha cache ativo -- `cache_is_valid(cache_id)` — verifica se cache existe -- `cache_get_query_string(dest[], max_len)` — query original - -## Cache — natives novas - -| Native | Descricao | -|---|---| -| `cache_get_field_type(idx)` | Tipo MySQL da coluna | -| `cache_get_query_exec_time()` | Tempo de execucao em ms | - -## Cache — natives removidas - -| R41-4 | Razao | -|---|---| -| `cache_get_result_count(&dest)` | Multi-result sets desnecessario para SA:MP | -| `cache_set_result(idx)` | Multi-result sets desnecessario para SA:MP | -| `cache_get_query_exec_time(unit)` | Substituido por versao simplificada (sempre ms) | - -## ORM - -A API e similar ao R41-4, com diferencas: - -| R41-4 | mysql_samp | Diferenca | -|---|---|---| -| `ORM:orm_create(table, MySQL:handle)` | `orm_create(table, connId)` | Sem tags | -| `orm_destroy(ORM:id)` | `orm_destroy(orm_id)` retorna `bool` | Retorno adicionado | -| `E_ORM_ERROR:orm_errno(ORM:id)` | `orm_errno(orm_id)` retorna `int` | Sem tag de enum | -| `orm_apply_cache(ORM:id, row, result_idx)` | `orm_apply_cache(orm_id, row)` | Sem result_idx | -| `orm_load(...)` | *(removido)* | Alias de orm_select | - -**Enum de erro diferente:** - -| R41-4 | mysql_samp | -|---|---| -| `ERROR_INVALID = 0` | *(nao existe)* | -| `ERROR_OK = 1` | `ORM_OK = 0` | -| `ERROR_NO_DATA = 2` | `ORM_NO_DATA = 1` | - -Demais natives ORM (select, update, insert, delete, save, addvar_*, delvar, clear_vars, setkey) tem a mesma assinatura, apenas sem tags `ORM:`. - -## Erro - -| R41-4 | mysql_samp | Diferenca | -|---|---|---| -| `mysql_errno(MySQL:handle)` | `mysql_errno(connId)` | Sem tag, connId primeiro | -| `mysql_error(dest, max_len, MySQL:handle)` | `mysql_error(connId, dest, max_len)` | connId primeiro (era ultimo) | - -## Log - -| R41-4 | mysql_samp | -|---|---| -| `mysql_log(E_LOGLEVEL:level)` | `mysql_log(level)` | -| Bitflags: DEBUG=1, INFO=2, WARNING=4, ERROR=8 | Sequencial: NONE=0, ERROR=1, WARNING=2, INFO=3, ALL=4 | - -```pawn -// R41-4 -mysql_log(ERROR | WARNING); - -// mysql_samp -mysql_log(MYSQL_LOG_WARNING); // inclui ERROR e WARNING -``` - -## Forward - -| R41-4 | mysql_samp | -|---|---| -| `OnQueryError(errorid, error[], callback[], query[], MySQL:handle)` | `OnQueryError(errorid, error[], callback[], query[], connId)` | - -Mesma assinatura, sem tag `MySQL:`. - -## Resumo das mudancas obrigatorias - -1. Trocar `#include ` por `#include ` -2. Trocar `mysql_tquery` por `mysql_query` -3. Trocar `%s` (raw) por `%r` no `mysql_format` onde usava strings nao-escapadas -4. Trocar `mysql_escape_string(..., MySQL:handle)` por `mysql_escape_string(src, dest, max_len)` (sem handle) -5. Trocar `cache_num_rows()` por `cache_get_row_count()` -6. Adaptar `cache_get_value_*_int` e `cache_get_value_*_float` de by-ref para retorno direto -7. Adaptar `cache_is_value_*_null` de by-ref para retorno direto -8. Adaptar `cache_get_row_count` e `cache_get_field_count` de by-ref para retorno direto -9. Remover todas as tags (`MySQL:`, `Cache:`, `ORM:`, `MySQLOpt:`, `E_ORM_ERROR:`) -10. Trocar `mysql_init_options()` por `mysql_options_new()` -11. Trocar `mysql_set_option(opt, type, ...)` por `mysql_options_set_int/set_str` -12. Inverter ordem dos params em `mysql_error` (connId agora e primeiro) -13. Trocar `mysql_stat` por `mysql_status` (connId primeiro) -14. Mover codigo que acessava cache apos query sincrona para dentro de callbacks - -**Mudancas opcionais:** -- `"i"` → `"d"` nos formatos de callback (ambos funcionam no mysql_samp) diff --git a/docs/options.md b/docs/options.md index 5eb3b17..8e00e99 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1,25 +1,8 @@ -# Options de conexão +# Options -O mysql_samp permite configurar parâmetros opcionais de conexão antes de chamar `mysql_connect`. Sem options, os valores padrão são usados automaticamente. +Options are **optional**. A bare `mysql_connect("host", "user", "pass", "db")` uses the defaults below. Build an options handle only when you need a non-default port, a connection timeout or to opt out of auto-reconnect. ---- - -## Como usar - -```pawn -new opts = mysql_options_new(); -mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); -mysql_options_set_int(opts, MYSQL_OPT_SSL, 1); -mysql_options_set_str(opts, MYSQL_OPT_SSL_CA, "/etc/ssl/certs/ca.pem"); - -new gMysql = mysql_connect("127.0.0.1", "root", "senha", "meu_banco", opts); -``` - -O handle de options pode ser reutilizado em múltiplas conexões. Ele não é destruído automaticamente — você pode descartá-lo após o `mysql_connect` sem efeitos colaterais. - ---- - -## Natives +## API ```pawn native mysql_options_new(); @@ -27,170 +10,93 @@ native bool:mysql_options_set_int(handle, option, value); native bool:mysql_options_set_str(handle, option, const value[]); ``` -- `mysql_options_new()` — cria um novo conjunto de options com valores padrão. Retorna o handle (>= 1) ou 0 em caso de falha. -- `mysql_options_set_int(handle, option, value)` — define uma option de valor inteiro. Retorna `true` em sucesso. -- `mysql_options_set_str(handle, option, value[])` — define uma option de valor string. Retorna `true` em sucesso. - ---- +- `mysql_options_new()` creates a fresh handle (`>= 1`) populated with the defaults. +- `mysql_options_set_int(handle, option, value)` returns `true` on success, `false` if the handle is invalid, the option does not exist, the option is a string-only option, or the integer is out of range for the option type (for example a negative port). +- `mysql_options_set_str(handle, option, value)` returns `true` on success, `false` if the handle is invalid, the option does not exist, or the option is an integer-only option. -## Options disponíveis +Handles are not destroyed automatically. There is no explicit destroy native — once you pass the handle to `mysql_connect`, you can drop the reference. Memory used by the handle stays alive for the duration of the plugin (handles are stored in a `HashMap` keyed by id) and the cost is negligible (a small struct per handle). -### `MYSQL_OPT_PORT` (int) - -**Padrão:** `3306` - -Porta TCP do servidor MySQL. +## Usage ```pawn +new opts = mysql_options_new(); mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); -``` - -Use quando o MySQL não está na porta padrão, ou quando você usa um proxy/túnel em outra porta. - ---- - -### `MYSQL_OPT_SSL` (int, bool) - -**Padrão:** `0` (desativado) - -Ativa ou desativa o uso de SSL/TLS na conexão. +mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 10); +mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 0); -```pawn -mysql_options_set_int(opts, MYSQL_OPT_SSL, 1); // ativar -mysql_options_set_int(opts, MYSQL_OPT_SSL, 0); // desativar +new g_mysql = mysql_connect("db.example.com", "user", "pass", "samp_db", opts); ``` -O mysql_samp usa **rustls** internamente — sem dependência de `libssl` do sistema. Funciona em qualquer distribuição Linux sem instalar nada. - -> **Atenção:** ativar SSL sem fornecer `MYSQL_OPT_SSL_CA` faz a conexão aceitar qualquer certificado do servidor. Para validação completa, forneça o caminho do CA via `MYSQL_OPT_SSL_CA`. - ---- +The same handle can be reused across multiple `mysql_connect` calls. -### `MYSQL_OPT_SSL_CA` (string) +## Available options -**Padrão:** nenhum (SSL sem verificação de CA) +| Constant | Integer / String | Default | Description | +|---|---|---|---| +| `MYSQL_OPT_PORT` | int | `3306` | TCP port. Must fit in `u16` (`0..=65535`). | +| `MYSQL_OPT_SSL` | int (bool) | `0` (off) | SSL toggle — **currently a no-op**, see [SSL caveat](#ssl) below. | +| `MYSQL_OPT_SSL_CA` | string | empty | Path to a PEM CA file — **currently a no-op**, see [SSL caveat](#ssl) below. | +| `MYSQL_OPT_CONNECT_TIMEOUT` | int (seconds) | none | TCP connect timeout. Must fit in `u32`. | +| `MYSQL_OPT_AUTO_RECONNECT` | int (bool) | `1` (on) | Retry a query once when the server drops the connection (see [MYSQL_OPT_AUTO_RECONNECT](#mysql_opt_auto_reconnect)). | -Caminho absoluto para o arquivo de certificado CA (`.pem`) usado para validar o certificado do servidor MySQL. +### MYSQL_OPT_PORT ```pawn -mysql_options_set_str(opts, MYSQL_OPT_SSL_CA, "/etc/ssl/certs/mysql-ca.pem"); +mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); ``` -Requer que `MYSQL_OPT_SSL` esteja ativado. Garante que a conexão seja estabelecida apenas com o servidor correto, prevenindo ataques man-in-the-middle. - -> Aceita somente valores via `mysql_options_set_str`. Chamar `mysql_options_set_int` com esta option retorna `false`. +Ignored when the host is a Unix socket (path starts with `/`). ---- +### MYSQL_OPT_CONNECT_TIMEOUT -### `MYSQL_OPT_CONNECT_TIMEOUT` (int, segundos) - -**Padrão:** sem timeout (aguarda indefinidamente) - -Tempo máximo em segundos para estabelecer a conexão TCP inicial com o servidor MySQL. +Time in seconds the plugin will wait for the initial TCP connection. Without this option the plugin waits indefinitely (the default of the `mysql` crate). ```pawn mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 10); ``` -Se o servidor não responder dentro do prazo, `mysql_connect` retorna 0 e o erro fica disponível via `mysql_errno` / `mysql_error`. - -Recomendado para ambientes de produção onde falhas de rede devem ser detectadas rapidamente em vez de travar o servidor por tempo indeterminado na inicialização. - ---- +If the timeout expires, `mysql_connect` returns `0` and `mysql_errno(0)` returns `MYSQL_ERROR_CONNECTION_FAILED`. -### `MYSQL_OPT_AUTO_RECONNECT` (int, bool) - -**Padrão:** `1` (ativado) - -Controla a reconexão automática em caso de queda da conexão com o MySQL. +### MYSQL_OPT_AUTO_RECONNECT ```pawn -mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 1); // ativar (padrão) -mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 0); // desativar +mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 1); // default, retry once +mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 0); // do not retry, report immediately ``` -**Comportamento quando ativado:** se uma query falhar por perda de conexão — por exemplo, o MySQL reiniciou, o `wait_timeout` expirou, ou houve uma interrupção de rede — o plugin tenta automaticamente obter uma nova conexão do pool e reexecutar a query uma vez antes de reportar o erro ao callback `OnQueryError`. - -**Comportamento quando desativado:** qualquer falha de conexão durante uma query é reportada imediatamente via `OnQueryError`, sem tentativa de reconexão. +**Behavior when enabled:** if a threaded query (`mysql_query` / `mysql_pquery`) fails with a connection-lost error (the `mysql` crate reports `code == 0` for IO and TCP errors), the plugin drops the current connection, fetches a new one from the pool and re-runs the same query once before surfacing the error to `OnQueryError`. -**Quando desativar:** se o seu gamemode precisa saber exatamente quando uma reconexão ocorreu (por exemplo, para reiniciar transações ou tomar ação específica em caso de queda), desative e trate o erro manualmente em `OnQueryError`. +**Behavior when disabled:** the very first failure is reported. No retry. -> Esta option afeta apenas queries em andamento (via `mysql_query` / `mysql_pquery`). A reconexão inicial ao banco na chamada de `mysql_connect` é controlada pelo `MYSQL_OPT_CONNECT_TIMEOUT`. +When to disable: if your gamemode needs to know exactly when a reconnect happened — for instance, to reapply session-scoped state such as `SET @user_id` — turn this off and handle the recovery yourself in `OnQueryError`. ---- +This option only affects in-flight queries. The TCP handshake inside `mysql_connect` is governed by `MYSQL_OPT_CONNECT_TIMEOUT`. -## Tabela resumo +### SSL -| Constant | Tipo | Padrão | Nativa | -|---|---|---|---| -| `MYSQL_OPT_PORT` | int | `3306` | `set_int` | -| `MYSQL_OPT_SSL` | int (bool) | `0` | `set_int` | -| `MYSQL_OPT_SSL_CA` | string | — | `set_str` | -| `MYSQL_OPT_CONNECT_TIMEOUT` | int (segundos) | sem timeout | `set_int` | -| `MYSQL_OPT_AUTO_RECONNECT` | int (bool) | `1` | `set_int` | +> **Important caveat.** `MYSQL_OPT_SSL` and `MYSQL_OPT_SSL_CA` are accepted by `mysql_options_set_int` / `mysql_options_set_str` and the values are stored in the options struct, but the connection builder in [`src/connection.rs`](https://github.com/NullSablex/mysql_samp/blob/master/src/connection.rs) does **not** wire them through to the `mysql` crate yet — there is a `TODO` waiting for the `mysql` crate to expose rustls-friendly options. Setting these today has no effect on the connection: the plugin always connects in clear text. ---- +The `mysql` crate is compiled with the `default-rust` feature (rustls is in the binary), so the SSL plumbing exists in the dependency tree — it just is not exposed by the plugin yet. Until that lands, plan around it (use a TLS-terminating proxy, or accept the limitation in your threat model). -## Exemplos completos +## Setting a string option on an int-only option (or vice-versa) -### Conexão básica sem options +The plugin rejects mismatched setters: ```pawn -new gMysql = mysql_connect("127.0.0.1", "root", "senha", "meu_banco"); +mysql_options_set_str(opts, MYSQL_OPT_PORT, "3307"); // returns false, port is int +mysql_options_set_int(opts, MYSQL_OPT_SSL_CA, 1); // returns false, ssl_ca is string ``` -Porta 3306, sem SSL, sem timeout, reconexão automática ativada. +The only string option today is `MYSQL_OPT_SSL_CA`. Everything else is int. ---- +## Out-of-range integers -### Porta customizada +`MYSQL_OPT_PORT` must fit in `u16`. `MYSQL_OPT_CONNECT_TIMEOUT` must fit in `u32`. Negative or oversized values are rejected: ```pawn -new opts = mysql_options_new(); -mysql_options_set_int(opts, MYSQL_OPT_PORT, 3307); - -new gMysql = mysql_connect("127.0.0.1", "root", "senha", "meu_banco", opts); +mysql_options_set_int(opts, MYSQL_OPT_PORT, -1); // false +mysql_options_set_int(opts, MYSQL_OPT_PORT, 70000); // false (over u16::MAX) +mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, -5); // false ``` ---- - -### SSL com CA + timeout - -```pawn -new opts = mysql_options_new(); -mysql_options_set_int(opts, MYSQL_OPT_PORT, 3306); -mysql_options_set_int(opts, MYSQL_OPT_SSL, 1); -mysql_options_set_str(opts, MYSQL_OPT_SSL_CA, "/etc/ssl/certs/mysql-ca.pem"); -mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 15); - -new gMysql = mysql_connect("db.servidor.com", "usuario", "senha", "producao", opts); -if (!gMysql) -{ - new err[256]; - mysql_error(0, err); - printf("[MySQL] Falha na conexao: %s", err); -} -``` - ---- - -### Desativar reconexão automática (controle manual) - -```pawn -new opts = mysql_options_new(); -mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 0); - -new gMysql = mysql_connect("127.0.0.1", "root", "senha", "meu_banco", opts); -``` - -```pawn -// Tratamento manual de erros de conexão -public OnQueryError(errorid, const error[], const callback[], const query[], connId) -{ - if (errorid == MYSQL_ERROR_QUERY_FAILED) - { - printf("[MySQL] Query falhou na conexao %d: %s", connId, error); - // Lógica de recuperação personalizada aqui - } -} -``` +This is stricter than the old MySQL R41-4 plugin, which silently wrapped negative ports to large positive values. Catch the `false` return and fix the input. diff --git a/docs/orm.md b/docs/orm.md index 7e0efe6..58820a9 100644 --- a/docs/orm.md +++ b/docs/orm.md @@ -1,15 +1,19 @@ # ORM -O ORM (Object-Relational Mapping) mapeia variaveis Pawn para colunas de uma tabela no banco de dados. Com ele voce pode fazer SELECT, INSERT, UPDATE e DELETE sem escrever SQL manualmente. +The ORM maps Pawn variables to MySQL columns. With it, SELECT / INSERT / UPDATE / DELETE / save are one-liners — the plugin generates the SQL from the bound variables. -## Conceito +## Concept -1. Crie uma instancia ORM vinculada a uma tabela e conexao -2. Associe variaveis Pawn as colunas (bindings) -3. Defina a coluna chave primaria -4. Execute operacoes CRUD — o SQL e gerado automaticamente +1. Create an ORM instance attached to a table and a connection. +2. Bind Pawn variables to columns (`orm_addvar_*`). +3. Declare the primary key column (`orm_setkey`). +4. Run CRUD operations — every call reads the current values from the bound variables, generates the SQL, and runs the query through the standard threaded pipeline. -## Criacao e destruicao +The five threaded CRUD natives (`orm_select`, `orm_update`, `orm_insert`, `orm_delete`, `orm_save`) accept an optional callback with the **same format string convention as `mysql_query`** (`d`/`i`, `f`, `s`). + +ORM instances are owned per-AMX. When the AMX that created the instance unloads, the plugin destroys every ORM bound to it automatically — `Plugin::on_amx_unload` calls `OrmManager::destroy_by_amx`. You do not need to clean up on script unload. + +## Lifecycle ### orm_create @@ -17,7 +21,7 @@ O ORM (Object-Relational Mapping) mapeia variaveis Pawn para colunas de uma tabe native orm_create(const table[], connId); ``` -**Retorno:** ID do ORM (>= 1) ou `0` se a conexao for invalida. +Creates an ORM bound to `table` on `connId`. Returns the ORM id (`>= 1`) or `0` if `connId` does not exist. ### orm_destroy @@ -25,242 +29,212 @@ native orm_create(const table[], connId); native bool:orm_destroy(orm_id); ``` +Returns `true` if the ORM existed and was removed, `false` otherwise. + ```pawn -new ORM:ormPlayer; +new g_orm; -public OnGameModeInit() { - ormPlayer = ORM:orm_create("players", gMysql); - // ... configurar bindings ... +public OnGameModeInit() +{ + g_orm = orm_create("players", g_mysql); + // ... bind variables, set key ... } -public OnGameModeExit() { - orm_destroy(_:ormPlayer); +public OnGameModeExit() +{ + orm_destroy(g_orm); } ``` ## Variable bindings -Associe variaveis Pawn as colunas do banco: - -### orm_addvar_int - ```pawn native bool:orm_addvar_int(orm_id, &var, const column_name[]); -``` - -### orm_addvar_float - -```pawn native bool:orm_addvar_float(orm_id, &Float:var, const column_name[]); -``` - -### orm_addvar_string - -```pawn native bool:orm_addvar_string(orm_id, var[], var_max_len, const column_name[]); -``` - -> `var_max_len` deve estar entre 1 e 4096. Valores fora deste intervalo sao rejeitados. - -### orm_delvar / orm_clear_vars - -```pawn native bool:orm_delvar(orm_id, const column_name[]); native bool:orm_clear_vars(orm_id); -``` - -### orm_setkey - -Define a coluna de chave primaria (necessaria para SELECT, UPDATE, DELETE). - -```pawn native bool:orm_setkey(orm_id, const column_name[]); ``` -### Exemplo completo de setup +- `orm_addvar_int` / `orm_addvar_float` take Pawn variables **by reference** — the ORM stores the AMX address, not the value, so changes from Pawn are picked up on the next query build. +- `orm_addvar_string` requires `1 <= var_max_len <= 4096`. Values outside that range are rejected (`false`). The 4096 cap protects the AMX heap against oversized writes when `orm_apply_cache` copies a column into the Pawn buffer. +- `orm_delvar(column_name)` removes the binding whose column name matches exactly (case-sensitive). +- `orm_clear_vars(orm_id)` drops every binding. +- `orm_setkey(column_name)` declares which bound column is the primary key. Required for `orm_select`, `orm_update`, `orm_delete`. `orm_insert` works without a key. + +### Setup pattern ```pawn -enum PlayerData { +enum PlayerData +{ pId, pName[MAX_PLAYER_NAME], Float:pScore, pLevel } -new gPlayerData[MAX_PLAYERS][PlayerData]; -new ORM:gPlayerORM[MAX_PLAYERS]; +new g_player_data[MAX_PLAYERS][PlayerData]; +new g_player_orm[MAX_PLAYERS]; -stock SetupPlayerORM(playerid) { - new oid = orm_create("players", gMysql); - gPlayerORM[playerid] = ORM:oid; +stock SetupPlayerORM(playerid) +{ + new oid = orm_create("players", g_mysql); + g_player_orm[playerid] = oid; - orm_addvar_int(oid, gPlayerData[playerid][pId], "id"); - orm_addvar_string(oid, gPlayerData[playerid][pName], MAX_PLAYER_NAME, "name"); - orm_addvar_float(oid, gPlayerData[playerid][pScore], "score"); - orm_addvar_int(oid, gPlayerData[playerid][pLevel], "level"); + orm_addvar_int(oid, g_player_data[playerid][pId], "id"); + orm_addvar_string(oid, g_player_data[playerid][pName], MAX_PLAYER_NAME, "name"); + orm_addvar_float(oid, g_player_data[playerid][pScore], "score"); + orm_addvar_int(oid, g_player_data[playerid][pLevel], "level"); orm_setkey(oid, "id"); } ``` -## Operacoes CRUD - -Todas as operacoes sao **non-blocking** (usam `mysql_query` internamente). +## CRUD operations -### orm_select - -Executa um SELECT usando o valor atual da chave primaria. +Every CRUD native shares the same signature: ```pawn native bool:orm_select(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); ``` -SQL gerado: `SELECT col1, col2, ... FROM tabela WHERE chave = valor` +All five route through `mysql_query` (FIFO ordering, non-blocking). Each returns `false` synchronously when: -```pawn -// Define o ID a buscar -gPlayerData[playerid][pId] = playerDBId; +- `orm_id` is unknown (`MYSQL_ERROR_INVALID_ORM`), +- the query cannot be built (`MYSQL_ERROR_ORM_KEY_NOT_SET` for select/update/delete, `MYSQL_ERROR_INVALID_ORM` for insert/save), +- the underlying connection has gone away. + +### orm_select -// Executa SELECT -orm_select(_:gPlayerORM[playerid], "OnPlayerDataLoaded", "d", playerid); +Generated SQL: `SELECT col1, col2, … FROM \`table\` WHERE \`key\` = `. + +```pawn +g_player_data[playerid][pId] = db_id; +orm_select(g_player_orm[playerid], "OnPlayerDataLoaded", "d", playerid); forward OnPlayerDataLoaded(playerid); -public OnPlayerDataLoaded(playerid) { - // Aplica os dados do cache nas variaveis vinculadas - orm_apply_cache(_:gPlayerORM[playerid]); +public OnPlayerDataLoaded(playerid) +{ + // copy cache row 0 into the bound variables + orm_apply_cache(g_player_orm[playerid]); + + if (orm_errno(g_player_orm[playerid]) == ORM_NO_DATA) + { + printf("no row for player %d", playerid); + return; + } - // Agora gPlayerData[playerid] tem os valores do banco - printf("Nome: %s, Level: %d", gPlayerData[playerid][pName], gPlayerData[playerid][pLevel]); + printf("loaded %s (level %d)", + g_player_data[playerid][pName], + g_player_data[playerid][pLevel]); } ``` ### orm_insert -Insere um novo registro com os valores atuais das variaveis vinculadas. +Generated SQL: `INSERT INTO \`table\` (col1, col2, …) VALUES (val1, val2, …)`. ```pawn -native bool:orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); -``` +g_player_data[playerid][pName] = "NewPlayer"; +g_player_data[playerid][pLevel] = 1; +g_player_data[playerid][pScore] = 0.0; -SQL gerado: `INSERT INTO tabela (col1, col2, ...) VALUES (val1, val2, ...)` - -```pawn -// Define os dados -gPlayerData[playerid][pName] = "NovoJogador"; -gPlayerData[playerid][pLevel] = 1; -gPlayerData[playerid][pScore] = 0.0; - -orm_insert(_:gPlayerORM[playerid], "OnPlayerInserted", "d", playerid); +orm_insert(g_player_orm[playerid], "OnPlayerInserted", "d", playerid); forward OnPlayerInserted(playerid); -public OnPlayerInserted(playerid) { - // Obtem o ID auto_increment gerado - gPlayerData[playerid][pId] = cache_insert_id(); - printf("Jogador inserido com ID: %d", gPlayerData[playerid][pId]); +public OnPlayerInserted(playerid) +{ + g_player_data[playerid][pId] = cache_insert_id(); + printf("inserted with id %d", g_player_data[playerid][pId]); } ``` ### orm_update -Atualiza o registro usando o valor atual da chave primaria. +Generated SQL: `UPDATE \`table\` SET col1=val1, col2=val2, … WHERE \`key\` = `. ```pawn -native bool:orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); -``` - -SQL gerado: `UPDATE tabela SET col1=val1, col2=val2, ... WHERE chave = valor` - -```pawn -// Modifica os dados -gPlayerData[playerid][pLevel] = 10; -gPlayerData[playerid][pScore] = 1500.0; +g_player_data[playerid][pLevel] = 10; +g_player_data[playerid][pScore] = 1500.0; -// Salva no banco -orm_update(_:gPlayerORM[playerid]); +orm_update(g_player_orm[playerid]); ``` ### orm_delete -Remove o registro usando o valor atual da chave primaria. +Generated SQL: `DELETE FROM \`table\` WHERE \`key\` = `. ```pawn -native bool:orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); -``` - -SQL gerado: `DELETE FROM tabela WHERE chave = valor` - -```pawn -orm_delete(_:gPlayerORM[playerid], "OnPlayerDeleted", "d", playerid); +orm_delete(g_player_orm[playerid], "OnPlayerDeleted", "d", playerid); forward OnPlayerDeleted(playerid); -public OnPlayerDeleted(playerid) { - printf("Jogador %d removido do banco", playerid); +public OnPlayerDeleted(playerid) +{ + printf("player %d removed", playerid); } ``` ### orm_save -Decide automaticamente entre INSERT e UPDATE baseado no valor da chave primaria: -- Se a chave for `0` (int), `0.0` (float) ou vazia (string) → **INSERT** -- Caso contrario → **UPDATE** +Decides between INSERT and UPDATE by looking at the current value of the key column: -```pawn -native bool:orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); -``` +- Int key equals `0` → INSERT. +- Float key equals `0.0` → INSERT. +- String key is empty (`""`) → INSERT. +- Anything else → UPDATE. ```pawn -// Se pId == 0, faz INSERT. Se pId > 0, faz UPDATE. -orm_save(_:gPlayerORM[playerid], "OnPlayerSaved", "d", playerid); +// First save after creation does an INSERT (id is 0), later saves do UPDATE. +orm_save(g_player_orm[playerid], "OnPlayerSaved", "d", playerid); ``` ## orm_apply_cache -Escreve os valores do cache ativo nas variaveis Pawn vinculadas. - ```pawn native bool:orm_apply_cache(orm_id, row = 0); ``` -| Parametro | Tipo | Descricao | +Writes one row of the active cache into the bound variables. Must be called from a query callback, while the cache is active. Returns `true` on success. + +Failure modes: + +| Condition | Return | `orm_errno` | |---|---|---| -| `orm_id` | int | ID do ORM | -| `row` | int | Indice da linha do cache (padrao: 0) | +| No cache currently active | `false` | unchanged (sets global plugin error to `MYSQL_ERROR_NO_CACHE_ACTIVE`) | +| `orm_id` is unknown | `false` | unchanged (sets global plugin error to `MYSQL_ERROR_INVALID_ORM`) | +| `row` is negative or `>= cache_get_row_count()` | `false` | `ORM_NO_DATA` | -Deve ser chamado **dentro de um callback de query** (quando o cache esta ativo). +When a row index is valid, the plugin walks each binding and copies the matching column into the Pawn variable. Bindings whose column is missing from the cache are silently skipped (the Pawn variable keeps its current value). ## orm_errno -Retorna o codigo de erro da ultima operacao ORM. - ```pawn native orm_errno(orm_id); ``` -| Codigo | Constante | Descricao | +Returns the ORM error code for the last operation on the instance, or `-1` if `orm_id` is unknown. + +| Code | Constant | Meaning | |---|---|---| -| 0 | `ORM_OK` | Sem erro | -| 1 | `ORM_NO_DATA` | SELECT nao retornou dados | +| 0 | `ORM_OK` | The last `orm_apply_cache` succeeded | +| 1 | `ORM_NO_DATA` | The last `orm_apply_cache` failed because the requested row does not exist | -```pawn -forward OnPlayerLoaded(playerid); -public OnPlayerLoaded(playerid) { - orm_apply_cache(_:gPlayerORM[playerid]); +This errno is **only updated by `orm_apply_cache`**. The threaded CRUD natives (`orm_select`/insert/update/delete/save) surface their failures synchronously (return `false`) and through the global `mysql_errno(0)` / `OnQueryError`, not through `orm_errno`. - if (orm_errno(_:gPlayerORM[playerid]) == ORM_NO_DATA) { - // Jogador nao encontrado — criar novo - orm_insert(_:gPlayerORM[playerid]); - } -} -``` +## Generated SQL: safety -## Seguranca +- String columns are escaped through the same `escape_string` used by `mysql_format %s`. +- Table and column names are sanitized through `escape_identifier`, which strips backticks. The output is then wrapped in backticks. +- Numeric columns are formatted directly (`{}` for ints, `{}` for floats — no locale-aware formatting). -- Strings vinculadas sao **escapadas automaticamente** no SQL gerado -- Nomes de tabela e coluna sao sanitizados via `escape_identifier` (backticks removidos) -- ORMs sao destruidos automaticamente quando o AMX e descarregado (previne acesso a memoria invalida) -- `max_len` em `orm_addvar_string` e limitado a 4096 para prevenir escrita fora dos limites +The result is safe against SQL injection through the bound variables. **Do not** insert hostile column names through `orm_setkey` or `orm_addvar_*`; the plugin removes backticks but does not run a full identifier validator. -## Exemplo completo: sistema de jogadores +## End-to-end example ```pawn #include @@ -268,7 +242,8 @@ public OnPlayerLoaded(playerid) { #define MAX_PLAYER_NAME 24 -enum pInfo { +enum pInfo +{ pDBId, pName[MAX_PLAYER_NAME], pLevel, @@ -276,68 +251,72 @@ enum pInfo { } new PlayerInfo[MAX_PLAYERS][pInfo]; -new ORM:PlayerORM[MAX_PLAYERS]; -new gMysql; - -public OnGameModeInit() { - gMysql = mysql_connect("127.0.0.1", "root", "", "samp_server"); - - if (mysql_errno()) { - printf("MySQL: falha na conexao"); +new PlayerORM[MAX_PLAYERS]; +new g_mysql; + +public OnGameModeInit() +{ + g_mysql = mysql_connect("127.0.0.1", "root", "", "samp_server"); + if (mysql_errno() != MYSQL_OK) + { + printf("[MySQL] connection failed"); + return 1; } return 1; } -public OnPlayerConnect(playerid) { - // Cria ORM para o jogador - new oid = orm_create("players", gMysql); - PlayerORM[playerid] = ORM:oid; +public OnPlayerConnect(playerid) +{ + new oid = orm_create("players", g_mysql); + PlayerORM[playerid] = oid; - orm_addvar_int(oid, PlayerInfo[playerid][pDBId], "id"); - orm_addvar_string(oid, PlayerInfo[playerid][pName], MAX_PLAYER_NAME, "name"); - orm_addvar_int(oid, PlayerInfo[playerid][pLevel], "level"); - orm_addvar_float(oid, PlayerInfo[playerid][pMoney], "money"); + orm_addvar_int(oid, PlayerInfo[playerid][pDBId], "id"); + orm_addvar_string(oid, PlayerInfo[playerid][pName], MAX_PLAYER_NAME, "name"); + orm_addvar_int(oid, PlayerInfo[playerid][pLevel], "level"); + orm_addvar_float(oid, PlayerInfo[playerid][pMoney], "money"); orm_setkey(oid, "id"); - // Busca pelo nome GetPlayerName(playerid, PlayerInfo[playerid][pName], MAX_PLAYER_NAME); new query[128]; - mysql_format(gMysql, query, sizeof(query), + mysql_format(g_mysql, query, sizeof(query), "SELECT * FROM players WHERE name = '%s' LIMIT 1", - PlayerInfo[playerid][pName] - ); - mysql_query(gMysql, query, "OnPlayerDataLoaded", "d", playerid); + PlayerInfo[playerid][pName]); + mysql_query(g_mysql, query, "OnPlayerLookup", "d", playerid); return 1; } -forward OnPlayerDataLoaded(playerid); -public OnPlayerDataLoaded(playerid) { - if (cache_get_row_count() > 0) { - orm_apply_cache(_:PlayerORM[playerid]); - printf("Jogador %s carregado (ID: %d, Level: %d)", +forward OnPlayerLookup(playerid); +public OnPlayerLookup(playerid) +{ + if (cache_get_row_count() > 0) + { + orm_apply_cache(PlayerORM[playerid]); + printf("loaded %s (id=%d, level=%d)", PlayerInfo[playerid][pName], PlayerInfo[playerid][pDBId], - PlayerInfo[playerid][pLevel] - ); - } else { - // Novo jogador — inserir + PlayerInfo[playerid][pLevel]); + } + else + { + // brand-new player PlayerInfo[playerid][pLevel] = 1; PlayerInfo[playerid][pMoney] = 0.0; - orm_insert(_:PlayerORM[playerid], "OnPlayerCreated", "d", playerid); + orm_insert(PlayerORM[playerid], "OnPlayerCreated", "d", playerid); } } forward OnPlayerCreated(playerid); -public OnPlayerCreated(playerid) { +public OnPlayerCreated(playerid) +{ PlayerInfo[playerid][pDBId] = cache_insert_id(); - printf("Novo jogador criado com ID: %d", PlayerInfo[playerid][pDBId]); + printf("new player saved with id %d", PlayerInfo[playerid][pDBId]); } -public OnPlayerDisconnect(playerid, reason) { - // Salva dados e destroi ORM - orm_save(_:PlayerORM[playerid]); - orm_destroy(_:PlayerORM[playerid]); +public OnPlayerDisconnect(playerid, reason) +{ + orm_save(PlayerORM[playerid]); + orm_destroy(PlayerORM[playerid]); return 1; } ``` diff --git a/docs/queries.md b/docs/queries.md index 34fa47e..fe19353 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -1,157 +1,195 @@ # Queries -Todas as queries no mysql_samp sao **non-blocking**. Elas executam em threads separadas e o resultado e entregue via callback no tick seguinte. O servidor nunca trava esperando o banco. +Every query in mysql_samp is **non-blocking**: the statement runs on a worker thread and the callback is invoked on a later tick. The server never freezes waiting for the database. -## mysql_query - -Query threaded com callback e ordenacao FIFO. Os callbacks sao chamados na **mesma ordem** em que as queries foram submetidas. +## mysql_query — FIFO-ordered ```pawn native bool:mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); ``` -| Parametro | Tipo | Descricao | +| Parameter | Type | Description | |---|---|---| -| `connId` | int | ID da conexao | -| `query` | string | Query SQL | -| `callback` | string | Nome da public a ser chamada (opcional) | -| `format` | string | Formato dos parametros extras: `d` ou `i`=int, `f`=float, `s`=string | -| `...` | variadic | Parametros extras passados ao callback | +| `connId` | int | Connection id returned by `mysql_connect` | +| `query` | string | Raw SQL statement (already escaped — use `mysql_format` to build it safely) | +| `callback` | string | Pawn public to invoke when the result is ready. Empty (`""`) means fire-and-forget | +| `format` | string | One char per variadic parameter (`d`/`i` = int, `f` = float, `s` = string) | +| `...` | variadic | Values forwarded to the callback in the order they appear | -**Retorno:** `true` se a query foi submetida, `false` em caso de erro. +**Returns:** `true` if the query was queued, `false` if `connId` is unknown. -### Exemplo basico +**Ordering guarantee:** callbacks are delivered in **submission order**, even when the underlying queries finish out of order. A slow query blocks the dispatch of later callbacks until it completes. ```pawn -mysql_query(gMysql, "SELECT * FROM players WHERE level > 5", "OnHighLevelPlayers"); +mysql_query(g_mysql, "SELECT * FROM players WHERE level > 5", "OnHighLevelPlayers"); forward OnHighLevelPlayers(); -public OnHighLevelPlayers() { +public OnHighLevelPlayers() +{ new rows = cache_get_row_count(); - printf("Jogadores nivel alto: %d", rows); + printf("high-level players: %d", rows); } ``` -### Passando parametros ao callback +### Passing parameters to the callback + +The `format` string declares the types; the variadics carry the actual values. The callback sees the parameters in left-to-right order. ```pawn -mysql_query(gMysql, query, "OnPlayerLoaded", "d", playerid); +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM players WHERE id = %d", target_id); +mysql_query(g_mysql, query, "OnPlayerLoaded", "d", playerid); forward OnPlayerLoaded(playerid); -public OnPlayerLoaded(playerid) { - if (cache_get_row_count() > 0) { +public OnPlayerLoaded(playerid) +{ + if (cache_get_row_count() > 0) + { new name[MAX_PLAYER_NAME]; cache_get_value_name(0, "name", name); - printf("Jogador %d: %s", playerid, name); + printf("player %d: %s", playerid, name); } } ``` -### Query sem callback (fire-and-forget) +Callback format characters: + +| Char | Pawn type | Notes | +|---|---|---| +| `d`, `i` | int | Both aliases work | +| `f` | float | | +| `s` | string | Pushed as an AMX string allocation | + +Any other character logs a warning at submission time and is skipped. + +### Fire and forget + +Omit the callback to discard the result: ```pawn -mysql_query(gMysql, "UPDATE players SET last_login = NOW() WHERE id = 1"); +mysql_query(g_mysql, "UPDATE players SET last_login = NOW() WHERE id = 1"); ``` -## mysql_pquery +A failure still fires `OnQueryError`. -Query paralela sem garantia de ordem. Os callbacks sao chamados assim que cada query termina, independente da ordem de submissao. +## mysql_pquery — parallel, no ordering ```pawn native bool:mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); ``` -A assinatura e identica a `mysql_query`. Use `mysql_pquery` quando a ordem nao importa e voce quer maxima performance. +Same signature as `mysql_query`, same callback semantics, but **no ordering guarantee**. Callbacks fire as soon as each query finishes; the dispatcher places parallel results behind ordered ones in the same tick. + +Use `mysql_pquery` when the gamemode does not care which callback runs first. ```pawn -// Estas 3 queries executam em paralelo -mysql_pquery(gMysql, "UPDATE stats SET kills = kills + 1 WHERE player_id = 1"); -mysql_pquery(gMysql, "INSERT INTO logs (action) VALUES ('kill')"); -mysql_pquery(gMysql, "SELECT * FROM rewards WHERE player_id = 1", "OnRewards"); +mysql_pquery(g_mysql, "UPDATE stats SET kills = kills + 1 WHERE id = 1"); +mysql_pquery(g_mysql, "INSERT INTO logs (action) VALUES ('kill')"); +mysql_pquery(g_mysql, "SELECT * FROM rewards WHERE id = 1", "OnRewards"); ``` -## mysql_query vs mysql_pquery +## Choosing between mysql_query and mysql_pquery -| Caracteristica | mysql_query | mysql_pquery | +| Concern | `mysql_query` | `mysql_pquery` | |---|---|---| -| Execucao | Threaded | Threaded | -| Ordem de callback | FIFO (garantida) | Sem garantia | -| Uso tipico | SELECT dependente de ordem | UPDATE, INSERT, fire-and-forget | -| Performance | Boa (reordena no dispatch) | Maxima (sem reordenacao) | +| Threading | One worker thread per query | One worker thread per query | +| Callback order | FIFO (submission order) | First done, first dispatched | +| Typical use | SELECT chains that depend on order | UPDATE/INSERT and independent reads | +| Reordering cost | Yes — results buffer until the next sequence is available | None | -## mysql_format +Both natives create one OS thread per query — the cost of a thread spawn is roughly the cost of one TCP round-trip, well below the cost of a real MySQL query. -Formatacao printf-like de queries com escape automatico de strings. +## mysql_tick — backwards compatibility ```pawn -native mysql_format(connId, dest[], max_len, const format[], {Float,_}:...); +native bool:mysql_tick(); ``` -**Retorno:** comprimento da string formatada. +Drains the threaded-query queue manually. **You do not need to call this.** Since rust-samp v3.0.0, the unified `on_tick` hook is driven by `ProcessTick` on SA-MP and by an `ITimersComponent` 5 ms timer on Open Multiplayer native mode; the plugin processes pending results automatically. `mysql_tick` is kept only so old gamemodes that called it explicitly still compile and behave. + +## mysql_format + +```pawn +native mysql_format(connId, dest[], max_len, const format[], {Float,_}:...); +``` -### Especificadores +`printf`-style query builder with **automatic SQL escaping** on `%s` / `%e`. Returns the byte length of the rendered string (after truncation, if any). The `connId` parameter is currently unused but kept for API compatibility — pass the connection you intend to run the query on. -| Especificador | Tipo | Descricao | +| Specifier | Type | Behavior | |---|---|---| -| `%d` ou `%i` | int | Numero inteiro | -| `%f` | float | Numero decimal (4 casas) | -| `%s` | string | String com **escape automatico** (seguro contra SQL injection) | -| `%e` | string | Alias de `%s` (escape automatico) | -| `%r` | string | String **raw** sem escape (use apenas com valores confiaveis) | -| `%%` | — | Literal `%` | +| `%d`, `%i` | int | Decimal integer | +| `%f` | float | 4 decimal places (`{:.4}`) | +| `%s`, `%e` | string | SQL-escaped (safe to drop into a quoted string literal) | +| `%r` | string | Raw, **not** escaped — only for trusted constants | +| `%%` | literal | Single `%` | -### Exemplo +Any other `%x` is left as-is in the output and a single warning is logged for the whole call. + +### Truncation + +If the rendered string is longer than `max_len - 1` (the `-1` reserves a slot for the AMX NUL terminator), the plugin truncates at the nearest UTF-8 char boundary, logs a warning and still returns the length actually written. Old behavior aborted the native; the new behavior is deterministic and survives oversized inputs gracefully. + +### Example ```pawn -new query[256], name[] = "O'Malley"; -mysql_format(gMysql, query, sizeof(query), "SELECT * FROM players WHERE name = '%s'", name); -// Resultado: SELECT * FROM players WHERE name = 'O\'Malley' +new query[256]; +new name[24] = "O'Brien"; -mysql_query(gMysql, query, "OnPlayerFound"); +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM players WHERE name = '%s'", name); +// Result: SELECT * FROM players WHERE name = 'O\'Brien' + +mysql_query(g_mysql, query, "OnPlayerFound"); ``` -### Combinando tipos +Combining types: ```pawn new query[256]; -mysql_format(gMysql, query, sizeof(query), +mysql_format(g_mysql, query, sizeof(query), "INSERT INTO scores (player_id, score, name) VALUES (%d, %f, '%s')", - playerid, 99.5, "Teste" -); + playerid, 99.5, "Player1"); ``` -> **Importante:** `%s` e `%e` sempre escapam a string. Se voce precisa inserir um valor sem escape (como um nome de tabela vindo de uma fonte confiavel), use `%r`. Nunca use `%r` com input do usuario. +> **Rule of thumb.** Use `%s` for every string that originated outside your code. Use `%r` only for compile-time constants such as table names. Never pass user input through `%r`. ## mysql_escape_string -Escapa uma string para uso seguro em SQL. Funcao pura, nao requer conexao. - ```pawn native bool:mysql_escape_string(const src[], dest[], max_len = sizeof(dest)); ``` +Pure escape function — no connection required because the escape rules are connection-independent under UTF-8 (which the plugin forces on every pool connection). Returns `true` on success, `false` only if writing into `dest` fails. + +Characters escaped: `\0`, `\n`, `\r`, `\\`, `'`, `"`, `\x1a` (Ctrl-Z). Every other byte (including `\t`, control bytes, and multi-byte UTF-8) passes through unchanged. + ```pawn -new escaped[128], input[] = "It's a test\""; +new escaped[128]; +new input[] = "It's a \"test\""; mysql_escape_string(input, escaped); -// escaped = "It\'s a test\\\"" +// escaped = "It\'s a \"test\"" ``` -Caracteres escapados: `\0`, `\n`, `\r`, `\\`, `'`, `"`, `\x1a` (EOF). +**Never call `mysql_escape_string` on a string and then pass the result through `%s`** — `%s` escapes again and you end up with `\\'` instead of `\'`. -## Limites - -| Limite | Valor | Descricao | -|---|---|---| -| Rows por resultado | 100.000 | Resultados maiores sao truncados com warning | - -## Queries pendentes +## Inspecting the queue ```pawn native mysql_unprocessed_queries(); ``` -Retorna o numero total de queries em execucao + aguardando dispatch. +Returns `in_flight + pending_ordered.len()` — queries currently spawned on a worker thread plus queries whose results arrived but whose callbacks are blocked behind an earlier sequence. ```pawn -printf("Queries pendentes: %d", mysql_unprocessed_queries()); +printf("pending queries: %d", mysql_unprocessed_queries()); ``` + +Useful for graceful shutdown loops: keep ticking until `mysql_unprocessed_queries() == 0`. + +## Result limits + +| Limit | Value | Behavior on overflow | +|---|---|---| +| Rows per result | 100 000 | Drained but discarded; a warning is logged once per oversized query | +| Saved caches | 1 024 | `cache_save()` returns `0` and logs a warning | +| ORM string buffer | 4 096 chars | `orm_addvar_string` returns `false` | diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..31cdfc8 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,13 @@ +# Build dependencies for the MkDocs Material documentation site. +# +# Pinned with `>=` so the docs workflow picks up patch and minor releases +# automatically (security fixes, new Material features). For a fully +# reproducible build, replace with exact `==` pins. +# +# Used by `.github/workflows/docs.yml` (which also reads this file as the +# `actions/setup-python` `cache: pip` key) and by anyone running +# `mkdocs serve` locally. + +mkdocs>=1.6 +mkdocs-material>=9.5 +pymdown-extensions>=10.7 diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..3b4eba9 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,141 @@ +# Security + +This page describes the security-relevant defaults and the rules you should follow to keep them effective. + +## SQL injection + +The plugin protects against SQL injection through three layers: + +1. **`mysql_format` with `%s` or `%e`** — the formatted string is escaped via the rules below before being substituted. +2. **`mysql_escape_string`** — pure escape function, no connection needed. +3. **ORM** — every bound string is escaped through the same rules; table and column names are sanitized via `escape_identifier`. + +### Safe pattern + +```pawn +new query[256]; +mysql_format(g_mysql, query, sizeof(query), + "SELECT * FROM players WHERE name = '%s'", player_name); +mysql_query(g_mysql, query, "OnPlayerFound"); +``` + +### Unsafe pattern — do not do this + +```pawn +new query[256]; +format(query, sizeof(query), + "SELECT * FROM players WHERE name = '%s'", player_name); // standard a_samp format, no escape +mysql_query(g_mysql, query); +// If player_name == "'; DROP TABLE players; --" → SQL injection. +``` + +### `%s` vs `%r` + +| Specifier | Escaped? | Use for | +|---|---|---| +| `%s`, `%e` | yes | Any value originating outside your code: player input, file contents, network data | +| `%r` | **no** | Compile-time constants only: table names, column names, fixed SQL fragments | + +> **Rule of thumb:** default to `%s`. Use `%r` only when the value is a string literal in your source. + +### Escape rules + +`mysql_escape_string` and `mysql_format %s` use the same backslash-escape rules over UTF-8 input. Bytes escaped: + +| Input | Output | +|---|---| +| `\0` (NUL) | `\0` | +| `\n` | `\n` | +| `\r` | `\r` | +| `\` (backslash) | `\\` | +| `'` | `\'` | +| `"` | `\"` | +| `\x1a` (Ctrl-Z) | `\Z` | +| every other byte | unchanged | + +The escape function is **not idempotent**: feeding its output back through itself produces a deeper-escaped string. Escape **once**, right before the value is interpolated into the SQL. + +## Multi-byte charsets + +The plugin forces `SET NAMES utf8mb4` on every new pool connection. This blocks a class of escape-bypass attacks where multi-byte sequences in legacy charsets (such as `gbk`) can "swallow" the backslash that the escape function added. + +`mysql_set_charset(connId, "...")` lets you change the charset at runtime. Avoid switching to a non-ASCII-safe charset such as `gbk`, `big5` or `sjis` unless you have a specific need — the escape rules above assume an ASCII-safe encoding. + +## Resource limits + +| Resource | Limit | Why | +|---|---|---| +| Saved caches | 1 024 | Prevents memory growth from misused `cache_save` (CWE-770) | +| Rows per single result | 100 000 | Caps the worst-case allocation for a single query (CWE-770) | +| `orm_addvar_string` `max_len` | 1..=4 096 | Bounds the size of writes into the AMX heap when `orm_apply_cache` copies a column (CWE-787) | + +When a limit is hit: + +- the native returns its failure sentinel (`false`, `0`), +- a warning is written to `logs/mysql.log`, +- the server keeps running. + +The 4096 cap on string bindings means a single ORM-managed string column cannot overflow a Pawn array even if a hostile `orm_addvar_string(orm, var, max_len, col)` were attempted with `max_len = INT_MAX`. + +## Integer-conversion safety + +Every cross-width or sign-changing integer conversion in the plugin uses explicit `TryFrom`/`From`, not the silent `as` cast: + +- `i32 → usize` (Pawn row/col indices into Rust container indices): rejected when negative. +- `usize / u64 / u128 → i32` (counts returned to Pawn): saturated at `i32::MAX` instead of wrapping to negative. +- `i32 → u16` (`MYSQL_OPT_PORT`): rejected when negative or `> 65535`. +- `i32 → u32` (`MYSQL_OPT_CONNECT_TIMEOUT`): rejected when negative. + +This is stricter than the old MySQL R41-4 plugin, which silently wrapped values. Callers that pass garbage now get a `false` return instead of an obscure misbehavior. + +## Callback dispatch + +The callback dispatcher checks every step: + +- `find_public` is required to succeed in the first AMX that has the callback; AMXes that do not are silently skipped. +- Every `push` of a parameter checks the result; one failed push aborts the call and logs an error naming the callback. +- A failed string allocation also aborts and logs an error. + +The server cannot crash because of a malformed callback format string — at worst the callback is skipped and a warning is logged. + +## Console hygiene + +`logs/mysql.log` gets full detail. The server console gets a short, sanitized message with the error code only. The console never prints: + +- SQL query text +- Credentials or hostnames +- Row data + +If the log file cannot be written, the plugin emits one console error (`Failed to write logs/mysql.log: …`) and then suppresses further file-write attempts to avoid flooding the console. + +## Authority and ABI + +The plugin uses Rust's `unsafe_op_in_unsafe_fn = "deny"` policy: every `unsafe` block at the call site is annotated, not inherited from a containing `unsafe fn`. The only `unsafe` blocks in the codebase are around the AMX pointer arithmetic in `orm.rs` (reading/writing AMX heap cells), which is bounded by `safe_max.saturating_sub(1)` and a NUL terminator slot. + +The FFI layer (the `samp` crate from rust-samp v3) wraps every native invocation in `catch_unwind`. A panic inside a native logs an error and returns `0` to the AMX caller; the server stays up. + +## Best practices + +1. **Always use `mysql_format` with `%s`** for user input. +2. **Implement `OnQueryError`** — undetected query failures hide bugs. +3. **Verify `cache_get_row_count()`** before reading rows. +4. **Release resources** when you are done with them: + ```pawn + orm_destroy(orm_id); + cache_delete(cache_id); + mysql_close(conn_id); + ``` + The plugin auto-cleans ORMs when their AMX unloads, but explicit destruction is cheap and clear. +5. **Never feed user input through `%r`.** +6. **Avoid `mysql_set_charset` to legacy multi-byte charsets** — stay on `utf8mb4`. + +## Threat model summary + +| CWE | Mitigation | +|---|---| +| CWE-89 (SQL injection) | Automatic escape on `%s` / `%e`, ORM string columns and identifiers | +| CWE-770 (resource exhaustion) | 1024-cache cap, 100k-row cap, 4096-byte ORM string cap | +| CWE-787 (out-of-bounds write) | `orm_addvar_string` `max_len` clamped at 4096; `orm_apply_cache` writes up to `safe_max - 1` bytes plus NUL | +| CWE-252 (unchecked error) | Callback dispatcher checks every AMX operation; failed pushes log and abort | +| CWE-190 (integer overflow) | `wrapping_add(...).max(1)` on every id counter; `TryFrom` on every cross-width conversion | +| Memory safety (general) | Rust borrow checker; no manual memory management; `unsafe` is opt-in per block | diff --git a/docs/seguranca.md b/docs/seguranca.md deleted file mode 100644 index 44b927b..0000000 --- a/docs/seguranca.md +++ /dev/null @@ -1,160 +0,0 @@ -# Seguranca - -O mysql_samp foi desenvolvido com seguranca como prioridade. Este documento descreve as protecoes implementadas e boas praticas de uso. - -## SQL Injection - -### Protecao automatica - -O plugin protege contra SQL injection em multiplas camadas: - -1. **`mysql_format` com `%s`** — Escapa automaticamente a string (aspas, barras, null bytes, etc.) -2. **`mysql_escape_string`** — Funcao pura para escape manual -3. **ORM** — Strings vinculadas sao escapadas automaticamente no SQL gerado -4. **Identificadores** — Nomes de tabela/coluna no ORM sao sanitizados via `escape_identifier` - -### Exemplo seguro - -```pawn -// SEGURO: %s escapa automaticamente -new query[256]; -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM players WHERE name = '%s'", playerName); -``` - -### Exemplo inseguro - -```pawn -// INSEGURO: concatenacao direta — NUNCA faca isso -new query[256]; -format(query, sizeof(query), - "SELECT * FROM players WHERE name = '%s'", playerName); -// Se playerName = "'; DROP TABLE players; --" → SQL injection! -``` - -### %s vs %r - -| Especificador | Escape | Quando usar | -|---|---|---| -| `%s` | Sim | Input do usuario, dados nao confiaveis | -| `%r` | Nao | Valores internos confiaveis (nomes de tabela, constantes) | - -> **Regra:** Use `%s` para tudo. Use `%r` apenas quando voce tem certeza absoluta de que o valor e seguro. - -## UTF-8 - -O plugin forca `SET NAMES utf8mb4` em **todas** as conexoes do pool via `init()` do `OptsBuilder`. Isso previne: - -- **Ataques multi-byte** (GBK injection) onde caracteres multi-byte podem "engolir" barras de escape -- **Truncamento de dados** em caracteres especiais -- **Inconsistencia de encoding** entre cliente e servidor - -Voce pode alterar o charset com `mysql_set_charset`, mas o padrao UTF-8 e o mais seguro. - -## Limites de recursos - -O plugin implementa limites para prevenir esgotamento de memoria e threads: - -| Recurso | Limite | Protecao | -|---|---|---| -| Caches salvos | 1024 | Previne memory exhaustion (CWE-770) | -| Rows por resultado | 100.000 | Previne alocacao massiva de memoria (CWE-770) | -| ORM string max_len | 4096 | Previne escrita fora dos limites (CWE-787) | - -Quando um limite e atingido: -- A operacao falha graciosamente (retorna false/0) -- Um warning e registrado em `logs/mysql.log` -- O servidor continua funcionando normalmente - -## Integer overflow - -Todos os contadores de ID internos (conexoes, options, ORM, cache) usam `wrapping_add` com validacao de minimo 1. Isso previne: - -- Overflow aritmetico em operacoes longas (CWE-190) -- IDs negativos ou zero acidentais - -## Callback safety - -O dispatch de callbacks verifica erros em cada operacao: -- Se um `push` de parametro na stack AMX falhar, o callback e abortado -- Um erro e registrado indicando qual callback falhou -- O servidor continua funcionando (nao crasheia) - -## Logs seguros - -| Destino | Conteudo | -|---|---| -| Console | Mensagens genericas com codigos de erro | -| `logs/mysql.log` | Detalhes completos com timestamp | - -O console **nunca** exibe: -- Queries SQL completas -- Senhas ou credenciais -- Dados de resultado - -## Boas praticas - -### 1. Sempre use `mysql_format` com `%s` - -```pawn -// Correto -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM users WHERE name = '%s'", input); - -// Errado — vulneravel a SQL injection -format(query, sizeof(query), - "SELECT * FROM users WHERE name = '%s'", input); -``` - -### 2. Implemente OnQueryError - -```pawn -public OnQueryError(errorid, const error[], const callback[], const query[], connId) { - printf("[MySQL] Error %d: %s", errorid, error); - return 1; -} -``` - -### 3. Valide dados antes de usar - -```pawn -forward OnData(); -public OnData() { - if (cache_get_row_count() <= 0) return; - - // Agora e seguro ler dados - new val = cache_get_value_name_int(0, "id"); -} -``` - -### 4. Libere recursos - -```pawn -// Destrua ORMs quando nao precisar mais -orm_destroy(ormId); - -// Delete caches salvos quando nao precisar mais -cache_delete(cacheId); - -// Feche conexoes ao descarregar -mysql_close(connId); -``` - -### 5. Nao confie em %r - -```pawn -// NUNCA faca isso com input do usuario -mysql_format(gMysql, query, sizeof(query), - "SELECT * FROM %r WHERE id = %d", userInput, id); -// userInput poderia ser "players; DROP TABLE users; --" -``` - -## CVEs/CWEs mitigados - -| CWE | Severidade | Mitigacao | -|---|---|---| -| CWE-89 | Alta | Escape automatico em %s, %e, ORM strings e identificadores | -| CWE-787 | Alta | Limite de max_len a 4096 no ORM | -| CWE-770 | Media | Limites em caches salvos e rows por resultado | -| CWE-252 | Media | Verificacao de erros no push de parametros AMX | -| CWE-190 | Baixa | wrapping_add em todos os contadores de ID | diff --git a/include/mysql_samp.inc b/include/mysql_samp.inc index d961204..f62faf3 100644 --- a/include/mysql_samp.inc +++ b/include/mysql_samp.inc @@ -1,8 +1,11 @@ /* - * mysql_samp - Plugin MySQL para SA:MP escrito em Rust - * Autor: NullSablex - * Repositorio: https://github.com/NullSablex/mysql_samp - * Licenca: GPL-3.0 + * mysql_samp v1.1.0 — MySQL plugin for SA-MP written in Rust. + * Author: NullSablex + * Repository: https://github.com/NullSablex/mysql_samp + * License: GPL-3.0 + * + * Auto-generated by build.rs — do not edit this file directly. + * To change declarations, edit include/mysql_samp.inc.in instead. */ #if defined _mysql_samp_included @@ -10,7 +13,9 @@ #endif #define _mysql_samp_included -// Opcoes de conexao +#define MYSQL_SAMP_VERSION "1.1.0" + +// Connection options enum { MYSQL_OPT_PORT = 0, MYSQL_OPT_SSL, @@ -19,7 +24,7 @@ enum { MYSQL_OPT_AUTO_RECONNECT, } -// Codigos de erro +// Error codes enum { MYSQL_OK = 0, MYSQL_ERROR_CONNECTION_FAILED, @@ -32,7 +37,7 @@ enum { MYSQL_ERROR_ORM_KEY_NOT_SET, } -// Niveis de log +// Log levels enum { MYSQL_LOG_NONE = 0, MYSQL_LOG_ERROR, @@ -47,7 +52,7 @@ enum { ORM_NO_DATA, } -// Forward: chamado quando uma query threaded falha +// Forward: invoked when a threaded query fails. forward OnQueryError(errorid, const error[], const callback[], const query[], connId); // Options @@ -55,12 +60,12 @@ native mysql_options_new(); native bool:mysql_options_set_int(handle, option, value); native bool:mysql_options_set_str(handle, option, const value[]); -// Conexao +// Connection native mysql_connect(const host[], const user[], const password[], const database[], options = 0); native bool:mysql_close(connId); native bool:mysql_status(connId, dest[], max_len = sizeof(dest)); -// Erro +// Error native mysql_errno(connId = 0); native bool:mysql_error(connId, dest[], max_len = sizeof(dest)); @@ -75,6 +80,9 @@ native bool:mysql_log(log_level); // Query (non-blocking) native bool:mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); native bool:mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); +// Kept for backwards compatibility only: since rust-samp v3.0.0 the unified +// on_tick already dispatches callbacks automatically on SA-MP and native open.mp. +native bool:mysql_tick(); native bool:mysql_escape_string(const src[], dest[], max_len = sizeof(dest)); native mysql_format(connId, dest[], max_len, const format[], {Float,_}:...); diff --git a/include/mysql_samp.inc.in b/include/mysql_samp.inc.in new file mode 100644 index 0000000..d452a1f --- /dev/null +++ b/include/mysql_samp.inc.in @@ -0,0 +1,129 @@ +/* + * mysql_samp v{{VERSION}} — MySQL plugin for SA-MP written in Rust. + * Author: NullSablex + * Repository: https://github.com/NullSablex/mysql_samp + * License: GPL-3.0 + * + * Auto-generated by build.rs — do not edit this file directly. + * To change declarations, edit include/mysql_samp.inc.in instead. + */ + +#if defined _mysql_samp_included + #endinput +#endif +#define _mysql_samp_included + +#define MYSQL_SAMP_VERSION "{{VERSION}}" + +// Connection options +enum { + MYSQL_OPT_PORT = 0, + MYSQL_OPT_SSL, + MYSQL_OPT_SSL_CA, + MYSQL_OPT_CONNECT_TIMEOUT, + MYSQL_OPT_AUTO_RECONNECT, +} + +// Error codes +enum { + MYSQL_OK = 0, + MYSQL_ERROR_CONNECTION_FAILED, + MYSQL_ERROR_INVALID_OPTIONS, + MYSQL_ERROR_INVALID_CONNECTION, + MYSQL_ERROR_PING_FAILED, + MYSQL_ERROR_QUERY_FAILED, + MYSQL_ERROR_NO_CACHE_ACTIVE, + MYSQL_ERROR_INVALID_ORM, + MYSQL_ERROR_ORM_KEY_NOT_SET, +} + +// Log levels +enum { + MYSQL_LOG_NONE = 0, + MYSQL_LOG_ERROR, + MYSQL_LOG_WARNING, + MYSQL_LOG_INFO, + MYSQL_LOG_ALL, +} + +// ORM error codes +enum { + ORM_OK = 0, + ORM_NO_DATA, +} + +// Forward: invoked when a threaded query fails. +forward OnQueryError(errorid, const error[], const callback[], const query[], connId); + +// Options +native mysql_options_new(); +native bool:mysql_options_set_int(handle, option, value); +native bool:mysql_options_set_str(handle, option, const value[]); + +// Connection +native mysql_connect(const host[], const user[], const password[], const database[], options = 0); +native bool:mysql_close(connId); +native bool:mysql_status(connId, dest[], max_len = sizeof(dest)); + +// Error +native mysql_errno(connId = 0); +native bool:mysql_error(connId, dest[], max_len = sizeof(dest)); + +// Charset +native bool:mysql_set_charset(connId, const charset[]); +native bool:mysql_get_charset(connId, dest[], max_len = sizeof(dest)); + +// Utility +native mysql_unprocessed_queries(); +native bool:mysql_log(log_level); + +// Query (non-blocking) +native bool:mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); +native bool:mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...); +// Kept for backwards compatibility only: since rust-samp v3.0.0 the unified +// on_tick already dispatches callbacks automatically on SA-MP and native open.mp. +native bool:mysql_tick(); +native bool:mysql_escape_string(const src[], dest[], max_len = sizeof(dest)); +native mysql_format(connId, dest[], max_len, const format[], {Float,_}:...); + +// Cache +native cache_get_row_count(); +native cache_get_field_count(); +native bool:cache_get_field_name(field_idx, dest[], max_len = sizeof(dest)); +native cache_get_field_type(field_idx); +native bool:cache_get_value_index(row, col, dest[], max_len = sizeof(dest)); +native cache_get_value_index_int(row, col); +native Float:cache_get_value_index_float(row, col); +native bool:cache_get_value_name(row, const field_name[], dest[], max_len = sizeof(dest)); +native cache_get_value_name_int(row, const field_name[]); +native Float:cache_get_value_name_float(row, const field_name[]); +native bool:cache_is_value_index_null(row, col); +native bool:cache_is_value_name_null(row, const field_name[]); +native cache_affected_rows(); +native cache_insert_id(); +native cache_warning_count(); +native cache_get_query_exec_time(); +native bool:cache_get_query_string(dest[], max_len = sizeof(dest)); +native cache_save(); +native bool:cache_delete(cache_id); +native bool:cache_set_active(cache_id); +native bool:cache_unset_active(); +native bool:cache_is_any_active(); +native bool:cache_is_valid(cache_id); + +// ORM +native orm_create(const table[], connId); +native bool:orm_destroy(orm_id); +native orm_errno(orm_id); +native bool:orm_select(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...); +native bool:orm_apply_cache(orm_id, row = 0); +native bool:orm_addvar_int(orm_id, &var, const column_name[]); +native bool:orm_addvar_float(orm_id, &Float:var, const column_name[]); +native bool:orm_addvar_string(orm_id, var[], var_max_len, const column_name[]); +native bool:orm_delvar(orm_id, const column_name[]); +native bool:orm_clear_vars(orm_id); +native bool:orm_setkey(orm_id, const column_name[]); diff --git a/mkdocs.yml b/mkdocs.yml index feffba3..9138e59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,69 +1,103 @@ site_name: mysql_samp -site_description: Plugin MySQL para SA:MP escrito em Rust -site_url: https://nullsablex.github.io/mysql_samp -repo_url: https://github.com/NullSablex/mysql_samp +site_description: MySQL plugin for SA-MP and Open Multiplayer, written in Rust +site_author: NullSablex +site_url: https://mysql-samp.nullsablex.com/ + repo_name: NullSablex/mysql_samp +repo_url: https://github.com/NullSablex/mysql_samp edit_uri: edit/master/docs/ +docs_dir: docs + theme: name: material - language: pt - palette: - - scheme: default - primary: deep orange - accent: orange - toggle: - icon: material/brightness-7 - name: Modo escuro - - scheme: slate - primary: deep orange - accent: orange - toggle: - icon: material/brightness-4 - name: Modo claro + language: en features: - navigation.tabs + - navigation.tabs.sticky - navigation.sections + - navigation.expand - navigation.top - - search.highlight + - navigation.tracking + - navigation.indexes + - toc.follow - search.suggest + - search.highlight + - search.share - content.code.copy + - content.code.annotate + - content.tabs.link - content.action.edit + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: deep orange + accent: orange + toggle: + icon: material/weather-night + name: Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: deep orange + accent: orange + toggle: + icon: material/weather-sunny + name: Light mode + icon: + repo: fontawesome/brands/github + edit: material/pencil + view: material/eye plugins: - search: - lang: pt + lang: en markdown_extensions: - admonition - attr_list + - def_list + - footnotes - md_in_html + - tables + - toc: + permalink: true + permalink_title: Permanent link - pymdownx.details - - pymdownx.superfences - pymdownx.highlight: anchor_linenums: true + line_spans: __span + pygments_lang_class: true - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences - pymdownx.tabbed: alternate_style: true - - tables - - toc: - permalink: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg nav: - - Início: index.md - - Guia: - - Instalação: instalacao.md - - Conexão: conexao.md + - Home: index.md + - Guide: + - Installation: installation.md + - Connection: connection.md + - Options: options.md - Queries: queries.md - Cache: cache.md - ORM: orm.md - - Options: options.md - - Segurança: seguranca.md - - Erros: erros.md - - Migração: - - Guia de Migração: migracao.md - - O que Mudou: mudancas.md - - Exemplos: exemplos-migracao.md - - Referência: - - API Completa: api.md + - Errors: errors.md + - Security: security.md + - Migration: + - Migration guide: migration.md + - What changed: migration-changes.md + - Examples: migration-examples.md + - Reference: + - API reference: api-reference.md - Benchmark: benchmark.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/NullSablex/mysql_samp diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100755 index 0000000..e1e7317 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Builds the plugin for Linux and Windows from Linux. +# +# Outputs: +# dist/.so — Linux (i686-unknown-linux-gnu) +# dist/.dll — Windows (i686-pc-windows-msvc via cargo-xwin) +# +# Always builds with SA-MP + native Open Multiplayer support. +# Requires cargo-xwin (installed automatically if missing). +# +# Usage: +# ./scripts/build-linux.sh # release +# PROFILE=dev ./scripts/build-linux.sh # dev build +# +# PLUGIN_NAME is read from the project's Cargo.toml. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" +PROFILE="${PROFILE:-release}" +PLUGIN_NAME="$(grep -m1 '^name' "$ROOT_DIR/Cargo.toml" | sed 's/.*= *"\(.*\)"/\1/' | tr '-' '_')" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[build] $*${NC}"; } +log_step() { echo -e "${YELLOW}[build] $*${NC}"; } +log_err() { echo -e "${RED}[build] $*${NC}" >&2; } + +for arg in "$@"; do + log_err "Unknown argument: $arg" + exit 1 +done + +ensure_target() { + if ! rustup target list --installed | grep -qx "$1"; then + log_step "Installing target: $1" + rustup target add "$1" + fi +} + +ensure_xwin() { + if ! command -v cargo-xwin >/dev/null 2>&1; then + log_step "Installing cargo-xwin..." + cargo install cargo-xwin + fi +} + +build_linux() { + local target="i686-unknown-linux-gnu" + ensure_target "$target" + log_step "Building: $target" + cargo build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_windows() { + local target="i686-pc-windows-msvc" + ensure_target "$target" + ensure_xwin + log_step "Building: $target" + cargo xwin build --xwin-arch x86 --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/${PLUGIN_NAME}.dll" + local dst="$DIST_DIR/${PLUGIN_NAME}.dll" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Windows: $dst" +} + +main() { + mkdir -p "$DIST_DIR" + log_info "Mode: SA-MP + native Open Multiplayer" + build_linux + build_windows + log_info "Done: $DIST_DIR/" +} + +main diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 0000000..1a9af56 --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Builds the plugin for Windows and Linux from Windows (Git Bash). +# +# Outputs: +# dist/.dll — Windows (i686-pc-windows-msvc) +# dist/.so — Linux (i686-unknown-linux-gnu via WSL or Docker/cross) +# +# Always builds with SA-MP + native Open Multiplayer support. +# +# Linux (.so) build: +# WSL — autodetected. Requires Rust inside WSL (https://rustup.rs). +# Docker — fallback when WSL is unavailable. Requires Docker Desktop + cross. +# Force a mode with --wsl or --docker. +# +# Usage: +# ./scripts/build-windows.sh # autodetect +# ./scripts/build-windows.sh --wsl # force WSL for Linux +# ./scripts/build-windows.sh --docker # force Docker for Linux +# PROFILE=dev ./scripts/build-windows.sh # dev build (default: release) +# +# PLUGIN_NAME is read from the project's Cargo.toml. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" +PROFILE="${PROFILE:-release}" +PLUGIN_NAME="$(grep -m1 '^name' "$ROOT_DIR/Cargo.toml" | sed 's/.*= *"\(.*\)"/\1/' | tr '-' '_')" +LINUX_BUILD="" # "wsl" | "docker" | "" (autodetect) + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[build] $*${NC}"; } +log_step() { echo -e "${YELLOW}[build] $*${NC}"; } +log_err() { echo -e "${RED}[build] $*${NC}" >&2; } + +for arg in "$@"; do + case "$arg" in + --wsl) LINUX_BUILD="wsl" ;; + --docker) LINUX_BUILD="docker" ;; + *) log_err "Unknown argument: $arg"; exit 1 ;; + esac +done + +ensure_target() { + if ! rustup target list --installed | grep -qx "$1"; then + log_step "Installing target: $1" + rustup target add "$1" + fi +} + +ensure_cross() { + if ! command -v cross >/dev/null 2>&1; then + log_step "Installing cross..." + cargo install cross + fi +} + +build_windows() { + local target="i686-pc-windows-msvc" + ensure_target "$target" + log_step "Building: $target" + cargo build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/${PLUGIN_NAME}.dll" + local dst="$DIST_DIR/${PLUGIN_NAME}.dll" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Windows: $dst" +} + +build_linux_wsl() { + local target="i686-unknown-linux-gnu" + local wsl_root="/mnt${ROOT_DIR}" + log_step "Building: $target (via WSL)" + wsl bash -c "rustup target add '$target' 2>/dev/null; cd '$wsl_root' && cargo build --profile '$PROFILE' --target '$target'" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_linux_docker() { + local target="i686-unknown-linux-gnu" + ensure_target "$target" + ensure_cross + log_step "Building: $target (via cross/Docker)" + cross build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_linux() { + case "$LINUX_BUILD" in + wsl) build_linux_wsl ;; + docker) build_linux_docker ;; + *) + if command -v wsl >/dev/null 2>&1; then + log_step "WSL detected." + build_linux_wsl + elif command -v docker >/dev/null 2>&1; then + log_step "Docker detected." + build_linux_docker + else + log_err "Neither WSL nor Docker found. Install one or force a mode with --wsl/--docker." + exit 1 + fi + ;; + esac +} + +main() { + mkdir -p "$DIST_DIR" + log_info "Mode: SA-MP + native Open Multiplayer" + build_windows + build_linux + log_info "Done: $DIST_DIR/" +} + +main diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index db96ca4..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -DIST_DIR="$ROOT_DIR/dist" -PROFILE="${PROFILE:-release}" -LINUX_TARGET="i686-unknown-linux-gnu" -WINDOWS_TARGET="i686-pc-windows-gnu" -PLUGIN_NAME="mysql_samp" - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}$*${NC}"; } -log_step() { echo -e "${YELLOW}$*${NC}"; } -log_err() { echo -e "${RED}$*${NC}" >&2; } - -ensure_target() { - local target="$1" - if ! rustup target list --installed | grep -qx "$target"; then - log_step "Instalando target Rust: $target" - rustup target add "$target" - fi -} - -ensure_sha256() { - if command -v sha256sum >/dev/null 2>&1; then - return 0 - fi - if command -v shasum >/dev/null 2>&1; then - return 0 - fi - log_err "Nenhum utilitário de hash encontrado (sha256sum/shasum)." - exit 1 -} - -write_sha256() { - local file="$1" - local out="$2" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$file" > "$out" - else - shasum -a 256 "$file" > "$out" - fi -} - -build_target() { - local target="$1" - log_step "Compilando target: $target" - cargo build --profile "$PROFILE" --target "$target" -} - -copy_linux() { - local src="$ROOT_DIR/target/$LINUX_TARGET/$PROFILE/lib${PLUGIN_NAME}.so" - local dst="$DIST_DIR/${PLUGIN_NAME}.so" - [[ -f "$src" ]] || { log_err "Artefato Linux não encontrado: $src"; exit 1; } - cp "$src" "$dst" - write_sha256 "$dst" "${dst}.sha256" -} - -copy_windows() { - local src="$ROOT_DIR/target/$WINDOWS_TARGET/$PROFILE/${PLUGIN_NAME}.dll" - local dst="$DIST_DIR/${PLUGIN_NAME}.dll" - [[ -f "$src" ]] || { log_err "Artefato Windows não encontrado: $src"; exit 1; } - cp "$src" "$dst" - write_sha256 "$dst" "${dst}.sha256" -} - -main() { - log_info "Build do plugin SA:MP mysql_samp (Linux + Windows 32-bit)" - mkdir -p "$DIST_DIR" - ensure_target "$LINUX_TARGET" - ensure_target "$WINDOWS_TARGET" - ensure_sha256 - build_target "$LINUX_TARGET" - build_target "$WINDOWS_TARGET" - copy_linux - copy_windows - log_info "Concluído. Arquivos gerados em: $DIST_DIR" -} - -main "$@" diff --git a/src/cache.rs b/src/cache.rs index 00ffd65..3785b02 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -224,7 +224,11 @@ mod tests { CacheEntry::new( vec![ vec![Some("1".to_string()), Some("Alice".to_string()), None], - vec![Some("2".to_string()), Some("Bob".to_string()), Some("bob@test.com".to_string())], + vec![ + Some("2".to_string()), + Some("Bob".to_string()), + Some("bob@test.com".to_string()), + ], ], vec!["id".to_string(), "name".to_string(), "email".to_string()], vec![3, 253, 253], // LONG, VAR_STRING, VAR_STRING @@ -324,16 +328,7 @@ mod tests { #[test] fn entry_warning_count() { - let entry = CacheEntry::new( - vec![], - vec![], - vec![], - 0, - 0, - 3, - 0, - "".to_string(), - ); + let entry = CacheEntry::new(vec![], vec![], vec![], 0, 0, 3, 0, "".to_string()); assert_eq!(entry.warning_count(), 3); } @@ -504,7 +499,10 @@ mod tests { // Manual active overrides mgr.set_active(saved_id); - assert_eq!(mgr.get_active().unwrap().query_string(), "SELECT * FROM users"); + assert_eq!( + mgr.get_active().unwrap().query_string(), + "SELECT * FROM users" + ); // Unset manual returns to stack mgr.unset_active(); diff --git a/src/callback.rs b/src/callback.rs index d793ce5..b704ee2 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -46,24 +46,22 @@ pub fn invoke_callback(amx_list: &[AmxIdent], info: &CallbackInfo) { break; } } - ('s', CallbackParam::String(v)) => { - match allocator.allot_string(v) { - Ok(s) => { - if amx.push(s).is_err() { - push_ok = false; - break; - } - } - Err(_) => { + ('s', CallbackParam::String(v)) => match allocator.allot_string(v) { + Ok(s) => { + if amx.push(s).is_err() { push_ok = false; - Logger::error(&format!( - "Failed to allocate string for callback '{}'.", - info.name - )); break; } } - } + Err(_) => { + push_ok = false; + Logger::error(&format!( + "Failed to allocate string for callback '{}'.", + info.name + )); + break; + } + }, _ => {} } } diff --git a/src/connection.rs b/src/connection.rs index b324fbb..4f06813 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -59,7 +59,7 @@ impl ConnectionManager { }; let builder = if let Some(timeout) = options.connect_timeout { - builder.tcp_connect_timeout(Some(Duration::from_secs(timeout as u64))) + builder.tcp_connect_timeout(Some(Duration::from_secs(u64::from(timeout)))) } else { builder }; @@ -204,9 +204,7 @@ impl ConnectionManager { pub fn get_charset(&self, conn_id: i32) -> Option { let entry = self.connections.get(&conn_id)?; let mut conn = entry.pool.get_conn().ok()?; - let result: Option = conn - .query_first("SELECT @@character_set_connection") - .ok()?; + let result: Option = conn.query_first("SELECT @@character_set_connection").ok()?; result } } @@ -239,7 +237,11 @@ pub fn escape_string(input: &str) -> String { /// Executes a query on a Pool, retrying once on connection-lost errors when auto_reconnect is true. /// Connection-lost errors are identified by error code 0 (non-MySQL errors such as IO errors, /// which the Rust mysql crate returns when the TCP connection is dropped by the server). -pub fn attempt_query(pool: &Pool, query: &str, auto_reconnect: bool) -> Result { +pub fn attempt_query( + pool: &Pool, + query: &str, + auto_reconnect: bool, +) -> Result { let mut conn = pool.get_conn().map_err(|e| QueryError { code: 0, message: e.to_string(), @@ -276,10 +278,14 @@ pub fn execute_query(conn: &mut PooledConn, query: &str) -> Result = cols_ref.as_ref().iter() + let columns: Vec = cols_ref + .as_ref() + .iter() .map(|c| c.name_str().to_string()) .collect(); - let field_types: Vec = cols_ref.as_ref().iter() + let field_types: Vec = cols_ref + .as_ref() + .iter() .map(|c| c.column_type() as u8) .collect(); @@ -413,6 +419,38 @@ mod tests { ); } + #[test] + fn escape_consecutive_quotes() { + // Three quotes in a row must each be escaped individually. + assert_eq!(escape_string("'''"), "\\'\\'\\'"); + } + + #[test] + fn escape_already_escaped_is_double_escaped() { + // Important invariant: feeding the function its own output produces + // a different (deeper-escaped) string. Callers must escape EXACTLY ONCE. + let once = escape_string("a'b"); + let twice = escape_string(&once); + assert_ne!(once, twice); + // After two escapes: 'a', '\\', '\\', '\\', '\'', 'b' → r"a\\\'b" with each char doubled. + assert_eq!(twice, "a\\\\\\'b"); + } + + #[test] + fn escape_all_specials_at_once() { + // \0 \n \r \\ \' \" \x1a + let input = "\0\n\r\\\'\"\x1a"; + let expected = "\\0\\n\\r\\\\\\'\\\"\\Z"; + assert_eq!(escape_string(input), expected); + } + + #[test] + fn escape_low_control_chars_passthrough() { + // Only \0 \n \r \x1a are special. Other low-ASCII control bytes + // pass through unchanged (no \xNN encoding is done). + assert_eq!(escape_string("\x01\x07\x08\x0b"), "\x01\x07\x08\x0b"); + } + // escape_identifier tests #[test] diff --git a/src/lib.rs b/src/lib.rs index b713f6e..e6e8d65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ initialize_plugin!( // Query MysqlPlugin::mysql_query, MysqlPlugin::mysql_pquery, + MysqlPlugin::mysql_tick, MysqlPlugin::mysql_escape_string, MysqlPlugin::mysql_format, // Cache @@ -78,7 +79,7 @@ initialize_plugin!( MysqlPlugin::orm_setkey, ], { - samp::plugin::enable_process_tick(); + samp::plugin::enable_tick(); return MysqlPlugin::new(); } ); diff --git a/src/logger.rs b/src/logger.rs index f60e63b..1008a24 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,6 +1,6 @@ use std::fs::{self, OpenOptions}; use std::io::Write; -use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; const LOG_DIR: &str = "logs"; const LOG_FILE: &str = "logs/mysql.log"; @@ -9,6 +9,10 @@ const PREFIX: &str = "[MySQL]"; /// Log level: 0=none, 1=error, 2=warning, 3=info, 4=all (default) static LOG_LEVEL: AtomicI32 = AtomicI32::new(4); +/// Set once if writing to `logs/mysql.log` fails — used to emit a single +/// console error instead of silently dropping every subsequent log line. +static FILE_WRITE_REPORTED: AtomicBool = AtomicBool::new(false); + pub struct Logger; impl Logger { @@ -23,28 +27,28 @@ impl Logger { pub fn info(msg: &str) { if LOG_LEVEL.load(Ordering::Relaxed) >= 3 { - log::info!("{} {}", PREFIX, msg); + samp::log::info!("{} {}", PREFIX, msg); Self::write_file("INFO", msg); } } pub fn warn(msg: &str) { if LOG_LEVEL.load(Ordering::Relaxed) >= 2 { - log::warn!("{} {}", PREFIX, msg); + samp::log::warn!("{} {}", PREFIX, msg); Self::write_file("WARNING", msg); } } pub fn error(msg: &str) { if LOG_LEVEL.load(Ordering::Relaxed) >= 1 { - log::error!("{} {}", PREFIX, msg); + samp::log::error!("{} {}", PREFIX, msg); Self::write_file("ERROR", msg); } } pub fn error_detail(console_msg: &str, detail: &str) { if LOG_LEVEL.load(Ordering::Relaxed) >= 1 { - log::error!("{} {}", PREFIX, console_msg); + samp::log::error!("{} {}", PREFIX, console_msg); Self::write_file("ERROR", detail); } } @@ -58,30 +62,37 @@ impl Logger { let build_time = env!("BUILD_TIME"); let build_year = env!("BUILD_YEAR"); - log::info!(""); - log::info!(" | {} {} | {}", name, version, build_year); - log::info!(" |-------------------------------"); - log::info!(" | Author and maintainer: {}", value_or(author, "Unknown")); - log::info!(""); - log::info!(" | Compiled: {} at {}", build_date, build_time); - log::info!(" |-------------------------------"); - log::info!(" | Repository: {}", value_or(repository, "N/A")); - log::info!(""); + samp::log::info!(""); + samp::log::info!(" | {} {} | {}", name, version, build_year); + samp::log::info!(" |-------------------------------"); + samp::log::info!(" | Author and maintainer: {}", value_or(author, "Unknown")); + samp::log::info!(""); + samp::log::info!(" | Compiled: {} at {}", build_date, build_time); + samp::log::info!(" |-------------------------------"); + samp::log::info!(" | Repository: {}", value_or(repository, "N/A")); + samp::log::info!(""); } fn write_file(level: &str, message: &str) { let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); let line = format!("[{}] [{}] {}\n", timestamp, level, message); - let Ok(mut file) = OpenOptions::new() + let result = OpenOptions::new() .create(true) .append(true) .open(LOG_FILE) - else { - return; - }; + .and_then(|mut file| file.write_all(line.as_bytes())); - let _ = file.write_all(line.as_bytes()); + if let Err(err) = result + && !FILE_WRITE_REPORTED.swap(true, Ordering::Relaxed) + { + samp::log::error!( + "{} Failed to write {}: {}. Further file-write errors will be suppressed.", + PREFIX, + LOG_FILE, + err + ); + } } } diff --git a/src/natives/cache.rs b/src/natives/cache.rs index fbfd059..3af0342 100644 --- a/src/natives/cache.rs +++ b/src/natives/cache.rs @@ -5,19 +5,17 @@ use crate::plugin::MysqlPlugin; impl MysqlPlugin { #[native(name = "cache_get_row_count")] - pub fn cache_get_row_count(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.row_count() as i32), - None => Ok(-1), - } + pub fn cache_get_row_count(&mut self, _amx: &Amx) -> i32 { + self.cache.get_active().map_or(-1, |entry| { + i32::try_from(entry.row_count()).unwrap_or(i32::MAX) + }) } #[native(name = "cache_get_field_count")] - pub fn cache_get_field_count(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.field_count() as i32), - None => Ok(-1), - } + pub fn cache_get_field_count(&mut self, _amx: &Amx) -> i32 { + self.cache.get_active().map_or(-1, |entry| { + i32::try_from(entry.field_count()).unwrap_or(i32::MAX) + }) } #[native(name = "cache_get_field_name")] @@ -28,15 +26,16 @@ impl MysqlPlugin { dest: UnsizedBuffer, dest_len: usize, ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(false), + let Ok(idx) = usize::try_from(field_idx) else { + return Ok(false); + }; + let Some(entry) = self.cache.get_active() else { + return Ok(false); }; - match entry.field_name(field_idx as usize) { + match entry.field_name(idx) { Some(name) => { - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, name); + dest.write_str(dest_len, name)?; Ok(true) } None => Ok(false), @@ -52,15 +51,16 @@ impl MysqlPlugin { dest: UnsizedBuffer, dest_len: usize, ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(false), + let (Ok(row_idx), Ok(col_idx)) = (usize::try_from(row), usize::try_from(col)) else { + return Ok(false); + }; + let Some(entry) = self.cache.get_active() else { + return Ok(false); }; - match entry.get_value(row as usize, col as usize) { + match entry.get_value(row_idx, col_idx) { Some(Some(val)) => { - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, val); + dest.write_str(dest_len, val)?; Ok(true) } _ => Ok(false), @@ -68,38 +68,32 @@ impl MysqlPlugin { } #[native(name = "cache_get_value_index_int")] - pub fn cache_get_value_index_int( - &mut self, - _amx: &Amx, - row: i32, - col: i32, - ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(0), + pub fn cache_get_value_index_int(&mut self, _amx: &Amx, row: i32, col: i32) -> i32 { + let (Ok(row_idx), Ok(col_idx)) = (usize::try_from(row), usize::try_from(col)) else { + return 0; + }; + let Some(entry) = self.cache.get_active() else { + return 0; }; - match entry.get_value(row as usize, col as usize) { - Some(Some(val)) => Ok(val.parse::().unwrap_or(0)), - _ => Ok(0), + match entry.get_value(row_idx, col_idx) { + Some(Some(val)) => val.parse::().unwrap_or(0), + _ => 0, } } #[native(name = "cache_get_value_index_float")] - pub fn cache_get_value_index_float( - &mut self, - _amx: &Amx, - row: i32, - col: i32, - ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(0.0), + pub fn cache_get_value_index_float(&mut self, _amx: &Amx, row: i32, col: i32) -> f32 { + let (Ok(row_idx), Ok(col_idx)) = (usize::try_from(row), usize::try_from(col)) else { + return 0.0; + }; + let Some(entry) = self.cache.get_active() else { + return 0.0; }; - match entry.get_value(row as usize, col as usize) { - Some(Some(val)) => Ok(val.parse::().unwrap_or(0.0)), - _ => Ok(0.0), + match entry.get_value(row_idx, col_idx) { + Some(Some(val)) => val.parse::().unwrap_or(0.0), + _ => 0.0, } } @@ -108,25 +102,23 @@ impl MysqlPlugin { &mut self, _amx: &Amx, row: i32, - field_name: AmxString, + field_name: &AmxString, dest: UnsizedBuffer, dest_len: usize, ) -> AmxResult { - let name = field_name.to_string(); - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(false), + let Ok(row_idx) = usize::try_from(row) else { + return Ok(false); }; - - let col = match entry.field_index(&name) { - Some(i) => i, - None => return Ok(false), + let Some(entry) = self.cache.get_active() else { + return Ok(false); + }; + let Some(col) = entry.field_index(field_name) else { + return Ok(false); }; - match entry.get_value(row as usize, col) { + match entry.get_value(row_idx, col) { Some(Some(val)) => { - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, val); + dest.write_str(dest_len, val)?; Ok(true) } _ => Ok(false), @@ -138,22 +130,21 @@ impl MysqlPlugin { &mut self, _amx: &Amx, row: i32, - field_name: AmxString, - ) -> AmxResult { - let name = field_name.to_string(); - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(0), + field_name: &AmxString, + ) -> i32 { + let Ok(row_idx) = usize::try_from(row) else { + return 0; }; - - let col = match entry.field_index(&name) { - Some(i) => i, - None => return Ok(0), + let Some(entry) = self.cache.get_active() else { + return 0; + }; + let Some(col) = entry.field_index(field_name) else { + return 0; }; - match entry.get_value(row as usize, col) { - Some(Some(val)) => Ok(val.parse::().unwrap_or(0)), - _ => Ok(0), + match entry.get_value(row_idx, col) { + Some(Some(val)) => val.parse::().unwrap_or(0), + _ => 0, } } @@ -162,41 +153,36 @@ impl MysqlPlugin { &mut self, _amx: &Amx, row: i32, - field_name: AmxString, - ) -> AmxResult { - let name = field_name.to_string(); - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(0.0), + field_name: &AmxString, + ) -> f32 { + let Ok(row_idx) = usize::try_from(row) else { + return 0.0; }; - - let col = match entry.field_index(&name) { - Some(i) => i, - None => return Ok(0.0), + let Some(entry) = self.cache.get_active() else { + return 0.0; + }; + let Some(col) = entry.field_index(field_name) else { + return 0.0; }; - match entry.get_value(row as usize, col) { - Some(Some(val)) => Ok(val.parse::().unwrap_or(0.0)), - _ => Ok(0.0), + match entry.get_value(row_idx, col) { + Some(Some(val)) => val.parse::().unwrap_or(0.0), + _ => 0.0, } } #[native(name = "cache_is_value_index_null")] - pub fn cache_is_value_index_null( - &mut self, - _amx: &Amx, - row: i32, - col: i32, - ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(true), + pub fn cache_is_value_index_null(&mut self, _amx: &Amx, row: i32, col: i32) -> bool { + let (Ok(row_idx), Ok(col_idx)) = (usize::try_from(row), usize::try_from(col)) else { + return true; + }; + let Some(entry) = self.cache.get_active() else { + return true; }; - match entry.get_value(row as usize, col as usize) { - Some(None) => Ok(true), - Some(Some(_)) => Ok(false), - None => Ok(true), + match entry.get_value(row_idx, col_idx) { + Some(None) | None => true, + Some(Some(_)) => false, } } @@ -205,48 +191,43 @@ impl MysqlPlugin { &mut self, _amx: &Amx, row: i32, - field_name: AmxString, - ) -> AmxResult { - let name = field_name.to_string(); - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(true), + field_name: &AmxString, + ) -> bool { + let Ok(row_idx) = usize::try_from(row) else { + return true; }; - - let col = match entry.field_index(&name) { - Some(i) => i, - None => return Ok(true), + let Some(entry) = self.cache.get_active() else { + return true; + }; + let Some(col) = entry.field_index(field_name) else { + return true; }; - match entry.get_value(row as usize, col) { - Some(None) => Ok(true), - Some(Some(_)) => Ok(false), - None => Ok(true), + match entry.get_value(row_idx, col) { + Some(None) | None => true, + Some(Some(_)) => false, } } #[native(name = "cache_affected_rows")] - pub fn cache_affected_rows(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.affected_rows() as i32), - None => Ok(-1), - } + pub fn cache_affected_rows(&mut self, _amx: &Amx) -> i32 { + self.cache.get_active().map_or(-1, |entry| { + i32::try_from(entry.affected_rows()).unwrap_or(i32::MAX) + }) } #[native(name = "cache_insert_id")] - pub fn cache_insert_id(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.insert_id() as i32), - None => Ok(-1), - } + pub fn cache_insert_id(&mut self, _amx: &Amx) -> i32 { + self.cache.get_active().map_or(-1, |entry| { + i32::try_from(entry.insert_id()).unwrap_or(i32::MAX) + }) } #[native(name = "cache_get_query_exec_time")] - pub fn cache_get_query_exec_time(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.exec_time_ms() as i32), - None => Ok(-1), - } + pub fn cache_get_query_exec_time(&mut self, _amx: &Amx) -> i32 { + self.cache.get_active().map_or(-1, |entry| { + i32::try_from(entry.exec_time_ms()).unwrap_or(i32::MAX) + }) } #[native(name = "cache_get_query_string")] @@ -256,65 +237,64 @@ impl MysqlPlugin { dest: UnsizedBuffer, dest_len: usize, ) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(false), + let Some(entry) = self.cache.get_active() else { + return Ok(false); }; let query = entry.query_string().to_string(); - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &query); + dest.write_str(dest_len, &query)?; Ok(true) } #[native(name = "cache_save")] - pub fn cache_save(&mut self, _amx: &Amx) -> AmxResult { - Ok(self.cache.save()) + pub fn cache_save(&mut self, _amx: &Amx) -> i32 { + self.cache.save() } #[native(name = "cache_delete")] - pub fn cache_delete(&mut self, _amx: &Amx, cache_id: i32) -> AmxResult { - Ok(self.cache.delete(cache_id)) + pub fn cache_delete(&mut self, _amx: &Amx, cache_id: i32) -> bool { + self.cache.delete(cache_id) } #[native(name = "cache_set_active")] - pub fn cache_set_active(&mut self, _amx: &Amx, cache_id: i32) -> AmxResult { - Ok(self.cache.set_active(cache_id)) + pub fn cache_set_active(&mut self, _amx: &Amx, cache_id: i32) -> bool { + self.cache.set_active(cache_id) } #[native(name = "cache_unset_active")] - pub fn cache_unset_active(&mut self, _amx: &Amx) -> AmxResult { - Ok(self.cache.unset_active()) + pub fn cache_unset_active(&mut self, _amx: &Amx) -> bool { + self.cache.unset_active() } #[native(name = "cache_is_any_active")] - pub fn cache_is_any_active(&mut self, _amx: &Amx) -> AmxResult { - Ok(self.cache.is_any_active()) + pub fn cache_is_any_active(&mut self, _amx: &Amx) -> bool { + self.cache.is_any_active() } #[native(name = "cache_is_valid")] - pub fn cache_is_valid(&mut self, _amx: &Amx, cache_id: i32) -> AmxResult { - Ok(self.cache.is_valid(cache_id)) + pub fn cache_is_valid(&mut self, _amx: &Amx, cache_id: i32) -> bool { + self.cache.is_valid(cache_id) } #[native(name = "cache_warning_count")] - pub fn cache_warning_count(&mut self, _amx: &Amx) -> AmxResult { - match self.cache.get_active() { - Some(entry) => Ok(entry.warning_count() as i32), - None => Ok(-1), - } + pub fn cache_warning_count(&mut self, _amx: &Amx) -> i32 { + self.cache + .get_active() + .map_or(-1, |entry| i32::from(entry.warning_count())) } #[native(name = "cache_get_field_type")] - pub fn cache_get_field_type(&mut self, _amx: &Amx, field_idx: i32) -> AmxResult { - let entry = match self.cache.get_active() { - Some(e) => e, - None => return Ok(-1), + pub fn cache_get_field_type(&mut self, _amx: &Amx, field_idx: i32) -> i32 { + let Ok(idx) = usize::try_from(field_idx) else { + return -1; + }; + let Some(entry) = self.cache.get_active() else { + return -1; }; - match entry.field_type(field_idx as usize) { - Some(t) => Ok(t as i32), - None => Ok(-1), + match entry.field_type(idx) { + Some(t) => i32::from(t), + None => -1, } } } diff --git a/src/natives/connection.rs b/src/natives/connection.rs index 26cae78..abb86fb 100644 --- a/src/natives/connection.rs +++ b/src/natives/connection.rs @@ -11,12 +11,12 @@ impl MysqlPlugin { pub fn mysql_connect( &mut self, _amx: &Amx, - host: AmxString, - user: AmxString, - password: AmxString, - database: AmxString, + host: &AmxString, + user: &AmxString, + password: &AmxString, + database: &AmxString, options_id: i32, - ) -> AmxResult { + ) -> i32 { let opts = if options_id == 0 { MysqlOptions::default() } else { @@ -24,22 +24,16 @@ impl MysqlPlugin { Some(o) => o.clone(), None => { Logger::error("Connection failed: invalid options handle."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOptions, - "Invalid options handle.", - ); - return Ok(0); + self.connections.global_error = + ErrorState::new(MysqlError::InvalidOptions, "Invalid options handle."); + return 0; } } }; - let id = self.connections.connect( - &host.to_string(), - &user.to_string(), - &password.to_string(), - &database.to_string(), - &opts, - ); + let id = self + .connections + .connect(host, user, password, database, &opts); if id > 0 { Logger::info(&format!("Connection {} established.", id)); @@ -47,7 +41,7 @@ impl MysqlPlugin { Logger::info("Connection failed."); } - Ok(id) + id } #[native(name = "mysql_status")] @@ -60,8 +54,7 @@ impl MysqlPlugin { ) -> AmxResult { match self.connections.get_status(conn_id) { Some(status) => { - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &status); + dest.write_str(dest_len, &status)?; Ok(true) } None => { @@ -76,24 +69,19 @@ impl MysqlPlugin { } #[native(name = "mysql_close")] - pub fn mysql_close(&mut self, _amx: &Amx, connection_id: i32) -> AmxResult { + pub fn mysql_close(&mut self, _amx: &Amx, connection_id: i32) -> bool { if self.connections.disconnect(connection_id) { Logger::info(&format!("Connection {} closed.", connection_id)); - Ok(true) + true } else { Logger::warn("Connection not found."); - Ok(false) + false } } #[native(name = "mysql_set_charset")] - pub fn mysql_set_charset( - &mut self, - _amx: &Amx, - conn_id: i32, - charset: AmxString, - ) -> AmxResult { - Ok(self.connections.set_charset(conn_id, &charset.to_string())) + pub fn mysql_set_charset(&mut self, _amx: &Amx, conn_id: i32, charset: &AmxString) -> bool { + self.connections.set_charset(conn_id, charset) } #[native(name = "mysql_get_charset")] @@ -106,8 +94,7 @@ impl MysqlPlugin { ) -> AmxResult { match self.connections.get_charset(conn_id) { Some(charset) => { - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &charset); + dest.write_str(dest_len, &charset)?; Ok(true) } None => Ok(false), @@ -115,13 +102,13 @@ impl MysqlPlugin { } #[native(name = "mysql_unprocessed_queries")] - pub fn mysql_unprocessed_queries(&mut self, _amx: &Amx) -> AmxResult { - Ok(self.queries.pending_count() as i32) + pub fn mysql_unprocessed_queries(&mut self, _amx: &Amx) -> i32 { + i32::try_from(self.queries.pending_count()).unwrap_or(i32::MAX) } #[native(name = "mysql_log")] - pub fn mysql_log(&mut self, _amx: &Amx, log_level: i32) -> AmxResult { + pub fn mysql_log(&mut self, _amx: &Amx, log_level: i32) -> bool { Logger::set_log_level(log_level); - Ok(true) + true } } diff --git a/src/natives/error.rs b/src/natives/error.rs index 469d034..8e76735 100644 --- a/src/natives/error.rs +++ b/src/natives/error.rs @@ -5,9 +5,8 @@ use crate::plugin::MysqlPlugin; impl MysqlPlugin { #[native(name = "mysql_errno")] - pub fn mysql_errno(&mut self, _amx: &Amx, conn_id: i32) -> AmxResult { - let error = self.connections.get_error(conn_id); - Ok(error.code.code()) + pub fn mysql_errno(&mut self, _amx: &Amx, conn_id: i32) -> i32 { + self.connections.get_error(conn_id).code.code() } #[native(name = "mysql_error")] @@ -20,8 +19,7 @@ impl MysqlPlugin { ) -> AmxResult { let error = self.connections.get_error(conn_id); let msg = error.message.clone(); - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &msg); + dest.write_str(dest_len, &msg)?; Ok(true) } } diff --git a/src/natives/options.rs b/src/natives/options.rs index 0dde123..cd99d29 100644 --- a/src/natives/options.rs +++ b/src/natives/options.rs @@ -6,9 +6,8 @@ use crate::plugin::MysqlPlugin; impl MysqlPlugin { #[native(name = "mysql_options_new")] - pub fn mysql_options_new(&mut self, _amx: &Amx) -> AmxResult { - let id = self.options.create(); - Ok(id) + pub fn mysql_options_new(&mut self, _amx: &Amx) -> i32 { + self.options.create() } #[native(name = "mysql_options_set_int")] @@ -18,10 +17,10 @@ impl MysqlPlugin { handle: i32, option: i32, value: i32, - ) -> AmxResult { + ) -> bool { match MysqlOptionKind::from_i32(option) { - Some(kind) => Ok(self.options.set_int(handle, kind, value)), - None => Ok(false), + Some(kind) => self.options.set_int(handle, kind, value), + None => false, } } @@ -31,11 +30,11 @@ impl MysqlPlugin { _amx: &Amx, handle: i32, option: i32, - value: AmxString, - ) -> AmxResult { + value: &AmxString, + ) -> bool { match MysqlOptionKind::from_i32(option) { - Some(kind) => Ok(self.options.set_str(handle, kind, value.to_string())), - None => Ok(false), + Some(kind) => self.options.set_str(handle, kind, value.to_string()), + None => false, } } } diff --git a/src/natives/orm.rs b/src/natives/orm.rs index 42da35c..653ac15 100644 --- a/src/natives/orm.rs +++ b/src/natives/orm.rs @@ -5,373 +5,173 @@ use samp::prelude::*; use crate::error::{ErrorState, MysqlError}; use crate::logger::Logger; use crate::natives::query::parse_variadic_params; -use crate::orm::{OrmError, OrmVarBinding}; +use crate::orm::{MAX_ORM_STRING_LEN, OrmError, OrmInstance, OrmVarBinding}; use crate::plugin::MysqlPlugin; use crate::query::CallbackInfo; +/// One of the five threaded CRUD operations exposed to Pawn. +/// +/// Bundles every per-operation detail (display name, build error code, +/// build error message, query builder) in one place so that +/// [`MysqlPlugin::run_orm_op`] can stay generic over the operation. +#[derive(Clone, Copy)] +enum OrmOp { + Select, + Update, + Insert, + Delete, + /// `Save` is `Insert` when the key column is empty, `Update` otherwise. + Save, +} + +impl OrmOp { + fn name(self) -> &'static str { + match self { + Self::Select => "select", + Self::Update => "update", + Self::Insert => "insert", + Self::Delete => "delete", + Self::Save => "save", + } + } + + /// Error code reported via `mysql_errno`/global state when + /// [`OrmOp::build`] returns `None`. + fn build_error_code(self) -> MysqlError { + match self { + Self::Select | Self::Update | Self::Delete => MysqlError::OrmKeyNotSet, + Self::Insert | Self::Save => MysqlError::InvalidOrm, + } + } + + /// Human-readable cause appended to the warning when + /// [`OrmOp::build`] returns `None`. + fn build_error_detail(self) -> &'static str { + match self { + Self::Select | Self::Update => "key column not set or no variables bound", + Self::Insert => "no variables bound", + Self::Delete => "key column not set", + Self::Save => "cannot build query", + } + } + + fn build(self, inst: &OrmInstance) -> Option { + match self { + Self::Select => inst.build_select(), + Self::Update => inst.build_update(), + Self::Insert => inst.build_insert(), + Self::Delete => inst.build_delete(), + Self::Save => { + if inst.is_key_empty() { + inst.build_insert() + } else { + inst.build_update() + } + } + } + } +} + impl MysqlPlugin { /// orm_create(const table[], connId) #[native(name = "orm_create")] - pub fn orm_create( - &mut self, - amx: &Amx, - table: AmxString, - conn_id: i32, - ) -> AmxResult { + pub fn orm_create(&mut self, amx: &Amx, table: &AmxString, conn_id: i32) -> i32 { if !self.connections.exists(conn_id) { Logger::warn("ORM create failed: invalid connection ID."); self.connections.global_error = ErrorState::new( MysqlError::InvalidConnection, "ORM create failed: invalid connection ID.", ); - return Ok(0); + return 0; } let ident = amx.ident(); - let id = self.orm.create(table.to_string(), conn_id, ident); - Ok(id) + self.orm.create(table.to_string(), conn_id, ident) } /// orm_destroy(orm_id) #[native(name = "orm_destroy")] - pub fn orm_destroy(&mut self, _amx: &Amx, orm_id: i32) -> AmxResult { - Ok(self.orm.destroy(orm_id)) + pub fn orm_destroy(&mut self, _amx: &Amx, orm_id: i32) -> bool { + self.orm.destroy(orm_id) } /// orm_errno(orm_id) #[native(name = "orm_errno")] - pub fn orm_errno(&mut self, _amx: &Amx, orm_id: i32) -> AmxResult { - match self.orm.get(orm_id) { - Some(inst) => Ok(inst.errno as i32), - None => Ok(-1), - } + pub fn orm_errno(&mut self, _amx: &Amx, orm_id: i32) -> i32 { + self.orm.get(orm_id).map_or(-1, |inst| inst.errno as i32) } /// orm_select(orm_id, const callback[] = "", const format[] = "", {Float,_}:...) #[native(name = "orm_select", raw)] - pub fn orm_select(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; - - let callback_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let format_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let inst = match self.orm.get(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM select failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM select failed: invalid ORM ID.", - ); - return Ok(false); - } - }; - - let query = match inst.build_select() { - Some(q) => q, - None => { - Logger::warn("ORM select failed: key column not set or no variables bound."); - self.connections.global_error = ErrorState::new( - MysqlError::OrmKeyNotSet, - "ORM select failed: key column not set or no variables bound.", - ); - return Ok(false); - } - }; - - let conn_id = inst.conn_id; - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("ORM select failed: invalid connection ID."); - return Ok(false); - } - }; - - let callback_info = if callback_str.is_empty() { - None - } else { - let params = parse_variadic_params(&mut args, &format_str, 3); - Some(CallbackInfo { - name: callback_str, - format: format_str, - params, - }) - }; - - self.queries - .submit_query(pool, query, callback_info, conn_id, self.connections.get_auto_reconnect(conn_id)); - Ok(true) + pub fn orm_select(&mut self, _amx: &Amx, args: Args) -> bool { + self.run_orm_op(OrmOp::Select, args) } /// orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...) #[native(name = "orm_update", raw)] - pub fn orm_update(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; - - let callback_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let format_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let inst = match self.orm.get(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM update failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM update failed: invalid ORM ID.", - ); - return Ok(false); - } - }; - - let query = match inst.build_update() { - Some(q) => q, - None => { - Logger::warn("ORM update failed: key column not set or no variables bound."); - self.connections.global_error = ErrorState::new( - MysqlError::OrmKeyNotSet, - "ORM update failed: key column not set or no variables bound.", - ); - return Ok(false); - } - }; - - let conn_id = inst.conn_id; - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("ORM update failed: invalid connection ID."); - return Ok(false); - } - }; - - let callback_info = if callback_str.is_empty() { - None - } else { - let params = parse_variadic_params(&mut args, &format_str, 3); - Some(CallbackInfo { - name: callback_str, - format: format_str, - params, - }) - }; - - self.queries - .submit_query(pool, query, callback_info, conn_id, self.connections.get_auto_reconnect(conn_id)); - Ok(true) + pub fn orm_update(&mut self, _amx: &Amx, args: Args) -> bool { + self.run_orm_op(OrmOp::Update, args) } /// orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...) #[native(name = "orm_insert", raw)] - pub fn orm_insert(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; - - let callback_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let format_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let inst = match self.orm.get(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM insert failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM insert failed: invalid ORM ID.", - ); - return Ok(false); - } - }; - - let query = match inst.build_insert() { - Some(q) => q, - None => { - Logger::warn("ORM insert failed: no variables bound."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM insert failed: no variables bound.", - ); - return Ok(false); - } - }; - - let conn_id = inst.conn_id; - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("ORM insert failed: invalid connection ID."); - return Ok(false); - } - }; - - let callback_info = if callback_str.is_empty() { - None - } else { - let params = parse_variadic_params(&mut args, &format_str, 3); - Some(CallbackInfo { - name: callback_str, - format: format_str, - params, - }) - }; - - self.queries - .submit_query(pool, query, callback_info, conn_id, self.connections.get_auto_reconnect(conn_id)); - Ok(true) + pub fn orm_insert(&mut self, _amx: &Amx, args: Args) -> bool { + self.run_orm_op(OrmOp::Insert, args) } /// orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...) #[native(name = "orm_delete", raw)] - pub fn orm_delete(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; - - let callback_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let format_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; - - let inst = match self.orm.get(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM delete failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM delete failed: invalid ORM ID.", - ); - return Ok(false); - } - }; - - let query = match inst.build_delete() { - Some(q) => q, - None => { - Logger::warn("ORM delete failed: key column not set."); - self.connections.global_error = ErrorState::new( - MysqlError::OrmKeyNotSet, - "ORM delete failed: key column not set.", - ); - return Ok(false); - } - }; - - let conn_id = inst.conn_id; - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("ORM delete failed: invalid connection ID."); - return Ok(false); - } - }; - - let callback_info = if callback_str.is_empty() { - None - } else { - let params = parse_variadic_params(&mut args, &format_str, 3); - Some(CallbackInfo { - name: callback_str, - format: format_str, - params, - }) - }; - - self.queries - .submit_query(pool, query, callback_info, conn_id, self.connections.get_auto_reconnect(conn_id)); - Ok(true) + pub fn orm_delete(&mut self, _amx: &Amx, args: Args) -> bool { + self.run_orm_op(OrmOp::Delete, args) } /// orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...) - /// If key value is 0/empty, does INSERT. Otherwise does UPDATE. + /// INSERT when the key column is empty, UPDATE otherwise. #[native(name = "orm_save", raw)] - pub fn orm_save(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; + pub fn orm_save(&mut self, _amx: &Amx, args: Args) -> bool { + self.run_orm_op(OrmOp::Save, args) + } - let callback_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), + /// Shared pipeline for the five threaded CRUD natives. Parses the + /// common prelude (orm_id, callback, format), looks up the instance, + /// asks `op` to build the SQL string, resolves the pool, builds the + /// callback descriptor and submits the query. + fn run_orm_op(&mut self, op: OrmOp, mut args: Args) -> bool { + let Some(orm_id) = args.next_arg::() else { + return false; }; + let callback_str: String = args + .next_arg::() + .map(|v| v.to_string()) + .unwrap_or_default(); + let format_str: String = args + .next_arg::() + .map(|v| v.to_string()) + .unwrap_or_default(); - let format_str: String = match args.next_arg::() { - Some(v) => v.to_string(), - None => String::new(), - }; + let name = op.name(); - let inst = match self.orm.get(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM save failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM save failed: invalid ORM ID.", - ); - return Ok(false); - } - }; + let (query, conn_id) = { + let Some(inst) = self.orm.get(orm_id) else { + let msg = format!("ORM {name} failed: invalid ORM ID."); + Logger::warn(&msg); + self.connections.global_error = ErrorState::new(MysqlError::InvalidOrm, msg); + return false; + }; - let is_insert = inst.is_key_empty(); - let query = if is_insert { - inst.build_insert() - } else { - inst.build_update() - }; + let Some(query) = op.build(inst) else { + let msg = format!("ORM {name} failed: {}.", op.build_error_detail()); + Logger::warn(&msg); + self.connections.global_error = ErrorState::new(op.build_error_code(), msg); + return false; + }; - let query = match query { - Some(q) => q, - None => { - Logger::warn("ORM save failed: cannot build query."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM save failed: cannot build query.", - ); - return Ok(false); - } + (query, inst.conn_id) }; - let conn_id = inst.conn_id; - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("ORM save failed: invalid connection ID."); - return Ok(false); - } + let Some(pool) = self.connections.get_pool(conn_id) else { + Logger::warn(&format!("ORM {name} failed: invalid connection ID.")); + return false; }; let callback_info = if callback_str.is_empty() { @@ -385,144 +185,124 @@ impl MysqlPlugin { }) }; - self.queries - .submit_query(pool, query, callback_info, conn_id, self.connections.get_auto_reconnect(conn_id)); - Ok(true) + self.queries.submit_query( + pool, + query, + callback_info, + conn_id, + self.connections.get_auto_reconnect(conn_id), + ); + true } /// orm_apply_cache(orm_id, row = 0) #[native(name = "orm_apply_cache")] - pub fn orm_apply_cache( - &mut self, - amx: &Amx, - orm_id: i32, - row: i32, - ) -> AmxResult { - let cache = match self.cache.get_active() { - Some(c) => c, - None => { - Logger::warn("ORM apply_cache failed: no active cache."); - self.connections.global_error = ErrorState::new( - MysqlError::NoCacheActive, - "ORM apply_cache failed: no active cache.", - ); - return Ok(false); - } + pub fn orm_apply_cache(&mut self, amx: &Amx, orm_id: i32, row: i32) -> bool { + let Some(cache) = self.cache.get_active() else { + Logger::warn("ORM apply_cache failed: no active cache."); + self.connections.global_error = ErrorState::new( + MysqlError::NoCacheActive, + "ORM apply_cache failed: no active cache.", + ); + return false; }; - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => { - Logger::warn("ORM apply_cache failed: invalid ORM ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidOrm, - "ORM apply_cache failed: invalid ORM ID.", - ); - return Ok(false); - } + let Some(inst) = self.orm.get_mut(orm_id) else { + Logger::warn("ORM apply_cache failed: invalid ORM ID."); + self.connections.global_error = ErrorState::new( + MysqlError::InvalidOrm, + "ORM apply_cache failed: invalid ORM ID.", + ); + return false; + }; + + let Ok(row_idx) = usize::try_from(row) else { + inst.errno = OrmError::NoData; + return false; }; - if row as usize >= cache.row_count() { + if row_idx >= cache.row_count() { inst.errno = OrmError::NoData; - return Ok(false); + return false; } - inst.apply_cache(amx, cache, row as usize); + inst.apply_cache(amx, cache, row_idx); inst.errno = OrmError::Ok; - Ok(true) + true } /// orm_addvar_int(orm_id, &var, const column_name[]) #[native(name = "orm_addvar_int", raw)] - pub fn orm_addvar_int(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + pub fn orm_addvar_int(&mut self, _amx: &Amx, mut args: Args) -> bool { + let Some(orm_id) = args.next_arg::() else { + return false; }; - - let var_ref: Ref = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(var_ref) = args.next_arg::>() else { + return false; }; - - let column: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(column) = args.next_arg::() else { + return false; }; - - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; inst.variables.push(OrmVarBinding::Int { amx_addr: var_ref.address(), column: column.to_string(), }); - Ok(true) + true } /// orm_addvar_float(orm_id, &Float:var, const column_name[]) #[native(name = "orm_addvar_float", raw)] - pub fn orm_addvar_float(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + pub fn orm_addvar_float(&mut self, _amx: &Amx, mut args: Args) -> bool { + let Some(orm_id) = args.next_arg::() else { + return false; }; - - let var_ref: Ref = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(var_ref) = args.next_arg::>() else { + return false; }; - - let column: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(column) = args.next_arg::() else { + return false; }; - - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; inst.variables.push(OrmVarBinding::Float { amx_addr: var_ref.address(), column: column.to_string(), }); - Ok(true) + true } /// orm_addvar_string(orm_id, var[], var_max_len, const column_name[]) #[native(name = "orm_addvar_string", raw)] - pub fn orm_addvar_string(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let orm_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + pub fn orm_addvar_string(&mut self, _amx: &Amx, mut args: Args) -> bool { + let Some(orm_id) = args.next_arg::() else { + return false; }; - - let var_ref: Ref = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(var_ref) = args.next_arg::>() else { + return false; }; - - let max_len: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(max_len) = args.next_arg::() else { + return false; }; - - let column: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(column) = args.next_arg::() else { + return false; }; - if max_len <= 0 || max_len > 4096 { - Logger::warn("ORM addvar_string failed: max_len must be between 1 and 4096."); - return Ok(false); + if max_len <= 0 || max_len > MAX_ORM_STRING_LEN { + Logger::warn(&format!( + "ORM addvar_string failed: max_len must be between 1 and {}.", + MAX_ORM_STRING_LEN + )); + return false; } - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; inst.variables.push(OrmVarBinding::String { @@ -530,54 +310,40 @@ impl MysqlPlugin { max_len, column: column.to_string(), }); - Ok(true) + true } /// orm_delvar(orm_id, const column_name[]) #[native(name = "orm_delvar")] - pub fn orm_delvar( - &mut self, - _amx: &Amx, - orm_id: i32, - column_name: AmxString, - ) -> AmxResult { - let name = column_name.to_string(); - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + pub fn orm_delvar(&mut self, _amx: &Amx, orm_id: i32, column_name: &AmxString) -> bool { + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; let before = inst.variables.len(); - inst.variables.retain(|v| v.column_name() != name); - Ok(inst.variables.len() < before) + inst.variables.retain(|v| v.column_name() != &**column_name); + inst.variables.len() < before } /// orm_clear_vars(orm_id) #[native(name = "orm_clear_vars")] - pub fn orm_clear_vars(&mut self, _amx: &Amx, orm_id: i32) -> AmxResult { - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + pub fn orm_clear_vars(&mut self, _amx: &Amx, orm_id: i32) -> bool { + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; inst.variables.clear(); - Ok(true) + true } /// orm_setkey(orm_id, const column_name[]) #[native(name = "orm_setkey")] - pub fn orm_setkey( - &mut self, - _amx: &Amx, - orm_id: i32, - column_name: AmxString, - ) -> AmxResult { - let inst = match self.orm.get_mut(orm_id) { - Some(i) => i, - None => return Ok(false), + pub fn orm_setkey(&mut self, _amx: &Amx, orm_id: i32, column_name: &AmxString) -> bool { + let Some(inst) = self.orm.get_mut(orm_id) else { + return false; }; inst.key_column = Some(column_name.to_string()); - Ok(true) + true } } diff --git a/src/natives/query.rs b/src/natives/query.rs index ef2e63d..8edb826 100644 --- a/src/natives/query.rs +++ b/src/natives/query.rs @@ -9,142 +9,149 @@ use crate::logger::Logger; use crate::plugin::MysqlPlugin; use crate::query::{CallbackInfo, CallbackParam}; +/// Parameters bundled for [`MysqlPlugin::submit_query`]. +/// Groups every value that describes a single query submission so the +/// internal helper has one data argument instead of seven positional ones. +struct QueryRequest<'a> { + conn_id: i32, + query: &'a str, + callback: &'a str, + format: &'a str, + variadic_start: usize, + /// `true` for FIFO-ordered submission, `false` for parallel. + ordered: bool, +} + impl MysqlPlugin { /// mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...) /// Non-blocking threaded query with FIFO ordering. #[native(name = "mysql_query", raw)] - pub fn mysql_query(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let conn_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + pub fn mysql_query(&mut self, _amx: &Amx, mut args: Args) -> bool { + let Some(conn_id) = args.next_arg::() else { + return false; }; - let query_str: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(false), - }; - let callback_str: AmxString = match args.next_arg() { - Some(v) => v, - None => { - // callback is optional, default to empty - return self.submit_query(conn_id, &query_str.to_string(), "", "", &mut args, 3, true); - } + let Some(query_str) = args.next_arg::() else { + return false; }; - let format_str: AmxString = match args.next_arg() { - Some(v) => v, - None => { - return self.submit_query( - conn_id, - &query_str.to_string(), - &callback_str.to_string(), - "", - &mut args, - 3, - true, - ); - } + + let callback = args + .next_arg::() + .map(|s| s.to_string()) + .unwrap_or_default(); + let format = args + .next_arg::() + .map(|s| s.to_string()) + .unwrap_or_default(); + let variadic_start = if callback.is_empty() || format.is_empty() { + 3 + } else { + 4 }; self.submit_query( - conn_id, - &query_str.to_string(), - &callback_str.to_string(), - &format_str.to_string(), + QueryRequest { + conn_id, + query: &query_str.to_string(), + callback: &callback, + format: &format, + variadic_start, + ordered: true, + }, &mut args, - 4, - true, ) } /// mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...) /// Non-blocking parallel query (no order guarantee). #[native(name = "mysql_pquery", raw)] - pub fn mysql_pquery(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let conn_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + pub fn mysql_pquery(&mut self, _amx: &Amx, mut args: Args) -> bool { + let Some(conn_id) = args.next_arg::() else { + return false; }; - let query_str: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(false), + let Some(query_str) = args.next_arg::() else { + return false; }; - let callback_str: AmxString = match args.next_arg() { - Some(v) => v, - None => { - return self.submit_query(conn_id, &query_str.to_string(), "", "", &mut args, 3, false); - } - }; - let format_str: AmxString = match args.next_arg() { - Some(v) => v, - None => { - return self.submit_query( - conn_id, - &query_str.to_string(), - &callback_str.to_string(), - "", - &mut args, - 3, - false, - ); - } + + let callback = args + .next_arg::() + .map(|s| s.to_string()) + .unwrap_or_default(); + let format = args + .next_arg::() + .map(|s| s.to_string()) + .unwrap_or_default(); + let variadic_start = if callback.is_empty() || format.is_empty() { + 3 + } else { + 4 }; self.submit_query( - conn_id, - &query_str.to_string(), - &callback_str.to_string(), - &format_str.to_string(), + QueryRequest { + conn_id, + query: &query_str.to_string(), + callback: &callback, + format: &format, + variadic_start, + ordered: false, + }, &mut args, - 4, - false, ) } /// Internal: submits a query (ordered or parallel). - #[allow(clippy::too_many_arguments)] - fn submit_query( - &mut self, - conn_id: i32, - query: &str, - callback: &str, - format: &str, - args: &mut Args, - variadic_start: usize, - ordered: bool, - ) -> AmxResult { - let pool = match self.connections.get_pool(conn_id) { - Some(p) => p, - None => { - Logger::warn("Query failed: invalid connection ID."); - self.connections.global_error = ErrorState::new( - MysqlError::InvalidConnection, - "Query failed: invalid connection ID.", - ); - return Ok(false); - } + fn submit_query(&mut self, req: QueryRequest<'_>, args: &mut Args) -> bool { + let Some(pool) = self.connections.get_pool(req.conn_id) else { + Logger::warn("Query failed: invalid connection ID."); + self.connections.global_error = ErrorState::new( + MysqlError::InvalidConnection, + "Query failed: invalid connection ID.", + ); + return false; }; - let auto_reconnect = self.connections.get_auto_reconnect(conn_id); + let auto_reconnect = self.connections.get_auto_reconnect(req.conn_id); - let callback_info = if callback.is_empty() { + let callback_info = if req.callback.is_empty() { None } else { - let params = parse_variadic_params(args, format, variadic_start); + let params = parse_variadic_params(args, req.format, req.variadic_start); Some(CallbackInfo { - name: callback.to_string(), - format: format.to_string(), + name: req.callback.to_string(), + format: req.format.to_string(), params, }) }; - if ordered { - self.queries - .submit_query(pool, query.to_string(), callback_info, conn_id, auto_reconnect); + if req.ordered { + self.queries.submit_query( + pool, + req.query.to_string(), + callback_info, + req.conn_id, + auto_reconnect, + ); } else { - self.queries - .submit_pquery(pool, query.to_string(), callback_info, conn_id, auto_reconnect); + self.queries.submit_pquery( + pool, + req.query.to_string(), + callback_info, + req.conn_id, + auto_reconnect, + ); } - Ok(true) + true + } + + /// mysql_tick() + /// Kept only for backwards compatibility — since rust-samp v3.0.0 the + /// unified `on_tick` already dispatches callbacks automatically on both + /// SA-MP and native Open Multiplayer. + #[native(name = "mysql_tick")] + pub fn mysql_tick(&mut self, _amx: &Amx) -> bool { + self.process_pending_queries(); + true } /// mysql_escape_string(const src[], dest[], max_len = sizeof(dest)) @@ -152,102 +159,70 @@ impl MysqlPlugin { pub fn mysql_escape_string( &mut self, _amx: &Amx, - src: AmxString, + src: &AmxString, dest: UnsizedBuffer, dest_len: usize, ) -> AmxResult { - let escaped = escape_string(&src.to_string()); - let mut buf = dest.into_sized_buffer(dest_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &escaped); + let escaped = escape_string(src); + dest.write_str(dest_len, &escaped)?; Ok(true) } /// mysql_format(connId, dest[], max_len, const format[], {Float,_}:...) + /// + /// Truncates the rendered string to `max_len - 1` characters (leaving room + /// for the AMX NUL terminator) instead of aborting when the buffer is too + /// small. A warning is logged once per call when truncation occurs. #[native(name = "mysql_format", raw)] - pub fn mysql_format(&mut self, _amx: &Amx, mut args: Args) -> AmxResult { - let _conn_id: i32 = match args.next_arg() { - Some(v) => v, - None => return Ok(0), + pub fn mysql_format(&mut self, _amx: &Amx, mut args: Args) -> i32 { + let Some(_conn_id) = args.next_arg::() else { + return 0; }; - let dest: UnsizedBuffer = match args.next_arg() { - Some(v) => v, - None => return Ok(0), + let Some(dest) = args.next_arg::() else { + return 0; }; - let max_len: usize = match args.next_arg() { - Some(v) => v, - None => return Ok(0), + let Some(max_len) = args.next_arg::() else { + return 0; }; - let format_str: AmxString = match args.next_arg() { - Some(v) => v, - None => return Ok(0), + let Some(format_str) = args.next_arg::() else { + return 0; }; let fmt = format_str.to_string(); - let mut output = String::new(); - let mut param_offset = 4; // variadic params start at index 4 - - let mut chars = fmt.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '%' { - match chars.next() { - Some('d') | Some('i') => { - let val: i32 = args.get::>(param_offset).map(|r| *r).unwrap_or(0); - output.push_str(&val.to_string()); - param_offset += 1; - } - Some('f') => { - let val: f32 = args.get::>(param_offset).map(|r| *r).unwrap_or(0.0); - output.push_str(&format!("{:.4}", val)); - param_offset += 1; - } - Some('s') | Some('e') => { - // %s and %e both escape strings (safe by default) - let val: AmxString = match args.get(param_offset) { - Some(v) => v, - None => { - output.push_str(""); - param_offset += 1; - continue; - } - }; - output.push_str(&escape_string(&val.to_string())); - param_offset += 1; - } - Some('r') => { - // %r = raw string (no escaping — use only for trusted values) - let val: AmxString = match args.get(param_offset) { - Some(v) => v, - None => { - output.push_str(""); - param_offset += 1; - continue; - } - }; - output.push_str(&val.to_string()); - param_offset += 1; - } - Some('%') => output.push('%'), - Some(other) => { - output.push('%'); - output.push(other); - } - None => output.push('%'), - } - } else { - output.push(ch); - } + let specs = parse_format(&fmt); + let values = collect_format_values(&args, &specs, 4); + let rendered = render_format(&specs, &values); + + if rendered.unknown_specs > 0 { + Logger::warn(&format!( + "mysql_format: {} unknown format specifier(s) in pattern.", + rendered.unknown_specs + )); } - let mut buf = dest.into_sized_buffer(max_len); - let _ = samp::cell::string::put_in_buffer(&mut buf, &output); - Ok(output.len() as i32) + let (output, truncated) = truncate_to_buffer(&rendered.output, max_len); + if truncated { + Logger::warn(&format!( + "mysql_format: output truncated to fit destination buffer ({} of {} bytes).", + output.len(), + rendered.output.len() + )); + } + + if let Err(err) = dest.write_str(max_len, output) { + Logger::warn(&format!("mysql_format: write failed: {:?}", err)); + return 0; + } + i32::try_from(output.len()).unwrap_or(i32::MAX) } } /// Parses variadic callback parameters based on the format string. +/// Unknown format characters trigger a warning but are otherwise skipped. pub fn parse_variadic_params(args: &mut Args, format: &str, start: usize) -> Vec { let mut params = Vec::new(); let mut offset = start; + let mut unknown = 0usize; for ch in format.chars() { match ch { @@ -262,20 +237,318 @@ pub fn parse_variadic_params(args: &mut Args, format: &str, start: usize) -> Vec offset += 1; } 's' => { - let val: AmxString = match args.get(offset) { - Some(v) => v, - None => { - params.push(CallbackParam::String(String::new())); - offset += 1; - continue; - } - }; - params.push(CallbackParam::String(val.to_string())); + let val: String = args + .get::(offset) + .map(|s| s.to_string()) + .unwrap_or_default(); + params.push(CallbackParam::String(val)); offset += 1; } - _ => {} + _ => unknown += 1, } } + if unknown > 0 { + Logger::warn(&format!( + "callback format string contains {} unknown specifier(s) — only 'd', 'i', 'f', 's' are recognized.", + unknown + )); + } + params } + +// --------------------------------------------------------------------------- +// Pure helpers (tested in #[cfg(test)] below). +// --------------------------------------------------------------------------- + +/// A single token produced by [`parse_format`]. +#[derive(Debug, Clone, PartialEq)] +pub enum FormatSpec { + Literal(String), + Int, + Float, + /// SQL-escaped string (`%s`, `%e`). + EscapedStr, + /// Raw string (`%r`) — inserted without escaping. + RawStr, + Percent, + Unknown(char), +} + +/// Value supplied for a single [`FormatSpec`] that needs one. +#[derive(Debug, Clone, PartialEq)] +pub enum FormatValue { + Int(i32), + Float(f32), + Str(String), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FormatResult { + pub output: String, + pub unknown_specs: usize, +} + +pub fn parse_format(fmt: &str) -> Vec { + let mut out = Vec::new(); + let mut buf = String::new(); + let mut chars = fmt.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch != '%' { + buf.push(ch); + continue; + } + if !buf.is_empty() { + out.push(FormatSpec::Literal(std::mem::take(&mut buf))); + } + match chars.next() { + Some('d') | Some('i') => out.push(FormatSpec::Int), + Some('f') => out.push(FormatSpec::Float), + Some('s') | Some('e') => out.push(FormatSpec::EscapedStr), + Some('r') => out.push(FormatSpec::RawStr), + Some('%') => out.push(FormatSpec::Percent), + Some(other) => out.push(FormatSpec::Unknown(other)), + None => out.push(FormatSpec::Literal("%".to_string())), + } + } + if !buf.is_empty() { + out.push(FormatSpec::Literal(buf)); + } + out +} + +pub fn render_format(specs: &[FormatSpec], values: &[FormatValue]) -> FormatResult { + let mut output = String::new(); + let mut values_iter = values.iter(); + let mut unknown = 0usize; + + for spec in specs { + match spec { + FormatSpec::Literal(text) => output.push_str(text), + FormatSpec::Percent => output.push('%'), + FormatSpec::Int => { + if let Some(FormatValue::Int(v)) = values_iter.next() { + output.push_str(&v.to_string()); + } + } + FormatSpec::Float => { + if let Some(FormatValue::Float(v)) = values_iter.next() { + output.push_str(&format!("{:.4}", v)); + } + } + FormatSpec::EscapedStr => { + if let Some(FormatValue::Str(s)) = values_iter.next() { + output.push_str(&escape_string(s)); + } + } + FormatSpec::RawStr => { + if let Some(FormatValue::Str(s)) = values_iter.next() { + output.push_str(s); + } + } + FormatSpec::Unknown(ch) => { + output.push('%'); + output.push(*ch); + unknown += 1; + } + } + } + + FormatResult { + output, + unknown_specs: unknown, + } +} + +/// Fetches one [`FormatValue`] per [`FormatSpec`] that requires a value, +/// reading positional args starting at `start_offset`. +fn collect_format_values( + args: &Args, + specs: &[FormatSpec], + start_offset: usize, +) -> Vec { + let mut values = Vec::new(); + let mut offset = start_offset; + + for spec in specs { + match spec { + FormatSpec::Int => { + let v: i32 = args.get::>(offset).map(|r| *r).unwrap_or(0); + values.push(FormatValue::Int(v)); + offset += 1; + } + FormatSpec::Float => { + let v: f32 = args.get::>(offset).map(|r| *r).unwrap_or(0.0); + values.push(FormatValue::Float(v)); + offset += 1; + } + FormatSpec::EscapedStr | FormatSpec::RawStr => { + let v: String = args + .get::(offset) + .map(|s| s.to_string()) + .unwrap_or_default(); + values.push(FormatValue::Str(v)); + offset += 1; + } + FormatSpec::Literal(_) | FormatSpec::Percent | FormatSpec::Unknown(_) => {} + } + } + values +} + +/// Truncates `s` so it fits in `max_len` (counting the AMX NUL terminator). +/// Returns the truncated slice and a flag indicating whether truncation +/// actually occurred. Respects UTF-8 char boundaries. +fn truncate_to_buffer(s: &str, max_len: usize) -> (&str, bool) { + if max_len == 0 { + return ("", !s.is_empty()); + } + let cap = max_len.saturating_sub(1); + if s.len() <= cap { + return (s, false); + } + // Walk back to the previous char boundary so we don't slice through UTF-8. + let mut end = cap; + while end > 0 && !s.is_char_boundary(end) { + end -= 1; + } + (&s[..end], true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_format_plain() { + assert_eq!( + parse_format("hello"), + vec![FormatSpec::Literal("hello".into())] + ); + } + + #[test] + fn parse_format_mix() { + let specs = parse_format("id=%d, name='%s', raw=%r, pct=%%, dunno=%z"); + assert_eq!( + specs, + vec![ + FormatSpec::Literal("id=".into()), + FormatSpec::Int, + FormatSpec::Literal(", name='".into()), + FormatSpec::EscapedStr, + FormatSpec::Literal("', raw=".into()), + FormatSpec::RawStr, + FormatSpec::Literal(", pct=".into()), + FormatSpec::Percent, + FormatSpec::Literal(", dunno=".into()), + FormatSpec::Unknown('z'), + ] + ); + } + + #[test] + fn parse_format_trailing_percent() { + assert_eq!( + parse_format("ends with %"), + vec![ + FormatSpec::Literal("ends with ".into()), + FormatSpec::Literal("%".into()), + ] + ); + } + + #[test] + fn render_format_basic() { + let specs = parse_format("INSERT INTO t (id, name) VALUES (%d, '%s')"); + let values = vec![FormatValue::Int(42), FormatValue::Str("O'Brien".into())]; + let r = render_format(&specs, &values); + assert_eq!( + r.output, + "INSERT INTO t (id, name) VALUES (42, 'O\\'Brien')" + ); + assert_eq!(r.unknown_specs, 0); + } + + #[test] + fn render_format_float_precision() { + let specs = parse_format("v=%f"); + let r = render_format(&specs, &[FormatValue::Float(1.5)]); + assert_eq!(r.output, "v=1.5000"); + } + + #[test] + fn render_format_raw_string_not_escaped() { + let specs = parse_format("raw=%r"); + let r = render_format(&specs, &[FormatValue::Str("a'b".into())]); + assert_eq!(r.output, "raw=a'b"); + } + + #[test] + fn render_format_escaped_string_is_escaped() { + let specs = parse_format("v=%e"); + let r = render_format(&specs, &[FormatValue::Str("a'b".into())]); + assert_eq!(r.output, "v=a\\'b"); + } + + #[test] + fn render_format_percent_literal() { + let specs = parse_format("100%%"); + let r = render_format(&specs, &[]); + assert_eq!(r.output, "100%"); + } + + #[test] + fn render_format_unknown_spec_counted() { + let specs = parse_format("%z %q"); + let r = render_format(&specs, &[]); + assert_eq!(r.unknown_specs, 2); + assert_eq!(r.output, "%z %q"); + } + + #[test] + fn render_format_missing_value_skipped() { + let specs = parse_format("%d-%d"); + let r = render_format(&specs, &[FormatValue::Int(5)]); + assert_eq!(r.output, "5-"); + } + + #[test] + fn truncate_within_capacity() { + let (s, t) = truncate_to_buffer("hello", 10); + assert_eq!(s, "hello"); + assert!(!t); + } + + #[test] + fn truncate_oversized_ascii() { + // max_len = 5 means 4 chars + NUL — output should be "hell". + let (s, t) = truncate_to_buffer("hello world", 5); + assert_eq!(s, "hell"); + assert!(t); + } + + #[test] + fn truncate_respects_utf8_boundary() { + // "café" = 5 bytes (c=1, a=1, f=1, é=2). cap = 4 would split é → walks back to 3. + let (s, t) = truncate_to_buffer("café", 5); + assert_eq!(s, "caf"); + assert!(t); + } + + #[test] + fn truncate_zero_capacity() { + let (s, t) = truncate_to_buffer("anything", 0); + assert_eq!(s, ""); + assert!(t); + } + + #[test] + fn truncate_zero_capacity_empty_input() { + let (s, t) = truncate_to_buffer("", 0); + assert_eq!(s, ""); + assert!(!t); + } +} diff --git a/src/options.rs b/src/options.rs index 78dc9d7..9deb964 100644 --- a/src/options.rs +++ b/src/options.rs @@ -74,9 +74,19 @@ impl OptionsManager { }; match option { - MysqlOptionKind::Port => opts.port = value as u16, + MysqlOptionKind::Port => { + let Ok(port) = u16::try_from(value) else { + return false; + }; + opts.port = port; + } MysqlOptionKind::Ssl => opts.ssl = value != 0, - MysqlOptionKind::ConnectTimeout => opts.connect_timeout = Some(value as u32), + MysqlOptionKind::ConnectTimeout => { + let Ok(timeout) = u32::try_from(value) else { + return false; + }; + opts.connect_timeout = Some(timeout); + } MysqlOptionKind::AutoReconnect => opts.auto_reconnect = value != 0, _ => return false, } @@ -109,8 +119,14 @@ mod tests { assert_eq!(MysqlOptionKind::from_i32(0), Some(MysqlOptionKind::Port)); assert_eq!(MysqlOptionKind::from_i32(1), Some(MysqlOptionKind::Ssl)); assert_eq!(MysqlOptionKind::from_i32(2), Some(MysqlOptionKind::SslCa)); - assert_eq!(MysqlOptionKind::from_i32(3), Some(MysqlOptionKind::ConnectTimeout)); - assert_eq!(MysqlOptionKind::from_i32(4), Some(MysqlOptionKind::AutoReconnect)); + assert_eq!( + MysqlOptionKind::from_i32(3), + Some(MysqlOptionKind::ConnectTimeout) + ); + assert_eq!( + MysqlOptionKind::from_i32(4), + Some(MysqlOptionKind::AutoReconnect) + ); } #[test] diff --git a/src/orm.rs b/src/orm.rs index e345062..8c0eec3 100644 --- a/src/orm.rs +++ b/src/orm.rs @@ -5,6 +5,10 @@ use samp::prelude::*; use crate::connection::{escape_identifier, escape_string}; +/// Upper bound on the buffer size accepted by `orm_addvar_string`. +/// Protects the AMX heap from oversized writes via a hostile `max_len`. +pub const MAX_ORM_STRING_LEN: i32 = 4096; + /// ORM error codes exposed to Pawn. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(i32)] @@ -16,9 +20,19 @@ pub enum OrmError { /// Represents a bound Pawn variable mapped to a database column. #[derive(Debug, Clone)] pub enum OrmVarBinding { - Int { amx_addr: i32, column: String }, - Float { amx_addr: i32, column: String }, - String { amx_addr: i32, max_len: i32, column: String }, + Int { + amx_addr: i32, + column: String, + }, + Float { + amx_addr: i32, + column: String, + }, + String { + amx_addr: i32, + max_len: i32, + column: String, + }, } impl OrmVarBinding { @@ -76,14 +90,19 @@ impl OrmInstance { let ptr = r.as_ptr(); match amx.strlen(ptr) { Ok(len) => { - let actual_len = len.min(max_len as usize); + let max_len_usize = usize::try_from(max_len).unwrap_or(0); + let actual_len = len.min(max_len_usize); let mut chars = Vec::with_capacity(actual_len); for i in 0..actual_len { let cell = unsafe { *ptr.add(i) }; if cell == 0 { break; } - chars.push(cell as u8 as char); + // AMX cells holding char data fit in 0..=255 by + // construction; anything outside that range is + // truncated to the low byte (matches AMX behavior). + let byte = u8::try_from(cell & 0xFF).unwrap_or(0); + chars.push(char::from(byte)); } chars.into_iter().collect() } @@ -103,7 +122,9 @@ impl OrmInstance { OrmVarBinding::Float { amx_addr, .. } => { format!("{}", self.read_float(amx, *amx_addr)) } - OrmVarBinding::String { amx_addr, max_len, .. } => { + OrmVarBinding::String { + amx_addr, max_len, .. + } => { let raw = self.read_string(amx, *amx_addr, *max_len); format!("'{}'", escape_string(&raw)) } @@ -230,19 +251,14 @@ impl OrmInstance { match var { OrmVarBinding::Int { amx_addr, .. } => self.read_int(amx, *amx_addr) == 0, OrmVarBinding::Float { amx_addr, .. } => self.read_float(amx, *amx_addr) == 0.0, - OrmVarBinding::String { amx_addr, max_len, .. } => { - self.read_string(amx, *amx_addr, *max_len).is_empty() - } + OrmVarBinding::String { + amx_addr, max_len, .. + } => self.read_string(amx, *amx_addr, *max_len).is_empty(), } } /// Writes cache values into the bound Pawn variables. - pub fn apply_cache( - &self, - amx: &Amx, - cache: &crate::cache::CacheEntry, - row: usize, - ) { + pub fn apply_cache(&self, amx: &Amx, cache: &crate::cache::CacheEntry, row: usize) { for var in &self.variables { let col_idx = match cache.field_index(var.column_name()) { Some(i) => i, @@ -265,9 +281,14 @@ impl OrmInstance { *r = value.parse::().unwrap_or(0.0); } } - OrmVarBinding::String { amx_addr, max_len, .. } => { - // Clamp max_len to a safe upper bound to prevent OOB writes - let safe_max = (*max_len).clamp(0, 4096) as usize; + OrmVarBinding::String { + amx_addr, max_len, .. + } => { + // Clamp max_len to a safe upper bound to prevent OOB writes. + // The clamp guarantees the value is in 0..=MAX_ORM_STRING_LEN, + // so the try_from below is infallible. + let safe_max = + usize::try_from((*max_len).clamp(0, MAX_ORM_STRING_LEN)).unwrap_or(0); if safe_max == 0 { continue; } @@ -277,7 +298,7 @@ impl OrmInstance { let bytes = value.as_bytes(); let write_len = bytes.len().min(max); for (i, &byte) in bytes.iter().enumerate().take(write_len) { - unsafe { *ptr.add(i) = byte as i32 }; + unsafe { *ptr.add(i) = i32::from(byte) }; } unsafe { *ptr.add(write_len) = 0 }; // null terminator } diff --git a/src/plugin.rs b/src/plugin.rs index 498c25a..53acd40 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,4 +1,5 @@ use samp::amx::AmxIdent; +use samp::plugin::TickContext; use samp::prelude::*; use crate::cache::CacheManager; @@ -34,7 +35,7 @@ impl MysqlPlugin { } /// Processes completed threaded queries and dispatches callbacks. - fn process_pending_queries(&mut self) { + pub fn process_pending_queries(&mut self) { let results = self.queries.poll_results(); for result in results { @@ -57,15 +58,12 @@ impl MysqlPlugin { // Update the per-connection error state self.connections.set_error( result.conn_id, - ErrorState::new( - MysqlError::QueryFailed, - error.message.clone(), - ), + ErrorState::new(MysqlError::QueryFailed, error.message.clone()), ); callback::fire_on_query_error( &self.amx_list, - error.code as i32, + i32::from(error.code), &error.message, callback_name, result.cache.query_string(), @@ -110,7 +108,22 @@ impl SampPlugin for MysqlPlugin { self.orm.destroy_by_amx(ident); } - fn process_tick(&mut self) { + /// Unified tick callback (v3.0.0+): fires on both SA-MP (ProcessTick) and + /// Open Multiplayer native mode (ITimersComponent timer). Drives query + /// dispatch automatically — no Pawn timer required anymore. + fn on_tick(&mut self, _ctx: TickContext) { self.process_pending_queries(); } + + fn on_omp_ready(&mut self) { + Logger::info("Open Multiplayer native mode: all components ready."); + } + + /// Fires when any Open Multiplayer component is being released (not just + /// ours). We don't query other components, so there's nothing to + /// invalidate — the log line just helps correlate "mysql_samp misbehaved + /// after plugin X was unloaded" reports. + fn on_component_free(&mut self) { + Logger::info("Open Multiplayer: a neighbouring component is being unloaded."); + } } diff --git a/src/query.rs b/src/query.rs index 4300306..7a368fc 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,13 +1,13 @@ use std::collections::BTreeMap; +use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc; -use std::sync::Arc; use std::thread; use mysql::Pool; use crate::cache::CacheEntry; -use crate::connection::{attempt_query, QueryError}; +use crate::connection::{QueryError, attempt_query}; /// Parameter type for callback invocation. #[derive(Debug, Clone)] @@ -35,6 +35,12 @@ pub struct QueryResult { pub sequence: u64, } +/// Sequence threshold used to mark parallel-query results inside the same +/// `pending_ordered` map. Ordered queries use sequences starting at 0 and +/// growing by one; parallel results are stored at `u64::MAX` and counting +/// downward, so anything `>= PARALLEL_KEY_THRESHOLD` is a parallel entry. +const PARALLEL_KEY_THRESHOLD: u64 = u64::MAX / 2; + /// Manages threaded query execution and result collection. pub struct QueryManager { sender: mpsc::Sender, @@ -60,10 +66,11 @@ impl QueryManager { /// Returns the number of queries currently in-flight (submitted but not yet dispatched). pub fn pending_count(&self) -> u64 { - self.in_flight.load(Ordering::Relaxed) + self.pending_ordered.len() as u64 + self.in_flight.load(Ordering::Relaxed) + + u64::try_from(self.pending_ordered.len()).unwrap_or(u64::MAX) } -/// Submits an ordered query (FIFO — callbacks dispatched in submission order). + /// Submits an ordered query (FIFO — callbacks dispatched in submission order). pub fn submit_query( &mut self, pool: Pool, @@ -150,7 +157,7 @@ impl QueryManager { } else { // Parallel queries go to pending with a special high sequence // so they are dispatched after ordered ones in this tick - let key = u64::MAX - self.pending_ordered.len() as u64; + let key = u64::MAX - u64::try_from(self.pending_ordered.len()).unwrap_or(u64::MAX); self.pending_ordered.insert(key, result); } } @@ -167,7 +174,7 @@ impl QueryManager { let parallel_keys: Vec = self .pending_ordered .keys() - .filter(|k| **k >= u64::MAX / 2) + .filter(|k| **k >= PARALLEL_KEY_THRESHOLD) .copied() .collect(); From 54a50e1aaaa2feda55436c7f256a8004c5ad8c56 Mon Sep 17 00:00:00 2001 From: NullSablex <244216261+NullSablex@users.noreply.github.com> Date: Mon, 18 May 2026 12:37:02 -0300 Subject: [PATCH 2/2] Sanitize .inc header and fix clippy 1.95 collapsible_match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit include/mysql_samp.inc.in - Replace the em-dash (U+2014) with a plain ASCII hyphen in the header. Pawn compilers read the .inc as Windows-1252 / ANSI; a UTF-8 em-dash rendered as the mojibake "â€"" sequence in editors that don't decode the file as UTF-8. - Drop the "Auto-generated by build.rs / edit the .inc.in" note from the public header. That guidance is only useful to contributors who clone the repo and already see it in CLAUDE.md and in build.rs itself; it is noise for gamemode authors who download the .inc from a release. - Rephrase the tagline to "MySQL plugin for SA-MP and Open Multiplayer" (more informative than the previous "written in Rust"; implementation details belong in the README, not in the public header). src/callback.rs - Collapse the two nested `if amx.push(*v).is_err()` blocks inside the match for `('d' | 'i', ...)` and `('f', ...)` into match guards. Required by clippy::collapsible_match (denied by `-D warnings`) since clippy 1.95. Behavior is preserved: the push runs in both branches (it is part of the guard expression), and on failure the new arm body still sets push_ok = false and breaks. The ('s', ...) arm is left as a nested match because its two branches do different work. include/mysql_samp.inc is regenerated automatically by build.rs from the template and committed in lockstep. --- include/mysql_samp.inc | 9 +++------ include/mysql_samp.inc.in | 9 +++------ src/callback.rs | 16 ++++++---------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/include/mysql_samp.inc b/include/mysql_samp.inc index f62faf3..7496110 100644 --- a/include/mysql_samp.inc +++ b/include/mysql_samp.inc @@ -1,11 +1,8 @@ /* - * mysql_samp v1.1.0 — MySQL plugin for SA-MP written in Rust. - * Author: NullSablex + * mysql_samp v1.1.0 - MySQL plugin for SA-MP and Open Multiplayer. + * Author: NullSablex * Repository: https://github.com/NullSablex/mysql_samp - * License: GPL-3.0 - * - * Auto-generated by build.rs — do not edit this file directly. - * To change declarations, edit include/mysql_samp.inc.in instead. + * License: GPL-3.0 */ #if defined _mysql_samp_included diff --git a/include/mysql_samp.inc.in b/include/mysql_samp.inc.in index d452a1f..d8ae5b9 100644 --- a/include/mysql_samp.inc.in +++ b/include/mysql_samp.inc.in @@ -1,11 +1,8 @@ /* - * mysql_samp v{{VERSION}} — MySQL plugin for SA-MP written in Rust. - * Author: NullSablex + * mysql_samp v{{VERSION}} - MySQL plugin for SA-MP and Open Multiplayer. + * Author: NullSablex * Repository: https://github.com/NullSablex/mysql_samp - * License: GPL-3.0 - * - * Auto-generated by build.rs — do not edit this file directly. - * To change declarations, edit include/mysql_samp.inc.in instead. + * License: GPL-3.0 */ #if defined _mysql_samp_included diff --git a/src/callback.rs b/src/callback.rs index b704ee2..db12206 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -34,17 +34,13 @@ pub fn invoke_callback(amx_list: &[AmxIdent], info: &CallbackInfo) { }; match (ch, param) { - ('d' | 'i', CallbackParam::Int(v)) => { - if amx.push(*v).is_err() { - push_ok = false; - break; - } + ('d' | 'i', CallbackParam::Int(v)) if amx.push(*v).is_err() => { + push_ok = false; + break; } - ('f', CallbackParam::Float(v)) => { - if amx.push(*v).is_err() { - push_ok = false; - break; - } + ('f', CallbackParam::Float(v)) if amx.push(*v).is_err() => { + push_ok = false; + break; } ('s', CallbackParam::String(v)) => match allocator.allot_string(v) { Ok(s) => {