├── .dockerignore ├── .github └── workflows │ └── validation-rust.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile.examples ├── Dockerfile.mysql ├── LICENSE.Apache-2.0 ├── LICENSE.GPL-2.0 ├── README.md ├── release.toml ├── rustfmt.toml ├── udf-examples ├── Cargo.toml ├── README.md ├── src │ ├── attribute.rs │ ├── avg2.rs │ ├── avg_cost.rs │ ├── empty.rs │ ├── is_const.rs │ ├── lib.rs │ ├── lipsum.rs │ ├── log_calls.rs │ ├── lookup.rs │ ├── median.rs │ ├── mishmash.rs │ ├── sequence.rs │ └── sum_int.rs └── tests │ ├── attribute.rs │ ├── avg2.rs │ ├── avg_cost.rs │ ├── backend │ └── mod.rs │ ├── is_const.rs │ ├── lipsum.rs │ ├── lookup.rs │ ├── median.rs │ ├── mishmash.rs │ ├── sequence.rs │ └── sum_int.rs ├── udf-macros ├── Cargo.toml ├── src │ ├── lib.rs │ ├── register.rs │ └── types.rs └── tests │ ├── fail │ ├── agg_missing_basic.rs │ ├── agg_missing_basic.stderr │ ├── bad_attributes.rs │ ├── bad_attributes.stderr │ ├── missing_rename.rs │ ├── missing_rename.stderr │ ├── wrong_block.rs │ ├── wrong_block.stderr │ ├── wrong_impl.rs │ └── wrong_impl.stderr │ ├── ok.rs │ ├── ok_agg.rs │ ├── ok_agg_alias.rs │ └── runner_fail.rs ├── udf-sys ├── Cargo.toml ├── README.md ├── src │ └── lib.rs └── udf_registration_types.c └── udf ├── Cargo.toml └── src ├── lib.rs ├── macros.rs ├── mock.rs ├── prelude.rs ├── traits.rs ├── types.rs ├── types ├── arg.rs ├── arg_list.rs ├── config.rs └── sql_types.rs ├── wrapper.rs └── wrapper ├── const_helpers.rs ├── functions.rs ├── helpers.rs ├── modded_types.rs ├── process.rs └── tests.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .docker-cargo 3 | .git 4 | .github 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.github/workflows/validation-rust.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Main "useful" actions config file 4 | # Cache config comes from https://github.com/actions/cache/blob/main/examples.md#rust---cargo 5 | # actions-rs/toolchain configures rustup 6 | # actions-rs/cargo actually runs cargo 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | 14 | name: Rust Validation 15 | 16 | env: 17 | RUSTDOCFLAGS: -D warnings 18 | RUSTFLAGS: -D warnings -C debuginfo=1 19 | RUST_BACKTRACE: 1 20 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 21 | 22 | jobs: 23 | clippy: 24 | name: "Clippy (cargo clippy)" 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - run: rustup default 1.78.0 && rustup component add clippy && rustup update 29 | - uses: Swatinem/rust-cache@v2 30 | - run: cargo clippy --all-features --all-targets 31 | - run: cargo clippy --no-default-features --all-targets 32 | 33 | msrv: 34 | name: "Check MSRV" 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - run: rustup default 1.65.0 && rustup update 39 | - uses: Swatinem/rust-cache@v2 40 | - run: cargo check --all-features -p udf 41 | - run: cargo check --no-default-features -p udf 42 | 43 | test: 44 | strategy: 45 | fail-fast: true 46 | matrix: 47 | include: 48 | - build: linux 49 | os: ubuntu-latest 50 | target: x86_64-unknown-linux-musl 51 | extension: '' 52 | # - build: macos 53 | # os: macos-latest 54 | # target: x86_64-apple-darwin 55 | # extension: '' 56 | - build: windows-msvc 57 | os: windows-latest 58 | target: x86_64-pc-windows-msvc 59 | name: "Test on ${{ matrix.os }} (cargo test)" 60 | runs-on: ${{ matrix.os }} 61 | env: 62 | MYSQLCLIENT_LIB_DIR: 'C:\Program Files\MySQL\MySQL Server 8.0\lib' 63 | # will be accepted by a later Diesel version 64 | MYSQLCLIENT_LIB_DIR_X86_64_PC_WINDOWS_MSVC: 'C:\Program Files\MySQL\MySQL Server 8.0\lib' 65 | steps: 66 | - uses: actions/checkout@v2 67 | # use a version later than MSRV for trybuild consistency 68 | - run: rustup default 1.70 && rustup update 69 | - run: rustc -vV 70 | - uses: Swatinem/rust-cache@v2 71 | - run: cargo test -vvv 72 | 73 | integration: 74 | name: "Integration testing (docker)" 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v2 78 | - run: rustup default stable && rustup update 79 | - uses: Swatinem/rust-cache@v2 80 | - name: Cache Docker layers 81 | uses: actions/cache@v2 82 | id: cache-docker 83 | with: 84 | path: /tmp/.buildx-cache 85 | key: ${{ runner.os }}-buildx-${{ github.sha }} 86 | restore-keys: | 87 | ${{ runner.os }}-buildx- 88 | - name: Enable logging-debug-calls 89 | run: | 90 | perl -0777 -i -pe 's/(udf =.*features = \[)/\1"logging-debug-calls",\3/g' udf-examples/Cargo.toml 91 | cat udf-examples/Cargo.toml 92 | - name: Set up Docker Buildx 93 | uses: docker/setup-buildx-action@v2 94 | - name: Build Image 95 | uses: docker/build-push-action@v3 96 | with: 97 | load: true 98 | tags: mdb-example-so:latest 99 | file: Dockerfile.examples 100 | cache-from: type=local,src=/tmp/.buildx-cache 101 | cache-to: type=local,dest=/tmp/.buildx-cache-new 102 | context: . 103 | - # Temp fix 104 | # https://github.com/docker/build-push-action/issues/252 105 | # https://github.com/moby/buildkit/issues/1896 106 | name: Move cache 107 | run: | 108 | rm -rf /tmp/.buildx-cache 109 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 110 | - name: Start docker 111 | run: > 112 | docker run --rm -d 113 | -p 12300:3306 114 | -e MARIADB_ROOT_PASSWORD=example 115 | -e RUST_LIB_BACKTRACE=1 116 | --name mdb-example-container 117 | mdb-example-so 118 | && sleep 4 119 | # verify we started successfully 120 | - run: mysql -uroot -pexample -h0.0.0.0 -P12300 --protocol=tcp -e 'show databases' 121 | - name: Run integration testing 122 | # Run only integration tests with `--test '*'` 123 | run: cargo test -p udf-examples --test '*' --features backend 124 | - name: Print docker logs 125 | if: always() 126 | run: | 127 | docker logs mdb-example-container 128 | # If any critical / debug options were printed, error out 129 | docker logs mdb-example-container 2>&1 | grep -E '\[(Critical|Error)\]' || exit 0 && exit 1; 130 | docker stop mdb-example-container 131 | 132 | miri: 133 | name: Miri 134 | runs-on: ubuntu-latest 135 | steps: 136 | - uses: actions/checkout@v2 137 | - run: rustup default nightly && rustup component add miri && rustup update 138 | - uses: Swatinem/rust-cache@v2 139 | - name: Run Miri 140 | env: 141 | # Can't use chrono for time in isolation 142 | MIRIFLAGS: -Zmiri-disable-isolation 143 | run: cargo miri test 144 | 145 | fmt: 146 | name: "Format (cargo fmt)" 147 | runs-on: ubuntu-latest 148 | steps: 149 | - uses: actions/checkout@v2 150 | - run: rustup default nightly && rustup component add rustfmt && rustup update 151 | - uses: Swatinem/rust-cache@v2 152 | - run: cargo fmt --all -- --check 153 | - uses: actions/setup-python@v3 154 | - name: Validate pre-commit 155 | uses: pre-commit/action@v3.0.0 156 | 157 | 158 | doc: 159 | name: "Docs (cargo doc)" 160 | runs-on: ubuntu-latest 161 | steps: 162 | - uses: actions/checkout@v2 163 | - run: rustup default nightly && rustup update 164 | - uses: Swatinem/rust-cache@v2 165 | - run: cargo doc 166 | 167 | outdated: 168 | name: Outdated 169 | runs-on: ubuntu-latest 170 | if: github.event_name != 'pull_request' 171 | timeout-minutes: 45 172 | steps: 173 | - uses: actions/checkout@v3 174 | - uses: dtolnay/install@cargo-outdated 175 | - uses: Swatinem/rust-cache@v2 176 | - run: cargo outdated --workspace --exit-code 1 --ignore lipsum 177 | 178 | security_audit: 179 | runs-on: ubuntu-latest 180 | steps: 181 | - uses: actions/checkout@v3 182 | - uses: rustsec/audit-check@v1.4.1 183 | with: 184 | token: ${{ secrets.GITHUB_TOKEN }} 185 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/.DS_Store 3 | .docker-cargo/ 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: fix-byte-order-marker 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - id: mixed-line-ending 11 | - id: check-added-large-files 12 | args: ['--maxkb=600'] 13 | 14 | - repo: local 15 | hooks: 16 | - id: cargo-fmt 17 | name: Cargo format 18 | language: system 19 | entry: cargo fmt 20 | args: ["--"] 21 | types_or: ["rust"] 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## [Unreleased] - ReleaseDate 6 | 7 | ### Added 8 | 9 | ### Changed 10 | 11 | ### Removed 12 | 13 | 14 | 15 | ## [0.5.5] - 2024-05-07 16 | 17 | Rework the validation of names and aliases for aggregate UDFs. This fixes an 18 | issue where aliases could not be used for aggregate UDFs, and provides better 19 | error messages. 20 | 21 | 22 | ## [0.5.4] - 2023-09-10 23 | 24 | Update the `register` macro to allow specifying a custom name or aliases 25 | 26 | ```rust 27 | #[register(name = "foo", alias = "bar")] 28 | impl BasicUdf for MyUdfTy { /* ... */ } 29 | ``` 30 | 31 | ## [0.5.3] - 2023-03-29 32 | 33 | ### Changed 34 | 35 | - Fixed compilation for `Option<&'a [u8]>` types. Added example `mishmash` to 36 | complement this 37 | 38 | 39 | ## [0.5.2] - 2023-03-23 40 | 41 | ### Changed 42 | 43 | - Bump dependencies to the latest version 44 | - Update docs on feature flags 45 | - [Internal] remove calls to `catch_unwind` 46 | - [Internal] refactor feature flag calls 47 | - [CI] refactor CI 48 | 49 | 50 | ## [0.5.1] - 2023-01-04 51 | 52 | Changed licensing from 'Apache-2.0' to 'Apache-2.0 OR GPL-2.0-or-later' 53 | 54 | 55 | ## [0.5.0] - 2022-12-20 56 | 57 | ### Added 58 | 59 | - Added feature `logging-debug-calls` for full debug printing of call parameters 60 | 61 | ### Changed 62 | 63 | - Reworked behind the scenes for returning owned results (e.g. `String`) of any 64 | length, which nicely simplifies the API. 65 | - Now using an internal type wrapper to solve `Miri`'s bad transmute suggestion 66 | 67 | 68 | ## [0.4.5] - 2022-12-10 69 | 70 | ### Added 71 | 72 | - Added option to print full backtraces for buffer issues with `RUST_LIB_BACKTRACE=1` 73 | 74 | 75 | 76 | ## [0.4.4] - 2022-12-10 77 | 78 | ### Fixed 79 | 80 | - Corrected issue with the size of `unsigned long` that would cause MSVC to not 81 | compile (`c_ulong` is 32 bits on MSVC, 64 bits everywhere else) 82 | 83 | 84 | ## [0.4.2] - 2022-12-08 85 | 86 | ### Added 87 | 88 | - Feature `logging-debug` for potentially helpful output messages 89 | 90 | ### Changed 91 | 92 | - Removed `MARIADB_ROOT_PASSWORD` from dockerfile 93 | - Updated return buffer overflow action to return `NULL` instead copying some 94 | data. 95 | - Added type names to printed output 96 | 97 | 98 | ## [0.4.1] - 2022-12-08 99 | 100 | ### Fixed 101 | 102 | - Corrected dependency version for `udf-macros` 103 | 104 | 105 | ## [0.4.0] - 2022-12-08 106 | 107 | This version is now yanked 108 | 109 | ### Added 110 | 111 | - Mocks: added the `mock` module that provides ways to unit test UDFs. This is 112 | still a work in progress, and requires the feature `mock`. 113 | 114 | ### Changed 115 | 116 | - Improved memory footprint of `SqlArg` 117 | - (internal) Cleaned up `wrapper` internal structure to some extent 118 | 119 | Unfortunately, this version brought some minor breaking changes. Luckily most of 120 | these have little to no impact: 121 | 122 | - Changed `SqlArg` `value` and `attribute` members to be methods 123 | instead. Migration: replace `.value` and `.attribute` with `.value()` and 124 | `.attribute()` 125 | - `get_type_coercion` now returns a `SqlType` instead of `Option` 126 | 127 | 128 | ## [0.3.10] - 2022-11-13 129 | 130 | ### Added 131 | 132 | - Added dockerfile to build examples 133 | - Added preliminary integration test framework 134 | 135 | ### Changed 136 | 137 | - [CI] cleaned up pipeline configuration 138 | - Refactor wrappers to make use of the new `if let ... else` statements 139 | - Changed wrapper structs to make use of `UnsafeCell` for better mutability 140 | controlability 141 | 142 | ### Removed 143 | 144 | 145 | 146 | ## [0.3.9] - 2022-11-09 147 | 148 | ### Changed 149 | 150 | - Gave a transparant repr to `ArgList` 151 | 152 | 153 | 154 | ## [0.3.8] - 2022-11-04 155 | 156 | ### Changed 157 | 158 | - Updated documentation 159 | 160 | 161 | 162 | ## [0.3.7] - 2022-11-03 163 | 164 | ### Changed 165 | 166 | - Fixed broken link to `README.md` in `udf/src/lib.rs` 167 | 168 | 169 | 170 | ## [0.3.6] - 2022-11-03 171 | 172 | ### Changed 173 | 174 | - Changed `SqlResult::Decimal(&'a [u8])` to be `SqlResult::Decimal(&'a str)`, 175 | since a decimal will always fall under ASCII. 176 | 177 | 178 | 179 | ## [0.3.5] - 2022-11-03 180 | 181 | ### Added 182 | 183 | - Added `Copy`, `Clone`, `Debug`, and `PartialEq` implementations to 184 | `MaxLenOptions` 185 | 186 | ### Changed 187 | 188 | ### Removed 189 | 190 | 191 | 192 | ## [0.3.4] - 2022-11-03 193 | 194 | ### Changed 195 | 196 | - Adjusted semantics of `udf_log` output 197 | 198 | 199 | ## [0.3.3] - 2022-11-03 200 | 201 | \[nonpublished\] 202 | 203 | 204 | 205 | ## [0.3.2] - 2022-11-03 206 | 207 | ### Changed 208 | 209 | - Updated documentation around `AggregateUdf::Remove` 210 | 211 | 212 | 213 | ## [0.3.1] - 2022-11-03 214 | 215 | ### Added 216 | 217 | - Example for `lipsum` is now working properly 218 | - Improved documentation 219 | 220 | 221 | ## [0.3.0] - 2022-10-26 222 | 223 | ### Added 224 | 225 | - Completed basic support for `#[register]` procedural macro 226 | - Added support for strings (buffers) as return type 227 | 228 | ### Changed 229 | 230 | - Changed trait signatures to include a `UdfCfg` 231 | - Changed config name to `UdfCfg`, added typestate 232 | 233 | ### Removed 234 | 235 | 236 | 237 | [Unreleased]: https://github.com/pluots/sql-udf/compare/v0.5.5...HEAD 238 | [0.5.5]: https://github.com/pluots/sql-udf/compare/v0.5.4...v0.5.5 239 | [0.5.4]: https://github.com/pluots/sql-udf/compare/v0.5.4...v0.5.4 240 | [0.5.4]: https://github.com/pluots/sql-udf/compare/v0.5.3...v0.5.4 241 | [0.5.3]: https://github.com/pluots/sql-udf/compare/v0.5.2...v0.5.3 242 | [0.5.2]: https://github.com/pluots/sql-udf/compare/v0.5.1...v0.5.2 243 | [0.5.1]: https://github.com/pluots/sql-udf/compare/v0.5.0...v0.5.1 244 | [0.5.0]: https://github.com/pluots/sql-udf/compare/v0.4.5...v0.5.0 245 | [0.4.5]: https://github.com/pluots/sql-udf/compare/v0.4.4...v0.4.5 246 | [0.4.4]: https://github.com/pluots/sql-udf/compare/v0.4.2...v0.4.4 247 | [0.4.2]: https://github.com/pluots/sql-udf/compare/v0.4.1...v0.4.2 248 | [0.4.1]: https://github.com/pluots/sql-udf/compare/v0.4.0...v0.4.1 249 | [0.4.0]: https://github.com/pluots/sql-udf/compare/v0.3.10...v0.4.0 250 | [0.3.10]: https://github.com/pluots/sql-udf/compare/v0.3.9...v0.3.10 251 | [0.3.9]: https://github.com/pluots/sql-udf/compare/v0.3.8...v0.3.9 252 | [0.3.8]: https://github.com/pluots/sql-udf/compare/v0.3.7...v0.3.8 253 | [0.3.7]: https://github.com/pluots/sql-udf/compare/v0.3.6...v0.3.7 254 | [0.3.6]: https://github.com/pluots/sql-udf/compare/v0.3.5...v0.3.6 255 | [0.3.5]: https://github.com/pluots/sql-udf/compare/v0.3.4...v0.3.5 256 | [0.3.4]: https://github.com/pluots/sql-udf/compare/v0.3.3...v0.3.4 257 | [0.3.3]: https://github.com/pluots/sql-udf/compare/v0.3.2...v0.3.3 258 | [0.3.2]: https://github.com/pluots/sql-udf/compare/v0.3.1...v0.3.2 259 | [0.3.1]: https://github.com/pluots/sql-udf/compare/v0.3.0...v0.3.1 260 | [0.3.0]: https://github.com/pluots/sql-udf/compare/v0.0.1...v0.3.0 261 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "udf", 5 | "udf-macros", 6 | "udf-sys", 7 | "udf-examples" 8 | ] 9 | -------------------------------------------------------------------------------- /Dockerfile.examples: -------------------------------------------------------------------------------- 1 | # Dockerfile to build the udf-examples crate and load it. Usage: 2 | # 3 | # ``` 4 | # # Build the image 5 | # docker build -f Dockerfile.examples . --tag mdb-example-so 6 | # 7 | # # Run the container 8 | # docker run --rm -e MARIADB_ROOT_PASSWORD=example \ 9 | # --name mdb-example-container mdb-example-so 10 | # 11 | # # Enter a SQL console 12 | # docker exec -it mdb-example-container mysql -pexample 13 | # ``` 14 | 15 | FROM rust:latest AS build 16 | 17 | ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse 18 | 19 | WORKDIR /build 20 | 21 | COPY . . 22 | 23 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 24 | --mount=type=cache,target=/build/target \ 25 | cargo build --release -p udf-examples \ 26 | && mkdir /output \ 27 | && cp target/release/libudf_examples.so /output 28 | 29 | FROM mariadb:11.3 30 | 31 | COPY --from=build /output/* /usr/lib/mysql/plugin/ 32 | -------------------------------------------------------------------------------- /Dockerfile.mysql: -------------------------------------------------------------------------------- 1 | # Dockerfile to build the udf-examples crate and load it with MySQL. Usage: 2 | # 3 | # ``` 4 | # # Build the image 5 | # docker build -f Dockerfile.mysql . --tag mysql-example-so 6 | # 7 | # # Run the container 8 | # docker run --rm -e MYSQL_ROOT_PASSWORD=example --name mysql-example-container mysql-example-so 9 | # 10 | # # Enter a SQL console 11 | # docker exec -it mysql-example-container mysql -pexample 12 | # ``` 13 | 14 | FROM rust:latest AS build 15 | 16 | ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse 17 | 18 | WORKDIR /build 19 | 20 | COPY . . 21 | 22 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 23 | --mount=type=cache,target=/build/target \ 24 | cargo build --release -p udf-examples \ 25 | && mkdir /output \ 26 | && cp target/release/libudf_examples.so /output 27 | 28 | FROM mysql:8.0-debian 29 | 30 | COPY --from=build /output/* /usr/lib/mysql/plugin/ 31 | -------------------------------------------------------------------------------- /LICENSE.Apache-2.0: -------------------------------------------------------------------------------- 1 | Copyright 2022 Trevor Gross 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /LICENSE.GPL-2.0: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UDF: MariaDB/MySQL User Defined Functions in Rust 2 | 3 | This crate aims to make it extremely simple to implement UDFs for SQL, in a 4 | minimally error-prone fashion. 5 | 6 | Looking for prewritten useful UDFs? Check out the UDF suite, which provides 7 | downloadable binaries for some useful functions: 8 | . 9 | 10 | View the docs here: 11 | 12 | ## UDF Theory 13 | 14 | Basic SQL UDFs consist of three exposed functions: 15 | 16 | - An initialization function where arguments are checked and memory is allocated 17 | - A processing function where a result is returned 18 | - A deinitialization function where anything on the heap is cleaned up (performed 19 | automatically in this library) 20 | 21 | Aggregate UDFs (those that work on more than one row at a time) simply need to 22 | register two to three additional functions. 23 | 24 | This library handles everything that used to be difficult about writing UDFs 25 | (dynamic registration, allocation/deallocation, error handling, nullable values, 26 | logging) and makes it _trivial_ to add any function to your SQL server instance. 27 | It also inclues a mock interface, for testing your function implementation 28 | without needing a server. 29 | 30 | ## Quickstart 31 | 32 | The steps to create a working UDF using this library are: 33 | 34 | - Create a new rust project (`cargo new --lib my-udf`), add `udf` as a 35 | dependency (`cd my-udf; cargo add udf`) and change the crate type to a 36 | `cdylib` by adding the following to `Cargo.toml`: 37 | 38 | ```toml 39 | [lib] 40 | crate-type = ["cdylib"] 41 | ``` 42 | 43 | - Make a struct or enum that will share data between initializing and processing 44 | steps (it may be empty). The default name of your UDF will be your struct's 45 | name converted to snake case. 46 | - Implement the `BasicUdf` trait on this struct 47 | - Implement the `AggregateUdf` trait if you want it to be an aggregate function 48 | - Add `#[udf::register]` to each of these `impl` blocks (optionally with a 49 | `(name = "my_name")` argument) 50 | - Compile the project with `cargo build --release` (output will be 51 | `target/release/libmy_udf.so`) 52 | - Load the struct into MariaDB/MySql using `CREATE FUNCTION ...` 53 | - Use the function in SQL! 54 | 55 | For an example of some UDFs written using this library, see either the 56 | `udf-examples/` directory or the [`udf-suite`](https://github.com/pluots/udf-suite) 57 | repository. 58 | 59 | ## Detailed overview 60 | 61 | This section goes into the details of implementing a UDF with this library, but 62 | it is non-exhaustive. For that, see the documentation, or the `udf-examples` 63 | directory for well-annotated examples. 64 | 65 | ### Struct creation 66 | 67 | The first step is to create a struct (or enum) that will be used to share data 68 | between all relevant SQL functions. These include: 69 | 70 | - `init` Called once per result set. Here, you can store const data to your 71 | struct (if applicable) 72 | - `process` Called once per row (or per group for aggregate functions). This 73 | function uses data in the struct and in the current row's arguments 74 | - `clear` Aggregate only, called once per group at the beginning. Reset the 75 | struct as needed. 76 | - `add` Aggregate only, called once per row within a group. Perform needed 77 | calculations and save the data in the struct. 78 | - `remove` Window functions only, called to remove a value from a group 79 | 80 | It is quite possible, especially for simple functions, that there is no data 81 | that needs sharing. In this case, just make an empty struct and no allocation 82 | will take place. 83 | 84 | 85 | ```rust 86 | /// Function `sum_int` just adds all arguments as integers and needs no shared data 87 | struct SumInt; 88 | 89 | /// Function `avg` on the other hand may want to save data to perform aggregation 90 | struct Avg { 91 | running_total: f64 92 | } 93 | ``` 94 | 95 | There is a bit of a caveat for functions returning buffers (string & decimal 96 | functions): if there is a possibility that string length exceeds 97 | `MYSQL_RESULT_BUFFER_SIZE` (255), then the string to be returned must be 98 | contained within the struct (the `process` function will then return a 99 | reference). 100 | 101 | ```rust 102 | /// Generate random lipsum that may be longer than 255 bytes 103 | struct Lipsum { 104 | res: String 105 | } 106 | ``` 107 | 108 | ### Trait Implementation 109 | 110 | The next step is to implement the `BasicUdf` and optionally `AggregateUdf` 111 | traits. See [the docs](https://docs.rs/udf/latest/udf/trait.BasicUdf.html) 112 | for more information. 113 | 114 | If you use rust-analyzer with your IDE, it can help you out. Just type 115 | `impl BasicUdf for MyStruct {}` and place your cursor between the brackets - 116 | it will offer to autofill the function skeletons (`ctrl+.` or `cmd+.` 117 | brings up this menu if it doesn't show up by default). 118 | 119 | ```rust 120 | use udf::prelude::*; 121 | 122 | struct SumInt; 123 | 124 | #[register] 125 | impl BasicUdf for SumInt { 126 | type Returns<'a> = Option; 127 | 128 | fn init<'a>( 129 | cfg: &UdfCfg, 130 | args: &'a ArgList<'a, Init> 131 | ) -> Result { 132 | // ... 133 | } 134 | 135 | fn process<'a>( 136 | &'a mut self, 137 | cfg: &UdfCfg, 138 | args: &ArgList, 139 | error: Option, 140 | ) -> Result, ProcessError> { 141 | // ... 142 | } 143 | } 144 | ``` 145 | 146 | ### Compiling 147 | 148 | Assuming the above has been followed, all that is needed is to produce a C 149 | dynamic library for the project. This can be done by specifying 150 | `crate-type = ["cdylib"]` in your `Cargo.toml`. After this, compiling with 151 | `cargo build --release` will produce a loadable `.so` file (located in 152 | `target/release`). 153 | 154 | Important version note: this crate relies on a feature called generic associated 155 | types (GATs) which are only available on rust >= 1.65. This version only just 156 | became stable (2022-11-03), so be sure to run `rustup update` if you run into 157 | compiler issues. 158 | 159 | CI runs tests on both Linux and Windows, and this crate should work for either. 160 | MacOS is untested, but will likely work as well. 161 | 162 | ### Symbol Inspection 163 | 164 | If you would like to verify that the correct C-callable functions are present, 165 | you can inspect the dynamic library with `nm`. 166 | 167 | ```sh 168 | # Output of example .so 169 | $ nm -gC --defined-only target/release/libudf_examples.so 170 | 00000000000081b0 T avg_cost 171 | 0000000000008200 T avg_cost_add 172 | 00000000000081e0 T avg_cost_clear 173 | 0000000000008190 T avg_cost_deinit 174 | 0000000000008100 T avg_cost_init 175 | 0000000000009730 T is_const 176 | 0000000000009710 T is_const_deinit 177 | 0000000000009680 T is_const_init 178 | 0000000000009320 T sql_sequence 179 | ... 180 | ``` 181 | 182 | ### Usage 183 | 184 | Once compiled, the produced object file needs to be copied to the location of 185 | the `plugin_dir` SQL variable - usually, this is `/usr/lib/mysql/plugin/`. 186 | 187 | Once that has been done, `CREATE FUNCTION` can be used in MariaDB/MySql to load 188 | it. 189 | 190 | 191 | ## Docker Use 192 | 193 | Testing in Docker is highly recommended, so as to avoid disturbing a host SQL 194 | installation. See [the udf-examples readme](udf-examples/README.md) for 195 | instructions on how to do this. 196 | 197 | 198 | ## Examples 199 | 200 | The `udf-examples` crate contains examples of various UDFs, as well as 201 | instructions on how to compile them. See [the readme](udf-examples/README.md) 202 | there. 203 | 204 | 205 | ## Logging & Debugging Note 206 | 207 | If you need to log things like warnings during normal use of the function, 208 | anything printed to `stderr` will appear in the server logs (which can be viewed 209 | with e.g. `docker logs mariadb_udf_test` if testing in Docker). The `udf_log!` 210 | macro will print a message that matches the formatting of other SQL log 211 | information. You can also enable the crate features `logging-debug` for function 212 | entry/exitpoint debugging, or `logging-debug-calls` for information on the exact 213 | call parameters from the MariaDB/MySQL server. 214 | 215 | The best way to debug is to use the `udf::mock` module to create s.all unit 216 | tests. These can be run to validate correctness, or stepped through with a 217 | debugger if needed (this use case is likely somewhat rare). All types implement 218 | `Debug` so they can also be easily printed (the builtin `dbg!` macro prints to 219 | `stderr`, so this will also appear in logs): 220 | 221 | ```rust 222 | dbg!(&self); 223 | let arg0 = dbg!(args.get(0).unwrap()) 224 | ``` 225 | 226 | ``` 227 | [udf_examples/src/avgcost.rs:58] &self = AvgCost { 228 | count: 0, 229 | total_qty: 0, 230 | total_price: 0.0, 231 | } 232 | 233 | [udf_examples/src/avgcost.rs:60] args.get(0).unwrap() = SqlArg { 234 | value: Int( 235 | Some( 236 | 10, 237 | ), 238 | ), 239 | attribute: "qty", 240 | maybe_null: true, 241 | arg_type: Cell { 242 | value: INT_RESULT, 243 | }, 244 | marker: PhantomData, 245 | } 246 | ``` 247 | 248 | ## License 249 | 250 | This work is dual-licensed under Apache 2.0 and GPL 2.0 (or any later version) 251 | as of version 0.5.1. You can choose either of them if you use this work. 252 | 253 | `SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later` 254 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["main", "release"] 2 | shared-version = true 3 | # Single commit for all crates since we are in one repo 4 | consolidate-commits = true 5 | tag-name = "v{{version}}" 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | newline_style = "Unix" 3 | group_imports = "StdExternalCrate" 4 | format_code_in_doc_comments = true 5 | format_macro_bodies = true 6 | format_macro_matchers = true 7 | -------------------------------------------------------------------------------- /udf-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udf-examples" 3 | version = "0.5.5" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | lipsum = "0.8.2" 12 | sha2 = "0.10.8" 13 | udf = { path = "../udf", features = ["mock", "logging-debug"] } 14 | uuid = { version = "1.8.0", features = ["v1", "v3", "v4", "v5", "fast-rng"] } 15 | 16 | [dev-dependencies] 17 | mysql = { version = "25.0.0", default-features = false, features = ["minimal"] } 18 | 19 | [features] 20 | # Used to optionally enable integration tests 21 | backend = [] 22 | -------------------------------------------------------------------------------- /udf-examples/README.md: -------------------------------------------------------------------------------- 1 | # Basic UDF Examples 2 | 3 | This crate contains various simple examples for various UDFs. 4 | 5 | ## Building & Loading 6 | 7 | ### Docker 8 | 9 | It is highly recommended to always test your functions in a docker container, 10 | since minor mistakes can crash your server (e.g. loading a different `.so` file 11 | with the same name). 12 | 13 | Getting this going is easy for examples, because of the provided 14 | `Dockerfile.examples` file. Just run the following: 15 | 16 | ```shell 17 | docker build -f Dockerfile.examples . --tag mdb-example-so 18 | ``` 19 | 20 | Depending on your Docker version, you may need to prepend `DOCKER_BUILDKIT=1` to 21 | that command (the Dockerfile leverages cache only available with buildkit). 22 | 23 | Once built, you can start a container: 24 | 25 | ```bash 26 | docker run --rm -d -e MARIADB_ROOT_PASSWORD=example --name mariadb_udf_test mdb-example-so 27 | ``` 28 | 29 | This will start it in headless mode. If you need to stop it, you can use 30 | `docker stop mariadb_udf_test`. (Note that the server will delete its docker image 31 | and thus its data upon stop, due to the `--rm` flag, so don't use this example 32 | for anything permanent). 33 | 34 | ## Local OS 35 | 36 | If you are building for your local SQL server, `cargo build` will create the 37 | correct library. Just copy the resulting `.so` file (within the 38 | `target/release/` directory) to your server's plugin directory. 39 | 40 | ```bash 41 | cargo build -p udf-examples --release 42 | ``` 43 | 44 | ## Testing 45 | 46 | You will need to enter a SQL console to load the functions. This can be done 47 | with the `mariadb`/`mysql` command, either on your host system or within the 48 | docker container. If you used the provided command above, the password 49 | is `example`. 50 | 51 | ```sh 52 | docker exec -it mariadb_udf_test mariadb -pexample 53 | ``` 54 | 55 | Note that this won't work immediately after launching the server, it takes a few 56 | seconds to start. 57 | 58 | Once logged in, you can load all available example functions: 59 | 60 | ```sql 61 | CREATE FUNCTION is_const RETURNS string SONAME 'libudf_examples.so'; 62 | CREATE FUNCTION lookup6 RETURNS string SONAME 'libudf_examples.so'; 63 | CREATE FUNCTION sum_int RETURNS integer SONAME 'libudf_examples.so'; 64 | CREATE FUNCTION udf_sequence RETURNS integer SONAME 'libudf_examples.so'; 65 | CREATE FUNCTION lipsum RETURNS string SONAME 'libudf_examples.so'; 66 | CREATE FUNCTION log_calls RETURNS integer SONAME 'libudf_examples.so'; 67 | CREATE AGGREGATE FUNCTION avg2 RETURNS real SONAME 'libudf_examples.so'; 68 | CREATE AGGREGATE FUNCTION avg_cost RETURNS real SONAME 'libudf_examples.so'; 69 | CREATE AGGREGATE FUNCTION udf_median RETURNS integer SONAME 'libudf_examples.so'; 70 | ``` 71 | 72 | And try them out! 73 | 74 | ``` 75 | MariaDB [(none)]> select sum_int(1, 2, 3, 4, 5, 6.4, '1'); 76 | +----------------------------------+ 77 | | sum_int(1, 2, 3, 4, 5, 6.4, '1') | 78 | +----------------------------------+ 79 | | 22 | 80 | +----------------------------------+ 81 | 1 row in set (0.000 sec) 82 | 83 | MariaDB [(none)]> select is_const(2); 84 | +-------------+ 85 | | is_const(2) | 86 | +-------------+ 87 | | const | 88 | +-------------+ 89 | 1 row in set (0.000 sec) 90 | 91 | MariaDB [(none)]> select lookup6('localhost'), lookup6('google.com'); 92 | +----------------------+--------------------------+ 93 | | lookup6('localhost') | lookup6('google.com') | 94 | +----------------------+--------------------------+ 95 | | ::1 | 2607:f8b0:4009:818::200e | 96 | +----------------------+--------------------------+ 97 | 1 row in set (0.027 sec) 98 | 99 | 100 | -- Create table to test aggregate functions 101 | MariaDB [(none)]> create database db; use db; 102 | MariaDB [db]> create table t1 ( 103 | id int not null auto_increment, 104 | qty int, 105 | cost real, 106 | class varchar(30), 107 | primary key (id) 108 | ); 109 | Query OK, 0 rows affected (0.016 sec) 110 | 111 | MariaDB [db]> insert into t1 (qty, cost, class) values 112 | (10, 50, "a"), 113 | (8, 5.6, "c"), 114 | (5, 20.7, "a"), 115 | (10, 12.78, "b"), 116 | (6, 7.2, "c"), 117 | (2, 10.3, "b"), 118 | (3, 9.1, "c"); 119 | Query OK, 7 rows affected (0.007 sec) 120 | Records: 7 Duplicates: 0 Warnings: 0 121 | 122 | MariaDB [db]> select qty, udf_sequence() from t1 limit 4; 123 | +------+----------------+ 124 | | qty | udf_sequence() | 125 | +------+----------------+ 126 | | 10 | 1 | 127 | | 8 | 2 | 128 | | 5 | 3 | 129 | | 10 | 4 | 130 | +------+----------------+ 131 | 4 rows in set (0.000 sec) 132 | 133 | MariaDB [db]> select avg_cost(qty, cost) from t1 group by class order by class; 134 | +-------------------------+ 135 | | avg_cost(qty, cost) | 136 | +-------------------------+ 137 | | 40.23333333333333400000 | 138 | | 12.36666666666666700000 | 139 | | 6.78235294117647050000 | 140 | +-------------------------+ 141 | 3 rows in set (0.001 sec) 142 | 143 | -- Check server logs after running this! 144 | MariaDB [(db)]> select log_calls(); 145 | 146 | ``` 147 | 148 | If you check your log files, you will notice that full call logging is enabled. You 149 | can disable this by removing the `logging-debug` feature in the `udf-examples` 150 | `Cargo.toml`. 151 | -------------------------------------------------------------------------------- /udf-examples/src/attribute.rs: -------------------------------------------------------------------------------- 1 | //! Just print the attribute of whatever is called, usually variable name 2 | //! 3 | //! We also add an alias for demonstration purposes. Note that aliases must 4 | //! be registered separately. 5 | //! 6 | //! # Usage 7 | //! 8 | //! ```sql 9 | //! CREATE FUNCTION udf_attribute RETURNS string SONAME 'libudf_examples.so'; 10 | //! CREATE FUNCTION attr RETURNS string SONAME 'libudf_examples.so'; 11 | //! SELECT attribute("abcd"); 12 | //! SELECT attr(1234); 13 | //! ``` 14 | 15 | use udf::prelude::*; 16 | 17 | #[derive(Debug, PartialEq, Eq, Default)] 18 | struct UdfAttribute; 19 | 20 | #[register(alias = "attr")] 21 | impl BasicUdf for UdfAttribute { 22 | type Returns<'a> = String; 23 | 24 | /// Nothing to do here 25 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 26 | Ok(Self) 27 | } 28 | 29 | /// Just iterate the arguments and add their atttribute to the string 30 | fn process<'a>( 31 | &'a mut self, 32 | _cfg: &UdfCfg, 33 | args: &ArgList, 34 | _error: Option, 35 | ) -> Result, ProcessError> { 36 | let mut v: Vec = Vec::new(); 37 | 38 | for arg in args { 39 | v.push(arg.attribute().to_owned()); 40 | } 41 | 42 | Ok(v.join(", ")) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use udf::mock::*; 49 | 50 | use super::*; 51 | 52 | #[test] 53 | fn test_init() { 54 | // Not really anything to test here 55 | let mut mock_cfg = MockUdfCfg::new(); 56 | let mut mock_args = mock_args![]; 57 | 58 | assert!(UdfAttribute::init(mock_cfg.as_init(), mock_args.as_init()).is_ok()); 59 | } 60 | 61 | #[test] 62 | fn process_empty() { 63 | // No arguments should give us an empty string 64 | let mut inited = UdfAttribute; 65 | let mut mock_cfg = MockUdfCfg::new(); 66 | let mut mock_args = mock_args![]; 67 | 68 | let res = UdfAttribute::process( 69 | &mut inited, 70 | mock_cfg.as_process(), 71 | mock_args.as_process(), 72 | None, 73 | ); 74 | 75 | assert_eq!(res.unwrap(), ""); 76 | } 77 | 78 | #[test] 79 | fn process_nonempty() { 80 | // Test with some random arguments 81 | let mut inited = UdfAttribute; 82 | let mut mock_cfg = MockUdfCfg::new(); 83 | let mut mock_args = mock_args![ 84 | (String None, "attr1", false), 85 | (Int 42, "attr2", false), 86 | (Decimal None, "attr3", false), 87 | (Int None, "attr4", false), 88 | ]; 89 | 90 | let res = UdfAttribute::process( 91 | &mut inited, 92 | mock_cfg.as_process(), 93 | mock_args.as_process(), 94 | None, 95 | ); 96 | 97 | assert_eq!(res, Ok("attr1, attr2, attr3, attr4".to_owned())); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /udf-examples/src/avg2.rs: -------------------------------------------------------------------------------- 1 | //! Example average function 2 | //! 3 | //! This is just a reimplemenation of builtin `AVG` 4 | //! 5 | //! ```sql 6 | //! CREATE FUNCTION avg2 RETURNS integer SONAME 'libudf_examples.so'; 7 | //! SELECT avg2(value); 8 | //! ``` 9 | // Ignore loss of precision when we cast i64 to f64 10 | #![allow(clippy::cast_precision_loss)] 11 | #![allow(clippy::cast_sign_loss)] 12 | 13 | use udf::prelude::*; 14 | 15 | #[derive(Debug, Default)] 16 | struct Avg2 { 17 | count: u64, 18 | sum: f64, 19 | } 20 | 21 | #[register(alias = "test_avg2_alias")] 22 | impl BasicUdf for Avg2 { 23 | type Returns<'a> = Option; 24 | 25 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 26 | if args.len() != 1 { 27 | return Err(format!( 28 | "this function expected 1 argument; got {}", 29 | args.len() 30 | )); 31 | } 32 | 33 | let mut a0 = args.get(0).unwrap(); 34 | a0.set_type_coercion(SqlType::Real); 35 | 36 | cfg.set_maybe_null(true); 37 | cfg.set_decimals(10); 38 | cfg.set_max_len(20); 39 | 40 | Ok(Self::default()) 41 | } 42 | 43 | fn process<'a>( 44 | &'a mut self, 45 | _cfg: &UdfCfg, 46 | _args: &ArgList, 47 | _error: Option, 48 | ) -> Result, ProcessError> { 49 | if self.count == 0 { 50 | return Ok(None); 51 | } 52 | 53 | Ok(Some(self.sum / self.count as f64)) 54 | } 55 | } 56 | 57 | #[register(alias = "test_avg2_alias")] 58 | impl AggregateUdf for Avg2 { 59 | fn clear( 60 | &mut self, 61 | _cfg: &UdfCfg, 62 | _error: Option, 63 | ) -> Result<(), NonZeroU8> { 64 | *self = Self::default(); 65 | Ok(()) 66 | } 67 | 68 | fn add( 69 | &mut self, 70 | _cfg: &UdfCfg, 71 | args: &ArgList, 72 | _error: Option, 73 | ) -> Result<(), NonZeroU8> { 74 | self.count += 1; 75 | self.sum += args.get(0).unwrap().value().as_real().unwrap(); 76 | 77 | Ok(()) 78 | } 79 | 80 | /// For MariaDB only: 81 | fn remove( 82 | &mut self, 83 | _cfg: &UdfCfg, 84 | args: &ArgList, 85 | _error: Option, 86 | ) -> Result<(), NonZeroU8> { 87 | self.count -= 1; 88 | self.sum -= args.get(0).unwrap().value().as_real().unwrap(); 89 | 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /udf-examples/src/avg_cost.rs: -------------------------------------------------------------------------------- 1 | //! Aggregate cost function 2 | //! 3 | //! Takes a quantity and a price to return the average cost within the group 4 | //! 5 | //! # Usage 6 | //! 7 | //! ```sql 8 | //! CREATE AGGREGATE FUNCTION avg_cost RETURNS real SONAME 'libudf_examples.so'; 9 | //! SELECT avg_cost(int_column, real_column); 10 | //! ``` 11 | 12 | #![allow(clippy::cast_precision_loss)] 13 | 14 | use udf::prelude::*; 15 | 16 | #[derive(Debug, Default, PartialEq)] 17 | struct AvgCost { 18 | count: usize, 19 | total_qty: i64, 20 | total_price: f64, 21 | } 22 | 23 | #[register] 24 | impl BasicUdf for AvgCost { 25 | type Returns<'a> = Option 26 | where 27 | Self: 'a; 28 | 29 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 30 | if args.len() != 2 { 31 | return Err(format!("expected two arguments; got {}", args.len())); 32 | } 33 | 34 | let mut a0 = args.get(0).unwrap(); 35 | let mut a1 = args.get(1).unwrap(); 36 | 37 | a0.set_type_coercion(SqlType::Int); 38 | a1.set_type_coercion(SqlType::Real); 39 | 40 | cfg.set_maybe_null(true); 41 | cfg.set_decimals(10); 42 | cfg.set_max_len(20); 43 | 44 | // Derived default just has 0 at all fields 45 | Ok(Self::default()) 46 | } 47 | 48 | fn process<'a>( 49 | &'a mut self, 50 | _cfg: &UdfCfg, 51 | _args: &ArgList, 52 | _error: Option, 53 | ) -> Result, ProcessError> { 54 | if self.count == 0 || self.total_qty == 0 { 55 | return Ok(None); 56 | } 57 | dbg!(Ok(Some(self.total_price / self.total_qty as f64))) 58 | } 59 | } 60 | 61 | #[register] 62 | impl AggregateUdf for AvgCost { 63 | fn clear( 64 | &mut self, 65 | _cfg: &UdfCfg, 66 | _error: Option, 67 | ) -> Result<(), NonZeroU8> { 68 | // Reset our struct and return 69 | *self = Self::default(); 70 | Ok(()) 71 | } 72 | 73 | fn add( 74 | &mut self, 75 | _cfg: &UdfCfg, 76 | args: &ArgList, 77 | _error: Option, 78 | ) -> Result<(), NonZeroU8> { 79 | // We can unwrap because we are guaranteed to have 2 args (from checks in init) 80 | let in_qty = args.get(0).unwrap().value().as_int().unwrap(); 81 | let mut price = args.get(1).unwrap().value().as_real().unwrap(); 82 | 83 | dbg!(&in_qty, &price, &self); 84 | 85 | self.count += 1; 86 | 87 | if (self.total_qty >= 0 && in_qty < 0) || (self.total_qty < 0 && in_qty > 0) { 88 | // Case where given quantity has an opposite sign from our current quantity 89 | let newqty = self.total_qty + in_qty; 90 | 91 | if !((in_qty < 0 && newqty < 0) || (in_qty > 0 && newqty > 0)) { 92 | // If we will be switching from - to +, 93 | price = self.total_price / self.total_qty as f64; 94 | } 95 | 96 | self.total_price = price * newqty as f64; 97 | } else { 98 | // Normal case 99 | self.total_qty += in_qty; 100 | self.total_price += price * in_qty as f64; 101 | } 102 | 103 | if self.total_qty == 0 { 104 | self.total_price = 0.0; 105 | } 106 | 107 | Ok(()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /udf-examples/src/empty.rs: -------------------------------------------------------------------------------- 1 | //! This function is the bare minimum to do literally nothing 2 | 3 | use udf::prelude::*; 4 | 5 | struct EmptyCall; 6 | 7 | #[register] 8 | impl BasicUdf for EmptyCall { 9 | type Returns<'a> = Option; 10 | 11 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 12 | Ok(Self) 13 | } 14 | 15 | fn process<'a>( 16 | &'a mut self, 17 | _cfg: &UdfCfg, 18 | _args: &ArgList, 19 | _error: Option, 20 | ) -> Result, ProcessError> { 21 | Ok(None) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /udf-examples/src/is_const.rs: -------------------------------------------------------------------------------- 1 | //! A very simple function that checks whether an argument is const or not 2 | //! 3 | //! Functionality is simple: check for constness in `init` (the only time this 4 | //! is possible), save the result in the struct, and return it in `process` 5 | //! 6 | //! # Usage 7 | //! 8 | //! ```sql 9 | //! CREATE FUNCTION is_const RETURNS string SONAME 'libudf_examples.so'; 10 | //! SELECT is_const(4); 11 | //! ``` 12 | 13 | use udf::prelude::*; 14 | 15 | #[derive(Debug)] 16 | struct IsConst { 17 | is_const: bool, 18 | } 19 | 20 | #[register] 21 | impl BasicUdf for IsConst { 22 | type Returns<'a> = &'static str; 23 | 24 | fn init(_cfg: &UdfCfg, args: &ArgList) -> Result { 25 | if args.len() != 1 { 26 | return Err("IS_CONST only accepts one argument".to_owned()); 27 | } 28 | 29 | // Get the first argument, check if it is const, and store it in our 30 | // struct 31 | Ok(Self { 32 | is_const: args.get(0).unwrap().is_const(), 33 | }) 34 | } 35 | 36 | fn process<'a>( 37 | &'a mut self, 38 | _cfg: &UdfCfg, 39 | _args: &ArgList, 40 | _error: Option, 41 | ) -> Result, ProcessError> { 42 | // Just return a result based on our init step 43 | Ok(if self.is_const { "const" } else { "not const" }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /udf-examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Crate for example UDFs 2 | //! 3 | //! See this crate's README for details 4 | 5 | #![warn( 6 | clippy::pedantic, 7 | clippy::nursery, 8 | clippy::str_to_string, 9 | clippy::missing_inline_in_public_items, 10 | clippy::expect_used 11 | )] 12 | // Pedantic config 13 | #![allow( 14 | clippy::missing_const_for_fn, 15 | clippy::missing_panics_doc, 16 | clippy::must_use_candidate, 17 | clippy::cast_possible_truncation, 18 | // New users probably like `match` better 19 | clippy::option_if_let_else, 20 | clippy::wildcard_imports 21 | )] 22 | 23 | mod attribute; 24 | mod avg2; 25 | mod avg_cost; 26 | mod empty; 27 | mod is_const; 28 | mod lipsum; 29 | mod log_calls; 30 | mod lookup; 31 | mod median; 32 | mod mishmash; 33 | mod sequence; 34 | mod sum_int; 35 | -------------------------------------------------------------------------------- /udf-examples/src/lipsum.rs: -------------------------------------------------------------------------------- 1 | //! A function to generate lipsum of a given word count 2 | //! 3 | //! # Usage 4 | //! 5 | //! ```sql 6 | //! CREATE FUNCTION lipsum RETURNS string SONAME 'libudf_examples.so'; 7 | //! SELECT lipsum(8); 8 | //! ``` 9 | 10 | use std::num::NonZeroU8; 11 | 12 | use lipsum::{lipsum as lipsum_fn, lipsum_from_seed}; 13 | use udf::prelude::*; 14 | 15 | // Cap potential resource usage, this gives us more than enough to 16 | // populate LONGTEXT 17 | const MAX_WORDS: i64 = (u32::MAX >> 4) as i64; 18 | 19 | /// We expect to return a long string here so we need to contain it in 20 | struct Lipsum { 21 | res: String, 22 | } 23 | 24 | #[register] 25 | impl BasicUdf for Lipsum { 26 | type Returns<'a> = &'a str; 27 | 28 | /// We expect LIPSUM(n) or LIPSUM(n, m) 29 | fn init(_cfg: &UdfCfg, args: &ArgList) -> Result { 30 | if args.is_empty() || args.len() > 2 { 31 | return Err(format!("Expected 1 or 2 args; got {}", args.len())); 32 | } 33 | 34 | let n = args 35 | .get(0) 36 | .unwrap() 37 | .value() 38 | .as_int() 39 | .ok_or_else(|| "First argument must be an integer".to_owned())?; 40 | 41 | // Perform error checks 42 | if n > MAX_WORDS { 43 | return Err(format!("Maximum of {MAX_WORDS} words, got {n}")); 44 | } 45 | if n < 0 { 46 | return Err(format!("Word count must be greater than 0, got {n}")); 47 | } 48 | 49 | // If there is an extra arg, verify it is also an integer 50 | if let Some(v) = args.get(1) { 51 | let seed = v 52 | .value() 53 | .as_int() 54 | .ok_or_else(|| "Second argument must be an integer".to_owned())?; 55 | if seed < 0 { 56 | return Err(format!("Seed must be a positive integer, got {seed}")); 57 | } 58 | }; 59 | 60 | Ok(Self { 61 | res: String::default(), 62 | }) 63 | } 64 | 65 | fn process<'a>( 66 | &'a mut self, 67 | _cfg: &UdfCfg, 68 | args: &ArgList, 69 | _error: Option, 70 | ) -> Result, ProcessError> { 71 | // We have already checked that these values fit into usize in init 72 | // Do need to ensure our argument isn't null 73 | let n = args 74 | .get(0) 75 | .unwrap() 76 | .value() 77 | .as_int() 78 | .ok_or(ProcessError)? 79 | .unsigned_abs() as usize; 80 | 81 | let res = match args.get(1) { 82 | Some(v) => { 83 | // If we have a seed argument, use it. 84 | let seed = v.value().as_int().ok_or(ProcessError)?; 85 | lipsum_from_seed(n, seed.unsigned_abs()) 86 | } 87 | None => { 88 | // If no seed argument, just generate word count 89 | lipsum_fn(n) 90 | } 91 | }; 92 | 93 | self.res = res; 94 | 95 | Ok(&self.res) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /udf-examples/src/log_calls.rs: -------------------------------------------------------------------------------- 1 | //! A pretty useless function that just writes to the server log whenever it's 2 | //! used. 3 | //! 4 | //! # Usage 5 | //! 6 | //! ```sql 7 | //! CREATE FUNCTION log_calls RETURNS integer SONAME 'libudf_examples.so'; 8 | //! SELECT log_calls(); 9 | //! ``` 10 | 11 | use udf::prelude::*; 12 | 13 | struct LogCalls {} 14 | 15 | #[register] 16 | impl BasicUdf for LogCalls { 17 | type Returns<'a> = Option; 18 | 19 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 20 | udf_log!(Note: "called init!"); 21 | Ok(Self {}) 22 | } 23 | 24 | fn process<'a>( 25 | &'a mut self, 26 | _cfg: &UdfCfg, 27 | _args: &ArgList, 28 | _error: Option, 29 | ) -> Result, ProcessError> { 30 | udf_log!(Note: "called process!"); 31 | Ok(None) 32 | } 33 | } 34 | 35 | #[register] 36 | impl AggregateUdf for LogCalls { 37 | fn clear( 38 | &mut self, 39 | _cfg: &UdfCfg, 40 | _error: Option, 41 | ) -> Result<(), NonZeroU8> { 42 | udf_log!(Note: "called clear!"); 43 | Ok(()) 44 | } 45 | 46 | fn add( 47 | &mut self, 48 | _cfg: &UdfCfg, 49 | _args: &ArgList, 50 | _error: Option, 51 | ) -> Result<(), NonZeroU8> { 52 | udf_log!(Note: "called add!"); 53 | Ok(()) 54 | } 55 | 56 | fn remove( 57 | &mut self, 58 | _cfg: &UdfCfg, 59 | _args: &ArgList, 60 | _error: Option, 61 | ) -> Result<(), NonZeroU8> { 62 | udf_log!(Note: "called remove!"); 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /udf-examples/src/lookup.rs: -------------------------------------------------------------------------------- 1 | //! Lookup hostname to IPv6 conversion 2 | //! 3 | //! # Usage 4 | //! 5 | //! ```sql 6 | //! CREATE FUNCTION lookup6 RETURNS string SONAME 'libudf_examples.so'; 7 | //! SELECT lookup6('0.0.0.0'); 8 | //! ``` 9 | 10 | #![allow(unused_variables)] 11 | 12 | use std::net::{SocketAddr, ToSocketAddrs}; 13 | 14 | use udf::prelude::*; 15 | 16 | /// No data to persist 17 | #[derive(Debug)] 18 | struct Lookup6; 19 | 20 | const IPV6_MAX_LEN: u64 = 39; 21 | 22 | #[register] 23 | impl BasicUdf for Lookup6 { 24 | type Returns<'a> = Option 25 | where 26 | Self: 'a; 27 | 28 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 29 | if args.len() != 1 { 30 | return Err(format!("Expected 1 argument; got {}", args.len())); 31 | } 32 | 33 | let arg_val = args.get(0).unwrap().value(); 34 | 35 | if !arg_val.is_string() { 36 | return Err(format!( 37 | "Expected string argument; got {}", 38 | arg_val.display_name() 39 | )); 40 | } 41 | 42 | // max ipv6 address with colons 43 | cfg.set_max_len(IPV6_MAX_LEN); 44 | cfg.set_maybe_null(true); 45 | 46 | Ok(Self) 47 | } 48 | 49 | fn process<'a>( 50 | &'a mut self, 51 | cfg: &UdfCfg, 52 | args: &ArgList, 53 | error: Option, 54 | ) -> Result, ProcessError> { 55 | let arg = args.get(0).unwrap().value(); 56 | 57 | let Some(hostname) = arg.as_string() else { 58 | return Err(ProcessError); 59 | }; 60 | 61 | // `to_socket_addrs` checks the given hostname and port (0) and returns 62 | // an iterator over all valid resolutions 63 | let Ok(mut sock_addrs) = (hostname, 0).to_socket_addrs() else { 64 | return Ok(None); 65 | }; 66 | 67 | // Prioritize an ipv6 address if it is available, take first address if 68 | // not. 69 | let first = sock_addrs.next(); 70 | 71 | let Some(ret_sock_addr) = sock_addrs.find(SocketAddr::is_ipv6).or(first) else { 72 | return Ok(None); 73 | }; 74 | 75 | // Get an ipv6 version 76 | let ret_addr = match ret_sock_addr { 77 | SocketAddr::V4(a) => a.ip().to_ipv6_mapped(), 78 | SocketAddr::V6(a) => *a.ip(), 79 | }; 80 | 81 | Ok(Some(ret_addr.to_string())) 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use udf::mock::*; 88 | 89 | use super::*; 90 | 91 | #[test] 92 | fn test_init_ok() { 93 | let mut mock_cfg = MockUdfCfg::new(); 94 | let mut mock_args = mock_args![("localhost", "attr1", true)]; 95 | let res = Lookup6::init(mock_cfg.as_init(), mock_args.as_init()); 96 | 97 | assert_eq!(*mock_cfg.max_len(), IPV6_MAX_LEN); 98 | assert!(res.is_ok()); 99 | } 100 | 101 | #[test] 102 | fn test_init_wrong_arg_count() { 103 | let mut mock_cfg = MockUdfCfg::new(); 104 | let mut mock_args = mock_args![("localhost", "attr1", true), ("localhost", "attr2", true)]; 105 | let res = Lookup6::init(mock_cfg.as_init(), mock_args.as_init()); 106 | 107 | assert_eq!(res.unwrap_err(), "Expected 1 argument; got 2"); 108 | } 109 | 110 | #[test] 111 | fn test_init_wrong_arg_type() { 112 | let mut mock_cfg = MockUdfCfg::new(); 113 | let mut mock_args = mock_args![(Int 500, "attr1", true)]; 114 | let res = Lookup6::init(mock_cfg.as_init(), mock_args.as_init()); 115 | 116 | assert_eq!(res.unwrap_err(), "Expected string argument; got int"); 117 | } 118 | 119 | #[test] 120 | #[cfg(not(miri))] // need to skip Miri because. it can't cross FFI 121 | fn process() { 122 | // Test with some random arguments 123 | let mut inited = Lookup6; 124 | let mut mock_cfg = MockUdfCfg::new(); 125 | let mut mock_args = mock_args![("localhost", "attr1", false),]; 126 | 127 | let res = Lookup6::process( 128 | &mut inited, 129 | mock_cfg.as_process(), 130 | mock_args.as_process(), 131 | None, 132 | ); 133 | 134 | // Our result can be weird, so we just check it has a colon 135 | assert!(res.unwrap().unwrap().contains(':')); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /udf-examples/src/median.rs: -------------------------------------------------------------------------------- 1 | //! Basic aggregate UDF to get the median of each group 2 | //! 3 | //! If arguments are reals, they are rounded to integers 4 | //! 5 | //! # Usage 6 | //! 7 | //! ```sql 8 | //! CREATE AGGREGATE FUNCTION udf_median RETURNS integer SONAME 'libudf_examples.so'; 9 | //! SELECT median(int_column); 10 | //! ``` 11 | 12 | use udf::prelude::*; 13 | 14 | #[derive(Debug)] 15 | struct UdfMedian { 16 | v: Vec, 17 | } 18 | 19 | #[register] 20 | impl BasicUdf for UdfMedian { 21 | type Returns<'a> = Option; 22 | 23 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 24 | Ok(Self { v: Vec::new() }) 25 | } 26 | 27 | fn process<'a>( 28 | &'a mut self, 29 | _cfg: &UdfCfg, 30 | _args: &ArgList, 31 | _error: Option, 32 | ) -> Result, ProcessError> { 33 | if self.v.is_empty() { 34 | Ok(None) 35 | } else { 36 | // To get the median we need to sort first. Not sure why the SQL reference 37 | // implementation doesn't do this. 38 | self.v.sort_unstable(); 39 | 40 | // Safely get the middle element 41 | Ok(self.v.get(self.v.len() / 2).copied()) 42 | } 43 | } 44 | } 45 | 46 | #[register] 47 | impl AggregateUdf for UdfMedian { 48 | fn clear( 49 | &mut self, 50 | _cfg: &UdfCfg, 51 | _error: Option, 52 | ) -> Result<(), NonZeroU8> { 53 | self.v.clear(); 54 | Ok(()) 55 | } 56 | 57 | fn add( 58 | &mut self, 59 | _cfg: &UdfCfg, 60 | args: &ArgList, 61 | _error: Option, 62 | ) -> Result<(), NonZeroU8> { 63 | if let Some(a) = args.get(0) { 64 | if let Some(v) = a.value().as_int() { 65 | self.v.push(v); 66 | } else if let Some(v) = a.value().as_real() { 67 | self.v.push(v as i64); 68 | } 69 | } 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /udf-examples/src/mishmash.rs: -------------------------------------------------------------------------------- 1 | //! A dummy function that combines the binary representation of all its 2 | //! arguments 3 | //! 4 | //! # Usage 5 | //! 6 | //! ```sql 7 | //! CREATE FUNCTION mismmash RETURNS string SONAME 'libudf_examples.so'; 8 | //! SELECT lipsum(8); 9 | //! ``` 10 | 11 | use udf::prelude::*; 12 | 13 | #[derive(Debug, Default)] 14 | pub struct Mishmash(Vec); 15 | 16 | #[register] 17 | impl BasicUdf for Mishmash { 18 | type Returns<'a> = Option<&'a [u8]>; 19 | 20 | /// We expect LIPSUM(n) or LIPSUM(n, m) 21 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 22 | Ok(Self::default()) 23 | } 24 | 25 | fn process<'a>( 26 | &'a mut self, 27 | _cfg: &UdfCfg, 28 | args: &ArgList, 29 | _error: Option, 30 | ) -> Result, ProcessError> { 31 | for arg in args { 32 | match arg.value() { 33 | SqlResult::String(Some(v)) => self.0.extend_from_slice(v), 34 | SqlResult::Real(Some(v)) => self.0.extend_from_slice(&v.to_ne_bytes()), 35 | SqlResult::Int(Some(v)) => self.0.extend_from_slice(&v.to_ne_bytes()), 36 | SqlResult::Decimal(Some(v)) => self.0.extend_from_slice(v.as_bytes()), 37 | other => panic!("unexpected type {other:?}"), 38 | } 39 | } 40 | 41 | let res = if self.0.is_empty() { 42 | None 43 | } else { 44 | Some(self.0.as_ref()) 45 | }; 46 | 47 | Ok(res) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /udf-examples/src/sequence.rs: -------------------------------------------------------------------------------- 1 | //! Function to create a sequence 2 | //! 3 | //! This will return an incrementing value for each row, starting with 1 by 4 | //! default, or any given 5 | //! 6 | //! ```sql 7 | //! CREATE FUNCTION udf_sequence RETURNS integer SONAME 'libudf_examples.so'; 8 | //! SELECT some_col, sequence() from some_table; 9 | //! SELECT some_col, sequence(8) from some_table; 10 | //! ``` 11 | 12 | use udf::prelude::*; 13 | 14 | struct UdfSequence { 15 | last_val: i64, 16 | } 17 | 18 | #[register] 19 | impl BasicUdf for UdfSequence { 20 | type Returns<'a> = i64 21 | where 22 | Self: 'a; 23 | 24 | /// Init just validates the argument count and initializes our empty struct 25 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 26 | if args.len() > 1 { 27 | return Err(format!( 28 | "This function takes 0 or 1 arguments; got {}", 29 | args.len() 30 | )); 31 | } 32 | 33 | // If we have an argument, set its type coercion to an integer 34 | if let Some(mut a) = args.get(0) { 35 | a.set_type_coercion(SqlType::Int); 36 | } 37 | 38 | // Result will differ for each call 39 | cfg.set_is_const(false); 40 | Ok(Self { last_val: 0 }) 41 | } 42 | 43 | fn process<'a>( 44 | &'a mut self, 45 | _cfg: &UdfCfg, 46 | args: &ArgList, 47 | _error: Option, 48 | ) -> Result, ProcessError> { 49 | // If we have an argument, that will provide our offset value 50 | let arg_val = match args.get(0) { 51 | Some(v) => v.value().as_int().unwrap(), 52 | None => 0, 53 | }; 54 | 55 | // Increment our last value, return the total 56 | self.last_val += 1; 57 | Ok(self.last_val + arg_val) 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use udf::mock::*; 64 | 65 | use super::*; 66 | 67 | #[test] 68 | fn test_init() { 69 | // Not really anything to test here 70 | let mut mock_cfg = MockUdfCfg::new(); 71 | let mut mock_args = mock_args![(Int 1, "", false)]; 72 | 73 | assert!(UdfSequence::init(mock_cfg.as_init(), mock_args.as_init()).is_ok()); 74 | } 75 | 76 | #[test] 77 | fn test_process() { 78 | // Test with some random arguments 79 | let mut inited = UdfSequence { last_val: 0 }; 80 | let mut mock_cfg = MockUdfCfg::new(); 81 | let mut arglist: Vec<(_, i64)> = vec![ 82 | (mock_args![(Int 0, "", false)], 1), 83 | (mock_args![(Int 0, "", false)], 2), 84 | (mock_args![(Int 0, "", false)], 3), 85 | ]; 86 | 87 | for (arg, expected) in &mut arglist { 88 | let res = 89 | UdfSequence::process(&mut inited, mock_cfg.as_process(), arg.as_process(), None) 90 | .unwrap(); 91 | assert_eq!(res, *expected); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /udf-examples/src/sum_int.rs: -------------------------------------------------------------------------------- 1 | //! Create a function called `sum_int` that coerces all arguments to integers and 2 | //! adds them. Accepts any number of arguments. 3 | //! 4 | //! # Usage 5 | //! 6 | //! ```sql 7 | //! CREATE FUNCTION sum_int RETURNS integer SONAME 'libudf_examples.so'; 8 | //! SELECT sum_int(1, 2, 3, 4, '5', 6.2) 9 | //! ``` 10 | 11 | use udf::prelude::*; 12 | 13 | #[derive(Debug, PartialEq, Eq, Default)] 14 | struct SumInt {} 15 | 16 | #[register] 17 | impl BasicUdf for SumInt { 18 | type Returns<'a> = i64; 19 | 20 | /// All we do here is set our type coercion. SQL will cancel our function if 21 | /// the coercion is not possible. 22 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 23 | // Coerce each arg to an integer 24 | args.iter() 25 | .for_each(|mut arg| arg.set_type_coercion(udf::SqlType::Int)); 26 | 27 | // This will produce the same result 28 | cfg.set_is_const(true); 29 | Ok(Self {}) 30 | } 31 | 32 | /// This is the process 33 | fn process<'a>( 34 | &'a mut self, 35 | _cfg: &UdfCfg, 36 | args: &ArgList, 37 | _error: Option, 38 | ) -> Result, ProcessError> { 39 | // Iterate all arguments, sum all that are integers. This should 40 | // be all of them, since we set coercion 41 | Ok(args.iter().filter_map(|arg| arg.value().as_int()).sum()) 42 | 43 | // If you're not familiar with rust's combinators, here's the for loop 44 | // version: 45 | // let mut res = 0; 46 | // for arg in args { 47 | // if let Some(v) = arg.value.as_int() { 48 | // res += v 49 | // } 50 | // } 51 | 52 | // Ok(res) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /udf-examples/tests/attribute.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "create or replace function udf_attribute 10 | returns string 11 | soname 'libudf_examples.so'", 12 | "create or replace function attr 13 | returns string 14 | soname 'libudf_examples.so'", 15 | "create or replace table test_attribute ( 16 | id int auto_increment, 17 | val int, 18 | primary key (id) 19 | )", 20 | "insert into test_attribute (val) values (2)", 21 | ]; 22 | 23 | #[test] 24 | fn test_basic() { 25 | let conn = &mut get_db_connection(SETUP); 26 | 27 | let res: String = conn 28 | .query_first("select udf_attribute(1, 'string', val, 3.2) from test_attribute") 29 | .unwrap() 30 | .unwrap(); 31 | assert_eq!(res, "1, 'string', val, 3.2"); 32 | 33 | let res: String = conn 34 | .query_first("select attr(1, 'string', val, 3.2) from test_attribute") 35 | .unwrap() 36 | .unwrap(); 37 | assert_eq!(res, "1, 'string', val, 3.2"); 38 | } 39 | -------------------------------------------------------------------------------- /udf-examples/tests/avg2.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::{approx_eq, get_db_connection}; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "CREATE OR REPLACE AGGREGATE function avg2 10 | RETURNS real 11 | SONAME 'libudf_examples.so'", 12 | "CREATE OR REPLACE AGGREGATE FUNCTION test_avg2_alias 13 | RETURNS real 14 | SONAME 'libudf_examples.so'", 15 | "CREATE OR REPLACE TABLE test_avg2 ( 16 | id int auto_increment, 17 | val int, 18 | primary key (id) 19 | )", 20 | "INSERT INTO test_avg2 (val) VALUES (2), (1), (3), (4), (-3), (7), (-1)", 21 | ]; 22 | 23 | #[test] 24 | fn test_avg2() { 25 | let conn = &mut get_db_connection(SETUP); 26 | 27 | let res: f32 = conn 28 | .query_first("select avg2(val) from test_avg2") 29 | .unwrap() 30 | .unwrap(); 31 | 32 | assert!(approx_eq(res, 1.857)); 33 | } 34 | 35 | #[test] 36 | fn test_avg2_alias() { 37 | let conn = &mut get_db_connection(SETUP); 38 | 39 | let res: f32 = conn 40 | .query_first("select test_avg2_alias(val) from test_avg2") 41 | .unwrap() 42 | .unwrap(); 43 | 44 | assert!(approx_eq(res, 1.857)); 45 | } 46 | -------------------------------------------------------------------------------- /udf-examples/tests/avg_cost.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::{approx_eq, get_db_connection}; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "CREATE OR REPLACE AGGREGATE FUNCTION avg_cost 10 | RETURNS real 11 | SONAME 'libudf_examples.so'", 12 | "CREATE OR REPLACE TABLE test_avgcost ( 13 | id int auto_increment, 14 | qty int, 15 | cost real, 16 | class varchar(30), 17 | primary key (id) 18 | )", 19 | r#"INSERT INTO test_avgcost (qty, cost, class) values 20 | (10, 50, "a"), 21 | (8, 5.6, "c"), 22 | (5, 20.7, "a"), 23 | (10, 12.78, "b"), 24 | (6, 7.2, "c"), 25 | (2, 10.3, "b"), 26 | (3, 9.1, "c") 27 | "#, 28 | ]; 29 | 30 | #[test] 31 | fn test_empty() { 32 | let conn = &mut get_db_connection(SETUP); 33 | 34 | let res: Vec = conn 35 | .query("SELECT avg_cost(qty, cost) FROM test_avgcost GROUP BY class") 36 | .unwrap(); 37 | 38 | println!("{res:?}"); 39 | assert_eq!(res.len(), 3); 40 | 41 | let expected = [40.233, 12.366, 6.782]; 42 | for (a, b) in res.iter().zip(expected.iter()) { 43 | assert!(approx_eq(*a, *b)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /udf-examples/tests/backend/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module to run tests when a backend is available 2 | //! 3 | //! This module requires a database that matches the description in 4 | //! `DEFAULT_DATABASE_URI`. If that is available, run these tests with `cargo t 5 | //! --features backend` 6 | //! 7 | //! Run the container with `docker run --rm -d -p 12300:3300 mdb-example-so` 8 | 9 | #![cfg(feature = "backend")] 10 | use std::collections::HashSet; 11 | use std::env; 12 | use std::sync::{Mutex, OnceLock}; 13 | 14 | use mysql::prelude::*; 15 | use mysql::{Pool, PooledConn}; 16 | 17 | const URI_ENV: &str = "UDF_TEST_BACKEND_URI"; 18 | const DEFAULT_DATABASE_URI: &str = "mysql://root:example@0.0.0.0:12300/udf_tests"; 19 | 20 | static POOL: OnceLock = OnceLock::new(); 21 | static SETUP_STATE: OnceLock>> = OnceLock::new(); 22 | 23 | fn get_database_uri() -> String { 24 | match env::var(URI_ENV) { 25 | Ok(s) => s, 26 | Err(_) => DEFAULT_DATABASE_URI.to_owned(), 27 | } 28 | } 29 | 30 | fn build_pool() -> Pool { 31 | let db_url = get_database_uri(); 32 | 33 | { 34 | // Ensure the database exists then reconnect 35 | let (url, db) = db_url.rsplit_once('/').unwrap(); 36 | let pool = Pool::new(url).expect("pool failed"); 37 | let mut conn = pool.get_conn().expect("initial connection failed"); 38 | 39 | // Create default database 40 | conn.query_drop(format!("CREATE OR REPLACE DATABASE {db}")) 41 | .unwrap(); 42 | } 43 | 44 | Pool::new(db_url.as_str()).expect("pool failed") 45 | } 46 | 47 | /// Ensures that init items have been run 48 | pub fn get_db_connection(init: &[&str]) -> PooledConn { 49 | let mut conn = POOL 50 | .get_or_init(build_pool) 51 | .get_conn() 52 | .expect("failed to get conn"); 53 | 54 | let ran_stmts = &mut *SETUP_STATE 55 | .get_or_init(|| Mutex::new(HashSet::new())) 56 | .lock() 57 | .unwrap(); 58 | 59 | // Store a list of our init calls so we don't repeat them 60 | for stmt in init { 61 | if ran_stmts.contains(*stmt) { 62 | continue; 63 | } 64 | 65 | conn.query_drop(stmt).expect("could not run setup"); 66 | 67 | ran_stmts.insert((*stmt).to_owned()); 68 | } 69 | 70 | conn 71 | } 72 | 73 | /// Check if two floats are within a tolerance. Also prints them for debugging. 74 | #[allow(dead_code)] 75 | pub fn approx_eq(a: f32, b: f32) -> bool { 76 | const TOLERANCE: f32 = 0.001; 77 | 78 | println!("a: {a}, b: {b}"); 79 | (a - b).abs() < TOLERANCE 80 | } 81 | -------------------------------------------------------------------------------- /udf-examples/tests/is_const.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "create or replace function is_const 10 | returns string 11 | soname 'libudf_examples.so'", 12 | "create or replace table test_is_const ( 13 | id int auto_increment, 14 | val int, 15 | primary key (id) 16 | )", 17 | "insert into test_is_const (val) values (2)", 18 | ]; 19 | 20 | #[test] 21 | fn test_true() { 22 | let conn = &mut get_db_connection(SETUP); 23 | 24 | let res: String = conn.query_first("select is_const(1)").unwrap().unwrap(); 25 | 26 | assert_eq!(res, "const"); 27 | } 28 | 29 | #[test] 30 | fn test_false() { 31 | let conn = &mut get_db_connection(SETUP); 32 | 33 | let res: String = conn 34 | .query_first("select is_const(val) from test_is_const") 35 | .unwrap() 36 | .unwrap(); 37 | 38 | assert_eq!(res, "not const"); 39 | } 40 | 41 | #[test] 42 | fn test_too_many_args() { 43 | let conn = &mut get_db_connection(SETUP); 44 | 45 | let res = conn.query_first::("select is_const(1, 2)"); 46 | 47 | let Err(mysql::Error::MySqlError(e)) = res else { 48 | panic!("Got unexpected response: {res:?}"); 49 | }; 50 | 51 | assert!(e.message.contains("only accepts one argument")); 52 | } 53 | -------------------------------------------------------------------------------- /udf-examples/tests/lipsum.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &["create or replace function lipsum 9 | returns string 10 | soname 'libudf_examples.so'"]; 11 | 12 | #[test] 13 | fn test_short() { 14 | let conn = &mut get_db_connection(SETUP); 15 | 16 | let res: Option = conn.query_first("select lipsum(10)").unwrap().unwrap(); 17 | 18 | assert!(res.unwrap().split_whitespace().count() == 10); 19 | } 20 | 21 | #[test] 22 | fn test_short_seed() { 23 | let conn = &mut get_db_connection(SETUP); 24 | 25 | let res: Option = conn 26 | .query_first("select lipsum(10, 12345)") 27 | .unwrap() 28 | .unwrap(); 29 | 30 | assert!(res.unwrap().split_whitespace().count() == 10); 31 | } 32 | 33 | #[test] 34 | fn test_long() { 35 | let conn = &mut get_db_connection(SETUP); 36 | 37 | let res: Option = conn.query_first("select lipsum(5000)").unwrap().unwrap(); 38 | assert!(res.unwrap().split_whitespace().count() == 5000); 39 | } 40 | -------------------------------------------------------------------------------- /udf-examples/tests/lookup.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &["create or replace function lookup6 9 | returns string 10 | soname 'libudf_examples.so'"]; 11 | 12 | #[test] 13 | fn test_zeros() { 14 | let conn = &mut get_db_connection(SETUP); 15 | 16 | let res: Option = conn 17 | .query_first("select lookup6('0.0.0.0')") 18 | .unwrap() 19 | .unwrap(); 20 | 21 | assert_eq!(res.unwrap(), "::ffff:0.0.0.0"); 22 | } 23 | 24 | #[test] 25 | fn test_localhost() { 26 | let conn = &mut get_db_connection(SETUP); 27 | 28 | let res: Option = conn 29 | .query_first("select lookup6('localhost')") 30 | .unwrap() 31 | .unwrap(); 32 | 33 | assert_eq!(res.unwrap(), "::1"); 34 | } 35 | 36 | #[test] 37 | fn test_nonexistant() { 38 | let conn = &mut get_db_connection(SETUP); 39 | 40 | let res: Option = conn 41 | .query_first("select lookup6('nonexistant')") 42 | .unwrap() 43 | .unwrap(); 44 | 45 | assert!(res.is_none()); 46 | } 47 | 48 | #[test] 49 | fn test_sql_buffer_bug() { 50 | // This is intended to catch a buffer problem in mysql/mariadb 51 | // See link: https://github.com/pluots/sql-udf/issues/39 52 | 53 | let conn = &mut get_db_connection(SETUP); 54 | 55 | conn.exec_drop("set @testval = (select lookup6('0.0.0.0'))", ()) 56 | .unwrap(); 57 | 58 | let res: Option = conn 59 | .query_first("select regexp_replace(@testval,'[:.]','')") 60 | .unwrap() 61 | .unwrap(); 62 | 63 | assert_eq!(res.unwrap(), "ffff0000"); 64 | } 65 | -------------------------------------------------------------------------------- /udf-examples/tests/median.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "CREATE OR REPLACE AGGREGATE FUNCTION udf_median 10 | RETURNS integer 11 | SONAME 'libudf_examples.so'", 12 | "CREATE OR REPLACE TABLE test_median ( 13 | id int auto_increment, 14 | val int, 15 | primary key (id) 16 | )", 17 | "INSERT INTO test_median (val) VALUES (2), (1), (3), (4), (-3), (7), (-1)", 18 | ]; 19 | 20 | #[test] 21 | fn test_empty() { 22 | let conn = &mut get_db_connection(SETUP); 23 | 24 | let res: i32 = conn 25 | .query_first("select udf_median(val) from test_median") 26 | .unwrap() 27 | .unwrap(); 28 | 29 | assert_eq!(res, 2); 30 | } 31 | -------------------------------------------------------------------------------- /udf-examples/tests/mishmash.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &["create or replace function mishmash 9 | returns string 10 | soname 'libudf_examples.so'"]; 11 | 12 | #[test] 13 | fn test_empty() { 14 | let conn = &mut get_db_connection(SETUP); 15 | 16 | let res: Option> = conn.query_first("select mishmash()").unwrap().unwrap(); 17 | 18 | assert!(res.is_none()); 19 | } 20 | 21 | #[test] 22 | fn test_single() { 23 | let conn = &mut get_db_connection(SETUP); 24 | 25 | let res: Option> = conn 26 | .query_first("select mishmash('banana')") 27 | .unwrap() 28 | .unwrap(); 29 | 30 | assert_eq!(res.unwrap(), b"banana"); 31 | } 32 | 33 | #[test] 34 | fn test_many() { 35 | let conn = &mut get_db_connection(SETUP); 36 | 37 | let res: Option> = conn 38 | .query_first("select mishmash('banana', 'is', 'a', 'fruit')") 39 | .unwrap() 40 | .unwrap(); 41 | 42 | assert_eq!(res.unwrap(), b"bananaisafruit"); 43 | } 44 | -------------------------------------------------------------------------------- /udf-examples/tests/sequence.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &[ 9 | "CREATE OR REPLACE FUNCTION udf_sequence 10 | RETURNS integer 11 | SONAME 'libudf_examples.so'", 12 | "CREATE OR REPLACE TABLE test_seq ( 13 | id int 14 | )", 15 | "INSERT INTO test_seq (id) VALUES (1), (2), (3), (4), (5), (6)", 16 | ]; 17 | 18 | #[test] 19 | fn test_single() { 20 | let conn = &mut get_db_connection(SETUP); 21 | 22 | // First result should be 1 23 | let res: i32 = conn.query_first("select udf_sequence()").unwrap().unwrap(); 24 | 25 | assert_eq!(res, 1); 26 | } 27 | 28 | #[test] 29 | fn test_offset() { 30 | let conn = &mut get_db_connection(SETUP); 31 | 32 | // With argument specified, we should have one more than the 33 | // specified value 34 | let res: i32 = conn.query_first("select udf_sequence(4)").unwrap().unwrap(); 35 | 36 | assert_eq!(res, 5); 37 | } 38 | 39 | #[test] 40 | fn test_incrementing() { 41 | let conn = &mut get_db_connection(SETUP); 42 | 43 | // Test results with multiple rows 44 | let res: Vec<(i32, i32)> = conn 45 | .query("select id, udf_sequence() from test_seq") 46 | .unwrap(); 47 | 48 | assert_eq!(res, vec![(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6)]); 49 | } 50 | 51 | #[test] 52 | fn test_incrementing_offset() { 53 | let conn = &mut get_db_connection(SETUP); 54 | 55 | // Test results with multiple rows 56 | let res: Vec<(i32, i32)> = conn 57 | .query("select id, udf_sequence(10) from test_seq") 58 | .unwrap(); 59 | 60 | assert_eq!( 61 | res, 62 | vec![(1, 11), (2, 12), (3, 13), (4, 14), (5, 15), (6, 16)] 63 | ); 64 | } 65 | 66 | #[test] 67 | fn test_incrementing_offset_negative() { 68 | let conn = &mut get_db_connection(SETUP); 69 | 70 | // Test results with multiple rows 71 | let res: Vec<(i32, i32)> = conn 72 | .query("select id, udf_sequence(-2) from test_seq") 73 | .unwrap(); 74 | 75 | assert_eq!(res, vec![(1, -1), (2, 0), (3, 1), (4, 2), (5, 3), (6, 4)]); 76 | } 77 | -------------------------------------------------------------------------------- /udf-examples/tests/sum_int.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "backend")] 2 | 3 | mod backend; 4 | 5 | use backend::get_db_connection; 6 | use mysql::prelude::*; 7 | 8 | const SETUP: &[&str] = &["create or replace function sum_int 9 | returns integer 10 | soname 'libudf_examples.so'"]; 11 | 12 | #[test] 13 | fn test_empty() { 14 | let conn = &mut get_db_connection(SETUP); 15 | 16 | let res: i32 = conn.query_first("select sum_int()").unwrap().unwrap(); 17 | 18 | assert_eq!(res, 0); 19 | } 20 | 21 | #[test] 22 | fn test_basic() { 23 | let conn = &mut get_db_connection(SETUP); 24 | 25 | let res: i32 = conn 26 | .query_first("select sum_int(1, 2, 3, 4, -6)") 27 | .unwrap() 28 | .unwrap(); 29 | 30 | assert_eq!(res, 4); 31 | } 32 | 33 | #[test] 34 | fn test_coercion() { 35 | let conn = &mut get_db_connection(SETUP); 36 | 37 | let res: i32 = conn 38 | .query_first("select sum_int(1, 2, '-5', '11')") 39 | .unwrap() 40 | .unwrap(); 41 | 42 | assert_eq!(res, 9); 43 | } 44 | -------------------------------------------------------------------------------- /udf-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udf-macros" 3 | version = "0.5.5" 4 | edition = "2021" 5 | description = "UDF procedural macros implementation" 6 | repository = "https://github.com/pluots/sql-udf/tree/main/udf_macros" 7 | license = "Apache-2.0 OR GPL-2.0-or-later" 8 | publish = true 9 | # autotests = false 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | heck = "0.5.0" 16 | lazy_static = "1.4.0" 17 | proc-macro2 = "1.0.82" 18 | quote = "1.0.36" 19 | syn = { version = "2.0.61", features = ["full", "extra-traits"] } 20 | 21 | [dev-dependencies] 22 | trybuild = { version = "1.0.94", features = ["diff"] } 23 | udf = { path = "../udf" } 24 | udf-sys = { path = "../udf-sys" } 25 | 26 | [package.metadata.release] 27 | shared-version = true 28 | -------------------------------------------------------------------------------- /udf-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::pedantic, 3 | clippy::nursery, 4 | clippy::str_to_string, 5 | clippy::missing_inline_in_public_items 6 | )] 7 | // Pedantic config 8 | #![allow( 9 | clippy::missing_const_for_fn, 10 | clippy::missing_panics_doc, 11 | clippy::must_use_candidate, 12 | clippy::cast_possible_truncation 13 | )] 14 | 15 | mod register; 16 | mod types; 17 | 18 | use proc_macro::TokenStream; 19 | 20 | macro_rules! match_variant { 21 | ($variant:path) => { 22 | |x| { 23 | if let $variant(value) = x { 24 | Some(value) 25 | } else { 26 | None 27 | } 28 | } 29 | }; 30 | } 31 | 32 | pub(crate) use match_variant; 33 | 34 | /// # Register exposed function names required for a UDF 35 | /// 36 | /// This macro is applied to an `impl BasicUdf` block (and an `AggregateUdf` 37 | /// block, if applicable) and exposed the C-callable functions that 38 | /// `MariaDB`/`MySQL` expect. 39 | /// 40 | /// Usage: 41 | /// 42 | /// ```ignore 43 | /// #[register] 44 | /// impl BasicUdf for MyStruct { 45 | /// ... 46 | /// } 47 | /// 48 | /// #[register] 49 | /// impl AggregateUdf for MyStruct { 50 | /// ... 51 | /// } 52 | /// ``` 53 | /// 54 | /// Its process is as follows: 55 | /// 56 | /// - Convert the implemented struct's name to snake case to create the function 57 | /// name (unless a name is specified) 58 | /// - Obtain the return type from the `Returns` type in `BasicUdf` 59 | /// - Create functions `fn_name`, `fn_name_init`, and `fn_name_deinit` with 60 | /// correct signatures and interfaces 61 | /// - If applied on an `impl AggregateUdf` block, create `fn_name_clear` and 62 | /// `fn_name_add`. `fn_name_remove` is also included if it is redefined 63 | /// 64 | /// # Arguments 65 | /// 66 | /// - `#[udf::register(name = "new_name")]` will specify a name for your SQL 67 | /// function. If this is not specified, your struct name will be converted to 68 | /// snake case and used (e.g. `AddAllNumbers` would become `add_all_numbers` 69 | /// by default). 70 | /// - `#[udf::register(alias = "alias")]` will specify an alias for this function. 71 | /// More than one alias can be specified, and it can be combined with a `name` attribute. 72 | /// 73 | /// **IMPORTANT**: if using aggregate UDFs, the exact same renaming must be applied to 74 | /// both the `impl BasicUdf` and the `impl AggregateUdf` blocks! If this is not followed, 75 | /// your function will not act as an aggregate (there may also be a compile error). 76 | #[proc_macro_attribute] 77 | pub fn register(args: TokenStream, item: TokenStream) -> TokenStream { 78 | // Keep this file clean by keeping the dirty work in entry 79 | register::register(&args, item) 80 | } 81 | -------------------------------------------------------------------------------- /udf-macros/src/types.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, Type}; 2 | 3 | /// Allowable signatures 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 5 | pub enum ImplType { 6 | Basic, 7 | Aggregate, 8 | } 9 | 10 | /// Possible return types in SQL 11 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 12 | pub enum TypeClass { 13 | Int, 14 | Float, 15 | /// Bytes that can be properly returned 16 | BytesRef, 17 | /// Bytest that must be truncated 18 | Bytes, 19 | } 20 | 21 | /// Struct containing information about a return type 22 | #[derive(Clone, Debug)] 23 | pub struct RetType { 24 | pub type_: Type, 25 | pub is_optional: bool, 26 | pub type_cls: TypeClass, 27 | } 28 | 29 | impl RetType { 30 | fn new(type_: Type, is_optional: bool, fn_sig: TypeClass) -> Self { 31 | Self { 32 | type_, 33 | is_optional, 34 | type_cls: fn_sig, 35 | } 36 | } 37 | } 38 | 39 | /// Brute force list of acceptable types 40 | /// 41 | /// We cannot accept `String` directly because that would imply allocation that 42 | /// we can't allow (we would have to turn the `String` into a pointer to return 43 | /// it, and we would never get the pointer back to free it). 44 | pub fn make_type_list() -> Vec { 45 | vec![ 46 | // Only valid integer types 47 | RetType::new(parse_quote! { i64 }, false, TypeClass::Int), 48 | RetType::new(parse_quote! { Option }, true, TypeClass::Int), 49 | // Only valid float types 50 | RetType::new(parse_quote! { f64 }, false, TypeClass::Float), 51 | RetType::new(parse_quote! { Option }, true, TypeClass::Float), 52 | // Tons of possible byte slice references. We could probably make these 53 | // generic somehow in the future, but it is proving tough. 54 | RetType::new(parse_quote! { &'a [u8] }, false, TypeClass::BytesRef), 55 | RetType::new(parse_quote! { Option<&'a [u8]> }, true, TypeClass::BytesRef), 56 | RetType::new(parse_quote! { &str }, false, TypeClass::BytesRef), 57 | RetType::new(parse_quote! { Option<&str> }, true, TypeClass::BytesRef), 58 | RetType::new(parse_quote! { &'a str }, false, TypeClass::BytesRef), 59 | RetType::new(parse_quote! { Option<&'a str> }, true, TypeClass::BytesRef), 60 | RetType::new(parse_quote! { &'static str }, false, TypeClass::BytesRef), 61 | RetType::new( 62 | parse_quote! { Option<&'static str> }, 63 | true, 64 | TypeClass::BytesRef, 65 | ), 66 | RetType::new(parse_quote! { &'a String }, false, TypeClass::BytesRef), 67 | RetType::new( 68 | parse_quote! { Option<&'a String> }, 69 | true, 70 | TypeClass::BytesRef, 71 | ), 72 | // Bytes types that aren't in a reference. These will get copied if they fit, 73 | // truncated with a stderr message if not 74 | RetType::new(parse_quote! { Vec }, false, TypeClass::Bytes), 75 | RetType::new(parse_quote! { Option>}, true, TypeClass::Bytes), 76 | RetType::new(parse_quote! { String }, false, TypeClass::Bytes), 77 | RetType::new(parse_quote! { Option}, true, TypeClass::Bytes), 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/agg_missing_basic.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf; 6 | 7 | impl AggregateUdf for MyUdf { 8 | // Required methods 9 | fn clear(&mut self, cfg: &UdfCfg, error: Option) -> Result<(), NonZeroU8> { 10 | todo!() 11 | } 12 | fn add( 13 | &mut self, 14 | cfg: &UdfCfg, 15 | args: &ArgList<'_, Process>, 16 | error: Option, 17 | ) -> Result<(), NonZeroU8> { 18 | todo!() 19 | } 20 | } 21 | 22 | fn main() {} 23 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/agg_missing_basic.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `MyUdf: BasicUdf` is not satisfied 2 | --> tests/fail/agg_missing_basic.rs:7:23 3 | | 4 | 7 | impl AggregateUdf for MyUdf { 5 | | ^^^^^ the trait `BasicUdf` is not implemented for `MyUdf` 6 | | 7 | note: required by a bound in `udf::AggregateUdf` 8 | --> $WORKSPACE/udf/src/traits.rs 9 | | 10 | | pub trait AggregateUdf: BasicUdf { 11 | | ^^^^^^^^ required by this bound in `AggregateUdf` 12 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/bad_attributes.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf1; 6 | struct MyUdf2; 7 | 8 | #[register(foo = "foo")] 9 | impl BasicUdf for MyUdf1 { 10 | type Returns<'a> = Option; 11 | 12 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 13 | todo!(); 14 | } 15 | 16 | fn process<'a>( 17 | &'a mut self, 18 | cfg: &UdfCfg, 19 | args: &ArgList, 20 | error: Option, 21 | ) -> Result, ProcessError> { 22 | todo!(); 23 | } 24 | } 25 | 26 | #[register(name = "bar", name = "name")] 27 | impl BasicUdf for MyUdf2 { 28 | type Returns<'a> = Option; 29 | 30 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 31 | todo!(); 32 | } 33 | 34 | fn process<'a>( 35 | &'a mut self, 36 | cfg: &UdfCfg, 37 | args: &ArgList, 38 | error: Option, 39 | ) -> Result, ProcessError> { 40 | todo!(); 41 | } 42 | } 43 | 44 | fn main() {} 45 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/bad_attributes.stderr: -------------------------------------------------------------------------------- 1 | error: unexpected key (only `name` and `alias` are accepted) 2 | --> tests/fail/bad_attributes.rs:8:12 3 | | 4 | 8 | #[register(foo = "foo")] 5 | | ^^^ 6 | 7 | error: `name` can only be specified once 8 | --> tests/fail/bad_attributes.rs:26:26 9 | | 10 | 26 | #[register(name = "bar", name = "name")] 11 | | ^^^^ 12 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/missing_rename.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf; 6 | 7 | #[register(name = "foo", alias = "bar")] 8 | impl BasicUdf for MyUdf { 9 | type Returns<'a> = Option; 10 | 11 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 12 | todo!(); 13 | } 14 | 15 | fn process<'a>( 16 | &'a mut self, 17 | cfg: &UdfCfg, 18 | args: &ArgList, 19 | error: Option, 20 | ) -> Result, ProcessError> { 21 | todo!(); 22 | } 23 | } 24 | 25 | #[register] 26 | impl AggregateUdf for MyUdf { 27 | // Required methods 28 | fn clear(&mut self, cfg: &UdfCfg, error: Option) -> Result<(), NonZeroU8> { 29 | todo!() 30 | } 31 | fn add( 32 | &mut self, 33 | cfg: &UdfCfg, 34 | args: &ArgList<'_, Process>, 35 | error: Option, 36 | ) -> Result<(), NonZeroU8> { 37 | todo!() 38 | } 39 | } 40 | 41 | fn main() {} 42 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/missing_rename.stderr: -------------------------------------------------------------------------------- 1 | error[E0080]: evaluation of constant value failed 2 | --> $WORKSPACE/udf/src/wrapper.rs 3 | | 4 | | panic!("{}", msg); 5 | | ^^^^^^^^^^^^^^^^^ the evaluated program panicked at '`#[register]` on `BasicUdf` and `AggregateUdf` must have the same `name` argument; got `foo` and `my_udf` (default from struct name)', $WORKSPACE/udf/src/wrapper.rs:83:5 6 | | 7 | note: inside `wrapper::verify_aggregate_attributes_name::` 8 | --> $WORKSPACE/udf/src/wrapper.rs 9 | | 10 | | panic!("{}", msg); 11 | | ^^^^^^^^^^^^^^^^^ 12 | note: inside `verify_aggregate_attributes::` 13 | --> $WORKSPACE/udf/src/wrapper.rs 14 | | 15 | | verify_aggregate_attributes_name::(); 16 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | note: inside `_` 18 | --> tests/fail/missing_rename.rs:25:1 19 | | 20 | 25 | #[register] 21 | | ^^^^^^^^^^^ 22 | = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the attribute macro `register` (in Nightly builds, run with -Z macro-backtrace for more info) 23 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/wrong_block.rs: -------------------------------------------------------------------------------- 1 | //! Registration should fail no anything that is not an impl 2 | 3 | use udf_macros::register; 4 | 5 | // Registration is not allowed on non-impls 6 | #[register] 7 | struct X {} 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/wrong_block.stderr: -------------------------------------------------------------------------------- 1 | error: expected `impl` 2 | --> tests/fail/wrong_block.rs:7:1 3 | | 4 | 7 | struct X {} 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/wrong_impl.rs: -------------------------------------------------------------------------------- 1 | //! Registration should fail no anything that is not an impl 2 | 3 | use udf_macros::register; 4 | 5 | // Registration is not allowed on non-impls 6 | struct X; 7 | 8 | #[register] 9 | impl Foo for X {} 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /udf-macros/tests/fail/wrong_impl.stderr: -------------------------------------------------------------------------------- 1 | error: Expected trait `BasicUdf` or `AggregateUdf` 2 | --> tests/fail/wrong_impl.rs:9:1 3 | | 4 | 9 | impl Foo for X {} 5 | | ^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /udf-macros/tests/ok.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf1; 6 | struct MyUdf2; 7 | struct MyUdf3; 8 | 9 | #[register] 10 | impl BasicUdf for MyUdf1 { 11 | type Returns<'a> = Option; 12 | 13 | fn init(_cfg: &UdfCfg, args: &ArgList) -> Result { 14 | todo!(); 15 | } 16 | 17 | fn process<'a>( 18 | &'a mut self, 19 | cfg: &UdfCfg, 20 | args: &ArgList, 21 | error: Option, 22 | ) -> Result, ProcessError> { 23 | todo!(); 24 | } 25 | } 26 | 27 | #[register(name = "foo")] 28 | impl BasicUdf for MyUdf2 { 29 | type Returns<'a> = Option; 30 | 31 | fn init(_cfg: &UdfCfg, args: &ArgList) -> Result { 32 | todo!(); 33 | } 34 | 35 | fn process<'a>( 36 | &'a mut self, 37 | cfg: &UdfCfg, 38 | args: &ArgList, 39 | error: Option, 40 | ) -> Result, ProcessError> { 41 | todo!(); 42 | } 43 | } 44 | 45 | #[register(alias = "banana")] 46 | impl BasicUdf for MyUdf3 { 47 | type Returns<'a> = Option; 48 | 49 | fn init(_cfg: &UdfCfg, args: &ArgList) -> Result { 50 | todo!(); 51 | } 52 | 53 | fn process<'a>( 54 | &'a mut self, 55 | cfg: &UdfCfg, 56 | args: &ArgList, 57 | error: Option, 58 | ) -> Result, ProcessError> { 59 | todo!(); 60 | } 61 | } 62 | 63 | fn main() { 64 | // check that expected symbols exist 65 | let _ = my_udf1 as *const (); 66 | let _ = my_udf1_init as *const (); 67 | let _ = my_udf1_deinit as *const (); 68 | let _ = foo as *const (); 69 | let _ = foo_init as *const (); 70 | let _ = foo_deinit as *const (); 71 | let _ = my_udf3 as *const (); 72 | let _ = my_udf3_init as *const (); 73 | let _ = my_udf3_deinit as *const (); 74 | let _ = banana as *const (); 75 | let _ = banana_init as *const (); 76 | let _ = banana_deinit as *const (); 77 | } 78 | -------------------------------------------------------------------------------- /udf-macros/tests/ok_agg.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf; 6 | 7 | #[register(name = "foo")] 8 | impl BasicUdf for MyUdf { 9 | type Returns<'a> = Option; 10 | 11 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 12 | todo!(); 13 | } 14 | 15 | fn process<'a>( 16 | &'a mut self, 17 | cfg: &UdfCfg, 18 | args: &ArgList, 19 | error: Option, 20 | ) -> Result, ProcessError> { 21 | todo!(); 22 | } 23 | } 24 | 25 | #[register(name = "foo")] 26 | impl AggregateUdf for MyUdf { 27 | // Required methods 28 | fn clear(&mut self, cfg: &UdfCfg, error: Option) -> Result<(), NonZeroU8> { 29 | todo!() 30 | } 31 | fn add( 32 | &mut self, 33 | cfg: &UdfCfg, 34 | args: &ArgList<'_, Process>, 35 | error: Option, 36 | ) -> Result<(), NonZeroU8> { 37 | todo!() 38 | } 39 | } 40 | 41 | fn main() { 42 | let _ = foo as *const (); 43 | let _ = foo_init as *const (); 44 | let _ = foo_deinit as *const (); 45 | let _ = foo_add as *const (); 46 | let _ = foo_clear as *const (); 47 | } 48 | -------------------------------------------------------------------------------- /udf-macros/tests/ok_agg_alias.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use udf::prelude::*; 4 | 5 | struct MyUdf; 6 | 7 | #[register(name = "foo", alias = "bar")] 8 | impl BasicUdf for MyUdf { 9 | type Returns<'a> = Option; 10 | 11 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 12 | todo!(); 13 | } 14 | 15 | fn process<'a>( 16 | &'a mut self, 17 | cfg: &UdfCfg, 18 | args: &ArgList, 19 | error: Option, 20 | ) -> Result, ProcessError> { 21 | todo!(); 22 | } 23 | } 24 | 25 | #[register(name = "foo", alias = "bar")] 26 | impl AggregateUdf for MyUdf { 27 | fn clear(&mut self, cfg: &UdfCfg, error: Option) -> Result<(), NonZeroU8> { 28 | todo!() 29 | } 30 | fn add( 31 | &mut self, 32 | cfg: &UdfCfg, 33 | args: &ArgList<'_, Process>, 34 | error: Option, 35 | ) -> Result<(), NonZeroU8> { 36 | todo!() 37 | } 38 | } 39 | 40 | fn main() { 41 | let _ = foo as *const (); 42 | let _ = foo_init as *const (); 43 | let _ = foo_deinit as *const (); 44 | let _ = foo_add as *const (); 45 | let _ = foo_clear as *const (); 46 | let _ = bar as *const (); 47 | let _ = bar_init as *const (); 48 | let _ = bar_deinit as *const (); 49 | let _ = bar_add as *const (); 50 | let _ = bar_clear as *const (); 51 | } 52 | -------------------------------------------------------------------------------- /udf-macros/tests/runner_fail.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | #[cfg(not(miri))] 3 | fn tests() { 4 | let t = trybuild::TestCases::new(); 5 | t.compile_fail("tests/fail/*.rs"); 6 | } 7 | -------------------------------------------------------------------------------- /udf-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udf-sys" 3 | version = "0.5.5" 4 | edition = "2021" 5 | description = "UDF system bindings" 6 | repository = "https://github.com/pluots/sql-udf/tree/main/udf_sys" 7 | license = "Apache-2.0 OR GPL-2.0-or-later" 8 | publish = true 9 | 10 | 11 | [dependencies] 12 | 13 | 14 | [package.metadata.release] 15 | shared-version = true 16 | -------------------------------------------------------------------------------- /udf-sys/README.md: -------------------------------------------------------------------------------- 1 | # udf-sys; bindings for `udf_registration_types.c` 2 | 3 | This crate provides rust bindings for `udf_registration_types.c`. Its main 4 | purpose is for use by the [`udf`](https://crates.io/crates/udf) crate. 5 | -------------------------------------------------------------------------------- /udf-sys/udf_registration_types.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2017, 2021, Oracle and/or its affiliates. 2 | 3 | This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License, version 2.0, 5 | as published by the Free Software Foundation. 6 | 7 | This program is also distributed with certain software (including 8 | but not limited to OpenSSL) that is licensed under separate terms, 9 | as designated in a particular file or component or in included license 10 | documentation. The authors of MySQL hereby grant you an additional 11 | permission to link the program and your derivative works with the 12 | separately licensed software that they have included with MySQL. 13 | 14 | Without limiting anything contained in the foregoing, this file, 15 | which is part of C Driver for MySQL (Connector/C), is also subject to the 16 | Universal FOSS Exception, version 1.0, a copy of which can be found at 17 | http://oss.oracle.com/licenses/universal-foss-exception. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License, version 2.0, for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program; if not, write to the Free Software 26 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ 27 | 28 | #ifndef UDF_REGISTRATION_TYPES_H 29 | #define UDF_REGISTRATION_TYPES_H 30 | 31 | #ifndef MYSQL_ABI_CHECK 32 | #include 33 | #endif 34 | 35 | /** 36 | Type of the user defined function return slot and arguments 37 | */ 38 | enum Item_result 39 | { 40 | INVALID_RESULT = -1, /** not valid for UDFs */ 41 | STRING_RESULT = 0, /** char * */ 42 | REAL_RESULT, /** double */ 43 | INT_RESULT, /** long long */ 44 | ROW_RESULT, /** not valid for UDFs */ 45 | DECIMAL_RESULT /** char *, to be converted to/from a decimal */ 46 | }; 47 | 48 | typedef struct UDF_ARGS 49 | { 50 | unsigned int arg_count; /**< Number of arguments */ 51 | enum Item_result *arg_type; /**< Pointer to item_results */ 52 | char **args; /**< Pointer to argument */ 53 | unsigned long *lengths; /**< Length of string arguments */ 54 | char *maybe_null; /**< Set to 1 for all maybe_null args */ 55 | char **attributes; /**< Pointer to attribute name */ 56 | unsigned long *attribute_lengths; /**< Length of attribute arguments */ 57 | void *extension; 58 | } UDF_ARGS; 59 | 60 | /** 61 | Information about the result of a user defined function 62 | 63 | @todo add a notion for determinism of the UDF. 64 | 65 | @sa Item_udf_func::update_used_tables() 66 | */ 67 | typedef struct UDF_INIT 68 | { 69 | bool maybe_null; /** 1 if function can return NULL */ 70 | unsigned int decimals; /** for real functions */ 71 | unsigned long max_length; /** For string functions */ 72 | char *ptr; /** free pointer for function data */ 73 | bool const_item; /** 1 if function always returns the same value */ 74 | void *extension; 75 | } UDF_INIT; 76 | 77 | enum Item_udftype 78 | { 79 | UDFTYPE_FUNCTION = 1, 80 | UDFTYPE_AGGREGATE 81 | }; 82 | 83 | typedef void (*Udf_func_clear)(UDF_INIT *, unsigned char *, unsigned char *); 84 | typedef void (*Udf_func_add)(UDF_INIT *, UDF_ARGS *, unsigned char *, 85 | unsigned char *); 86 | typedef void (*Udf_func_deinit)(UDF_INIT *); 87 | typedef bool (*Udf_func_init)(UDF_INIT *, UDF_ARGS *, char *); 88 | typedef void (*Udf_func_any)(void); 89 | typedef double (*Udf_func_double)(UDF_INIT *, UDF_ARGS *, unsigned char *, 90 | unsigned char *); 91 | typedef long long (*Udf_func_longlong)(UDF_INIT *, UDF_ARGS *, unsigned char *, 92 | unsigned char *); 93 | typedef char *(*Udf_func_string)(UDF_INIT *, UDF_ARGS *, char *, 94 | unsigned long *, unsigned char *, 95 | unsigned char *); 96 | 97 | #endif /* UDF_REGISTRATION_TYPES_H */ 98 | -------------------------------------------------------------------------------- /udf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udf" 3 | version = "0.5.5" 4 | rust-version = "1.65" 5 | edition = "2021" 6 | description = "Easily create user defined functions (UDFs) for MariaDB and MySQL." 7 | repository = "https://github.com/pluots/sql-udf" 8 | readme = "../README.md" 9 | license = "Apache-2.0 OR GPL-2.0-or-later" 10 | keywords = ["sql", "udf"] 11 | publish = true 12 | 13 | [dependencies] 14 | chrono = "0.4.38" 15 | udf-macros = { path = "../udf-macros", version = "0.5.5" } 16 | udf-sys = { path = "../udf-sys", version = "0.5.5" } 17 | cfg-if = "1.0" 18 | 19 | [features] 20 | mock = [] # enable this feature for the `mock` module 21 | logging-debug = [] # enable this feature to turn on debug printing 22 | logging-debug-calls = ["logging-debug"] # enable this feature to turn on logging calls 23 | 24 | [package.metadata.release] 25 | shared-version = true 26 | 27 | [package.metadata.docs.rs] 28 | features = ["mock"] 29 | 30 | # Can't run replacements at workspace root. Need to use this "hacky" sort of way. 31 | [[package.metadata.release.pre-release-replacements]] 32 | file = "../CHANGELOG.md" 33 | search = "Unreleased" 34 | replace = "{{version}}" 35 | 36 | [[package.metadata.release.pre-release-replacements]] 37 | file = "../CHANGELOG.md" 38 | search = "\\.\\.\\.HEAD" 39 | replace = "...{{tag_name}}" 40 | exactly = 1 41 | 42 | [[package.metadata.release.pre-release-replacements]] 43 | file = "../CHANGELOG.md" 44 | search = "ReleaseDate" 45 | replace = "{{date}}" 46 | 47 | [[package.metadata.release.pre-release-replacements]] 48 | file = "../CHANGELOG.md" 49 | search = "" 50 | replace = """\ 51 | \n\n\ 52 | ## [Unreleased] - ReleaseDate\n\n\ 53 | ### Added\n\n\ 54 | ### Changed\n\n\ 55 | ### Removed\n\n\ 56 | """ 57 | exactly = 1 58 | 59 | [[package.metadata.release.pre-release-replacements]] 60 | file = "../CHANGELOG.md" 61 | search = "" 62 | replace = """\ 63 | \n\ 64 | [Unreleased]: https://github.com/pluots/sql-udf/compare/{{tag_name}}...HEAD\ 65 | """ 66 | exactly = 1 67 | 68 | [[package.metadata.release.pre-release-replacements]] 69 | file = "Cargo.toml" 70 | # Need \d match so we don't accidentally match our pattern here 71 | search = 'udf-macros = \{ path = "../udf-macros", version = "\d.*" \}' 72 | replace = 'udf-macros = { path = "../udf-macros", version = "{{version}}" }' 73 | 74 | [[package.metadata.release.pre-release-replacements]] 75 | file = "Cargo.toml" 76 | search = 'udf-sys = \{ path = "../udf-sys", version = "\d.*" \}' 77 | replace = 'udf-sys = { path = "../udf-sys", version = "{{version}}" }' 78 | -------------------------------------------------------------------------------- /udf/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A wrapper crate to make writing SQL user-defined functions (UDFs) easy 2 | //! 3 | //! This crate provides bindings for easy creation of SQL user-defined functions 4 | //! in Rust. See [the 5 | //! readme](https://github.com/pluots/sql-udf/blob/main/README.md) for more 6 | //! background information on how UDFs work in general. 7 | //! 8 | //! # Usage 9 | //! 10 | //! Using this crate is fairly simple: create a struct that will be used to 11 | //! share data among UDF function calls (which can be zero-sized), then 12 | //! implement needed traits for it. [`BasicUdf`] provides function signatures 13 | //! for standard UDFs, and [`AggregateUdf`] provides signatures for aggregate 14 | //! (and window) UDFs. See the documentation there for a step-by-step guide. 15 | //! 16 | //! ``` 17 | //! use udf::prelude::*; 18 | //! 19 | //! // Our struct that will produce a UDF of name `my_udf` 20 | //! // If there is no data to store between calls, it can be zero sized 21 | //! struct MyUdf; 22 | //! 23 | //! // Specifying a name is optional; `#[register]` uses a snake case version of 24 | //! // the struct name by default (`my_udf` in this case) 25 | //! #[register(name = "my_shiny_udf")] 26 | //! impl BasicUdf for MyUdf { 27 | //! // Specify return type of this UDF to be a nullable integer 28 | //! type Returns<'a> = Option; 29 | //! 30 | //! // Perform initialization steps here 31 | //! fn init(cfg: &UdfCfg, args: &ArgList) -> Result { 32 | //! todo!(); 33 | //! } 34 | //! 35 | //! // Create a result here 36 | //! fn process<'a>( 37 | //! &'a mut self, 38 | //! cfg: &UdfCfg, 39 | //! args: &ArgList, 40 | //! error: Option, 41 | //! ) -> Result, ProcessError> { 42 | //! todo!(); 43 | //! } 44 | //! } 45 | //! ``` 46 | //! 47 | //! # Building & Usage 48 | //! 49 | //! The above example will create three C-callable functions: `my_udf`, 50 | //! `my_udf_init`, and `my_udf_deinit`, which is what `MariaDB` and `MySql` 51 | //! expect for UDFs. To create a C dynamic library (as is required for usage), 52 | //! add the following to your `Cargo.toml` 53 | //! 54 | //! ```toml 55 | //! [lib] 56 | //! crate-type = ["cdylib"] 57 | //! ``` 58 | //! 59 | //! The next time you run `cargo build --release`, in `target/release` there 60 | //! will be a shared library `.so` file. Copy this to your `plugin_dir` location 61 | //! (usually `/usr/lib/mysql/plugin/`), and load the function with the 62 | //! following: 63 | //! 64 | //! ```sql 65 | //! CREATE FUNCTION my_udf RETURNS integer SONAME 'libudf_test.so'; 66 | //! ``` 67 | //! 68 | //! Replace `my_udf` with the function name, `integer` with the return type, and 69 | //! `libudf_test.so` with the correct file name. 70 | //! 71 | //! More details on building are discussed in [the project 72 | //! readme](https://github.com/pluots/sql-udf/blob/main/README.md). See [the 73 | //! `MariaDB` documentation](https://mariadb.com/kb/en/create-function-udf/) for 74 | //! more detailed information on how to load the created libraries. 75 | //! 76 | //! # Crate Features 77 | //! 78 | //! This crate includes some optional features. They can be enabled in your 79 | //! `Cargo.toml`. 80 | //! 81 | //! - `mock`: enable this feature to add the [mock] module for easier unit 82 | //! testing. _(note: this feature will become unneeded in the future and 83 | //! `mock` will be available by default. It is currently feature-gated because 84 | //! it is considered unstable.)_ 85 | //! - `logging-debug`: enable this feature to turn on debug level logging for 86 | //! this crate. This uses the `udf_log!` macro and includes information about 87 | //! memory management and function calls. These will show up with your SQL 88 | //! server logs, like: 89 | //! 90 | //! ```text 91 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: ENTER init for 'udf_examples::lookup::Lookup6' 92 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: 0x7fdea4022220 24 bytes udf->server control transfer 93 | //! (BufConverter>) 94 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: EXIT init for 'udf_examples::lookup::Lookup6' 95 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: ENTER process for 'udf_examples::lookup::Lookup6' 96 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: 0x7fdea4022220 24 bytes server->udf control transfer 97 | //! (BufConverter>) 98 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: 0x7fdea4022220 24 bytes udf->server control transfer 99 | //! (BufConverter>) 100 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: EXIT process for 'udf_examples::lookup::Lookup6' 101 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: ENTER deinit for 'udf_examples::lookup::Lookup6' 102 | //! 2023-03-23 00:45:53+00:00 [Debug] UDF: 0x7fdea4022220 24 bytes server->udf control transfer 103 | //! (BufConverter>) 104 | //! ``` 105 | //! 106 | //! This output can be helpful to understand the exact data flow between 107 | //! the library and the server. They are enabled by default in the `udf-examples` 108 | //! library. 109 | //! 110 | //! - `logging-debug-calls` full debugging printing of the structs passed 111 | //! between this library and the SQL server. Implies `logging-debug`. This 112 | //! output can be noisy, but can help to debug issues related to the lower 113 | //! level interfaces (i.e. problems with this library or with the server 114 | //! itself). 115 | //! 116 | //! # Version Note 117 | //! 118 | //! Because of reliance on a feature called GATs, this library requires Rust 119 | //! version >= 1.65. This only became stable on 2022-11-03; if you encounter 120 | //! issues compiling, be sure to update your toolchain. 121 | 122 | // Strict clippy 123 | #![warn( 124 | clippy::pedantic, 125 | // clippy::cargo, 126 | clippy::nursery, 127 | clippy::str_to_string, 128 | clippy::exhaustive_enums, 129 | clippy::pattern_type_mismatch 130 | )] 131 | // Pedantic config 132 | #![allow( 133 | clippy::missing_const_for_fn, 134 | // clippy::missing_panics_doc, 135 | clippy::must_use_candidate, 136 | clippy::cast_possible_truncation 137 | )] 138 | 139 | // We re-export this so we can use it in our macro, but don't need it 140 | // to show up in our docs 141 | #[doc(hidden)] 142 | pub extern crate chrono; 143 | 144 | #[doc(hidden)] 145 | pub extern crate udf_sys; 146 | 147 | extern crate udf_macros; 148 | 149 | pub use udf_macros::register; 150 | 151 | #[macro_use] 152 | mod macros; 153 | pub mod prelude; 154 | pub mod traits; 155 | pub mod types; 156 | 157 | // We hide this because it's really only used by our proc macros 158 | #[doc(hidden)] 159 | pub mod wrapper; 160 | 161 | #[doc(inline)] 162 | pub use traits::*; 163 | #[doc(inline)] 164 | pub use types::{MYSQL_ERRMSG_SIZE, *}; 165 | 166 | pub mod mock; 167 | -------------------------------------------------------------------------------- /udf/src/macros.rs: -------------------------------------------------------------------------------- 1 | //! macro definitions 2 | 3 | /// Print a formatted log message to `stderr` to display in server logs 4 | /// 5 | /// Performs formatting to match other common SQL error logs, roughly: 6 | /// 7 | /// ```text 8 | /// 2022-10-15 13:12:54+00:00 [Warning] Udf: this is the message 9 | /// ``` 10 | /// 11 | /// ``` 12 | /// # #[cfg(not(miri))] // need to skip Miri because. it can't cross FFI 13 | /// # fn test() { 14 | /// 15 | /// use udf::udf_log; 16 | /// 17 | /// // Prints "2022-10-08 05:27:30+00:00 [Error] UDF: this is an error" 18 | /// // This matches the default entrypoint log format 19 | /// udf_log!(Error: "this is an error"); 20 | /// 21 | /// udf_log!(Warning: "this is a warning"); 22 | /// 23 | /// udf_log!(Note: "this is info: value {}", 10 + 10); 24 | /// 25 | /// udf_log!(Debug: "this is a debug message"); 26 | /// 27 | /// udf_log!("i print without the '[Level] UDF:' formatting"); 28 | /// 29 | /// # } 30 | /// # #[cfg(not(miri))] 31 | /// # test(); 32 | /// ``` 33 | #[macro_export] 34 | macro_rules! udf_log { 35 | (Critical: $($msg:tt)*) => {{ 36 | let formatted = format!("[Critical] UDF: {}", format!($($msg)*)); 37 | udf_log!(formatted); 38 | }}; 39 | (Error: $($msg:tt)*) => {{ 40 | let formatted = format!("[Error] UDF: {}", format!($($msg)*)); 41 | udf_log!(formatted); 42 | }}; 43 | (Warning: $($msg:tt)*) => {{ 44 | let formatted = format!("[Warning] UDF: {}", format!($($msg)*)); 45 | udf_log!(formatted); 46 | }}; 47 | (Note: $($msg:tt)*) => {{ 48 | let formatted = format!("[Note] UDF: {}", format!($($msg)*)); 49 | udf_log!(formatted); 50 | }}; 51 | (Debug: $($msg:tt)*) => {{ 52 | let formatted = format!("[Debug] UDF: {}", format!($($msg)*)); 53 | udf_log!(formatted); 54 | }}; 55 | ($msg:tt) => { 56 | eprintln!( 57 | "{} {}", 58 | $crate::chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%:z"), 59 | $msg 60 | ); 61 | }; 62 | } 63 | 64 | /// Log a call to a function, with optional printing of state 65 | /// 66 | /// Log calls are only printed with feature "logging-debug". Call state is only 67 | /// printed with feature "logging-debug-calls". 68 | macro_rules! log_call { 69 | // Log for entering a function 70 | (enter: $name:literal, $type:ty, $($state:expr),*) => { 71 | log_call!(@common: "ENTER", "receive", $name, $type, $($state),*) 72 | }; 73 | 74 | // Logging for exiting a function 75 | (exit: $name:literal, $type:ty, $($state:expr),*) => { 76 | log_call!(@common: "EXIT", "return", $name, $type, $($state),*) 77 | }; 78 | 79 | // Internal macro, common enter/exit printing 80 | (@common: 81 | $enter_or_exit:literal, 82 | $receive_or_return:literal, 83 | $fn_name:literal, 84 | $type:ty, 85 | $($state:expr),* 86 | ) => {{ 87 | #[cfg(feature = "logging-debug")] 88 | $crate::udf_log!( 89 | Debug: "{} {} for '{}'", 90 | $enter_or_exit, $fn_name, std::any::type_name::<$type>() 91 | ); 92 | 93 | cfg_if::cfg_if! { 94 | if #[cfg(feature = "logging-debug-calls")] { 95 | $crate::udf_log!(Debug: "data {} state at {}", $receive_or_return, $fn_name); 96 | // For each specified item, print the expression and its value 97 | $( 98 | $crate::udf_log!( 99 | Debug: "[{}]: {} = {:#?}", 100 | $receive_or_return, std::stringify!($state), $state 101 | ); 102 | )* 103 | } 104 | } 105 | }} 106 | } 107 | -------------------------------------------------------------------------------- /udf/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Module that can be imported with `use udf::prelude::*;` to quickly get the 2 | //! most often used imports. 3 | 4 | pub use std::num::NonZeroU8; 5 | 6 | pub use crate::{ 7 | register, udf_log, AggregateUdf, ArgList, BasicUdf, Init, Process, ProcessError, SqlArg, 8 | SqlResult, SqlType, UdfCfg, 9 | }; 10 | -------------------------------------------------------------------------------- /udf/src/traits.rs: -------------------------------------------------------------------------------- 1 | //! Module containing traits to be implemented by a user 2 | //! 3 | //! A basic UDF just needs to implement [`BasicUdf`]. An aggregate UDF needs to 4 | //! implement both [`BasicUdf`] and [`AggregateUdf`]. 5 | 6 | use core::fmt::Debug; 7 | use std::num::NonZeroU8; 8 | 9 | use crate::types::{ArgList, UdfCfg}; 10 | use crate::ProcessError; 11 | 12 | /// This trait specifies the functions needed for a standard (non-aggregate) UDF 13 | /// 14 | /// Implement this on any struct in order to create a UDF. That struct can 15 | /// either be empty (usually the case for simple functions), or contain data 16 | /// that will be shared among all the UDF functions. 17 | /// 18 | /// If the UDF is basic (non-aggregate), the process is: 19 | /// 20 | /// - Caller (SQL server) calls `init()` with basic argument information 21 | /// - `init()` function (defined here) validates the arguments, does 22 | /// configuration (if needed), and configures and returns the `Self` struct 23 | /// - For each row, the caller calls `process(...)` with the relevant arguments 24 | /// - `process()` function (defined here) accepts an instance of `self` (created 25 | /// during init) and updates it as needed, and produces a result for that row 26 | /// 27 | /// The UDF specification also calls out a `deinit()` function to deallocate any 28 | /// memory, but this is not needed here (handled by this wrapper). 29 | pub trait BasicUdf: Sized { 30 | /// This type represents the return type of the UDF function. 31 | /// 32 | /// There are a lot of options, with some rules to follow. Warning! tedious 33 | /// explanation below, just skip to the next section if you don't need the 34 | /// details. 35 | /// 36 | /// - `f64` (real), `i64` (integer), and `[u8]` (string/blob) are the three 37 | /// fundamental types 38 | /// - Any `Return` can be an `Option` if the result is 39 | /// potentially nullable 40 | /// - There is no meaningful difference between `String`, `Vec`, `str`, 41 | /// and `[u8]` - return whichever is most convenient (following the below 42 | /// rules). Any of these types are acceptable for returning `string` or 43 | /// `decimal` types. 44 | /// - Out of these buffer options, prefer returning `&'static str` or 45 | /// `&'static [u8]` where possible. These are usable when only returning 46 | /// const/static values. 47 | /// - "Owned allocated" types (`String`, `Vec`) are the next preference 48 | /// for buffer types, and can be used whenever 49 | /// - If you have an owned type that updates itself, you can store the 50 | /// relevant `String` or `Vec` in your struct and return a `&'a str` 51 | /// or `&'a [u8]` that references them. This is useful for something like 52 | /// a `concat` function that updates its result string with each call 53 | /// (GATs allow this to work). 54 | /// 55 | /// Choosing a type may seem tricky at first but anything that successfully 56 | /// compiles will likely work. The flow chart below helps clarify some of 57 | /// the decisions making: 58 | /// 59 | /// ```text 60 | /// Desired Use Option if the result may be null 61 | /// Return Type 62 | /// ┉┉┉┉┉┉┉┉┉┉┉┉┉ 63 | /// ╭─────────────╮ 64 | /// │ integer ├─> i64 / Option 65 | /// ╰─────────────╯ 66 | /// ╭─────────────╮ 67 | /// │ float ├─> f64 / Option 68 | /// ╰─────────────╯ 69 | /// ╭───────────╮ 70 | /// ╭─────────────╮ │ static ├─> &'static str / Option<&'static str> 71 | /// │ utf8 string ├─> │ │ 72 | /// ╰─────────────╯ │ │ ╭───────────────╮ 73 | /// │ dynamic ├─> │ independent ├─> String / Option 74 | /// ╰───────────╯ │ │ 75 | /// │ self-updating ├─> &'a str / Option<&'a str> 76 | /// ╰───────────────╯ 77 | /// ╭─────────────╮ ╭───────────╮ 78 | /// │ non utf8 │ │ static ├─> &'static [u8] / Option<&'static [u8]> 79 | /// │ string/blob ├─> │ │ 80 | /// ╰─────────────╯ │ │ ╭───────────────╮ 81 | /// │ dynamic ├─> │ independent ├─> Vec / Option> 82 | /// ╰───────────╯ │ │ 83 | /// │ self-updating ├─> &'a [u8] / Option<&'a [u8]> 84 | /// ╰───────────────╯ 85 | /// ``` 86 | type Returns<'a> 87 | where 88 | Self: 'a; 89 | 90 | /// This is the initialization function 91 | /// 92 | /// It is expected that this function do the following: 93 | /// 94 | /// - Check that arguments are the proper type 95 | /// - Check whether the arguments are const and have a usable value (can 96 | /// provide some optimizations) 97 | /// 98 | /// # Errors 99 | /// 100 | /// If your function is not able to work with the given arguments, return a 101 | /// helpful error message explaining why. Max error size is 102 | /// `MYSQL_ERRMSG_SIZE` (512) bits, and will be truncated if any longer. 103 | /// 104 | /// `MySql` recommends keeping these error messages under 80 characters to 105 | /// fit in a terminal, but personal I'd prefer a helpful message over 106 | /// something useless that fits in one line. 107 | /// 108 | /// Error handling options are limited in all other functions, so make sure 109 | /// you check thoroughly for any possible errors that may arise, to the best 110 | /// of your ability. These may include: 111 | /// 112 | /// - Incorrect argument quantity or position 113 | /// - Incorrect argument types 114 | /// - Values that are `maybe_null()` when you cannot accept them 115 | fn init(cfg: &UdfCfg, args: &ArgList) -> Result; 116 | 117 | /// Process the actual values and return a result 118 | /// 119 | /// If you are unfamiliar with Rust, don't worry too much about the `'a` you 120 | /// see thrown around a lot. They are lifetime annotations and more or less 121 | /// say, "`self` lives at least as long as my return type does so I can 122 | /// return a reference to it, but `args` may not last as long so I cannot 123 | /// return a reference to that". 124 | /// 125 | /// # Arguments 126 | /// 127 | /// - `args`: Iterable list of arguments of the `Process` type 128 | /// - `error`: This is only applicable when using aggregate functions and 129 | /// can otherwise be ignored. If using aggregate functions, this provides 130 | /// the current error value as described in [`AggregateUdf::add()`]. 131 | /// 132 | /// # Return Value 133 | /// 134 | /// Assuming success, this function must return something of type 135 | /// `Self::Returns`. This will be the value for the row (standard functions) 136 | /// or for the entire group (aggregate functions). 137 | /// 138 | /// # Errors 139 | /// 140 | /// If there is some sort of unrecoverable problem at this point, just 141 | /// return a [`ProcessError`]. This will make the SQL server return `NULL`. 142 | /// As mentioned, there really aren't any good error handling options at 143 | /// this point other than that, so try to catch all possible errors in 144 | /// [`BasicUdf::init`]. 145 | /// 146 | /// [`ProcessError`] is just an empty type. 147 | fn process<'a>( 148 | &'a mut self, 149 | cfg: &UdfCfg, 150 | args: &ArgList, 151 | error: Option, 152 | ) -> Result, ProcessError>; 153 | } 154 | 155 | /// This trait must be implemented if this function performs aggregation. 156 | /// 157 | /// The basics of aggregation are simple: 158 | /// 159 | /// - `init` is called once per result set (same as non-aggregate) 160 | /// - `clear` is called once per group within the result set, and should reset 161 | /// your struct 162 | /// - `add` is called once per row in the group, and should add the current row 163 | /// to the struct as needed 164 | /// - `process` is called at the end of each group, and should produce the 165 | /// result value for that group 166 | /// 167 | /// # Aggregate Error Handling 168 | /// 169 | /// Error handling for aggregate functions is weird, and does not lend itself to 170 | /// easy understandability. The following is my best understanding of the 171 | /// process: 172 | /// 173 | /// - Any aggregate function may set a nonzero error (Represented here in return 174 | /// value by `Err(NonZeroU8)`). The value is not important, can be something 175 | /// internal 176 | /// - These errors do not stop the remaining `add()`/`remove()` functions from 177 | /// being called, but these functions do receive the error (and so may choose 178 | /// to do nothing if there is an error set) 179 | /// - Errors are not reset on `clear()`; you must do this manually (Hence 180 | /// `error` being mutable in this function signature) 181 | /// 182 | /// In order to enforce some of these constraints, we use `NonZeroU8` to 183 | /// represent error types (which has the nice side effect of being optimizable). 184 | /// Unfortunately, it is somewhat cumbersome to use, e.g.: `return 185 | /// Err(NonZeroU8::new(1).unwrap());` 186 | pub trait AggregateUdf: BasicUdf { 187 | /// Clear is run once at the beginning of each aggregate group and should 188 | /// reset everything needed in the struct. 189 | /// 190 | /// # Errors 191 | /// 192 | /// The `error` arg provides the error value from the previous group, and 193 | /// this function may choose to reset it (that is probably a good idea to 194 | /// do). `error` will be `None` if there is currently no error. 195 | /// 196 | /// To clear the error, simply return `Ok(())`. 197 | /// 198 | /// Return an error if something goes wrong within this function, or if you 199 | /// would like to propegate the previous error. 200 | fn clear(&mut self, cfg: &UdfCfg, error: Option) -> Result<(), NonZeroU8>; 201 | 202 | /// Add an item to the aggregate 203 | /// 204 | /// Usually this is implemented by adding something to an intemdiate value 205 | /// inside the core struct type. 206 | /// 207 | /// # Errors 208 | /// 209 | /// Hit a problem? Return an integer, which may or may not be meaningful to 210 | /// you. This can be done with `return Err(NonZeroU8::new(1).unwrap());`. 211 | /// 212 | /// The `error` argument tells you if there has been an error at some point, 213 | /// and the return value also detemines whether to propegate/modify the 214 | /// error (probably what you want) or clear it (I can't think of any good 215 | /// reason to do this in `add()`). If you would like to propegate the error 216 | /// without action, just add the following as the first line of the 217 | /// function: 218 | /// 219 | /// ``` 220 | /// # use std::num::NonZeroU8; 221 | /// # fn tmp(error: Option) -> Result<(), NonZeroU8> { 222 | /// error.map_or(Ok(()), Err)?; 223 | /// # Ok(()) 224 | /// # } 225 | /// ``` 226 | /// 227 | /// If you do this, 228 | fn add( 229 | &mut self, 230 | cfg: &UdfCfg, 231 | args: &ArgList, 232 | error: Option, 233 | ) -> Result<(), NonZeroU8>; 234 | 235 | /// Remove only applies to `MariaDB`, for use with window functions; i.e., 236 | /// `remove` will be called on a row that should be removed from the current 237 | /// set (has moved out of the window). 238 | /// 239 | /// This is optional; a default is supplied so no action is needed. If you 240 | /// would like to use `remove`, just reimplement it. 241 | /// 242 | /// 243 | /// 244 | /// # Errors 245 | /// 246 | /// Errors are handled the same as with [`AggregateUdf::add()`], see the 247 | /// description there 248 | #[inline] 249 | #[allow(unused_variables)] // Allow without an underscore for cleaner docs 250 | fn remove( 251 | &mut self, 252 | cfg: &UdfCfg, 253 | args: &ArgList, 254 | error: Option, 255 | ) -> Result<(), NonZeroU8> { 256 | unimplemented!() 257 | } 258 | } 259 | 260 | /// A state of the UDF, representing either [`Init`] or [`Process`] 261 | /// 262 | /// This is a zero-sized type used to control what operations are allowed at 263 | /// different times. 264 | pub trait UdfState: Debug + PartialEq {} 265 | 266 | /// Typestate marker for the initialization phase 267 | /// 268 | /// This is a zero-sized type. It just allows for specific methods to be 269 | /// implemented only on types that were created during the `init` function. 270 | #[derive(Debug, PartialEq, Eq)] 271 | pub struct Init; 272 | 273 | /// Typestate marker for the processing phase 274 | /// 275 | /// This is a zero-sized type, indicating that a type was created in the 276 | /// `process` function. 277 | #[derive(Debug, PartialEq, Eq)] 278 | pub struct Process; 279 | 280 | impl UdfState for Init {} 281 | impl UdfState for Process {} 282 | -------------------------------------------------------------------------------- /udf/src/types.rs: -------------------------------------------------------------------------------- 1 | //! Types that represent SQL interfaces 2 | 3 | use std::fmt; 4 | 5 | mod arg; 6 | mod arg_list; 7 | mod config; 8 | mod sql_types; 9 | 10 | // Document everything inline 11 | #[doc(inline)] 12 | pub use arg::*; 13 | #[doc(inline)] 14 | pub use arg_list::*; 15 | #[doc(inline)] 16 | pub use config::*; 17 | #[doc(inline)] 18 | pub use sql_types::*; 19 | 20 | /// Max error message size, 0x200 = 512 bytes 21 | pub const MYSQL_ERRMSG_SIZE: usize = 0x200; 22 | 23 | /// Minimum size of a buffer for string results 24 | pub const MYSQL_RESULT_BUFFER_SIZE: usize = 255; 25 | 26 | /// A zero-sized struct indicating that something went wrong 27 | /// 28 | /// If you return an instance of this, it is likely a good idea to log to stderr 29 | /// what went wrong. 30 | #[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] 31 | pub struct ProcessError; 32 | 33 | impl fmt::Display for ProcessError { 34 | #[inline] 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | write!(f, "udf processing error") 37 | } 38 | } 39 | 40 | impl std::error::Error for ProcessError {} 41 | -------------------------------------------------------------------------------- /udf/src/types/arg.rs: -------------------------------------------------------------------------------- 1 | //! Rust representation of SQL arguments 2 | 3 | use core::fmt::Debug; 4 | use std::marker::PhantomData; 5 | use std::{slice, str}; 6 | 7 | use coerce::{get_coercion, get_current_type, get_desired_or_current, set_coercion}; 8 | use udf_sys::Item_result; 9 | 10 | use crate::types::{SqlResult, SqlType}; 11 | use crate::wrapper::UDF_ARGSx; 12 | use crate::{ArgList, Init, UdfState}; 13 | 14 | /// A single SQL argument, including its attributes 15 | /// 16 | /// This struct contains the argument itself. It uses a typestate pattern (`S`) 17 | /// to have slightly different functionality when used during initialization and 18 | /// during processing. 19 | #[derive(Debug)] 20 | #[allow(clippy::module_name_repetitions)] 21 | pub struct SqlArg<'a, S: UdfState> { 22 | pub(super) base: &'a ArgList<'a, S>, 23 | pub(super) index: usize, 24 | pub(super) marker: PhantomData, 25 | } 26 | 27 | impl<'a, T: UdfState> SqlArg<'a, T> { 28 | /// The actual argument type and value 29 | #[inline] 30 | #[allow(clippy::missing_panics_doc)] 31 | pub fn value(&self) -> SqlResult<'a> { 32 | // SAFETY: Initializing API guarantees the inner struct to be valid 33 | unsafe { 34 | let base = self.get_base(); 35 | let arg_buf_ptr: *const u8 = (*base.args.add(self.index)).cast(); 36 | let arg_type = *base.arg_types.add(self.index); 37 | let arg_len = *base.lengths.add(self.index); 38 | 39 | // We can unwrap because the tag will be valid 40 | SqlResult::from_ptr(arg_buf_ptr, arg_type.try_into().unwrap(), arg_len as usize) 41 | .unwrap() 42 | } 43 | } 44 | 45 | /// A string representation of this argument's identifier 46 | #[inline] 47 | #[allow(clippy::missing_panics_doc)] 48 | pub fn attribute(&'a self) -> &'a str { 49 | let attr_slice; 50 | unsafe { 51 | let base = self.get_base(); 52 | let attr_buf_ptr: *const u8 = *base.attributes.add(self.index).cast(); 53 | let attr_len = *base.attribute_lengths.add(self.index) as usize; 54 | attr_slice = slice::from_raw_parts(attr_buf_ptr, attr_len); 55 | } 56 | // Ok to unwrap here, attributes must be utf8 57 | str::from_utf8(attr_slice) 58 | .map_err(|e| format!("unexpected: attribute is not valid utf8. Error: {e:?}")) 59 | .unwrap() 60 | } 61 | 62 | /// Simple helper method to get the internal base 63 | unsafe fn get_base(&'a self) -> &'a UDF_ARGSx { 64 | &(*self.base.0.get()) 65 | } 66 | 67 | /// Helper method to get a pointer to this item's arg type 68 | unsafe fn arg_type_ptr(&self) -> *mut i32 { 69 | self.get_base().arg_types.add(self.index) 70 | } 71 | } 72 | 73 | /// This includes functions that are only applicable during initialization 74 | impl<'a> SqlArg<'a, Init> { 75 | /// Determine whether an argument **may** be constant 76 | /// 77 | /// During initialization, a value is const if it is not `None`. This 78 | /// provides a simple test to see if this is true. 79 | /// 80 | /// There is no way to differentiate between "not const" and "const but 81 | /// NULL" when we are in the `Process` step. 82 | #[inline] 83 | pub fn is_const(&self) -> bool { 84 | match self.value() { 85 | SqlResult::String(v) => v.is_some(), 86 | SqlResult::Decimal(v) => v.is_some(), 87 | SqlResult::Real(v) => v.is_some(), 88 | SqlResult::Int(v) => v.is_some(), 89 | } 90 | } 91 | 92 | /// Whether or not this argument may be `NULL` 93 | #[inline] 94 | pub fn maybe_null(&self) -> bool { 95 | unsafe { *self.get_base().maybe_null.add(self.index) != 0 } 96 | } 97 | 98 | /// Instruct the SQL application to coerce the argument's type. This does 99 | /// not change the underlying value visible in `.value`. 100 | #[inline] 101 | #[allow(clippy::missing_panics_doc)] // We will have a valid type 102 | pub fn set_type_coercion(&mut self, newtype: SqlType) { 103 | // We use some tricks here to store both the current type and the 104 | // desired coercion in `*arg_ptr`. See the `coerce` module for more 105 | // info. 106 | unsafe { 107 | // SAFETY: caller guarantees validity of memory location 108 | let arg_ptr = self.arg_type_ptr(); 109 | 110 | // SAFETY: our tests validate size & align line up, so a C enum will 111 | // be the same layout as a C `int` 112 | *arg_ptr = set_coercion(*arg_ptr, newtype as i32); 113 | } 114 | } 115 | 116 | /// Retrieve the current type coercision 117 | #[inline] 118 | #[allow(clippy::missing_panics_doc)] // We will have a valid type 119 | pub fn get_type_coercion(&self) -> SqlType { 120 | // SAFETY: Caller guarantees 121 | unsafe { 122 | let arg_type = *self.arg_type_ptr(); 123 | let coerced_type = get_coercion(arg_type).unwrap_or_else(|| get_current_type(arg_type)); 124 | SqlType::try_from(coerced_type as i8).expect("critical: invalid sql type") 125 | } 126 | } 127 | 128 | /// Assign the currently desired coercion 129 | #[inline] 130 | pub(crate) fn flush_coercion(&mut self) { 131 | // SAFETY: we validate that we are setting a valid value 132 | unsafe { 133 | let to_set = get_desired_or_current(*self.arg_type_ptr()); 134 | let _ = Item_result::try_from(to_set).unwrap(); 135 | *self.arg_type_ptr() = to_set; 136 | } 137 | } 138 | } 139 | 140 | mod coerce { 141 | //! Represent a current type and a future type within a single `.arg_type` value 142 | //! 143 | //! The purpose here is to avoid UB when we set a type coercion then try to 144 | //! recreate a the value-containing enum. This was only a change when we moved to 145 | //! the index-based representation. 146 | //! 147 | //! Representation: First byte: mask indicating if coercion is set Second byte: 148 | //! unused Third byte: Desired coercion Final byte: Current type 149 | 150 | const COERCION_SET: i32 = 0b1010_1010 << (3 * 8); 151 | const COERCION_SET_MASK: i32 = 0b1111_1111 << (3 * 8); 152 | const DESIRED_MASK: i32 = 0b1111_1111 << 8; 153 | const BYTE_MASK: i32 = 0b1111_1111; 154 | // Undo both the set mask and the desired mask 155 | const RESET_COERCION_DESIRED_MASK: i32 = !(COERCION_SET_MASK | DESIRED_MASK); 156 | 157 | /// Check if coercion is set 158 | fn coercion_is_set(value: i32) -> bool { 159 | value & COERCION_SET_MASK == COERCION_SET 160 | } 161 | 162 | /// Set coercion to a desired value 163 | pub fn set_coercion(current: i32, desired: i32) -> i32 { 164 | RESET_COERCION_DESIRED_MASK & current | COERCION_SET | ((desired & BYTE_MASK) << 8) 165 | } 166 | 167 | /// Get the desired coercion, ignoring currently active type 168 | #[allow(clippy::cast_lossless)] 169 | pub fn get_coercion(value: i32) -> Option { 170 | if coercion_is_set(value) { 171 | Some(((value & DESIRED_MASK) >> 8) as i8 as i32) 172 | } else { 173 | None 174 | } 175 | } 176 | 177 | /// Get the currently active type, ignoring coercion 178 | #[allow(clippy::cast_lossless)] 179 | pub fn get_current_type(value: i32) -> i32 { 180 | // We use these casts to easily sign extend 181 | (value & BYTE_MASK) as i8 as i32 182 | } 183 | 184 | /// Get the desiered coercion if set, otherwise the current type 185 | pub fn get_desired_or_current(value: i32) -> i32 { 186 | get_coercion(value).unwrap_or_else(|| get_current_type(value)) 187 | } 188 | 189 | #[cfg(test)] 190 | mod tests { 191 | use super::*; 192 | 193 | const TESTVALS: [i32; 8] = [-10, -5, -1, 0, 1, 5, 10, 20]; 194 | 195 | #[test] 196 | fn test_unset_coercion() { 197 | for val in TESTVALS.iter().copied() { 198 | assert!(!coercion_is_set(val)); 199 | assert_eq!(get_coercion(val), None); 200 | assert_eq!(get_current_type(val), val); 201 | assert_eq!(get_desired_or_current(val), val); 202 | } 203 | } 204 | 205 | #[test] 206 | fn test_coercion() { 207 | for current in TESTVALS.iter().copied() { 208 | for desired in TESTVALS.iter().copied() { 209 | let res = set_coercion(current, desired); 210 | 211 | assert!(coercion_is_set(res)); 212 | assert_eq!(get_coercion(res), Some(desired)); 213 | assert_eq!(get_current_type(res), current); 214 | assert_eq!(get_desired_or_current(res), desired); 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | use std::mem; 224 | 225 | use super::*; 226 | 227 | // Ensure our fake transmutes are sound 228 | #[test] 229 | fn verify_item_result_layout() { 230 | assert_eq!(mem::size_of::(), mem::size_of::()); 231 | assert_eq!(mem::align_of::(), mem::align_of::()); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /udf/src/types/arg_list.rs: -------------------------------------------------------------------------------- 1 | //! Define a list of arguments to a SQL function 2 | 3 | #![allow(dead_code)] 4 | 5 | use std::cell::UnsafeCell; 6 | use std::fmt; 7 | use std::fmt::Debug; 8 | use std::marker::PhantomData; 9 | 10 | use udf_sys::UDF_ARGS; 11 | 12 | use crate::wrapper::UDF_ARGSx; 13 | use crate::{Init, SqlArg, UdfState}; 14 | 15 | /// A collection of SQL arguments 16 | /// 17 | /// This is rusty wrapper around SQL's `UDF_ARGS` struct, providing methods to 18 | /// easily work with arguments. 19 | #[repr(transparent)] 20 | pub struct ArgList<'a, S: UdfState>( 21 | /// `UnsafeCell` indicates to the compiler that this struct may have interior 22 | /// mutability (i.e., cannot make some optimizations) 23 | pub(super) UnsafeCell, 24 | /// We use this zero-sized marker to hold our state 25 | PhantomData<&'a S>, 26 | ); 27 | 28 | /// Derived formatting is a bit ugly, so we clean it up by using the `Vec` 29 | /// format. 30 | impl<'a, S: UdfState> Debug for ArgList<'a, S> { 31 | #[inline] 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | f.debug_struct("ArgList") 34 | .field("items", &self.as_vec()) 35 | .finish() 36 | } 37 | } 38 | 39 | impl<'a, S: UdfState> ArgList<'a, S> { 40 | /// Create an `ArgList` type on a `UDF_ARGS` struct by casting 41 | pub(crate) unsafe fn from_raw_ptr<'p>(ptr: *mut UDF_ARGS) -> &'p Self { 42 | &*ptr.cast() 43 | } 44 | 45 | /// Create a vector of arguments for easy use 46 | #[inline] 47 | pub fn as_vec(&'a self) -> Vec> { 48 | self.iter().collect() 49 | } 50 | 51 | /// Construct an iterator over arguments 52 | #[inline] 53 | pub fn iter(&'a self) -> Iter<'a, S> { 54 | self.into_iter() 55 | } 56 | 57 | /// Return `true` if there are no arguments 58 | #[inline] 59 | pub fn is_empty(&self) -> bool { 60 | self.len() == 0 61 | } 62 | 63 | /// Get the number of arguments 64 | #[inline] 65 | pub fn len(&self) -> usize { 66 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 67 | unsafe { (*self.0.get()).arg_count as usize } 68 | } 69 | 70 | /// Safely get an argument at a given index. It it is not available, `None` 71 | /// will be returned. 72 | #[inline] 73 | #[allow(clippy::missing_panics_doc)] // Attributes are identifiers in SQL and are always UTF8 74 | pub fn get(&'a self, index: usize) -> Option> { 75 | let base = unsafe { &*self.0.get() }; 76 | 77 | if index >= base.arg_count as usize { 78 | return None; 79 | } 80 | Some(SqlArg { 81 | base: self, 82 | index, 83 | marker: PhantomData, 84 | }) 85 | } 86 | } 87 | 88 | impl<'a> ArgList<'a, Init> { 89 | /// Apply the pending coercion for all arguments. Meant to be run just 90 | /// before exiting the `init` function within proc macro calls. 91 | #[inline] 92 | #[doc(hidden)] 93 | pub fn flush_all_coercions(&self) { 94 | self.iter().for_each(|mut a| a.flush_coercion()); 95 | } 96 | } 97 | 98 | /// Trait for being able to iterate arguments 99 | impl<'a, S: UdfState> IntoIterator for &'a ArgList<'a, S> { 100 | type Item = SqlArg<'a, S>; 101 | 102 | type IntoIter = Iter<'a, S>; 103 | 104 | /// Construct a new 105 | #[inline] 106 | fn into_iter(self) -> Self::IntoIter { 107 | Iter::new(self) 108 | } 109 | } 110 | 111 | /// Iterator over arguments in a [`ArgList`] 112 | /// 113 | /// This struct is produced by invoking `into_iter()` on a [`ArgList`] 114 | #[derive(Debug)] 115 | pub struct Iter<'a, S: UdfState> { 116 | base: &'a ArgList<'a, S>, 117 | n: usize, 118 | } 119 | 120 | impl<'a, S: UdfState> Iter<'a, S> { 121 | fn new(base: &'a ArgList<'a, S>) -> Self { 122 | Self { base, n: 0 } 123 | } 124 | } 125 | 126 | impl<'a, S: UdfState> Iterator for Iter<'a, S> { 127 | type Item = SqlArg<'a, S>; 128 | 129 | /// Get the next argument 130 | #[inline] 131 | fn next(&mut self) -> Option { 132 | // Increment counter, check if we are out of bounds 133 | if self.n >= self.base.len() { 134 | return None; 135 | } 136 | 137 | let ret = self.base.get(self.n); 138 | self.n += 1; 139 | 140 | ret 141 | } 142 | 143 | /// We know exactly how many items we have remaining, so can implement this 144 | /// (which allows some optimizations). 145 | /// 146 | /// See [`std::iter::Iterator::size_hint`] for this method's use. 147 | #[inline] 148 | fn size_hint(&self) -> (usize, Option) { 149 | let remaining = self.base.len() - self.n; 150 | (remaining, Some(remaining)) 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use std::mem::{align_of, size_of}; 157 | 158 | use super::*; 159 | use crate::prelude::*; 160 | 161 | // Verify no size issues 162 | #[test] 163 | fn args_size_init() { 164 | assert_eq!( 165 | size_of::(), 166 | size_of::>(), 167 | concat!("Size of: ", stringify!(UDF_ARGS)) 168 | ); 169 | assert_eq!( 170 | align_of::(), 171 | align_of::>(), 172 | concat!("Alignment of ", stringify!(UDF_ARGS)) 173 | ); 174 | } 175 | 176 | // Verify no size issues 177 | #[test] 178 | fn args_size_process() { 179 | assert_eq!( 180 | size_of::(), 181 | size_of::>(), 182 | concat!("Size of: ", stringify!(UDF_ARGS)) 183 | ); 184 | assert_eq!( 185 | align_of::(), 186 | align_of::>(), 187 | concat!("Alignment of ", stringify!(UDF_ARGS)) 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /udf/src/types/config.rs: -------------------------------------------------------------------------------- 1 | //! Rust representation of `UDF_INIT` 2 | 3 | #![allow(clippy::useless_conversion, clippy::unnecessary_cast)] 4 | 5 | use std::cell::UnsafeCell; 6 | use std::ffi::c_ulong; 7 | use std::fmt::Debug; 8 | use std::marker::PhantomData; 9 | #[cfg(feature = "logging-debug")] 10 | use std::{any::type_name, mem::size_of}; 11 | 12 | use udf_sys::UDF_INIT; 13 | 14 | #[cfg(feature = "logging-debug")] 15 | use crate::udf_log; 16 | use crate::{Init, UdfState}; 17 | 18 | /// Helpful constants related to the `max_length` parameter 19 | /// 20 | /// These can be helpful when calling [`UdfCfg::set_max_len()`] 21 | #[repr(u32)] 22 | #[non_exhaustive] 23 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 24 | pub enum MaxLenOptions { 25 | /// The default max length for integers is 21 26 | IntDefault = 21, 27 | 28 | /// The default max length of a real value is 13 plus the result of 29 | /// [`UdfCfg::get_decimals()`] 30 | RealBase = 13, 31 | 32 | /// A `blob` can be up to 65 KiB. 33 | Blob = 1 << 16, 34 | 35 | /// A `mediumblob` can be up to 16 MiB. 36 | MediumBlob = 1 << 24, 37 | } 38 | 39 | /// A collection of SQL arguments 40 | /// 41 | /// This is rusty wrapper around SQL's `UDF_INIT` struct, providing methods to 42 | /// easily and safely work with arguments. 43 | #[repr(transparent)] 44 | pub struct UdfCfg(pub(crate) UnsafeCell, PhantomData); 45 | 46 | impl UdfCfg { 47 | /// Create an `ArgList` type on a `UDF_ARGS` struct 48 | /// 49 | /// # Safety 50 | /// 51 | /// The caller must guarantee that `ptr` is valid and remains valid for the 52 | /// lifetime of the returned value 53 | #[inline] 54 | pub(crate) unsafe fn from_raw_ptr<'p>(ptr: *const UDF_INIT) -> &'p Self { 55 | &*ptr.cast() 56 | } 57 | 58 | /// Consume a box and store its pointer in this `UDF_INIT` 59 | /// 60 | /// This takes a boxed object, turns it into a pointer, and stores that 61 | /// pointer in this struct. After calling this function, [`retrieve_box`] 62 | /// _must_ be called to free the memory! 63 | pub(crate) fn store_box(&self, b: Box) { 64 | let box_ptr = Box::into_raw(b); 65 | 66 | // Note: if T is zero-sized, this will print `0x1` for the address 67 | #[cfg(feature = "logging-debug")] 68 | udf_log!( 69 | Debug: "{box_ptr:p} {} bytes udf->server control transfer ({})", 70 | size_of::(),type_name::() 71 | ); 72 | 73 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 74 | // here 75 | unsafe { (*self.0.get()).ptr = box_ptr.cast() }; 76 | } 77 | 78 | /// Given this struct's `ptr` field is a boxed object, turn that pointer 79 | /// back into a box 80 | /// 81 | /// # Safety 82 | /// 83 | /// T _must_ be the type of this struct's pointer, likely created with 84 | /// [`store_box`] 85 | pub(crate) unsafe fn retrieve_box(&self) -> Box { 86 | let box_ptr = (*self.0.get()).ptr.cast::(); 87 | 88 | #[cfg(feature = "logging-debug")] 89 | udf_log!( 90 | Debug: "{box_ptr:p} {} bytes server->udf control transfer ({})", 91 | size_of::(),type_name::() 92 | ); 93 | 94 | Box::from_raw(box_ptr) 95 | } 96 | 97 | /// Retrieve the setting for whether this UDF may return `null` 98 | /// 99 | /// This defaults to true if any argument is nullable, false otherwise 100 | #[inline] 101 | pub fn get_maybe_null(&self) -> bool { 102 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 103 | unsafe { (*self.0.get()).maybe_null } 104 | } 105 | 106 | /// Retrieve the setting for number of decimal places 107 | /// 108 | /// This defaults to the longest number of digits of any argument, or 31 if 109 | /// there is no fixed number 110 | #[inline] 111 | pub fn get_decimals(&self) -> u32 { 112 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 113 | unsafe { (*self.0.get()).decimals as u32 } 114 | } 115 | 116 | /// Set the number of decimals this function returns 117 | /// 118 | /// This can be changed at any point in the UDF (init or process) 119 | #[inline] 120 | pub fn set_decimals(&self, v: u32) { 121 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 122 | unsafe { (*self.0.get()).decimals = v.into() } 123 | } 124 | 125 | /// Retrieve the current maximum length setting for this in-progress UDF 126 | #[inline] 127 | pub fn get_max_len(&self) -> u64 { 128 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 129 | unsafe { (*self.0.get()).max_length as u64 } 130 | } 131 | 132 | /// Get the current `const_item` value 133 | #[inline] 134 | pub fn get_is_const(&self) -> bool { 135 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 136 | unsafe { (*self.0.get()).const_item } 137 | } 138 | } 139 | 140 | /// Implementations of actions on a `UdfCfg` that are only possible during 141 | /// initialization 142 | impl UdfCfg { 143 | /// Set whether or not this function may return null 144 | #[inline] 145 | pub fn set_maybe_null(&self, v: bool) { 146 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 147 | unsafe { (*self.0.get()).maybe_null = v }; 148 | } 149 | 150 | /// Set the maximum possible length of this UDF's result 151 | /// 152 | /// This is mostly relevant for String and Decimal return types. See 153 | /// [`MaxLenOptions`] for possible defaults, including `BLOB` sizes. 154 | #[inline] 155 | pub fn set_max_len(&self, v: u64) { 156 | // Need to try_into because ulong is 64 bits in GNU but 32 bits MSVC 157 | let set: c_ulong = v.try_into().unwrap_or(c_ulong::MAX); 158 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 159 | unsafe { (*self.0.get()).max_length = set }; 160 | } 161 | 162 | /// Set a new `const_item` value 163 | /// 164 | /// Set this to true if your function always returns the same values with 165 | /// the same arguments 166 | #[inline] 167 | pub fn set_is_const(&self, v: bool) { 168 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 169 | unsafe { (*self.0.get()).const_item = v }; 170 | } 171 | } 172 | 173 | impl Debug for UdfCfg { 174 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 175 | // SAFETY: unsafe when called from different threads, but we are `!Sync` 176 | // here 177 | let base = unsafe { &*self.0.get() }; 178 | f.debug_struct("UdfCfg") 179 | .field("maybe_null", &base.maybe_null) 180 | .field("decimals", &base.decimals) 181 | .field("max_len", &base.max_length) 182 | .field("is_const", &base.const_item) 183 | .field("ptr", &base.ptr) 184 | .finish() 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use std::collections::HashMap; 191 | use std::mem::{align_of, size_of}; 192 | 193 | use super::*; 194 | use crate::mock::MockUdfCfg; 195 | use crate::{Init, Process}; 196 | 197 | // Verify no size issues 198 | #[test] 199 | fn cfg_init_size() { 200 | assert_eq!( 201 | size_of::(), 202 | size_of::>(), 203 | concat!("Size of: ", stringify!(UDF_INIT)) 204 | ); 205 | assert_eq!( 206 | align_of::(), 207 | align_of::>(), 208 | concat!("Alignment of ", stringify!(UDF_INIT)) 209 | ); 210 | } 211 | 212 | #[test] 213 | fn cfg_proc_size() { 214 | assert_eq!( 215 | size_of::(), 216 | size_of::>(), 217 | concat!("Size of: ", stringify!(UDF_INIT)) 218 | ); 219 | assert_eq!( 220 | align_of::(), 221 | align_of::>(), 222 | concat!("Alignment of ", stringify!(UDF_INIT)) 223 | ); 224 | } 225 | 226 | #[test] 227 | fn test_box_load_store() { 228 | // Verify store & retrieve on a box works 229 | #[derive(PartialEq, Debug, Clone)] 230 | struct X { 231 | s: String, 232 | map: HashMap, 233 | } 234 | 235 | let mut map = HashMap::new(); 236 | map.insert(930_984_098, 4_525_435_435.900_981); 237 | map.insert(12_341_234, -234.090_909_092); 238 | map.insert(-23_412_343_453, 838_383.6); 239 | 240 | let stored = X { 241 | s: "This is a string".to_owned(), 242 | map, 243 | }; 244 | 245 | let mut m = MockUdfCfg::new(); 246 | let cfg = m.as_init(); 247 | cfg.store_box(Box::new(stored.clone())); 248 | 249 | let loaded: X = unsafe { *cfg.retrieve_box() }; 250 | assert_eq!(stored, loaded); 251 | } 252 | 253 | #[test] 254 | fn maybe_null() { 255 | let mut m = MockUdfCfg::new(); 256 | 257 | *m.maybe_null() = false; 258 | assert!(!m.as_init().get_maybe_null()); 259 | *m.maybe_null() = true; 260 | assert!(m.as_init().get_maybe_null()); 261 | } 262 | 263 | #[test] 264 | fn decimals() { 265 | let mut m = MockUdfCfg::new(); 266 | 267 | *m.decimals() = 1234; 268 | assert_eq!(m.as_init().get_decimals(), 1234); 269 | *m.decimals() = 0; 270 | assert_eq!(m.as_init().get_decimals(), 0); 271 | *m.decimals() = 1; 272 | assert_eq!(m.as_init().get_decimals(), 1); 273 | 274 | m.as_init().set_decimals(4); 275 | assert_eq!(*m.decimals(), 4); 276 | } 277 | #[test] 278 | fn max_len() { 279 | let mut m = MockUdfCfg::new(); 280 | 281 | *m.max_len() = 1234; 282 | assert_eq!(m.as_init().get_max_len(), 1234); 283 | *m.max_len() = 0; 284 | assert_eq!(m.as_init().get_max_len(), 0); 285 | *m.max_len() = 1; 286 | assert_eq!(m.as_init().get_max_len(), 1); 287 | } 288 | #[test] 289 | fn test_const() { 290 | let mut m = MockUdfCfg::new(); 291 | 292 | *m.is_const() = false; 293 | assert!(!m.as_init().get_is_const()); 294 | *m.is_const() = true; 295 | assert!(m.as_init().get_is_const()); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /udf/src/types/sql_types.rs: -------------------------------------------------------------------------------- 1 | //! Module containing bindings & wrappers to SQL types 2 | 3 | use std::{slice, str}; 4 | 5 | use udf_sys::Item_result; 6 | 7 | /// Enum representing possible SQL result types 8 | /// 9 | /// This simply represents the possible types, but does not contain any values. 10 | /// [`SqlResult`] is the corresponding enum that actually contains data. 11 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 12 | #[non_exhaustive] 13 | #[repr(i8)] 14 | pub enum SqlType { 15 | /// Integer result 16 | Int = Item_result::INT_RESULT as i8, 17 | /// Real result 18 | Real = Item_result::REAL_RESULT as i8, 19 | /// String result 20 | String = Item_result::STRING_RESULT as i8, 21 | /// Decimal result 22 | Decimal = Item_result::DECIMAL_RESULT as i8, 23 | } 24 | 25 | impl SqlType { 26 | /// Convert this enum to a SQL [`Item_result`]. This is only useful if you 27 | /// work with [`udf_sys`] bindings directly. 28 | #[inline] 29 | pub fn to_item_result(&self) -> Item_result { 30 | match *self { 31 | Self::Int => Item_result::INT_RESULT, 32 | Self::Real => Item_result::REAL_RESULT, 33 | Self::String => Item_result::STRING_RESULT, 34 | Self::Decimal => Item_result::DECIMAL_RESULT, 35 | } 36 | } 37 | 38 | /// Small helper function to get a displayable type name. 39 | #[inline] 40 | pub fn display_name(&self) -> &'static str { 41 | match *self { 42 | Self::String => "string", 43 | Self::Real => "real", 44 | Self::Int => "int", 45 | Self::Decimal => "decimal", 46 | } 47 | } 48 | } 49 | 50 | // struct InternalSqLType(i8); 51 | 52 | // impl InternalSqLType { 53 | // fn current_type(&self) -> SqlType { 54 | 55 | // } 56 | 57 | // fn current_coercion(&self) -> SqlType { 58 | 59 | // } 60 | 61 | // fn 62 | // } 63 | 64 | impl TryFrom for SqlType { 65 | type Error = String; 66 | 67 | /// Create an [`SqlType`] from an integer 68 | #[inline] 69 | fn try_from(tag: i8) -> Result { 70 | let val = match tag { 71 | x if x == Self::String as i8 => Self::String, 72 | x if x == Self::Real as i8 => Self::Real, 73 | x if x == Self::Int as i8 => Self::Int, 74 | x if x == Self::Decimal as i8 => Self::Decimal, 75 | _ => return Err(format!("invalid arg type {tag} received")), 76 | }; 77 | 78 | Ok(val) 79 | } 80 | } 81 | 82 | impl TryFrom for SqlType { 83 | type Error = String; 84 | 85 | /// Create an [`SqlType`] from an [`Item_result`], located in the `bindings` 86 | /// module. 87 | #[inline] 88 | fn try_from(tag: Item_result) -> Result { 89 | let val = match tag { 90 | Item_result::STRING_RESULT => Self::String, 91 | Item_result::REAL_RESULT => Self::Real, 92 | Item_result::INT_RESULT => Self::Int, 93 | Item_result::DECIMAL_RESULT => Self::Decimal, 94 | _ => return Err(format!("invalid arg type {tag:?} received")), 95 | }; 96 | 97 | Ok(val) 98 | } 99 | } 100 | 101 | impl TryFrom<&SqlResult<'_>> for SqlType { 102 | type Error = String; 103 | 104 | /// Create an [`SqlType`] from an [`SqlResult`] 105 | #[inline] 106 | fn try_from(tag: &SqlResult) -> Result { 107 | let val = match *tag { 108 | SqlResult::String(_) => Self::String, 109 | SqlResult::Real(_) => Self::Real, 110 | SqlResult::Int(_) => Self::Int, 111 | SqlResult::Decimal(_) => Self::Decimal, 112 | }; 113 | 114 | Ok(val) 115 | } 116 | } 117 | 118 | /// A possible SQL result consisting of a type and nullable value 119 | /// 120 | /// This enum is similar to [`SqlType`], but actually contains the object. 121 | /// 122 | /// It is of note that both [`SqlResult::String`] contains a `u8` slice rather 123 | /// than a representation like `&str`. This is because there is no guarantee 124 | /// that the data is `utf8`. Use [`SqlResult::as_string()`] if you need an easy 125 | /// way to get a `&str`. 126 | /// 127 | /// This enum is labeled `non_exhaustive` to leave room for future types and 128 | /// coercion options. 129 | #[derive(Debug, PartialEq, Clone)] 130 | #[non_exhaustive] 131 | pub enum SqlResult<'a> { 132 | // INVALID_RESULT and ROW_RESULT are other options, but not valid for UDFs 133 | /// A string result 134 | String(Option<&'a [u8]>), 135 | /// A floating point result 136 | Real(Option), 137 | /// A nullable integer 138 | Int(Option), 139 | /// This is a string that is to be represented as a decimal 140 | Decimal(Option<&'a str>), 141 | } 142 | 143 | impl<'a> SqlResult<'a> { 144 | /// Construct a `SqlResult` from a pointer and a tag 145 | /// 146 | /// SAFETY: pointer must not be null. If a string or decimal result, must be 147 | /// exactly `len` long. 148 | pub(crate) unsafe fn from_ptr( 149 | ptr: *const u8, 150 | tag: Item_result, 151 | len: usize, 152 | ) -> Result { 153 | // Handle nullptr right away here 154 | 155 | let marker = 156 | SqlType::try_from(tag).map_err(|_| format!("invalid arg type {tag:?} received"))?; 157 | 158 | let arg = if ptr.is_null() { 159 | match marker { 160 | SqlType::Int => SqlResult::Int(None), 161 | SqlType::Real => SqlResult::Real(None), 162 | SqlType::String => SqlResult::String(None), 163 | SqlType::Decimal => SqlResult::Decimal(None), 164 | } 165 | } else { 166 | // SAFETY: `tag` guarantees type. If decimal or String, caller 167 | // guarantees length 168 | unsafe { 169 | #[allow(clippy::cast_ptr_alignment)] 170 | match marker { 171 | SqlType::Int => SqlResult::Int(Some(*(ptr.cast::()))), 172 | SqlType::Real => SqlResult::Real(Some(*(ptr.cast::()))), 173 | SqlType::String => SqlResult::String(Some(slice::from_raw_parts(ptr, len))), 174 | // SAFETY: decimals should always be UTF8 175 | SqlType::Decimal => SqlResult::Decimal(Some(str::from_utf8_unchecked( 176 | slice::from_raw_parts(ptr, len), 177 | ))), 178 | } 179 | } 180 | }; 181 | 182 | Ok(arg) 183 | } 184 | 185 | /// Small helper function to get a displayable type name. 186 | #[inline] 187 | pub fn display_name(&self) -> &'static str { 188 | SqlType::try_from(self).map_or("unknown", |v| v.display_name()) 189 | } 190 | 191 | /// Check if this argument is an integer type, even if it may be null 192 | #[inline] 193 | pub fn is_int(&self) -> bool { 194 | matches!(*self, Self::Int(_)) 195 | } 196 | /// Check if this argument is an real type, even if it may be null 197 | #[inline] 198 | pub fn is_real(&self) -> bool { 199 | matches!(*self, Self::Real(_)) 200 | } 201 | /// Check if this argument is an string type, even if it may be null 202 | #[inline] 203 | pub fn is_string(&self) -> bool { 204 | matches!(*self, Self::String(_)) 205 | } 206 | /// Check if this argument is an decimal type, even if it may be null 207 | #[inline] 208 | pub fn is_decimal(&self) -> bool { 209 | matches!(*self, Self::Decimal(_)) 210 | } 211 | 212 | /// Return this type as an integer if possible 213 | /// 214 | /// This will exist if the variant is [`SqlResult::Int`], and it contains a 215 | /// value. 216 | /// 217 | /// These `as_*` methods are helpful to quickly obtain a value when you 218 | /// expect it to be of a specific type and present. 219 | #[inline] 220 | pub fn as_int(&self) -> Option { 221 | match *self { 222 | Self::Int(v) => v, 223 | _ => None, 224 | } 225 | } 226 | 227 | /// Return this type as a float if possible 228 | /// 229 | /// This will exist if the variant is [`SqlResult::Real`], and it contains a 230 | /// value. See [`SqlResult::as_int()`] for further details on `as_*` methods 231 | #[inline] 232 | pub fn as_real(&'a self) -> Option { 233 | match *self { 234 | Self::Real(v) => v, 235 | _ => None, 236 | } 237 | } 238 | 239 | /// Return this type as a string if possible 240 | /// 241 | /// This will exist if the variant is [`SqlResult::String`], or 242 | /// [`SqlResult::Decimal`], and it contains a value, _and_ the string can 243 | /// successfully be converted to `utf8` (using [`str::from_utf8`]). It does 244 | /// not distinguish among errors (wrong type, `None` value, or invalid utf8) 245 | /// - use pattern matching if you need that. 246 | /// 247 | /// See [`SqlResult::as_int()`] for further details on `as_*` methods 248 | #[inline] 249 | pub fn as_string(&'a self) -> Option<&'a str> { 250 | match *self { 251 | Self::String(Some(v)) => Some(str::from_utf8(v).ok()?), 252 | Self::Decimal(Some(v)) => Some(v), 253 | _ => None, 254 | } 255 | } 256 | 257 | /// Return this type as a byte slice if possible 258 | /// 259 | /// This will exist if the variant is [`SqlResult::String`], or 260 | /// [`SqlResult::Decimal`]. See [`SqlResult::as_int()`] for further details 261 | /// on `as_*` methods 262 | #[inline] 263 | pub fn as_bytes(&'a self) -> Option<&'a [u8]> { 264 | match *self { 265 | Self::String(Some(v)) => Some(v), 266 | Self::Decimal(Some(v)) => Some(v.as_bytes()), 267 | _ => None, 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /udf/src/wrapper.rs: -------------------------------------------------------------------------------- 1 | //! Non-public module to assist macro with wrapping functions 2 | //! 3 | //! Warning: This module should be considered unstable and generally not for 4 | //! public use 5 | 6 | #[macro_use] 7 | mod const_helpers; 8 | mod functions; 9 | mod helpers; 10 | mod modded_types; 11 | mod process; 12 | 13 | use std::str; 14 | 15 | use const_helpers::{const_slice_eq, const_slice_to_str, const_str_eq}; 16 | pub use functions::{wrap_add, wrap_clear, wrap_deinit, wrap_init, wrap_remove, BufConverter}; 17 | pub(crate) use helpers::*; 18 | pub use modded_types::UDF_ARGSx; 19 | pub use process::{ 20 | wrap_process_basic, wrap_process_basic_option, wrap_process_buf, wrap_process_buf_option, 21 | wrap_process_buf_option_ref, 22 | }; 23 | 24 | /// A trait implemented by the proc macro 25 | // FIXME: on unimplemented 26 | pub trait RegisteredBasicUdf { 27 | /// The main function name 28 | const NAME: &'static str; 29 | /// Aliases, if any 30 | const ALIASES: &'static [&'static str]; 31 | /// True if `NAME` comes from the default value for the struct 32 | const DEFAULT_NAME_USED: bool; 33 | } 34 | 35 | /// Implemented by the proc macro. This is used to enforce that the basic UDF and aggregate 36 | /// UDF have the same name and aliases. 37 | pub trait RegisteredAggregateUdf: RegisteredBasicUdf { 38 | /// The main function name 39 | const NAME: &'static str; 40 | /// Aliases, if any 41 | const ALIASES: &'static [&'static str]; 42 | /// True if `NAME` comes from the default value for the struct 43 | const DEFAULT_NAME_USED: bool; 44 | } 45 | 46 | const NAME_MSG: &str = "`#[register]` on `BasicUdf` and `AggregateUdf` must have the same "; 47 | 48 | /// Enforce that a struct has the same basic and aggregate UDF names. 49 | pub const fn verify_aggregate_attributes() { 50 | verify_aggregate_attributes_name::(); 51 | verify_aggregate_attribute_aliases::(); 52 | } 53 | 54 | const fn verify_aggregate_attributes_name() { 55 | let basic_name = ::NAME; 56 | let agg_name = ::NAME; 57 | let basic_default_name = ::DEFAULT_NAME_USED; 58 | let agg_default_name = ::DEFAULT_NAME_USED; 59 | 60 | if const_str_eq(basic_name, agg_name) { 61 | return; 62 | } 63 | 64 | let mut msg_buf = [0u8; 512]; 65 | let mut curs = 0; 66 | curs += const_write_all!( 67 | msg_buf, 68 | [NAME_MSG, "`name` argument; got `", basic_name, "`",], 69 | curs 70 | ); 71 | 72 | if basic_default_name { 73 | curs += const_write_all!(msg_buf, [" (default from struct name)"], curs); 74 | } 75 | 76 | curs += const_write_all!(msg_buf, [" and `", agg_name, "`"], curs); 77 | 78 | if agg_default_name { 79 | curs += const_write_all!(msg_buf, [" (default from struct name)"], curs); 80 | } 81 | 82 | let msg = const_slice_to_str(msg_buf.as_slice(), curs); 83 | panic!("{}", msg); 84 | } 85 | 86 | #[allow(clippy::cognitive_complexity)] 87 | const fn verify_aggregate_attribute_aliases() { 88 | let basic_aliases = ::ALIASES; 89 | let agg_aliases = ::ALIASES; 90 | 91 | if const_slice_eq(basic_aliases, agg_aliases) { 92 | return; 93 | } 94 | 95 | let mut msg_buf = [0u8; 512]; 96 | let mut curs = 0; 97 | 98 | curs += const_write_all!(msg_buf, [NAME_MSG, "`alias` arguments; got [",], 0); 99 | 100 | let mut i = 0; 101 | while i < basic_aliases.len() { 102 | if i > 0 { 103 | curs += const_write_all!(msg_buf, [", "], curs); 104 | } 105 | curs += const_write_all!(msg_buf, ["`", basic_aliases[i], "`",], curs); 106 | i += 1; 107 | } 108 | 109 | curs += const_write_all!(msg_buf, ["] and ["], curs); 110 | 111 | let mut i = 0; 112 | while i < agg_aliases.len() { 113 | if i > 0 { 114 | curs += const_write_all!(msg_buf, [", "], curs); 115 | } 116 | curs += const_write_all!(msg_buf, ["`", agg_aliases[i], "`",], curs); 117 | i += 1; 118 | } 119 | 120 | curs += const_write_all!(msg_buf, ["]"], curs); 121 | 122 | let msg = const_slice_to_str(msg_buf.as_slice(), curs); 123 | panic!("{}", msg); 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests; 128 | -------------------------------------------------------------------------------- /udf/src/wrapper/const_helpers.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | /// Similar to `copy_from_slice` but works at comptime. 4 | /// 5 | /// Takes a `start` offset so we can index into an existing slice. 6 | macro_rules! const_arr_copy { 7 | ($dst:expr, $src:expr, $start:expr) => {{ 8 | let max_idx = $dst.len() - $start; 9 | let (to_write, add_ellipsis) = if $src.len() <= (max_idx.saturating_sub($start)) { 10 | ($src.len(), false) 11 | } else { 12 | ($src.len().saturating_sub(4), true) 13 | }; 14 | 15 | let mut i = 0; 16 | while i < to_write { 17 | $dst[i + $start] = $src[i]; 18 | i += 1; 19 | } 20 | 21 | if add_ellipsis { 22 | while i < $dst.len() - $start { 23 | $dst[i + $start] = b'.'; 24 | i += 1; 25 | } 26 | } 27 | 28 | i 29 | }}; 30 | } 31 | 32 | macro_rules! const_write_all { 33 | ($dst:expr, $src_arr:expr, $start:expr) => {{ 34 | let mut offset = $start; 35 | 36 | let mut i = 0; 37 | while i < $src_arr.len() && offset < $dst.len() { 38 | offset += const_arr_copy!($dst, $src_arr[i].as_bytes(), offset); 39 | i += 1; 40 | } 41 | 42 | offset - $start 43 | }}; 44 | } 45 | 46 | pub const fn const_str_eq(a: &str, b: &str) -> bool { 47 | let a = a.as_bytes(); 48 | let b = b.as_bytes(); 49 | if a.len() != b.len() { 50 | return false; 51 | } 52 | 53 | let mut i = 0; 54 | while i < a.len() { 55 | if a[i] != b[i] { 56 | return false; 57 | } 58 | 59 | i += 1; 60 | } 61 | 62 | true 63 | } 64 | 65 | pub const fn const_slice_eq(a: &[&str], b: &[&str]) -> bool { 66 | if a.len() != b.len() { 67 | return false; 68 | } 69 | 70 | let mut i = 0; 71 | while i < a.len() { 72 | if !const_str_eq(a[i], b[i]) { 73 | return false; 74 | } 75 | 76 | i += 1; 77 | } 78 | 79 | true 80 | } 81 | 82 | pub const fn const_slice_to_str(s: &[u8], len: usize) -> &str { 83 | assert!(len <= s.len()); 84 | // FIXME(msrv): use const `split_at` once our MSRV gets to 1.71 85 | // SAFETY: validated inbounds above 86 | let buf = unsafe { std::slice::from_raw_parts(s.as_ptr(), len) }; 87 | 88 | match str::from_utf8(buf) { 89 | Ok(v) => v, 90 | Err(_e) => panic!("utf8 error"), 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn test_arr_copy() { 100 | let mut x = [0u8; 20]; 101 | let w1 = const_arr_copy!(x, b"foobar", 0); 102 | let s = const_slice_to_str(x.as_slice(), w1); 103 | assert_eq!(s, "foobar"); 104 | 105 | let w2 = const_arr_copy!(x, b"foobar", w1); 106 | let s = const_slice_to_str(x.as_slice(), w1 + w2); 107 | assert_eq!(s, "foobarfoobar"); 108 | 109 | let mut x = [0u8; 6]; 110 | let written = const_arr_copy!(x, b"foobar", 0); 111 | let s = const_slice_to_str(x.as_slice(), written); 112 | assert_eq!(s, "foobar"); 113 | 114 | let mut x = [0u8; 5]; 115 | let written = const_arr_copy!(x, b"foobar", 0); 116 | let s = const_slice_to_str(x.as_slice(), written); 117 | assert_eq!(s, "fo..."); 118 | } 119 | 120 | #[test] 121 | fn test_const_write_all() { 122 | let mut x = [0u8; 20]; 123 | let w1 = const_write_all!(x, ["foo", "bar", "baz"], 0); 124 | let s = const_slice_to_str(x.as_slice(), w1); 125 | assert_eq!(s, "foobarbaz"); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /udf/src/wrapper/functions.rs: -------------------------------------------------------------------------------- 1 | //! Functions designed to safely wrap rust definitions within C bindings 2 | //! 3 | //! This file ties together C types and rust types, providing a safe wrapper. 4 | //! Functions in this module are generally not meant to be used directly. 5 | 6 | use std::ffi::{c_char, c_uchar}; 7 | use std::num::NonZeroU8; 8 | 9 | use udf_sys::{UDF_ARGS, UDF_INIT}; 10 | 11 | use crate::wrapper::write_msg_to_buf; 12 | use crate::{AggregateUdf, ArgList, BasicUdf, Process, UdfCfg, MYSQL_ERRMSG_SIZE}; 13 | 14 | /// A wrapper that lets us handle return types when the user returns an 15 | /// allocated buffer (rather than a reference). We wrap the user's type within 16 | /// this struct and need to be sure to negotiate correctly 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | pub struct BufConverter 19 | where 20 | U: BasicUdf, 21 | B: Default, 22 | { 23 | udf: U, 24 | buf: B, 25 | } 26 | 27 | impl BufConverter 28 | where 29 | U: BasicUdf, 30 | B: Default, 31 | { 32 | #[allow(dead_code)] 33 | fn set_retval(&mut self, val: B) { 34 | self.buf = val; 35 | } 36 | } 37 | 38 | /// Trait to allow interfacing a buffered or plain type 39 | pub trait UdfConverter { 40 | fn as_mut_ref(&mut self) -> &mut U; 41 | fn into_storable(source: U) -> Self; 42 | } 43 | 44 | impl UdfConverter for BufConverter 45 | where 46 | U: BasicUdf, 47 | B: Default, 48 | { 49 | fn as_mut_ref(&mut self) -> &mut U { 50 | &mut self.udf 51 | } 52 | 53 | fn into_storable(source: U) -> Self { 54 | Self { 55 | udf: source, 56 | buf: B::default(), 57 | } 58 | } 59 | } 60 | 61 | impl UdfConverter for U { 62 | fn as_mut_ref(&mut self) -> &mut U { 63 | self 64 | } 65 | 66 | fn into_storable(source: U) -> Self { 67 | source 68 | } 69 | } 70 | 71 | /// This function provides the same signature as the C FFI expects. It is used 72 | /// to perform setup within a renamed function, and will apply it to a specific 73 | /// type that implements `BasicUDF`. 74 | /// 75 | /// # Arguments 76 | /// 77 | /// - initd: Settable nformation about the return type of the function 78 | /// - args: A list of arguments passed to theis function 79 | /// 80 | /// # Result 81 | /// 82 | /// Return true if there is an error, as expected by the UDF interface 83 | /// 84 | /// # Modifies 85 | /// 86 | /// - `initid.ptr` is set to the contained struct 87 | /// 88 | /// # Panics 89 | /// 90 | /// - Panics if the error message contains "\0", or if the message is too long ( 91 | /// greater than 511 bytes). 92 | /// - Panics if the provides error message string contains null characters 93 | /// 94 | /// # Interface 95 | /// 96 | /// Based on the SQL UDF spec, we need to perform the following here: 97 | /// - Verify the number of arguments to `XXX()` (handled by `U::init`) 98 | /// - Verify that the arguments are of a required type or, alternatively, to 99 | /// tell `MySQL` to coerce arguments to the required types when the main 100 | /// function is called. (handled by `U::init`) 101 | /// - To allocate any memory required by the main function. (We box our struct 102 | /// for this) 103 | /// - To specify the maximum length of the result 104 | /// - To specify (for REAL functions) the maximum number of decimal places in 105 | /// the result. 106 | /// - To specify whether the result can be NULL. (handled by proc macro based on 107 | /// `Returns`) 108 | #[inline] 109 | pub unsafe fn wrap_init, U: BasicUdf>( 110 | initid: *mut UDF_INIT, 111 | args: *mut UDF_ARGS, 112 | message: *mut c_char, 113 | ) -> bool { 114 | log_call!(enter: "init", U, args, message); 115 | 116 | let cfg = UdfCfg::from_raw_ptr(initid); 117 | let arglist = ArgList::from_raw_ptr(args); 118 | 119 | // Call the user's init function 120 | let init_res = U::init(cfg, arglist); 121 | 122 | // Apply any pending coercions 123 | arglist.flush_all_coercions(); 124 | 125 | // If initialization succeeds, put our UDF info struct on the heap 126 | // If initialization fails, copy a message to the buffer 127 | let ret = match init_res { 128 | Ok(v) => { 129 | // set the `initid` struct to contain our struct 130 | // SAFETY: must be cleaned up in deinit function, or we will leak! 131 | let boxed_struct: Box = Box::new(W::into_storable(v)); 132 | cfg.store_box(boxed_struct); 133 | false 134 | } 135 | Err(e) => { 136 | // SAFETY: buffer size is correct 137 | write_msg_to_buf::(e.as_bytes(), message); 138 | true 139 | } 140 | }; 141 | 142 | log_call!(exit: "init", U, &*args, &*message, ret); 143 | ret 144 | } 145 | 146 | /// For our deinit function, all we need to do is take ownership of the boxed 147 | /// value on the stack. The function ends, it goes out of scope and gets 148 | /// dropped. 149 | /// 150 | /// There is no specific wrapped function here 151 | #[inline] 152 | pub unsafe fn wrap_deinit, U: BasicUdf>(initid: *const UDF_INIT) { 153 | log_call!(enter: "deinit", U, &*initid); 154 | 155 | // SAFETY: we constructed this box so it is formatted correctly 156 | // caller ensures validity of initid 157 | let cfg: &UdfCfg = UdfCfg::from_raw_ptr(initid); 158 | cfg.retrieve_box::(); 159 | } 160 | 161 | #[inline] 162 | pub unsafe fn wrap_add, U: AggregateUdf>( 163 | initid: *mut UDF_INIT, 164 | args: *mut UDF_ARGS, 165 | _is_null: *mut c_uchar, 166 | error: *mut c_uchar, 167 | ) { 168 | log_call!(enter: "add", U, &*initid, &*args, &*error); 169 | 170 | let cfg = UdfCfg::from_raw_ptr(initid); 171 | let arglist = ArgList::from_raw_ptr(args); 172 | let err = *(error as *const Option); 173 | let mut b = cfg.retrieve_box::(); 174 | let res = U::add(b.as_mut_ref(), cfg, arglist, err); 175 | cfg.store_box(b); 176 | 177 | if let Err(e) = res { 178 | *error = e.into(); 179 | } 180 | } 181 | 182 | #[inline] 183 | pub unsafe fn wrap_clear, U: AggregateUdf>( 184 | initid: *mut UDF_INIT, 185 | _is_null: *mut c_uchar, 186 | error: *mut c_uchar, 187 | ) { 188 | log_call!(enter: "clear", U, &*initid, &*error); 189 | 190 | let cfg = UdfCfg::from_raw_ptr(initid); 191 | let err = *(error as *const Option); 192 | let mut b = cfg.retrieve_box::(); 193 | let res = U::clear(b.as_mut_ref(), cfg, err); 194 | cfg.store_box(b); 195 | 196 | if let Err(e) = res { 197 | *error = e.into(); 198 | } 199 | } 200 | 201 | #[inline] 202 | pub unsafe fn wrap_remove, U: AggregateUdf>( 203 | initid: *mut UDF_INIT, 204 | args: *mut UDF_ARGS, 205 | _is_null: *mut c_uchar, 206 | error: *mut c_uchar, 207 | ) { 208 | log_call!(enter: "remove", U, &*initid, &*args, &*error); 209 | 210 | let cfg = UdfCfg::from_raw_ptr(initid); 211 | let arglist = ArgList::from_raw_ptr(args); 212 | let err = *(error as *const Option); 213 | let mut b = cfg.retrieve_box::(); 214 | let res = U::remove(b.as_mut_ref(), cfg, arglist, err); 215 | cfg.store_box(b); 216 | 217 | if let Err(e) = res { 218 | *error = e.into(); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /udf/src/wrapper/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Private module that handles the implementation of the wrapper module 2 | 3 | use std::cmp::min; 4 | use std::ffi::{c_char, c_ulong}; 5 | use std::ptr; 6 | 7 | use crate::udf_log; 8 | 9 | /// Write a string message to a buffer. Accepts a const generic size `N` that 10 | /// length of the message will check against (N must be the size of the buffer) 11 | /// 12 | /// # Safety 13 | /// 14 | /// `N` must be the buffer size. If it is inaccurate, memory safety cannot be 15 | /// guaranteed. 16 | /// 17 | /// This is public within the crate, since the parent model is not public 18 | pub unsafe fn write_msg_to_buf(msg: &[u8], buf: *mut c_char) { 19 | // message plus null terminator must fit in buffer 20 | let bytes_to_write = min(msg.len(), N - 1); 21 | 22 | unsafe { 23 | ptr::copy_nonoverlapping(msg.as_ptr().cast::(), buf, bytes_to_write); 24 | *buf.add(bytes_to_write) = 0; 25 | } 26 | } 27 | 28 | /// Data that is only relevant to buffer return types 29 | pub struct BufOptions { 30 | res_buf: *mut c_char, 31 | length: *mut c_ulong, 32 | } 33 | 34 | impl BufOptions { 35 | /// Create a new `BufOptions` struct 36 | pub fn new(res_buf: *mut c_char, length: *mut c_ulong) -> Self { 37 | Self { res_buf, length } 38 | } 39 | } 40 | 41 | /// Handle the result of SQL function that returns a buffer 42 | /// 43 | /// Accept any input byte slice and a set of buffer options. Performs one of 44 | /// three: 45 | /// 46 | /// - If slice fits in buffer: copy to buffer, return pointer to the buffer 47 | /// - If slice does not fit in the buffer and returning references are 48 | /// permitted: return pointer to the source slice 49 | /// - If slice does not fit and returning references is not permitted: print 50 | /// an error message, return None 51 | /// 52 | /// The `U` type parameter is just used for output formatting 53 | pub unsafe fn buf_result_callback>(input: T, opts: &BufOptions) -> *const c_char { 54 | let slice_ref = input.as_ref(); 55 | let slice_len = slice_ref.len(); 56 | let slice_len_ulong: c_ulong = slice_len.try_into().unwrap_or_else(|_| { 57 | udf_log!(Error: "Buffer size {}, platform limitation of {}. Truncating", slice_len, c_ulong::MAX); 58 | c_ulong::MAX}); 59 | let slice_ptr: *const c_char = slice_ref.as_ptr().cast(); 60 | let buf_len = *opts.length as usize; 61 | 62 | if slice_len <= buf_len { 63 | // If we fit in the buffer, just copy 64 | ptr::copy(slice_ptr, opts.res_buf, slice_len); 65 | *opts.length = slice_len_ulong; 66 | return opts.res_buf; 67 | } 68 | 69 | // If we don't fit in the buffer but can return a reference, do so 70 | *opts.length = slice_len_ulong; 71 | slice_ptr 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | #![allow(clippy::similar_names)] 77 | 78 | use std::ffi::{c_ulong, c_void, CStr}; 79 | use std::ptr; 80 | 81 | use udf_sys::{Item_result, UDF_ARGS}; 82 | 83 | use super::*; 84 | use crate::prelude::*; 85 | 86 | const MSG: &str = "message"; 87 | const BUF_SIZE: usize = MSG.len() + 1; 88 | 89 | #[test] 90 | fn write_msg_ok() { 91 | let mut mbuf = [1 as c_char; BUF_SIZE]; 92 | 93 | unsafe { 94 | write_msg_to_buf::(MSG.as_bytes(), mbuf.as_mut_ptr()); 95 | let s = CStr::from_ptr(mbuf.as_ptr()).to_str().unwrap(); 96 | 97 | assert_eq!(s, MSG); 98 | } 99 | } 100 | 101 | #[test] 102 | fn write_message_too_long() { 103 | const NEW_BUF_SIZE: usize = BUF_SIZE - 1; 104 | 105 | let mut mbuf = [1 as c_char; NEW_BUF_SIZE]; 106 | unsafe { 107 | write_msg_to_buf::(MSG.as_bytes(), mbuf.as_mut_ptr()); 108 | let s = CStr::from_ptr(mbuf.as_ptr()).to_str().unwrap(); 109 | assert_eq!(*s, MSG[..MSG.len() - 1]); 110 | }; 111 | } 112 | 113 | #[test] 114 | fn argtype_from_ptr_null() { 115 | // Just test null pointers here 116 | unsafe { 117 | assert_eq!( 118 | SqlResult::from_ptr(ptr::null(), Item_result::INT_RESULT, 0), 119 | Ok(SqlResult::Int(None)) 120 | ); 121 | assert_eq!( 122 | SqlResult::from_ptr(ptr::null(), Item_result::REAL_RESULT, 0), 123 | Ok(SqlResult::Real(None)) 124 | ); 125 | assert_eq!( 126 | SqlResult::from_ptr(ptr::null(), Item_result::STRING_RESULT, 0), 127 | Ok(SqlResult::String(None)) 128 | ); 129 | assert_eq!( 130 | SqlResult::from_ptr(ptr::null(), Item_result::DECIMAL_RESULT, 0), 131 | Ok(SqlResult::Decimal(None)) 132 | ); 133 | assert!(SqlResult::from_ptr(ptr::null(), Item_result::INVALID_RESULT, 0).is_err()); 134 | } 135 | } 136 | 137 | #[test] 138 | fn argtype_from_ptr_notnull() { 139 | // Just test null pointers here 140 | unsafe { 141 | let ival = -1000i64; 142 | assert_eq!( 143 | SqlResult::from_ptr(ptr::addr_of!(ival).cast(), Item_result::INT_RESULT, 0), 144 | Ok(SqlResult::Int(Some(ival))) 145 | ); 146 | 147 | let rval = -1000.0f64; 148 | assert_eq!( 149 | SqlResult::from_ptr(ptr::addr_of!(rval).cast(), Item_result::REAL_RESULT, 0), 150 | Ok(SqlResult::Real(Some(rval))) 151 | ); 152 | 153 | let sval = "this is a string"; 154 | assert_eq!( 155 | SqlResult::from_ptr(sval.as_ptr(), Item_result::STRING_RESULT, sval.len()), 156 | Ok(SqlResult::String(Some(sval.as_bytes()))) 157 | ); 158 | 159 | let dval = "123.456"; 160 | assert_eq!( 161 | SqlResult::from_ptr(dval.as_ptr(), Item_result::DECIMAL_RESULT, dval.len()), 162 | Ok(SqlResult::Decimal(Some(dval))) 163 | ); 164 | 165 | assert!( 166 | SqlResult::from_ptr(dval.as_ptr(), Item_result::INVALID_RESULT, dval.len()) 167 | .is_err() 168 | ); 169 | } 170 | } 171 | 172 | const ARG_COUNT: usize = 4; 173 | 174 | static IVAL: i64 = -1000i64; 175 | static RVAL: f64 = -1234.5678f64; 176 | static SVAL: &str = "this is a string"; 177 | static DVAL: &str = "123.456"; 178 | 179 | #[test] 180 | fn process_args_ok() { 181 | let mut arg_types = [ 182 | Item_result::INT_RESULT, 183 | Item_result::REAL_RESULT, 184 | Item_result::STRING_RESULT, 185 | Item_result::DECIMAL_RESULT, 186 | ]; 187 | 188 | let arg_ptrs: [*const u8; ARG_COUNT] = [ 189 | ptr::addr_of!(IVAL).cast(), 190 | ptr::addr_of!(RVAL).cast(), 191 | SVAL.as_ptr(), 192 | DVAL.as_ptr(), 193 | ]; 194 | 195 | let arg_lens: [c_ulong; 4] = [0, 0, SVAL.len() as c_ulong, DVAL.len() as c_ulong]; 196 | let maybe_null = [true, true, false, false]; 197 | let attrs = ["ival", "rval", "sval", "dval"]; 198 | let attr_ptrs = [ 199 | attrs[0].as_ptr(), 200 | attrs[1].as_ptr(), 201 | attrs[2].as_ptr(), 202 | attrs[3].as_ptr(), 203 | ]; 204 | let attr_lens: [c_ulong; 4] = [ 205 | attrs[0].len() as c_ulong, 206 | attrs[1].len() as c_ulong, 207 | attrs[2].len() as c_ulong, 208 | attrs[3].len() as c_ulong, 209 | ]; 210 | 211 | let mut udf_args = UDF_ARGS { 212 | arg_count: ARG_COUNT as u32, 213 | arg_types: arg_types.as_mut_ptr(), 214 | args: arg_ptrs.as_ptr().cast::<*const c_char>(), 215 | lengths: arg_lens.as_ptr(), 216 | maybe_null: maybe_null.as_ptr().cast(), 217 | attributes: attr_ptrs.as_ptr().cast::<*const c_char>(), 218 | attribute_lengths: attr_lens.as_ptr().cast::(), 219 | extension: ptr::null_mut::(), 220 | }; 221 | 222 | let arglist: &ArgList = unsafe { ArgList::from_raw_ptr(&mut udf_args) }; 223 | let res: Vec<_> = arglist.into_iter().collect(); 224 | 225 | let expected_args = [ 226 | SqlResult::Int(Some(IVAL)), 227 | SqlResult::Real(Some(RVAL)), 228 | SqlResult::String(Some(SVAL.as_bytes())), 229 | SqlResult::Decimal(Some(DVAL)), 230 | ]; 231 | 232 | for i in 0..ARG_COUNT { 233 | assert_eq!(res[i].value(), expected_args[i]); 234 | assert_eq!(res[i].maybe_null(), maybe_null[i]); 235 | assert_eq!(res[i].attribute(), attrs[i]); 236 | // assert_eq!(unsafe { *res[i].type_ptr }, arg_types[i]); 237 | } 238 | } 239 | } 240 | 241 | #[cfg(test)] 242 | mod buffer_tests { 243 | use core::slice; 244 | 245 | use super::*; 246 | 247 | const BUF_LEN: usize = 10; 248 | 249 | #[test] 250 | fn test_buf_fits() { 251 | // Test a buffer that simply fits into the available result buffer 252 | let input = b"1234"; 253 | let mut res_buf = [0u8; BUF_LEN]; 254 | let zeroes = [0u8; BUF_LEN]; 255 | let mut len = res_buf.len() as c_ulong; 256 | let buf_opts = BufOptions::new(res_buf.as_mut_ptr().cast(), &mut len); 257 | 258 | let res_ptr: *const u8 = unsafe { buf_result_callback(input, &buf_opts).cast() }; 259 | let res_slice = unsafe { slice::from_raw_parts(res_ptr, len as usize) }; 260 | 261 | assert_eq!(len as usize, input.len()); 262 | assert_eq!(res_slice, input); 263 | assert_eq!(res_ptr.cast(), res_buf.as_ptr()); 264 | // Check residual buffer 265 | assert_eq!( 266 | res_buf[input.len()..res_buf.len()], 267 | zeroes[input.len()..res_buf.len()] 268 | ); 269 | } 270 | 271 | #[test] 272 | fn test_buf_no_fit_ref() { 273 | // Test a buffer that does not fit but can be used as a ref 274 | let input = b"123456789012345"; 275 | let mut res_buf = [0u8; BUF_LEN]; 276 | let mut len = res_buf.len() as c_ulong; 277 | let buf_opts = BufOptions::new(res_buf.as_mut_ptr().cast(), &mut len); 278 | 279 | let res_ptr: *const u8 = unsafe { buf_result_callback(input, &buf_opts).cast() }; 280 | let res_slice = unsafe { slice::from_raw_parts(res_ptr, len as usize) }; 281 | 282 | assert_eq!(len as usize, input.len()); 283 | assert_eq!(res_slice, input); 284 | assert_eq!(res_ptr.cast(), input.as_ptr()); 285 | } 286 | 287 | // #[test] 288 | // fn test_buf_no_fit_no_ref() { 289 | // // Test a buffer that does not fit but can not be used as a ref 290 | // // This must return an error 291 | // let input = b"123456789012345"; 292 | // let mut res_buf = [0u8; BUF_LEN]; 293 | // let mut len = res_buf.len() as c_ulong; 294 | // let buf_opts = BufOptions::new(res_buf.as_mut_ptr().cast(), &mut len, false); 295 | 296 | // let res = unsafe { buf_result_callback::(input, &buf_opts) }; 297 | // assert_eq!(len, 0); 298 | // assert_eq!(res, None); 299 | // } 300 | } 301 | -------------------------------------------------------------------------------- /udf/src/wrapper/modded_types.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | 3 | /// Representation of a sequence of SQL arguments 4 | /// 5 | /// This should be identical to `udf_sys::UDF_ARGS` except `arg_types` is a 6 | /// `c_int` rather than an `Item_result`. This just allows us to 7 | #[repr(C)] 8 | #[derive(Debug, Clone)] 9 | pub struct UDF_ARGSx { 10 | /// Number of arguments present 11 | pub arg_count: ::std::ffi::c_uint, 12 | 13 | /// Buffer of `item_result` pointers that indicate argument type 14 | /// 15 | /// Remains mutable because it can be set in `xxx_init` 16 | pub arg_types: *mut ::std::ffi::c_int, 17 | 18 | /// Buffer of pointers to the arguments. Arguments may be of any type 19 | /// (specified in `arg_type`). 20 | pub args: *const *const ::std::ffi::c_char, 21 | 22 | /// Buffer of lengths for string arguments 23 | pub lengths: *const ::std::ffi::c_ulong, 24 | 25 | /// Indicates whether the argument may be null or not 26 | pub maybe_null: *const ::std::ffi::c_char, 27 | 28 | /// Buffer of string pointers that hold variable names, for use with error 29 | /// messages 30 | pub attributes: *const *const ::std::ffi::c_char, 31 | 32 | /// Buffer of lengths of attributes 33 | pub attribute_lengths: *const ::std::ffi::c_ulong, 34 | 35 | /// Extension is currently unused 36 | pub extension: *const ::std::ffi::c_void, 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use std::alloc::Layout; 42 | 43 | use udf_sys::UDF_ARGS; 44 | 45 | use super::*; 46 | 47 | #[test] 48 | fn test_layout() { 49 | let layout_default = Layout::new::(); 50 | let layout_modded = Layout::new::(); 51 | assert_eq!(layout_default, layout_modded); 52 | } 53 | 54 | // Below couple tests are taken from bindgen 55 | #[test] 56 | fn test_field_arg_type() { 57 | assert_eq!( 58 | unsafe { 59 | let uninit = ::std::mem::MaybeUninit::::uninit(); 60 | let ptr = uninit.as_ptr(); 61 | ::std::ptr::addr_of!((*ptr).arg_types) as usize - ptr as usize 62 | }, 63 | 8usize, 64 | concat!( 65 | "Offset of field: ", 66 | stringify!(UDF_ARGS), 67 | "::", 68 | stringify!(arg_type) 69 | ) 70 | ); 71 | } 72 | 73 | #[test] 74 | fn test_field_lengths() { 75 | assert_eq!( 76 | unsafe { 77 | let uninit = ::std::mem::MaybeUninit::::uninit(); 78 | let ptr = uninit.as_ptr(); 79 | ::std::ptr::addr_of!((*ptr).lengths) as usize - ptr as usize 80 | }, 81 | 24usize, 82 | concat!( 83 | "Offset of field: ", 84 | stringify!(UDF_ARGS), 85 | "::", 86 | stringify!(lengths) 87 | ) 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /udf/src/wrapper/process.rs: -------------------------------------------------------------------------------- 1 | //! Functions related to strictly the `process` UDF components 2 | 3 | #![allow(clippy::module_name_repetitions)] 4 | #![allow(clippy::option_if_let_else)] 5 | 6 | use std::ffi::{c_char, c_uchar, c_ulong}; 7 | use std::num::NonZeroU8; 8 | use std::ptr; 9 | 10 | use udf_sys::{UDF_ARGS, UDF_INIT}; 11 | 12 | use super::functions::UdfConverter; 13 | use super::helpers::{buf_result_callback, BufOptions}; 14 | use crate::{ArgList, BasicUdf, ProcessError, UdfCfg}; 15 | 16 | /// Callback for properly unwrapping and setting values for `Option` 17 | /// 18 | /// Returns `None` if the value is `Err` or `None`, `Some` otherwise 19 | #[inline] 20 | unsafe fn ret_callback_option( 21 | res: Result, ProcessError>, 22 | error: *mut c_uchar, 23 | is_null: *mut c_uchar, 24 | ) -> Option { 25 | let transposed = res.transpose(); 26 | 27 | // Perform action for if internal is `None` 28 | let Some(res_some) = transposed else { 29 | // We have a None result 30 | *is_null = 1; 31 | return None; 32 | }; 33 | 34 | // Rest of the behavior is in `ret_callback` 35 | ret_callback(res_some, error, is_null) 36 | } 37 | 38 | /// Callback for properly unwrapping and setting values for any `T` 39 | /// 40 | /// Returns `None` if the value is `Err`, `Some` otherwise 41 | #[inline] 42 | unsafe fn ret_callback( 43 | res: Result, 44 | error: *mut c_uchar, 45 | is_null: *mut c_uchar, 46 | ) -> Option { 47 | // Error case: set an error, and set length to 0 if applicable 48 | let Ok(val) = res else { 49 | *error = 1; 50 | return None; 51 | }; 52 | 53 | // Ok case: just return the desired value 54 | *is_null = c_uchar::from(false); 55 | 56 | Some(val) 57 | } 58 | 59 | /// Apply the `process` function for any implementation returning a nonbuffer type 60 | /// (`f64`, `i64`) 61 | #[inline] 62 | #[allow(clippy::let_and_return)] 63 | pub unsafe fn wrap_process_basic( 64 | initid: *mut UDF_INIT, 65 | args: *mut UDF_ARGS, 66 | is_null: *mut c_uchar, 67 | error: *mut c_uchar, 68 | ) -> R 69 | where 70 | W: UdfConverter, 71 | for<'a> U: BasicUdf = R>, 72 | R: Default + std::fmt::Debug, 73 | { 74 | log_call!(enter: "process", U, &*initid, &*args, &*is_null, &*error); 75 | 76 | let cfg = UdfCfg::from_raw_ptr(initid); 77 | let arglist = ArgList::from_raw_ptr(args); 78 | let mut b = cfg.retrieve_box::(); 79 | let err = *(error as *const Option); 80 | let proc_res = U::process(b.as_mut_ref(), cfg, arglist, err); 81 | cfg.store_box(b); 82 | 83 | let ret = ret_callback(proc_res, error, is_null).unwrap_or_default(); 84 | log_call!(exit: "process", U, &*initid, &*args, &*is_null, &*error, ret); 85 | ret 86 | } 87 | 88 | /// Apply the `process` function for any implementation returning an optional 89 | /// nonbuffer type (`Option`, `Option`) 90 | #[inline] 91 | #[allow(clippy::let_and_return)] 92 | pub unsafe fn wrap_process_basic_option( 93 | initid: *mut UDF_INIT, 94 | args: *mut UDF_ARGS, 95 | is_null: *mut c_uchar, 96 | error: *mut c_uchar, 97 | ) -> R 98 | where 99 | W: UdfConverter, 100 | for<'a> U: BasicUdf = Option>, 101 | R: Default + std::fmt::Debug, 102 | { 103 | log_call!(enter: "process", U, &*initid, &*args, &*is_null, &*error); 104 | 105 | let cfg = UdfCfg::from_raw_ptr(initid); 106 | let arglist = ArgList::from_raw_ptr(args); 107 | let mut b = cfg.retrieve_box::(); 108 | let err = *(error as *const Option); 109 | let proc_res = U::process(b.as_mut_ref(), cfg, arglist, err); 110 | cfg.store_box(b); 111 | 112 | let ret = ret_callback_option(proc_res, error, is_null).unwrap_or_default(); 113 | log_call!(exit: "process", U, &*initid, &*args, &*is_null, &*error, ret); 114 | ret 115 | } 116 | 117 | /// Apply the `process` function for any implementation returning a buffer type 118 | /// (`String`, `Vec`, `str`, `[u8]`) 119 | #[inline] 120 | pub unsafe fn wrap_process_buf( 121 | initid: *mut UDF_INIT, 122 | args: *mut UDF_ARGS, 123 | result: *mut c_char, 124 | length: *mut c_ulong, 125 | is_null: *mut c_uchar, 126 | error: *mut c_uchar, 127 | ) -> *const c_char 128 | where 129 | W: UdfConverter, 130 | for<'b> U: BasicUdf, 131 | for<'a> ::Returns<'a>: AsRef<[u8]>, 132 | { 133 | log_call!(exit: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error); 134 | 135 | let cfg = UdfCfg::from_raw_ptr(initid); 136 | let arglist = ArgList::from_raw_ptr(args); 137 | let mut b = cfg.retrieve_box::(); 138 | let err = *(error as *const Option); 139 | let binding = b.as_mut_ref(); 140 | let proc_res = U::process(binding, cfg, arglist, err); 141 | let buf_opts = BufOptions::new(result, length); 142 | 143 | let post_effects_val = ret_callback(proc_res, error, is_null); 144 | 145 | let ret = match post_effects_val { 146 | Some(ref v) => buf_result_callback(v, &buf_opts), 147 | None => ptr::null(), 148 | }; 149 | 150 | std::mem::forget(post_effects_val); 151 | cfg.store_box(b); 152 | 153 | log_call!(exit: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error, ret); 154 | ret 155 | } 156 | 157 | /// Apply the `process` function for any implementation returning a buffer type 158 | /// (`Option`, `Option>`, `Option`, `Option<[u8]>`) 159 | #[inline] 160 | pub unsafe fn wrap_process_buf_option( 161 | initid: *mut UDF_INIT, 162 | args: *mut UDF_ARGS, 163 | result: *mut c_char, 164 | length: *mut c_ulong, 165 | is_null: *mut c_uchar, 166 | error: *mut c_uchar, 167 | ) -> *const c_char 168 | where 169 | W: UdfConverter, 170 | for<'a> U: BasicUdf = Option>, 171 | B: AsRef<[u8]>, 172 | { 173 | log_call!(enter: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error); 174 | 175 | let cfg = UdfCfg::from_raw_ptr(initid); 176 | let arglist = ArgList::from_raw_ptr(args); 177 | let err = *(error as *const Option); 178 | let mut b = cfg.retrieve_box::(); 179 | let proc_res = U::process(b.as_mut_ref(), cfg, arglist, err); 180 | let buf_opts = BufOptions::new(result, length); 181 | 182 | let post_effects_val = ret_callback_option(proc_res, error, is_null); 183 | 184 | let ret = match post_effects_val { 185 | Some(ref v) => buf_result_callback(v, &buf_opts), 186 | None => ptr::null(), 187 | }; 188 | 189 | std::mem::forget(post_effects_val); 190 | cfg.store_box(b); 191 | 192 | log_call!(exit: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error, ret); 193 | ret 194 | } 195 | 196 | #[inline] 197 | pub unsafe fn wrap_process_buf_option_ref( 198 | initid: *mut UDF_INIT, 199 | args: *mut UDF_ARGS, 200 | result: *mut c_char, 201 | length: *mut c_ulong, 202 | is_null: *mut c_uchar, 203 | error: *mut c_uchar, 204 | ) -> *const c_char 205 | where 206 | W: UdfConverter, 207 | for<'a> U: BasicUdf = Option<&'a B>>, 208 | B: AsRef<[u8]> + ?Sized, 209 | { 210 | log_call!(enter: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error); 211 | 212 | let cfg = UdfCfg::from_raw_ptr(initid); 213 | let arglist = ArgList::from_raw_ptr(args); 214 | let err = *(error as *const Option); 215 | let mut b = cfg.retrieve_box::(); 216 | let proc_res = U::process(b.as_mut_ref(), cfg, arglist, err); 217 | let buf_opts = BufOptions::new(result, length); 218 | 219 | let post_effects_val = ret_callback_option(proc_res, error, is_null); 220 | 221 | let ret = match post_effects_val { 222 | Some(ref v) => buf_result_callback(v, &buf_opts), 223 | None => ptr::null(), 224 | }; 225 | 226 | cfg.store_box(b); 227 | 228 | log_call!(exit: "process", U, &*initid, &*args, result, &*length, &*is_null, &*error, ret); 229 | ret 230 | } 231 | -------------------------------------------------------------------------------- /udf/src/wrapper/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::prelude::*; 3 | 4 | struct ExampleInt; 5 | struct ExampleIntOpt; 6 | struct ExampleBufRef; 7 | struct ExampleBufOpt; 8 | struct ExampleBufOptRef; 9 | 10 | impl BasicUdf for ExampleInt { 11 | type Returns<'a> = i64; 12 | 13 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 14 | todo!() 15 | } 16 | 17 | fn process<'a>( 18 | &'a mut self, 19 | _cfg: &UdfCfg, 20 | _args: &ArgList, 21 | _error: Option, 22 | ) -> Result, ProcessError> { 23 | todo!() 24 | } 25 | } 26 | impl BasicUdf for ExampleIntOpt { 27 | type Returns<'a> = Option; 28 | 29 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 30 | todo!() 31 | } 32 | 33 | fn process<'a>( 34 | &'a mut self, 35 | _cfg: &UdfCfg, 36 | _args: &ArgList, 37 | _error: Option, 38 | ) -> Result, ProcessError> { 39 | todo!() 40 | } 41 | } 42 | 43 | impl BasicUdf for ExampleBufRef { 44 | type Returns<'a> = &'a str; 45 | 46 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 47 | todo!() 48 | } 49 | 50 | fn process<'a>( 51 | &'a mut self, 52 | _cfg: &UdfCfg, 53 | _args: &ArgList, 54 | _error: Option, 55 | ) -> Result, ProcessError> { 56 | todo!() 57 | } 58 | } 59 | impl BasicUdf for ExampleBufOpt { 60 | type Returns<'a> = Option>; 61 | 62 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 63 | Ok(Self) 64 | } 65 | 66 | fn process<'a>( 67 | &'a mut self, 68 | _cfg: &UdfCfg, 69 | _args: &ArgList, 70 | _error: Option, 71 | ) -> Result, ProcessError> { 72 | Ok(Some(vec![1, 2, 3, 4])) 73 | } 74 | } 75 | 76 | impl AggregateUdf for ExampleBufOpt { 77 | fn clear( 78 | &mut self, 79 | _cfg: &UdfCfg, 80 | _error: Option, 81 | ) -> Result<(), NonZeroU8> { 82 | todo!() 83 | } 84 | 85 | fn add( 86 | &mut self, 87 | _cfg: &UdfCfg, 88 | _args: &ArgList, 89 | _error: Option, 90 | ) -> Result<(), NonZeroU8> { 91 | todo!() 92 | } 93 | } 94 | 95 | impl BasicUdf for ExampleBufOptRef { 96 | type Returns<'a> = Option<&'a str>; 97 | 98 | fn init(_cfg: &UdfCfg, _args: &ArgList) -> Result { 99 | todo!() 100 | } 101 | 102 | fn process<'a>( 103 | &'a mut self, 104 | _cfg: &UdfCfg, 105 | _args: &ArgList, 106 | _error: Option, 107 | ) -> Result, ProcessError> { 108 | todo!() 109 | } 110 | } 111 | 112 | #[test] 113 | #[allow(unreachable_code)] 114 | #[should_panic = "not yet implemented"] 115 | #[allow(clippy::diverging_sub_expression)] 116 | fn test_fn_sig() { 117 | // Just validate our function signatures with compile tests 118 | 119 | unsafe { 120 | wrap_process_basic::(todo!(), todo!(), todo!(), todo!()); 121 | wrap_process_basic_option::(todo!(), todo!(), todo!(), todo!()); 122 | wrap_process_buf::(todo!(), todo!(), todo!(), todo!(), todo!(), todo!()); 123 | wrap_process_buf_option::( 124 | todo!(), 125 | todo!(), 126 | todo!(), 127 | todo!(), 128 | todo!(), 129 | todo!(), 130 | ); 131 | wrap_process_buf_option_ref::( 132 | todo!(), 133 | todo!(), 134 | todo!(), 135 | todo!(), 136 | todo!(), 137 | todo!(), 138 | ); 139 | } 140 | } 141 | 142 | #[test] 143 | #[allow(unreachable_code)] 144 | #[should_panic = "not yet implemented"] 145 | #[allow(clippy::diverging_sub_expression)] 146 | fn test_wrapper_basic() { 147 | type ExampleIntWrapper = ExampleInt; 148 | unsafe { 149 | wrap_init::(todo!(), todo!(), todo!()); 150 | } 151 | } 152 | 153 | #[test] 154 | #[allow(unreachable_code)] 155 | #[should_panic = "not yet implemented"] 156 | #[allow(clippy::diverging_sub_expression)] 157 | fn test_wrapper_bufwrapper() { 158 | unsafe { 159 | wrap_init::(todo!(), todo!(), todo!()); 160 | } 161 | } 162 | 163 | #[test] 164 | fn test_verify_aggregate_attributes() { 165 | struct Foo; 166 | impl RegisteredBasicUdf for Foo { 167 | const NAME: &'static str = "foo"; 168 | const ALIASES: &'static [&'static str] = &["foo", "bar"]; 169 | const DEFAULT_NAME_USED: bool = false; 170 | } 171 | impl RegisteredAggregateUdf for Foo { 172 | const NAME: &'static str = "foo"; 173 | const ALIASES: &'static [&'static str] = &["foo", "bar"]; 174 | const DEFAULT_NAME_USED: bool = false; 175 | } 176 | 177 | verify_aggregate_attributes::(); 178 | } 179 | 180 | #[test] 181 | #[should_panic = "#[register]` on `BasicUdf` and `AggregateUdf` must have the same `name` \ 182 | argument; got `foo` and `bar`"] 183 | fn test_verify_aggregate_attributes_mismatch_name() { 184 | struct Foo; 185 | impl RegisteredBasicUdf for Foo { 186 | const NAME: &'static str = "foo"; 187 | const ALIASES: &'static [&'static str] = &["foo", "bar"]; 188 | const DEFAULT_NAME_USED: bool = false; 189 | } 190 | impl RegisteredAggregateUdf for Foo { 191 | const NAME: &'static str = "bar"; 192 | const ALIASES: &'static [&'static str] = &["foo", "bar"]; 193 | const DEFAULT_NAME_USED: bool = false; 194 | } 195 | 196 | verify_aggregate_attributes::(); 197 | } 198 | 199 | #[test] 200 | #[should_panic = "`#[register]` on `BasicUdf` and `AggregateUdf` must have the same `alias` \ 201 | arguments; got [`foo`, `bar`, `baz`] and [`foo`, `bar`]"] 202 | fn test_verify_aggregate_attributes_mismatch_aliases() { 203 | struct Foo; 204 | impl RegisteredBasicUdf for Foo { 205 | const NAME: &'static str = "foo"; 206 | const ALIASES: &'static [&'static str] = &["foo", "bar", "baz"]; 207 | const DEFAULT_NAME_USED: bool = false; 208 | } 209 | impl RegisteredAggregateUdf for Foo { 210 | const NAME: &'static str = "foo"; 211 | const ALIASES: &'static [&'static str] = &["foo", "bar"]; 212 | const DEFAULT_NAME_USED: bool = false; 213 | } 214 | 215 | verify_aggregate_attributes::(); 216 | } 217 | --------------------------------------------------------------------------------