diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb1df32..cc902c5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,116 +4,147 @@ on: push: tags: - "*" + workflow_dispatch: +permissions: + contents: read + +env: + (^_^): &interpreters "3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t pypy3.11" + ○_○: &interpreters_without_pypy "3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t" + (o_o): &interpreters_win_arm64 "3.11 3.12 3.13 3.14 3.14t 3.15 3.15t" + + UV_FROZEN: true + jobs: - linux: - runs-on: ${{ matrix.platform.runner }} + build-linux: + runs-on: ubuntu-latest strategy: matrix: platform: - { - runner: ubuntu-latest, - target: x86_64, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11 graalpy3.11 graalpy3.12", + target: x86_64-unknown-linux-gnu, + arch: x86_64, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: i686, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11", + target: i686-unknown-linux-gnu, + arch: x86, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: aarch64, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11 graalpy3.11 graalpy3.12", + target: aarch64-unknown-linux-gnu, + arch: aarch64, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: armv7, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + target: armv7-unknown-linux-gnueabihf, + arch: armv7, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: s390x, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + target: s390x-unknown-linux-gnu, + arch: s390x, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: ppc64le, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + target: powerpc64le-unknown-linux-gnu, + arch: ppc64le, + interpreter: *interpreters, } - { - runner: ubuntu-latest, target: riscv64gc-unknown-linux-gnu, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + arch: riscv64, + interpreter: *interpreters, } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@v6 + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: - python-version: "3.x" + activate-environment: true + enable-cache: false + python-version: "3" - - uses: PyO3/maturin-action@v1.51.0 + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter }} - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + sccache: ${{ github.ref_type != 'tag' }} manylinux: auto - rust-toolchain: nightly + args: >- + --release + --out dist + --interpreter ${{ matrix.platform.interpreter }} + --compatibility pypi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: wheels-linux-${{ matrix.platform.target }} + name: wheels-linux-${{ matrix.platform.arch }} path: dist - musllinux: - runs-on: ${{ matrix.platform.runner }} + build-musllinux: + runs-on: ubuntu-latest strategy: matrix: platform: - { - runner: ubuntu-latest, - target: x86_64, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11", + target: x86_64-unknown-linux-musl, + arch: x86_64, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: aarch64, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11", + target: i686-unknown-linux-musl, + arch: x86, + interpreter: *interpreters, } - { - runner: ubuntu-latest, - target: armv7, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t pypy3.11", + target: aarch64-unknown-linux-musl, + arch: aarch64, + interpreter: *interpreters, + } + - { + target: armv7-unknown-linux-musleabihf, + arch: armv7, + interpreter: *interpreters, + } + - { + target: riscv64gc-unknown-linux-musl, + arch: riscv64, + interpreter: *interpreters, } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@v6 + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: - python-version: "3.x" + activate-environment: true + enable-cache: false + python-version: "3" - - uses: PyO3/maturin-action@v1.51.0 + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter }} - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: musllinux_1_1 - rust-toolchain: nightly + sccache: ${{ github.ref_type != 'tag' }} + manylinux: musllinux_1_2 + args: >- + --release + --out dist + --interpreter ${{ matrix.platform.interpreter }} + --compatibility pypi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: wheels-musllinux-${{ matrix.platform.target }} + name: wheels-musllinux-${{ matrix.platform.arch }} path: dist - windows: + build-windows: runs-on: ${{ matrix.platform.runner }} strategy: matrix: @@ -121,108 +152,121 @@ jobs: - { runner: windows-latest, target: x64, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + arch: x64, + interpreter: *interpreters, } - { runner: windows-latest, target: x86, - interpreter: "3.10 3.11 3.12 3.13 3.14 3.14t", + arch: x86, + interpreter: *interpreters_without_pypy, + } + - { + runner: windows-11-arm, + target: aarch64, + arch: arm64, + interpreter: *interpreters_win_arm64, } + steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@v6.0.0 + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: 3.x - architecture: ${{ matrix.platform.target }} - - uses: PyO3/maturin-action@v1.49.4 + python-version: "3.x" + architecture: ${{ matrix.platform.arch }} + + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter }} - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - rust-toolchain: nightly + sccache: ${{ github.ref_type != 'tag' }} + args: >- + --release + --out dist + --interpreter ${{ matrix.platform.interpreter }} + --compatibility pypi - - uses: actions/upload-artifact@v4.6.2 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-windows-${{ matrix.platform.target }} path: dist - macos: - runs-on: ${{ matrix.platform.runner }} + build-macos: + runs-on: macos-latest strategy: matrix: platform: - - { - runner: macos-latest, - target: x86_64, - interpreter: "3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11", - } - - { - runner: macos-latest, - target: aarch64, - interpreter: "3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11", - } + - { target: x86_64, interpreter: *interpreters } + - { target: aarch64, interpreter: *interpreters } + steps: - - uses: actions/checkout@v5.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@v6.0.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: 3.x + python-version: "3.x" - - uses: PyO3/maturin-action@v1.49.4 + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --interpreter ${{ matrix.platform.interpreter }} - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - rust-toolchain: nightly + sccache: ${{ github.ref_type != 'tag' }} + args: >- + --release + --out dist + --interpreter ${{ matrix.platform.interpreter }} + --compatibility pypi - - uses: actions/upload-artifact@v4.6.2 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-macos-${{ matrix.platform.target }} path: dist + build-sdist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: PyO3/maturin-action@v1.51.0 + - name: Build sdist + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: command: sdist args: --out dist - rust-toolchain: nightly - - uses: actions/upload-artifact@v4 + - name: Upload sdist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: wheels-sdist path: dist release: runs-on: ubuntu-latest - if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} - needs: [linux, musllinux, windows, macos, build-sdist] + if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch' + needs: + - build-linux + - build-musllinux + - build-windows + - build-macos + - build-sdist + permissions: - id-token: write # signing release artifacts - contents: write # uploading release artifacts - attestations: write + id-token: write + contents: write steps: - - uses: actions/download-artifact@v5 - - - uses: actions/attest-build-provenance@v3 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - subject-path: "wheels-*/*" + pattern: wheels-* + merge-multiple: true + path: dist - name: Publish to PyPI - if: ${{ startsWith(github.ref, 'refs/tags/') }} - uses: PyO3/maturin-action@v1.51.0 - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - with: - command: upload - args: --non-interactive --skip-existing wheels-*/* - rust-toolchain: nightly + if: github.ref_type == 'tag' + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1822d4..cbae70b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,81 +1,131 @@ -name: Test +name: Python Test on: pull_request: - types: [opened, reopened] + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: permissions: contents: read +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +env: + RUST_BACKTRACE: 1 + UV_PYTHON_DOWNLOADS: 0 + jobs: test-python: - name: test ${{ matrix.python-version }} + if: github.event.pull_request.draft == false + + name: ${{ matrix.python-version }} + runs-on: ubuntu-latest + continue-on-error: ${{ endsWith(matrix.python-version, 't') }} strategy: fail-fast: false matrix: python-version: - - "3.10" - - "3.14" - - "3.14t" - - "pypy3.11" + [ + "3.10", + "3.11", + "3.12", + "3.13", + "3.14", + "3.14t", + "3.15", + "3.15t", + "pypy3.11", + ] - runs-on: ubuntu-latest - continue-on-error: ${{ endsWith(matrix.python-version, 't') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: install rust stable - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + if: github.event_name == 'pull_request' && github.run_attempt == 1 with: - toolchain: nightly + lookup-only: false + shared-key: ${{ matrix.python-version }} - - name: install uv - uses: astral-sh/setup-uv@v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true - - name: set up python - run: uv python install ${{ matrix.python-version }} + - name: "Install uv" + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: false - - name: install dependencies - run: uv sync --all-extras --dev - env: - RUST_BACKTRACE: 1 + - name: "Create and activate .venv" + run: | + uv venv .venv --python ${{ matrix.python-version }} + echo ".venv/bin" >> "$GITHUB_PATH" + echo "PYO3_PYTHON=.venv/bin/python3" >> "$GITHUB_ENV" + + - name: "Install dependencies" + run: uv sync --group dev + + - name: "Run maturin" + run: maturin develop - - name: run tests - run: uv run pytest -vv + - name: "Run pytest" + run: pytest -v --workers auto env: - RUST_BACKTRACE: 1 HYPOTHESIS_PROFILE: slow test-os: - name: test on ${{ matrix.os }} + if: github.event.pull_request.draft == false + + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: os: [ubuntu, macos, windows] - runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v4 - - - name: install rust stable - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - toolchain: nightly - - - name: install uv - uses: astral-sh/setup-uv@v6 + persist-credentials: false - - name: set up python - run: uv python install 3.13 + - uses: dtolnay/rust-toolchain@stable - - name: install dependencies - run: uv sync --all-extras --dev - env: - RUST_BACKTRACE: 1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" - - name: run tests - run: uv run pytest -vv + - name: "Install uv" + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: false + + - name: "Create and activate .venv" + shell: bash + run: | + uv venv .venv --python 3.13 + if [[ "${{ matrix.os }}" == "windows" ]]; then + echo ".venv/Scripts" >> "$GITHUB_PATH" + echo "PYO3_PYTHON=.venv/Scripts/python" >> "$GITHUB_ENV" + echo "UV_LINK_MODE=copy" >> "$GITHUB_ENV" + else + echo ".venv/bin" >> "$GITHUB_PATH" + echo "PYO3_PYTHON=.venv/bin/python3" >> "$GITHUB_ENV" + fi + + - name: "Install dependencies" + run: uv pip install --group dev + + - name: "Run maturin" + run: maturin develop + + - name: "Run pytest" + run: pytest -v -n auto env: - RUST_BACKTRACE: 1 HYPOTHESIS_PROFILE: slow diff --git a/.gitignore b/.gitignore index 965592f..13ca0d8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ /target __pycache__/ *.so -backup/ -backup-py/ /.vscode /*_cache +/.zed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bdb488a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +fail_fast: true + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files + + - repo: local + hooks: + - id: lint-python + name: Lint Python + entry: make lint-python + types: [python] + language: system + pass_filenames: false + + - id: lint-rust + name: Lint Rust + entry: make lint-rust + types: [rust] + language: system + pass_filenames: false diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..1017b98 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +imports_granularity = "Item" +single_line_let_else_max_width = 100 +max_width = 120 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a77863a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# How to contribute to RapidQuery + +## Issues + +- Did you find a bug? +- Do you have any questions? +- Do you have any feature requests? + +These are all welcome as [issues](https://github.com/pydantic/pydantic/issues/new). + +> For reporting a security vulnerability, please see [Security Policy](./SECURITY.md). + +If you're going to report a bug, please: + +- Include RapidQuery version you're working with. +- Include a minimal example if possible. +- Include a reference if the bug is about SQL syntax. + +## Pull Requests + +It should be extremely simple to get started and create a PR. All PRs are welcome. + +To make contributing as easy and fast as possible, you'll want to run tests and linting locally. + +If you're going to add a feature or make a change to SQL, our _current_ priority is things that are supported in at least 3 dialects or defined in Standard SQL. + +> Use `make format` to fix formatting, and `make` to run tests and linting. + +## AI Policy + +We welcome the use of AI when contributing to RapidQuery. + +But, you should certify that they fully understand the code being submitted. We reserve the right to close any pull request at our discretion, without further justification. This includes, but is not limited to, cases where: + +- the contribution does not meet our quality standards, +- the author appears to be mass-submitting pull requests across multiple repositories (spam), +- the pull request description is AI generated and incoherent or nonsensical. + +## Installation & Setup + +You'll need the following prerequisites: + +- Any Python version between Python 3.10 and 3.15 +- uv +- git - For version control +- make - For running development commands (or use nmake on Windows) +- Rust - Both stable and nightly (nightly is used for formatting) + +Fork the repository on GitHub and clone your fork locally. + +```bash +# Clone your fork and cd into the repo directory +git clone git@github.com:/rapidquery.git +cd rapidquery + +# Install UV +# for other options see: +# https://docs.astral.sh/uv/getting-started/installation/ +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Setup +make install +``` + +### Check out a new branch and make your changes + +Create a new branch for your changes. + +```bash +git switch -c my-new-feature-branch +``` + +### Run tests and linting + +Run tests and linting locally to make sure everything is working as expected. + +```bash +# Run automated code formatting and linting +make format + +# Run tests and linting +make +``` + +### Commit and push your changes + +Commit your changes, push your branch to GitHub, and create a drafted pull request. + +When your pull request is ready for review, add a comment with the message "please review" and we'll take a look as soon as we can. diff --git a/Cargo.lock b/Cargo.lock index b3a81d7..c4ff744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,69 +2,87 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] [[package]] -name = "arrayvec" -version = "0.7.6" +name = "arcstr" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitfield-struct" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "3ca6739863c590881f038d033a146c51ddae239186a4327014839fd864f44ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "bitflags" -version = "2.9.4" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cc" -version = "1.2.33" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -74,28 +92,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "equivalent" -version = "1.0.2" +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] -name = "getrandom" -version = "0.3.3" +name = "futures-util" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi", + "futures-core", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "foldhash", +] [[package]] name = "heck" @@ -105,9 +145,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -128,24 +168,12 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "2.12.0" +name = "inventory" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inherent" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "rustversion", ] [[package]] @@ -154,60 +182,48 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.175" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mac_address" -version = "1.1.8" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381" dependencies = [ "nix", - "winapi", + "windows-sys", ] -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - [[package]] name = "memoffset" version = "0.9.1" @@ -219,9 +235,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -241,24 +257,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -266,70 +273,69 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] -name = "pgvector" -version = "0.4.1" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.28.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c738662e2181be11cb82487628404254902bb3225d8e9e99c31f3ef82a405c" +checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655" dependencies = [ "chrono", + "hashbrown", + "inventory", "libc", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "rust_decimal", - "uuid", ] [[package]] name = "pyo3-build-config" -version = "0.28.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ca0864a7dd3c133a7f3f020cbff2e12e88420da854c35540fd20ce2d60e435" +checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb" dependencies = [ - "python3-dll-a", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfc1956b709823164763a34cc42bbfd26b8730afa77809a3df8b94a3ae3b059" +checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330" dependencies = [ "libc", "pyo3-build-config", @@ -337,100 +343,81 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29dc660ad948bae134d579661d08033fbb1918f4529c3bbe3257a68f2009ddf2" +checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pyo3-macros-backend" -version = "0.28.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78cd6c6d718acfcedf26c3d21fe0f053624368b0d44298c55d7138fde9331f7" +checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", - "syn", -] - -[[package]] -name = "python3-dll-a" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" -dependencies = [ - "cc", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "rapidquery" -version = "0.1.0" +version = "0.2.0-alpha1" dependencies = [ + "arcstr", + "bitfield-struct", "chrono", - "indexmap", + "hashbrown", "ipnetwork", "mac_address", + "once_cell", "parking_lot", - "pgvector", "pyo3", - "rust_decimal", + "pyo3-build-config", + "rapidquery-macros", "sea-query", - "serde_json", + "smallvec", + "sqlparser", "uuid", ] [[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +name = "rapidquery-macros" +version = "1.0.0-alpha1" dependencies = [ - "bitflags", + "proc-macro2", + "quote", + "syn 3.0.9", ] [[package]] -name = "rust_decimal" -version = "1.38.0" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "arrayvec", - "num-traits", + "bitflags", ] [[package]] name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "scopeguard" @@ -440,79 +427,53 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sea-query" -version = "0.32.7" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" -dependencies = [ - "chrono", - "inherent", - "ipnetwork", - "mac_address", - "ordered-float", - "pgvector", - "rust_decimal", - "serde_json", - "uuid", -] +checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31" [[package]] -name = "serde" -version = "1.0.226" +name = "shlex" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" -dependencies = [ - "serde_core", -] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] -name = "serde_core" -version = "1.0.226" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" -dependencies = [ - "serde_derive", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "serde_derive" -version = "1.0.226" +name = "smallvec" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] -name = "serde_json" -version = "1.0.142" +name = "sqlparser" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", + "log", ] [[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" +name = "syn" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "2.0.117" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -521,50 +482,27 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "uuid" -version = "1.18.1" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -573,25 +511,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -599,179 +523,90 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-link", ] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index cf4e1e4..527ed0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,67 +1,70 @@ [package] name = "rapidquery" -version = "0.1.0" -edition = "2021" +version = "0.2.0-alpha1" +edition = "2024" description = "RapidQuery is the fastest, full-feature, and easy-to-use Python SQL query builder written in Rust." -readme = "README.md" -license = "GNU GPLv3" homepage = "https://github.com/awolverp/rapidquery" repository = "https://github.com/awolverp/rapidquery.git" -authors = ["Ali Pooralijan "] +license = "MIT" + +[workspace] +members = ["rapidquery-macros"] [lib] -name = "rapidquery" crate-type = ["cdylib", "rlib"] - -[profile.dev] -lto = true -panic = "unwind" -strip = true - -[profile.release] -codegen-units = 1 -debug = false -incremental = false -lto = true -panic = "unwind" -strip = true +name = "rapidquery" [dependencies] -pyo3 = { version = "0.28", default-features = false, features = ["macros", "extension-module", "chrono", "uuid", "rust_decimal", "generate-import-lib"] } -pgvector = { version = "~0.4", default-features = false } +arcstr = { version = "1.2" } +bitfield-struct = { version = "0.13.0", default-features = false } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +hashbrown = { version = "0.17.1", default-features = false, features = ["default-hasher", "allocator-api2", "inline-more"] } ipnetwork = { version = "0.20", default-features = false } mac_address = { version = "1.1", default-features = false } -uuid = { version = "1.18.1", default-features = false, features = ["v4"] } -chrono = { version = "0.4.27", default-features = false, features = ["clock"] } -serde_json = { version = "1", default-features = false, features = ["std"] } -rust_decimal = { version = "1.38.0", default-features = false } -indexmap = { version = "2.12.0", default-features = false, features = ["std"]} -parking_lot = { version = "0.12.4", default-features = false } +once_cell = { version = "1.21.4", default-features = false } +parking_lot = { version = "0.12", default-features = false } +pyo3 = { version = "0.29", default-features = false, features = ["chrono", "hashbrown", "macros", "multiple-pymethods", "py-clone"] } +rapidquery-macros = { path = "rapidquery-macros" } +smallvec = { version = "1.16.0", default-features = false } +uuid = { version = "1.26.1", default-features = false } -[dependencies.sea-query] -version = "0.32.7" -default-features = false -features = [ - "backend-mysql", - "backend-postgres", - "backend-sqlite", - "thread-safe", - "with-chrono", - "with-json", - "with-rust_decimal", - "with-uuid", - "with-ipnetwork", - "with-mac_address", - "postgres-array", - "postgres-interval", - "postgres-vector", - "hashable-value" -] +# ONLY for inspiring the source +sea-query = { version = "1.0.2", default-features = false } +# ONLY for inspiring the source +sqlparser = { version = "0.62.0", default-features = false } + +[build-dependencies] +pyo3-build-config = "0.29.3" [features] -default = ["optimize"] -optimize = [] +default = ["extension-module"] +extension-module = ["pyo3/extension-module"] +nightly = ["hashbrown/nightly"] + +# Test harness only. `extension-module` does not link libpython, so Rust tests +# that touch Python use `--no-default-features --features auto-initialize`. +auto-initialize = ["pyo3/auto-initialize"] + +# Testing feature +only-orjson = [] +# Testing feature +only-stdlib-json = [] [lints.clippy] dbg_macro = "warn" +print_stderr = "warn" print_stdout = "warn" + +[profile.dev] +strip = false +lto = true +panic = "unwind" + +[profile.release] +opt-level = 3 +debug = false +strip = false +lto = true +panic = "abort" +incremental = false +codegen-units = 1 diff --git a/LICENSE b/LICENSE index f288702..0e064c3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2026 Ali Pooralijan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4d4aadc --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +# Customized version of https://github.com/pydantic/pydantic/raw/refs/heads/main/pydantic-core/Makefile + +.DEFAULT_GOAL := all + +uv-run-no-sync = uv run --no-sync + +# using pip install cargo (via maturin via pip) doesn't get the tty handle +# so doesn't render color without some help +export CARGO_TERM_COLOR=$(shell (test -t 0 && echo "always") || echo "auto") + +.PHONY: .uv ## Check that uv is installed +.uv: + @uv -V || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/' + +.PHONY: install ## Install the package, dependencies, and pre-commit for local development +install: .uv + uv sync --frozen --all-groups + uv run pre-commit install --install-hooks + +.PHONY: rebuild-lockfiles ## Rebuild lockfiles from scratch, updating all dependencies +rebuild-lockfiles: .uv + uv lock --upgrade + cargo update + +.PHONY: build-dev ## Build the development version of the package +build-dev: + @rm -f rapidquery/*.so + $(uv-run-no-sync) maturin develop --uv + +.PHONY: build-prod ## Build the production version of the package +build-prod: + @rm -f rapidquery/*.so + $(uv-run-no-sync) maturin develop --uv --release + +.PHONY: format ## Auto-format rust and python source files +format: + uv run ruff check --fix . + uv run ruff format . + cargo +nightly fmt + +.PHONY: lint-python ## Lint python source files +lint-python: + uv run ruff check . + uv run ruff format --check . + +.PHONY: lint-rust ## Lint rust source files +lint-rust: + cargo +nightly fmt --version + cargo +nightly fmt --all -- --check + cargo clippy --version + cargo clippy --tests -- -D warnings + +.PHONY: lint ## Lint rust and python source files +lint: lint-python lint-rust + +.PHONY: test ## Run all tests +test: + cargo test --workspace --all --no-default-features --features auto-initialize + uv run pytest -n auto + +.PHONY: all ## Run the standard set of checks performed in CI +all: format lint build-dev test + +.PHONY: clean ## Clear local caches and build artifacts +clean: + rm -rf `find . -name __pycache__` + rm -f `find . -type f -name '*.py[co]' ` + rm -f `find . -type f -name '*~' ` + rm -f `find . -type f -name '.*~' ` + rm -rf .pytest_cache + rm -rf .ruff_cache + rm -rf *.egg-info + rm -rf rapidquery/*.so + +.PHONY: help ## Display this message +help: + @grep -E \ + '^.PHONY: .*?## .*$$' $(MAKEFILE_LIST) | \ + sort | \ + awk 'BEGIN {FS = ".PHONY: |## "}; {printf "\033[36m%-19s\033[0m %s\n", $$2, $$3}' diff --git a/README.md b/README.md index 8a4b726..8cedefe 100644 --- a/README.md +++ b/README.md @@ -1,748 +1,54 @@ # RapidQuery -__*RapidQuery: High-Performance SQL Query Builder for Python*__ -RapidQuery is a powerful SQL query builder library designed for Python, combining the simplicity of Python with the raw speed of **Rust**. Build complex SQL queries effortlessly and efficiently, with a library that prioritizes both performance and ease of use. - -- [**Installation**](#install) -- [**Why RapidQuery?**](#why-rapidquery) -- [**Supported Backends**](#backends) -- [**Usage**](#usage) -- [**Performance**](#performance) -- [**Known Issues**](#known-issues) -- [**Supported Platforms**](#supported-platforms) -- [**License**](#license) - -## Install - -You can use **PIP**: -```bash -pip3 install rapidquery -``` - -Or you can use **UV** (recommended): -```bash -uv add rapidquery -``` - -## Why RapidQuery? -**Features**: -- **🧶 Thread safe**: It's completely thread-safe and uses locks in internal to prevent concurrency problems. -- **⚡ Blazing High Performance**: Leveraging the power of Rust under the hood, RapidQuery ensures your query building process is as fast as possible. -- 🛡️ **SQL Injection Protection**: Built-in security measures to prevent SQL injection attacks by default. -- 📝 **Intuitive Pythonic API**: Write clean, readable code with an API that feels natural to Python developers. -- 🔥 **Built on Solid Foundations**: RapidQuery is built with **Rust** and powered by the robust **SeaQuery** crate, bringing enterprise-grade reliability and performance to your Python applications. - -> **Why RapidQuery Was Created** \ -> In a landscape filled with SQL libraries, we noticed a critical gap: **performance was often an afterthought**. That's why we built RapidQuery with speed as our primary and enduring focus. - -## Backends -RapidQuery supports `PostgreSQL`, `MySQL`, and `SQLite`. These are referred to as `backend`s. When building SQL statements, you must specify your target backend. - -## Usage - -1. Core Concepts - 1. [**Value**](#value) - 2. [**Expr**](#expr) - 3. [**Statement Builders**](#statement-builders) -2. Query Statements - 1. [**Query Select**](#query-select) - 2. [**Query Insert**](#query-insert) - 3. [**Query Update**](#query-update) - 4. [**Query Delete**](#query-delete) - 5. [**Query With**](#query-with) -3. More About Queries - 1. [**Custom Function**](#custom-functions) -4. Schema Statements - 1. [**Table Create**](#table-create) - 2. [**Table Alter**](#table-alter) - 3. [**Table Drop**](#table-drop) - 4. [**Table Rename**](#table-rename) - 5. [**Table Truncate**](#table-truncate) - 8. [**Index Create**](#index-create) - 9. [**Index Drop**](#index-drop) -5. Advanced Usage - 1. [**More About Column References**](#more-about-column-references) - 2. [**More About TableName**](#more-about-tablename) - 3. [**More About Expr**](#more-about-expr) - -### Core Concepts -#### Value -Bridges Python types, Rust types, and SQL types for seamless data conversion. - -This class handles validation, adaptation, and conversion between different -type systems used in the application stack. - -> [!NOTE]\ -> this class is immutable and frozen. - -> [!TIP]\ -> **Important**: `Value` is lazy. This means it keeps your value and never converts it to Rust and then SQL until needed. - -#### Expr -Represents a SQL expression that can be built into SQL code. - -This class provides a fluent interface for constructing complex SQL expressions -in a database-agnostic way. It supports arithmetic operations, comparisons, -logical operations, and database-specific functions. - -The class automatically handles SQL injection protection and proper quoting -when building the final SQL statement. - -> [!NOTE]\ -> `Expr` is immutable, so by calling each method you will give a new instance -of it which includes new change(s). - -**Basic** -```python -import rapidquery as rp - -rp.Expr(25) # -> 25 (literal value) -rp.Expr("Hello") # -> 'Hello' (literal value) -rp.Expr(rq.Value('World')) # -> 'World' (literal value) - -rp.Expr.col("id") # -> "id" (column reference) -rp.Expr.col("users.name") # -> "users"."name" (column reference) -rp.Expr(rq.ColumnRef("name", table="users")) # -> "users"."name" (column reference) -``` - -**Comparisons** -```python -rq.Expr.col("status") == "active" # -> "status" == 'active' -rq.Expr.col("age") > 16 # -> "age" > 16 - -# Note that `rq.all` is different from built-in `all` -rq.all( - rq.Expr.col("age") >= 18, - rq.Expr.col("subscription").is_null(), # same as rq.Expr.col("subscription").is_(Expr.null()) - rq.Expr.col("status").in_(["pending", "approved", "active"]) -) # -> "age" >= 18 AND "subscription" IS NULL AND "status" IN ('pending', 'approved', 'active') - -# Note that `rq.any` is different from built-in `any` -rq.any( - rq.Expr.col("is_admin").is_(True), - rq.Expr.col("is_moderator").is_not_null(), # same as rq.Expr.col("subscription").is_not(Expr.null()) - rq.Expr.col("price").between(10.00, 50.00) -) # -> "is_admin" IS TRUE OR "is_moderator" IS NOT NULL OR "price" BETWEEN 10.00 AND 50.00 -``` - -**Best Practices** -- Always use `Expr.col()` for column references: This ensures proper quoting for your target database -```python -# Column reference (properly quoted identifier) -rq.Expr.col("user_name") # → "user_name" - -# String literal (value) -rq.Expr("user_name") # → 'user_name' -``` - -- Use `rapidquery.all()` and `rapidquery.any()` for logical combinations: More readable than chaining `&` and `|` operators -```python -# Good -all(condition1, condition2, condition3) - -# Less readable -condition1 & condition2 & condition3 -``` - -- Be careful with `Expr.custom()`: It bypasses all safety checks -```python -# Dangerous - vulnerable to SQL injection -user_input = "'; DROP TABLE users; --" -Expr.custom(f"name = '{user_input}'") - -# Safe -Expr.col("name") == user_input -``` - -#### Statement Builders -Statements are divided into 2 categories: `QueryStatement`, and `SchemaStatement`. - -Some statements like `Select`, `Update`, `Delete`, `Insert`, ... are `QueryStatement`. -Other statements like `Table`, `AlterTable`, `Index`, ... are `SchemaStatement`. - -`QueryStatement` class interface is: -```python -class QueryStatement: - """Subclass of query statements.""" - - def build(self, backend: _BackendName, /) -> tuple[str, tuple[Value, ...]]: - """Build the SQL statement with parameter values.""" - ... - - def to_sql(self, backend: _BackendName, /) -> str: - """ - Build a SQL string representation. - - **This method is unsafe and can cause SQL injection.** use `.build()` method instead. - """ - ... -``` - -`SchemaStatement` class interface is: -```python -class SchemaStatement: - """Subclass of schema statements.""" - - def to_sql(self, backend: _BackendName, /) -> str: - """Build a SQL string representation.""" - ... -``` - -### Query Select -Use `rapidquery.SelectStatement` type to generate `SELECT` statements. - -```python -import rapidquery as rq - -stmt = ( - rq.SelectStatement() - .columns("character", "fonts.name") - .from_table("characters") - .join("fonts", rq.Expr.col("characters.font_id") == rq.Expr.col("fonts.id"), "LEFT") - .where(rq.Expr.col("size_w").in_([3, 4])) - .where(rq.Expr.col("characters").like("A%")) -) -print(stmt.to_sql("postgres")) -# SELECT "character" AS "character", "fonts"."name" AS "name" FROM "characters" -# LEFT JOIN "fonts" ON "characters"."font_id" = "fonts"."id" -# WHERE "size_w" IN (3, 4) AND "characters" LIKE 'A%' - -print(stmt.to_sql("mysql")) -# SELECT `character` AS `character`, `fonts`.`name` AS `name` FROM `characters` -# LEFT JOIN `fonts` ON `characters`.`font_id` = `fonts`.`id` -# WHERE `size_w` IN (3, 4) AND `characters` LIKE 'A%' - -print(stmt.to_sql("sqlite")) -# SELECT "character" AS "character", "fonts"."name" AS "name" FROM "characters" -# LEFT JOIN "fonts" ON "characters"."font_id" = "fonts"."id" -# WHERE "size_w" IN (3, 4) AND "characters" LIKE 'A%' -``` - -### Query Insert -Use `rapidquery.InsertStatement` type to generate `INSERT` statements. - -```python -import rapidquery as rq - -stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.14, image="A4") - .on_conflict(rq.OnConflict("id").do_update("image")) -) - -print(stmt.to_sql("postgres")) -# INSERT INTO "glyph" ("aspect", "image") VALUES (3.14, 'A4') -# ON CONFLICT ("id") DO UPDATE SET "image" = "excluded"."image" -# -print(stmt.to_sql("mysql")) -# INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.14, 'A4') -# ON DUPLICATE KEY UPDATE `image` = VALUES(`image`) - -print(stmt.to_sql("sqlite")) -# INSERT INTO "glyph" ("aspect", "image") VALUES (3.14, 'A4') -# ON CONFLICT ("id") DO UPDATE SET "image" = "excluded"."image" -``` - -### Query Update -Use `rapidquery.UpdateStatement` type to generate `UPDATE` statements. - -```python -import rapidquery as rq - -stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image="123") - .where(rq.Expr.col("id") == 1) -) - -print(stmt.to_sql("postgres")) -# UPDATE "glyph" SET "aspect" = 1.23, "image" = '123' WHERE "id" = 1 - -print(stmt.to_sql("mysql")) -# UPDATE `glyph` SET `aspect` = 1.23, `image` = '123' WHERE `id` = 1 - -print(stmt.to_sql("sqlite")) -# UPDATE "glyph" SET "aspect" = 1.23, "image" = '123' WHERE "id" = 1 -``` - -### Query Delete -Use `rapidquery.DeleteStatement` type to generate `DELETE` statements. - -```python -import rapidquery as rq - -stmt = rq.DeleteStatement("glyph").where( - rq.any( - rq.Expr.col("id") < 1, - rq.Expr.col("id") > 10, - ) -) - -print(stmt.to_sql("postgres")) -# DELETE FROM "glyph" WHERE "id" < 1 OR "id" > 10 - -print(stmt.to_sql("mysql")) -# DELETE FROM `glyph` WHERE `id` < 1 OR `id` > 10 - -print(stmt.to_sql("sqlite")) -# DELETE FROM "glyph" WHERE "id" < 1 OR "id" > 10 -``` - -### Query With -We have two types here: `rapidquery.WithClause` and `rapidquery.WithQuery`. - -```txt - WithQuery - | -|------------------------| -WITH [... CTEs ...] QUERY -|------------------| - | - WithClause -``` - - -As you can see, `rapidquery.WithClause` includes common table expressions (CTEs), -and `rapidquery.WithQuery` includes `rapidquery.WithClause` and the final query. - -```python -import rapidquery as rq - -clause = ( - rq.WithClause() - .cte( - "users_count", - ( - rq.UpdateStatement("users") - .values(amount=rq.Expr.col("amount") + 10) - .where(rq.Expr.col("id") > 50) - .returning(rq.Returning(rq.Expr.val(1))) - ), - ) - .cte( - "teams_count", - ( - rq.UpdateStatement("teams") - .values(amount=rq.Expr.col("amount") + 10) - .where(rq.Expr.col("id") > 50) - .returning(rq.Returning(rq.Expr.val(1))) - ), - ) -) - -users_count_select = rq.SelectStatement(rq.Func.count(rq.Expr.asterisk())).from_table("users_count") -teams_count_select = rq.SelectStatement(rq.Func.count(rq.Expr.asterisk())).from_table("teams_count") - -query: WithQuery = clause.query( - rq.SelectStatement( - users_count_select.label("users"), - teams_count_select.label("teams"), - ) -) -query.to_sql("postgres") -# WITH -# "users_count" AS ( -# UPDATE "users" SET "amount" = "amount" + 10 WHERE "id" > 50 RETURNING 1 -# ) , -# "teams_count" AS ( -# UPDATE "teams" SET "amount" = "amount" + 10 WHERE "id" > 50 RETURNING 1 -# ) -# SELECT -# (SELECT COUNT(*) FROM "users_count") AS "users", -# (SELECT COUNT(*) FROM "teams_count") AS "teams" -``` - -### Custom Functions -For working with functions in RapidQuery, you have to use `Func` class. -A lot of functions such as `SUM`, `AVG`, `MD5`, ... is ready to use. For example: - -```python -stmt = rq.SelectStatement(rq.Func.sum(rq.Expr.col("amount"))) -stmt.to_sql("postgres") -# SELECT SUM("amount") -``` - -But for functions not provided by the library, you can define custom functions. -Custom functions can be defined using the `Func` constructor: - -```python -stmt = rq.SelectStatement(rq.Func("CUSTOM", 1, 'hello')) -stmt.to_sql("postgres") -# SELECT CUSTOM(1, 'hello') -``` - -### Table Create -`rapidquery.Table` represents a complete database table definition. Use it to generate `CREATE TABLE` statements. - -```python -import rapidquery as rq - -characters = rq.Table( - "characters", - rq.Column("id", rq.sqltypes.Integer(), primary_key=True, auto_increment=True), - rq.Column("font_size", rq.sqltypes.Integer(), nullable=False), - rq.Column("character", rq.sqltypes.String(), nullable=False), - rq.Column("size_w", rq.sqltypes.Integer(), nullable=False), - rq.Column("size_h", rq.sqltypes.Integer(), nullable=False), - rq.Column("font_id", rq.sqltypes.Integer(), default=None), - rq.ForeignKey(["font_id"], ["fonts.id"], on_delete="CASCADE", on_update="CASCADE"), - rq.Index("idx_character", ["character"]), - if_not_exists=True, -) - -print(characters.to_sql("postgresql")) -# CREATE TABLE IF NOT EXISTS "characters" ( -# "id" serial PRIMARY KEY, -# "font_size" integer NOT NULL, -# "character" varchar NOT NULL, -# "size_w" integer NOT NULL, -# "size_h" integer NOT NULL, -# "font_id" integer DEFAULT NULL, -# FOREIGN KEY ("font_id") REFERENCES "fonts" ("id") ON DELETE CASCADE ON UPDATE CASCADE -# ); -# CREATE INDEX IF NOT EXISTS "idx_character" ON "characters" ("character") -``` - -### Table Alter -Use `rapidquery.AlterTable` type to generate `ALTER TABLE` statements. - -```python -import rapidquery as rq - -stmt = rq.AlterTable( - "fonts", - [ - rq.AlterTableAddColumnOption( - rq.Column( - "new_col", - rq.sqltypes.Integer(), - nullable=False, - default=100, - ) - ), - rq.AlterTableRenameColumnOption("hello", "world"), - ], -) -print(stmt.to_sql("mysql")) -# ALTER TABLE `fonts` ADD COLUMN `new_col` int NOT NULL DEFAULT 100, -# RENAME COLUMN `hello` TO `world` -``` - -### Table Drop -Use `rapidquery.DropTable` type to generate `DROP TABLE` statements. +**_RapidQuery: High-Performance SQL Query Builder for Python_** -```python -import rapidquery as rq - -stmt = rq.DropTable("glyph", if_exists=True) -print(stmt.to_sql("mysql")) -# DROP TABLE IF EXISTS `glyph` -``` - -### Table Rename -Use `rapidquery.RenameTable` type to generate `RENAME TABLE` statements. - -```python -import rapidquery as rq - -stmt = rq.RenameTable("old", "new") - -print(stmt.to_sql("sqlite")) -# ALTER TABLE "old" RENAME TO "new" - -print(stmt.to_sql("mysql")) -# RENAME TABLE `old` TO `new` -``` - -### Table Truncate -Use `rapidquery.TruncateTable` type to generate `TRUNCATE TABLE` statements. - -```python -import rapidquery as rq - -stmt = rq.TruncateTable("old") - -print(stmt.to_sql("mysql")) -# TRUNCATE TABLE `old` -``` - -### Index Create - -```python -import rapidquery as rq - -idx = rq.Index("idx_glyph_aspect", ["aspect"], "glyph") - -print(idx.to_sql("postgres")) -# CREATE INDEX "idx_glyph_aspect" ON "glyph" ("aspect") -``` - -### Index Drop -Use `rapidquery.DropIndex` type to generate `DROP INDEX` statements. - -```python -import rapidquery as rq - -idx = rq.DropIndex("idx_glyph_aspect", "glyph") - -print(idx.to_sql("postgres")) -# DROP INDEX "idx_glyph_aspect" -``` - -### More About Column Reference -Let's learn some tricks about column references. - -In RapidQuery, we have something called `ColumnRef`, which represents a reference to a database column with optional table and schema qualification. - -> This type is a final type, which means you cannot use it as subclass. - -In generating statements, we have a lot of situations that you need to work with column references. - -**❗ The Trick** \ -For the parameters which accept column references, you have 4 ways: - -1. Use `ColumnRef`: -```python -col_ref = rq.ColumnRef("id", "characters") -# OR -col_ref = rq.ColumnRef.parse("characters.id") - -stmt = rq.SelectStatement().columns(col_ref) -# SELECT "characters"."id" AS "id" -``` - -2. Use `str`: The easiest way -```python -stmt = rq.SelectStatement().columns("characters.id") -# SELECT "characters"."id" AS "id" -``` - -3. Use `__column_ref__` property: developer-friendly and expandable way. -```python -class IdColumnProperty: - @property - def __column_ref__(self): - # Can return ColumnRef or str - return "characters.id" - -class IdColumnClassVar: - __column_ref__ = "characters.id" - -stmt = rq.SelectStatement().columns(IdColumnProperty()) -# SELECT "characters"."id" AS "id" - -stmt = rq.SelectStatement().columns(IdColumnClassVar) -# SELECT "characters"."id" AS "id" -``` - -4. Use `Column`: It's possible because `Column` has `__column_ref__` property. -```python -id = Column("id", rq.Integer()) - -stmt = rq.SelectStatement().columns(id) -# SELECT "id" AS "id" -``` - - -### More About TableName -Let's learn some tricks about table name. - -In RapidQuery, we have something called `TableName`, which represents a table name reference with optional schema, database, and alias. - -> This type is a final type, which means you cannot use it as subclass. - -In generating statements, we have situations that you need to specify table name. - -**❗ The Trick** \ -For the parameters which accept table name, you have 4 ways: - -1. Use `TableName`: -```python -tbl = rq.TableName("users", schema="archive") -# OR -tbl = rq.TableName.parse("archive.users") - -stmt = rq.DeleteStatement(tbl) -# DELETE FROM "archive"."users" -``` - -2. Use `str`: The easiest way -```python -stmt = rq.DeleteStatement("archive.users") -# DELETE FROM "archive"."users" -``` - -3. Use `__table_name__` property: developer-friendly and expandable way. -```python -class UsersProperty: - @property - def __table_name__(self): - # Can return TableName or str - return "archive.users" - - -class UsersClassVar: - __table_name__ = "archive.users" - - -stmt = rq.DeleteStatement(UsersProperty()) -# DELETE FROM "archive"."users" - -stmt = rq.DeleteStatement(UsersClassVar) -# DELETE FROM "archive"."users" -``` - -4. Use `Table`: It's possible because `Table` has `__table_name__` property. -```python -users = Table( - "archive.users", - ... -) - -stmt = rq.DeleteStatement(users) -# DELETE FROM "archive"."users" -``` - -### More About Expr -You learned [here](#expr) about `Expr` type. - -**❗ The Trick** \ -This 2 tricks are notable and very good to know. - -1. First, like `ColumnRef` and `TableName`, `Expr` supports `__expr__` property, which should always return `Expr`. - -```python -class TextClause: - def __init__(self, expr: str) -> None: - self.expr = expr - - @property - def __expr__(self) -> rq.Expr: - return rq.Expr.custom(self.expr) - - -stmt = rq.SelectStatement(TextClause("WOW!")) -# SELECT WOW! -``` - -2. Second, same as `ColumnRef`, `Expr` also supports `__column_ref__` property. - -## Performance -### Benchmarks -Benchmarks run on *Linux 6.18.12-1-MANJARO x86_64* with CPython 3.14. Your results may vary. - -Iterations per test: 100,000 \ -Python version: 3.14.3 - -``` -📊 SELECT Query Benchmark ----------------------------------------------------------------------- -Library Time (ms) vs Fastest Status ----------------------------------------------------------------------- -RapidQuery 245.44 1.00x (FASTEST) 🏆 -PyPika 4327.18 17.63x slower -SQLAlchemy 8818.33 35.93x slower ----------------------------------------------------------------------- - -📊 INSERT Query Benchmark ----------------------------------------------------------------------- -Library Time (ms) vs Fastest Status ----------------------------------------------------------------------- -RapidQuery 640.63 1.00x (FASTEST) 🏆 -PyPika 4655.51 7.27x slower -SQLAlchemy 7085.74 11.06x slower ----------------------------------------------------------------------- - -📊 UPDATE Query Benchmark ----------------------------------------------------------------------- -Library Time (ms) vs Fastest Status ----------------------------------------------------------------------- -RapidQuery 557.21 1.00x (FASTEST) 🏆 -PyPika 4488.96 8.06x slower -SQLAlchemy 11839.85 21.25x slower ----------------------------------------------------------------------- - -📊 DELETE Query Benchmark ----------------------------------------------------------------------- -Library Time (ms) vs Fastest Status ----------------------------------------------------------------------- -RapidQuery 441.38 1.00x (FASTEST) 🏆 -PyPika 4517.16 10.23x slower -SQLAlchemy 7924.52 17.95x slower ----------------------------------------------------------------------- -``` - -## Known Issues -### Unmanaged Rust panic output in building SQL -The library may encounter errors during SQL query construction, which are correctly raised as *RuntimeError* exceptions. For instance, this occurs when using a function that isn't supported by your target database. **While this error-raising behavior is intentional and logical, the issue is that unmanaged Rust panic information is also printed to stderr**. Currently, there is no way to suppress or manage this panic output. We are working to resolve this problem as much as possible in future updates. - -```python ->>> import rapidquery as rq ->>> stmt = rq.TruncateTable("users") ->>> print(stmt.to_sql("sqlite")) - -thread '' (14206) panicked at sea-query-0.32.7/src/backend/sqlite/table.rs:58:9: -Sqlite doesn't support TRUNCATE statement -Traceback (most recent call last): - File "", line 1, in - print(stmt.to_sql("sqlite")) - ~~~~~~~~~~~^^^^^^^^^^ -RuntimeError: build failed -``` - -### Missing `__init__` Calls -If a RapidQuery object is instantiated without calling its `__init__` method (e.g., via certain serialization tricks or `__new__` alone), the internal Rust pointer will be null. Accessing methods on such objects will cause an unmanaged Rust panic. Always use the provided constructors. - -### Join conditions are not optional -Currently, `JOIN` operations require an explicit ON or USING condition. - -## Supported Platforms - -Pre-built wheels are available for the following platforms and Python interpreters: - -### Linux (manylinux) - -| Architecture | Python Versions | -|---|---| -| x86_64 | 3.10 – 3.14, 3.14t, PyPy 3.11, GraalPy 3.11/3.12 | -| i686 | 3.10 – 3.14, 3.14t, PyPy 3.11 | -| aarch64 | 3.10 – 3.14, 3.14t, PyPy 3.11, GraalPy 3.11/3.12 | -| armv7 | 3.10 – 3.14, 3.14t | -| s390x | 3.10 – 3.14, 3.14t | -| ppc64le | 3.10 – 3.14, 3.14t | -| riscv64 | 3.10 – 3.14, 3.14t | - -### Linux (musllinux 1.1) - -| Architecture | Python Versions | -|---|---| -| x86_64 | 3.10 – 3.14, 3.14t, PyPy 3.11 | -| aarch64 | 3.10 – 3.14, 3.14t, PyPy 3.11 | -| armv7 | 3.10 – 3.14, 3.14t, PyPy 3.11 | - -### Windows - -| Architecture | Python Versions | -|---|---| -| x64 | 3.10 – 3.14, 3.14t | -| x86 | 3.10 – 3.14, 3.14t | - -### macOS - -| Architecture | Python Versions | -|---|---| -| x86_64 | 3.10 – 3.14, 3.13t, 3.14t, PyPy 3.11 | -| aarch64 (Apple Silicon) | 3.10 – 3.14, 3.13t, 3.14t, PyPy 3.11 | +RapidQuery is a powerful SQL query builder library designed for Python, combining the simplicity of Python with the raw speed of **Rust**. Build complex SQL queries effortlessly and efficiently, with a library that prioritizes both performance and ease of use. -A source distribution (sdist) is also published for platforms not listed above, which requires a Rust nightly toolchain to build from source. +> [!WARNING]\ +> This branch is under development and incomplete. ## TODO -- [x] Write tests -- [x] Update & automate workflows -- [x] Write CTE -- [x] Complete README.md -- [x] Bump version to 0.1.0 -- [ ] Complete backend-only functions -## License -This repository is licensed under the [GNU GPLv3 License](LICENSE) +ASTs until being merged into default branch. + +### DDL + +- [ ] CREATE SCHEMA +- [ ] DROP SCHEMA +- [ ] CREATE TABLE +- [ ] ALTER TABLE +- [ ] DROP TABLE +- [ ] CREATE VIEW +- [ ] DROP VIEW +- [ ] CREATE TYPE +- [ ] ALTER TYPE +- [ ] DROP TYPE +- [ ] CREATE SEQUENCE +- [ ] ALTER SEQUENCE +- [ ] DROP SEQUENCE +- [ ] CREATE FUNCTION +- [ ] ALTER FUNCTION +- [ ] DROP FUNCTION + +### DCL + +- [ ] GRANT +- [ ] REVOKE + +### DML + +- [ ] INSERT +- [ ] UPDATE +- [ ] DELETE +- [ ] MERGE +- [ ] TRUNCATE TABLE +- [ ] SELECT + +### TCL + +- [ ] START TRANSACTION +- [ ] COMMIT +- [ ] ROLLBACK +- [ ] SAVEPOINT +- [ ] RELEASE SAVEPOINT +- [ ] SET TRANSACTION diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..570f1ee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Reporting a Vulnerability + +**DO NOT OPEN A PUBLIC ISSUE** if you think you have found a security issue with RapidQuery. + +Instead, navigate to "Security & quality" tab for the repository and use "Report a vulnerability" button. + +Be sure to: + +- Describe the issue clearly and +- include as much detail as necessary in your report. +- Also a minimal example will help to find the issue faster. + +Thank you for helping. diff --git a/benchmarks.py b/benchmarks.py deleted file mode 100644 index e6b8c80..0000000 --- a/benchmarks.py +++ /dev/null @@ -1,230 +0,0 @@ -import sys -import time -import typing - -import pypika -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import dialect - -import rapidquery as rq - -# Postgres dialect is faster than other dialects (according to benchmarks) -# and also providing dialect in SQLALchemy can make it faster -SA_DIALECT = dialect() - -# Benchmark configuration -ITERATIONS = 100_000 -WARMUP_ITERATIONS = 100 - - -def benchmark(func: typing.Callable, number=ITERATIONS) -> float: - for _ in range(min(WARMUP_ITERATIONS, number // 10)): - func() - - perf = time.perf_counter_ns() - for _ in range(number): - func() - perf = time.perf_counter_ns() - perf - - return perf / 1000000 - - -def format_results(results: typing.Dict[str, float]) -> str: - if not results: - return "No results to display" - - # Find fastest time - fastest = min(results.values()) - - lines = [] - lines.append("-" * 70) - lines.append(f"{'Library':<20} {'Time (ms)':<15} {'vs Fastest':<15} {'Status':<20}") - lines.append("-" * 70) - - for lib, time_ms in sorted(results.items(), key=lambda x: x[1]): - if time_ms == fastest: - ratio = "1.00x (FASTEST)" - status = "🏆" - else: - ratio = f"{time_ms / fastest:.2f}x slower" - status = "" - - lines.append(f"{lib:<20} {time_ms:>10.2f} {ratio:<15} {status}") - - lines.append("-" * 70) - return "\n".join(lines) - - -# SELECT Query Benchmarks - - -def bench_select_rapidquery(): - query = ( - rq.SelectStatement(rq.Expr.asterisk()) - .from_table("users") - .where(rq.Expr.col("name").like(r"%linus%")) - .offset(20) - .limit(20) - ) - query.to_sql("postgresql") - - -_users_tb = sa.table("users") - - -def bench_select_sqlalchemy(): - query = ( - sa.select(sa.text("*")) - .select_from(_users_tb) - .where(sa.column("name").like(r"%linus%")) - .offset(20) - .limit(20) - ) - str(query.compile(dialect=SA_DIALECT, compile_kwargs={"literal_binds": True})) - - -def bench_select_pypika(): - query = ( - pypika.Query.from_("users") - .where(pypika.Field("name").like(r"%linus%")) - .offset(20) - .limit(20) - .select("*") - ) - str(query) - - -# INSERT Query Benchmarks - - -def bench_insert_rapidquery(): - query = ( - rq.InsertStatement("glyph") - .columns("aspect", "image") - .values(5.15, "12A") - .values(16, "14A") - .returning(rq.query.Returning("id")) - ) - query.to_sql("postgresql") - - -sa_glyph = sa.table("glyph", sa.column("aspect", sa.Float), sa.column("image", sa.String)) - - -def bench_insert_sqlalchemy(): - query = sa.insert(sa_glyph).values( - [{"aspect": 5.15, "image": "12A"}, {"aspect": 16, "image": "14A"}] - ) - str(query.compile(dialect=SA_DIALECT, compile_kwargs={"literal_binds": True})) - - -def bench_insert_pypika(): - query = ( - pypika.Query.into("glyph").columns("aspect", "image").insert(5.15, "12A").insert(16, "14A") - ) - str(query) - - -# UPDATE Query Benchmarks - - -def bench_update_rapidquery(): - query = ( - rq.UpdateStatement("wallets") - .values(amount=rq.Expr.col("amount") + 10) - .where(rq.Expr.col("id").between(10, 30)) - ) - query.to_sql("postgresql") - - -sa_wallets = sa.table("wallets", sa.column("amount", sa.Integer), sa.column("id", sa.Integer)) - - -def bench_update_sqlalchemy(): - query = ( - sa.update(sa_wallets) - .values(amount=sa_wallets.c.amount + 10) - .where(sa.between(sa_wallets.c.id, 10, 30)) - ) - str(query.compile(dialect=SA_DIALECT, compile_kwargs={"literal_binds": True})) - - -def bench_update_pypika(): - query = ( - pypika.Query.update("wallets") - .set("amount", pypika.Field("amount") + 10) - .where(pypika.Field("id").between(10, 30)) - ) - str(query) - - -# DELETE Query Benchmarks - - -def bench_delete_rapidquery(): - query = rq.DeleteStatement("users").where( - rq.all( - rq.Expr.col("id") > 10, - rq.Expr.col("id") < 30, - ) - ) - query.to_sql("postgresql") - - -sa_users = sa.table("users", sa.column("id", sa.Integer)) - - -def bench_delete_sqlalchemy(): - query = sa.delete(sa_users).where(sa.and_(sa_users.c.id > 10, sa_users.c.id < 30)) - str(query.compile(dialect=SA_DIALECT, compile_kwargs={"literal_binds": True})) - - -def bench_delete_pypika(): - query = ( - pypika.Query.from_("users") - .where((pypika.Field("id") > 10) & (pypika.Field("id") < 30)) - .delete() - ) - str(query) - - -def run_benchmarks(): - print(f"Iterations per test: {ITERATIONS:,}") - print(f"Python version: {sys.version.split()[0]}") - print() - - print("\n📊 SELECT Query Benchmark") - results = { - "RapidQuery": benchmark(bench_select_rapidquery), - "SQLAlchemy": benchmark(bench_select_sqlalchemy), - "PyPika": benchmark(bench_select_pypika), - } - print(format_results(results)) - - print("\n📊 INSERT Query Benchmark") - results = { - "RapidQuery": benchmark(bench_insert_rapidquery), - "SQLAlchemy": benchmark(bench_insert_sqlalchemy), - "PyPika": benchmark(bench_insert_pypika), - } - print(format_results(results)) - - print("\n📊 UPDATE Query Benchmark") - results = { - "RapidQuery": benchmark(bench_update_rapidquery), - "SQLAlchemy": benchmark(bench_update_sqlalchemy), - "PyPika": benchmark(bench_update_pypika), - } - print(format_results(results)) - - print("\n📊 DELETE Query Benchmark") - results = { - "RapidQuery": benchmark(bench_delete_rapidquery), - "SQLAlchemy": benchmark(bench_delete_sqlalchemy), - "PyPika": benchmark(bench_delete_pypika), - } - print(format_results(results)) - - -if __name__ == "__main__": - run_benchmarks() diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..0475124 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} diff --git a/develop.sh b/develop.sh deleted file mode 100755 index 4a7b75f..0000000 --- a/develop.sh +++ /dev/null @@ -1,90 +0,0 @@ -function help() { - echo -e "RapidQuery Project Management\n" - echo -e " build build source" - echo -e " test run mypy & python tests" - echo -e " clippy run rust tests (check, clippy)" - echo -e " fmt format rust & python codes" - echo -e " bench run benchmarks" - echo -e "" - echo -e "Uses debug mode on default, use -p command to switch to production mode" -} - -function build() { - if [[ "$1" == "-p" ]]; then - echo "Building sources (production mode) ..." - UV_OFFLINE=1 maturin develop --uv --release - else - echo "Building sources (debug mode) ..." - UV_OFFLINE=1 maturin develop --uv - fi -} - -function test() { - echo "Running tests ..." - - pytest -s -vv - rm -rf .pytest_cache 2>/dev/null -} - -function clippy() { - if [[ "$1" == "-p" ]]; then - echo "Running checks (production mode) ..." - - cargo check --release - cargo clippy --release - ruff check . - mypy rapidquery --disable-error-code type-arg --strict - else - echo "Running checks (debug mode) ..." - - cargo check - cargo clippy - ruff check . - mypy rapidquery --disable-error-code type-arg --strict - fi -} - -function fmt() { - echo "Formatting codes ..." - - cargo fmt - ruff format --line-length=100 . - ruff clean -} - -function bench() { - echo "Running benchmark ..." - - build -p - python3 benchmarks.py -} - -case "${1:-help}" in - help) - help - ;; - build) - shift - build $@ - ;; - test) - shift - test $@ - ;; - clippy) - shift - clippy $@ - ;; - fmt) - shift - fmt $@ - ;; - bench) - shift - bench $@ - ;; - *) - echo -e "Unknown command: $1. Use 'help' command to see help menu." - exit 1 - ;; -esac diff --git a/pyproject.toml b/pyproject.toml index 3c34094..d58703a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,49 +1,95 @@ -[build-system] -requires = ["maturin>=1.9,<2.0"] -build-backend = "maturin" - [project] name = "rapidquery" -requires-python = ">=3.8" -keywords = ["SQL", "dynamic-sql", "sql-builder", "rapidorm", "seaql", "pypika", "orm", "rapidquery"] -authors = [ - {name = "awolverp", email = "awolverp@gmail.com"} +requires-python = ">=3.10" +authors = [{ name = "Ali Pooralijan", email = "awolverp@gmail.com" }] +keywords = [ + "SQL", + "dynamic-sql", + "orm", + "pypika", + "rapidorm", + "rapidquery", + "sql-builder", ] classifiers = [ - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python", - "Programming Language :: Rust", - "Intended Audience :: Developers", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS", - "Typing :: Typed", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Rust", + "Topic :: Software Development :: Libraries", + "Typing :: Typed", ] -dynamic = [ - 'description', - 'license', - 'readme', - 'version' -] +dynamic = ["description", "license", "readme", "version"] [project.urls] -Homepage = 'https://github.com/awolverp/rapidquery' +Homepage = "https://github.com/awolverp/rapidquery" +Issues = "https://github.com/awolverp/rapidquery/issues" +Repository = "https://github.com/awolverp/rapidquery" -[tool.maturin] -features = ["pyo3/extension-module"] -module-name = "rapidquery._lib" +[project.optional-dependencies] +orjson = [ + "orjson>=3.0.0", +] [dependency-groups] dev = [ - "maturin>=1.9.4", - "pytest>=8.3.5", + { include-group = "build" }, + { include-group = "lint" }, + { include-group = "test" }, +] + +build = ["maturin == 1.15.0"] +lint = [ + "mypy>=2.3.1", + "pre-commit>=4.6.2", + "ruff>=0.16.9", +] +test = [ + "hypothesis>=6.0.0", + "pytest>=8.0.0", + "pytest-xdist>=3.8.0", +] + +[build-system] +requires = ["maturin == 1.15.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["pyo3/extension-module"] +module-name = "rapidquery._core" + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +ignore = ["DTZ00"] +select = ["RUF022"] + +[tool.uv] +required-version = ">=0.10.0" +cache-keys = [ + { file = "pyproject.toml" }, + { file = "Cargo.toml" }, + { file = "Cargo.lock" }, + { dir = ".cargo" }, + { dir = "src" }, + { dir = "rapidquery" }, + { env = "MATURIN_PEP517_ARGS" }, + { env = "RUSTFLAGS" }, ] diff --git a/rapidquery-macros/Cargo.toml b/rapidquery-macros/Cargo.toml new file mode 100644 index 0000000..cd80df6 --- /dev/null +++ b/rapidquery-macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rapidquery-macros" +version = "1.0.0-alpha1" +edition = "2024" +description = "Procedural macros for RapidQuery." +homepage = "https://github.com/awolverp/rapidquery" +repository = "https://github.com/awolverp/rapidquery.git" +license = "MIT" + +[lib] +path = "lib.rs" +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "3", features = ["default", "full"] } diff --git a/rapidquery-macros/dialect_options.rs b/rapidquery-macros/dialect_options.rs new file mode 100644 index 0000000..d8e4dd2 --- /dev/null +++ b/rapidquery-macros/dialect_options.rs @@ -0,0 +1,189 @@ +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use syn::Token; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; + +struct Options { + flags: Vec, +} + +enum Flag { + Char { field: syn::Ident, default: syn::LitChar }, + Enum(EnumFlag), + Bool { field: syn::Ident, default: syn::Expr }, +} + +struct EnumFlag { + ty: syn::Ident, + field: syn::Ident, + variants: Vec, +} + +struct Variant { + name: syn::Ident, + opt: syn::Ident, +} + +impl Parse for Variant { + fn parse(input: ParseStream) -> syn::Result { + let name: syn::Ident = input.parse()?; + input.parse::]>()?; + let opt: syn::Ident = input.parse()?; + Ok(Self { name, opt }) + } +} + +impl Parse for EnumFlag { + fn parse(input: ParseStream) -> syn::Result { + let ty: syn::Ident = input.parse()?; + let field: syn::Ident = input.parse()?; + let body; + syn::braced!(body in input); + let variants: Punctuated = body.parse_terminated(Variant::parse, Token![,])?; + if variants.is_empty() { + return Err(syn::Error::new(field.span(), "flag enum needs at least one variant")); + } + Ok(Self { + ty, + field, + variants: variants.into_iter().collect(), + }) + } +} + +impl Parse for Options { + fn parse(input: ParseStream) -> syn::Result { + let mut flags = Vec::new(); + while !input.is_empty() { + if input.peek(Token![enum]) { + input.parse::()?; + flags.push(Flag::Enum(input.parse()?)); + continue; + } + + let kind: syn::Ident = input.parse()?; + if kind == "char" { + let field: syn::Ident = input.parse()?; + input.parse::()?; + let default: syn::LitChar = input.parse()?; + input.parse::()?; + flags.push(Flag::Char { field, default }); + } else if kind == "bool" { + let field: syn::Ident = input.parse()?; + input.parse::()?; + let default: syn::Expr = input.parse()?; + input.parse::()?; + flags.push(Flag::Bool { field, default }); + } else { + return Err(syn::Error::new(kind.span(), "expected `char`, `enum`, or `bool`")); + } + } + Ok(Self { flags }) + } +} + +fn setter(field: &syn::Ident) -> syn::Ident { + syn::Ident::new(&format!("with_{field}"), field.span()) +} + +pub fn generate(input: TokenStream) -> syn::Result { + let options: Options = syn::parse2(input)?; + + let mut consts = TokenStream::new(); + let mut exports = TokenStream::new(); + let mut params = TokenStream::new(); + let mut args = TokenStream::new(); + let mut checks = TokenStream::new(); + let mut setters = TokenStream::new(); + let mut getters = TokenStream::new(); + + for flag in &options.flags { + match flag { + Flag::Char { field, default } => { + let set = setter(field); + let get = format_ident!("get_{field}"); + params.extend(quote! { #field = #default, }); + args.extend(quote! { #field: char, }); + setters.extend(quote! { .#set(#field as u32) }); + getters.extend(quote! { + #[getter] + fn #get(&self) -> char { + // SAFETY: the constructor stores a `char` cast to `u32`. + unsafe { char::from_u32_unchecked(self.0.#field()) } + } + }); + } + Flag::Enum(flag) => { + let ty = &flag.ty; + let field = &flag.field; + let set = setter(field); + let get = format_ident!("get_{field}"); + params.extend(quote! { #field = 0, }); + args.extend(quote! { #field: u8, }); + checks.extend(quote! { let #field = flags::#ty::from_bits(#field); }); + setters.extend(quote! { .#set(#field) }); + getters.extend(quote! { + #[getter] + fn #get(&self) -> u8 { + self.0.#field().into_bits() + } + }); + for variant in &flag.variants { + let name = &variant.name; + let opt = &variant.opt; + consts.extend(quote! { + pub(super) const #opt: u8 = flags::#ty::#name.into_bits(); + }); + exports.extend(quote! { + module.add(stringify!(#opt), #opt)?; + }); + } + } + Flag::Bool { field, default } => { + let set = setter(field); + let get = format_ident!("get_{field}"); + params.extend(quote! { #field = #default, }); + args.extend(quote! { #field: bool, }); + setters.extend(quote! { .#set(#field) }); + getters.extend(quote! { + #[getter] + fn #get(&self) -> bool { + self.0.#field() + } + }); + } + } + } + + Ok(quote! { + #consts + + pub(super) fn export_options( + module: &pyo3::Bound<'_, pyo3::types::PyModule>, + ) -> pyo3::PyResult<()> { + use pyo3::types::PyModuleMethods; + #exports + Ok(()) + } + + #[pyo3::pymethods] + impl PyDialect { + #[new] + #[pyo3(signature = (*, #params))] + #[allow( + clippy::too_many_arguments, + clippy::fn_params_excessive_bools, + clippy::cognitive_complexity + )] + fn __new__(#args) -> pyo3::PyResult { + #checks + Ok(Self(flags::DialectFlags::new() #setters)) + } + + #getters + } + }) +} diff --git a/rapidquery-macros/lib.rs b/rapidquery-macros/lib.rs new file mode 100644 index 0000000..b7fc4c3 --- /dev/null +++ b/rapidquery-macros/lib.rs @@ -0,0 +1,382 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::ItemStruct; +use syn::parse::Parse; +use syn::parse::ParseStream; + +mod dialect_options; + +struct PyClassInput { + name: String, + module: String, + meta_args: Vec, +} + +impl Parse for PyClassInput { + fn parse(input: ParseStream) -> syn::Result { + let first: syn::LitStr = input.parse()?; + let (module, name) = if input.peek(syn::Token![+]) { + // Concat syntax + // + // "module" + "name" + input.parse::()?; + let class_name: syn::LitStr = input.parse()?; + let module = first.value(); + let module = module.strip_suffix('.').unwrap_or(&module); + if module.is_empty() || class_name.value().is_empty() { + return Err(syn::Error::new( + class_name.span(), + "expected \"module\" + \"Class\" with non-empty module and class name", + )); + } + (module.to_owned(), class_name.value()) + } else { + // Literal string syntax + // + // "module.name" + let value = first.value(); + let Some((module, name)) = value.rsplit_once('.') else { + return Err(syn::Error::new( + first.span(), + "expected \"module.Class\" or \"module\" + \"Class\"; module is required", + )); + }; + if module.is_empty() || name.is_empty() || module.split('.').any(|s| s.is_empty()) { + return Err(syn::Error::new( + first.span(), + "expected \"module.Class\" with non-empty module and class name", + )); + } + (module.to_owned(), name.to_owned()) + }; + + let mut meta_args = Vec::new(); + while input.peek(syn::token::Comma) { + input.parse::()?; + if input.is_empty() { + break; + } + meta_args.push(input.parse()?); + } + + Ok(Self { + name, + module, + meta_args, + }) + } +} + +/// Defines a pyo3 class with a required module path. +/// +/// ```ignore +/// #[pyclass("foo.Bar")] +/// struct Bar; +/// ``` +/// +/// ```ignore +/// #[pyclass("foo" + "Bar", subclass)] +/// struct Bar; +/// ``` +#[proc_macro_attribute] +pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream { + let PyClassInput { + name, + module, + meta_args, + } = syn::parse_macro_input!(attr as PyClassInput); + + let item = syn::parse_macro_input!(item as ItemStruct); + + // Fresh tokens, no input span, so rustc cannot substitute the dotted literal. + let args: proc_macro2::TokenStream = format!("module = \"{module}\", name = \"{name}\", immutable_type, frozen") + .parse() + .expect("pyclass args"); + + let extra = if meta_args.is_empty() { + quote! {} + } else { + quote! { , #(#meta_args),* } + }; + + quote! { + #[pyo3::pyclass(#args #extra)] + #item + } + .into() +} + +struct PyErrInput { + return_err_enum: bool, + name: syn::Ident, + msg_or_fmt: Option, + format_args: Vec, +} + +impl Parse for PyErrInput { + /// Acceptable formats: + /// - `#name` + /// - `Err(#name)` + /// - `Err(#name), #msg` + /// - `Err(#name), #fmt, #args...` + /// - `#name, #msg` + /// - `#name, #fmt, #args...` + fn parse(input: ParseStream<'_>) -> syn::Result { + let ident: syn::Ident = input.parse()?; + let (wrap_in_err, name) = if ident == "Err" && input.peek(syn::token::Paren) { + let inner; + syn::parenthesized!(inner in input); + let name: syn::Ident = inner.parse()?; + inner.parse::()?; + (true, name) + } else { + (false, ident) + }; + + let mut slf = Self { + return_err_enum: wrap_in_err, + name, + msg_or_fmt: None, + format_args: vec![], + }; + + if input.is_empty() { + return Ok(slf); + } + + input.parse::()?; + + slf.msg_or_fmt = Some(input.parse()?); + + while input.peek(syn::token::Comma) { + input.parse::()?; + + if input.is_empty() { + break; + } + + slf.format_args.push(input.parse()?); + } + + Ok(slf) + } +} + +/// Builds a [`PyErr`], or `Err(PyErr)` when the name is wrapped in `Err`. +/// +/// ```ignore +/// pyerr!(PyTypeError, "bad {}", x) +/// pyerr!(Err(PyValueError)) +/// ``` +#[proc_macro] +pub fn pyerr(item: TokenStream) -> TokenStream { + let PyErrInput { + return_err_enum, + name, + msg_or_fmt, + format_args, + } = syn::parse_macro_input!(item as PyErrInput); + + let constructed = match (msg_or_fmt, format_args.is_empty()) { + (Some(a), false) => { + quote! { ::pyo3::exceptions::#name::new_err(format!(#a, #(#format_args),*)) } + } + (Some(a), true) => { + quote! { ::pyo3::exceptions::#name::new_err(#a) } + } + (None, _) => quote! { ::pyo3::exceptions::#name::new_err(()) }, + }; + + if return_err_enum { + quote! { Err(#constructed) }.into() + } else { + constructed.into() + } +} + +/// Constructor arguments, getters, and `OPT_*` constants for dialect flags. +/// +/// Invoked from `src/dialect/base.rs`. Each item is `char name = '?';`, +/// `enum Type name { Variant => OPT_..., }`, or `bool name = false;`. +#[proc_macro] +pub fn dialect_options(input: TokenStream) -> TokenStream { + match dialect_options::generate(input.into()) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +struct CustomizationProtocolInput { + bound_object: syn::Ident, + attribute_name: syn::LitCStr, + pyobject_type: syn::Path, + ok_expr: syn::Expr, + err_expr: syn::Expr, +} + +impl Parse for CustomizationProtocolInput { + fn parse(input: ParseStream) -> syn::Result { + // on: $bound:ident, + let on_label: syn::Ident = input.parse()?; + if on_label != "on" { + return Err(input.error("expected `on`")); + } + input.parse::()?; + let bound_object: syn::Ident = input.parse()?; + input.parse::()?; + + // attr: $attribute_name:cstr, + let attr_label: syn::Ident = input.parse()?; + if attr_label != "attr" { + return Err(input.error("expected `attr`")); + } + input.parse::()?; + let attribute_name: syn::LitCStr = input.parse()?; + input.parse::()?; + + // ty: $pyobject_type:path, + let ty_label: syn::Ident = input.parse()?; + if ty_label != "ty" { + return Err(input.error("expected `ty`")); + } + input.parse::()?; + let pyobject_type: syn::Path = input.parse()?; + input.parse::()?; + + // matches: $ok_expr:expr, + let matches_label: syn::Ident = input.parse()?; + if matches_label != "matches" { + return Err(input.error("expected `matches`")); + } + input.parse::()?; + let ok_expr: syn::Expr = input.parse()?; + input.parse::()?; + + // mismatches: $err_expr:expr + let mismatches_label: syn::Ident = input.parse()?; + if mismatches_label != "mismatches" { + return Err(input.error("expected `mismatches`")); + } + input.parse::()?; + let err_expr: syn::Expr = input.parse()?; + + // Optional trailing comma + if input.peek(syn::Token![,]) { + input.parse::()?; + } + + Ok(Self { + bound_object, + attribute_name, + pyobject_type, + ok_expr, + err_expr, + }) + } +} + +/// Builds a customization protocol checking. +/// +/// Note: you can access to attribute result using `protocol` variable. +/// +/// For example: +/// ```ignore +/// customization_protocol! { +/// on: bound, +/// attr: c"__serializer__", +/// ty: crate::typeref::SERIALIZER_TYPE, +/// matches: { +/// // OK expr +/// }, +/// mismatches: { +/// // ERR expr +/// } +/// } +/// ``` +/// +/// becomes: +/// ```ignore +/// unsafe { +/// const PROTOCOL: &'static std::ffi::CStr = c"__serializer__"; +/// +/// let result = { +/// let attribute: *mut pyo3::ffi::PyObject = +/// pyo3::ffi::PyObject_GetAttrString(bound.as_ptr(), PROTOCOL.as_ptr()); +/// +/// if attribute.is_null() { +/// if pyo3::ffi::PyErr_ExceptionMatches(pyo3::ffi::PyExc_AttributeError) != 1 { +/// return Err(pyo3::PyErr::fetch(bound.py())); +/// } else { +/// pyo3::ffi::PyErr_Clear(); +/// None +/// } +/// } else { +/// Some(pyo3::Bound::from_owned_ptr(bound.py(), attribute)) +/// } +/// }; +/// +/// if let Some(protocol) = result { +/// if crate::typeref::check(protocol.as_ptr(), crate::typeref::SERIALIZER_TYPE) { +/// // OK expr +/// } else { +/// // ERR expr +/// } +/// } +/// } +/// ``` +#[proc_macro] +pub fn cust_protocol(item: TokenStream) -> TokenStream { + let CustomizationProtocolInput { + bound_object, + attribute_name, + pyobject_type, + ok_expr, + err_expr, + } = syn::parse_macro_input!(item as CustomizationProtocolInput); + + quote! { + unsafe { + const PROTOCOL: &'static std::ffi::CStr = #attribute_name; + + #[cfg(Py_3_13)] + let result = { + let mut attribute: *mut pyo3::ffi::PyObject = std::ptr::null_mut(); + + match pyo3::ffi::PyObject_GetOptionalAttrString( + #bound_object.as_ptr(), + PROTOCOL.as_ptr(), + &mut attribute + ) { + 1 => Some(pyo3::Bound::from_owned_ptr(#bound_object.py(), attribute)), + 0 => None, + _ => { + return Err(pyo3::PyErr::fetch(#bound_object.py())) + } + } + }; + + #[cfg(not(Py_3_13))] + let result = { + let attribute: *mut pyo3::ffi::PyObject = + pyo3::ffi::PyObject_GetAttrString(#bound_object.as_ptr(), PROTOCOL.as_ptr()); + + if attribute.is_null() { + if pyo3::ffi::PyErr_ExceptionMatches(pyo3::ffi::PyExc_AttributeError) != 1 { + return Err(pyo3::PyErr::fetch(#bound_object.py())); + } else { + pyo3::ffi::PyErr_Clear(); + None + } + } else { + Some(pyo3::Bound::from_owned_ptr(#bound_object.py(), attribute)) + } + }; + + if let Some(protocol) = result { + if pyo3::ffi::PyObject_TypeCheck(protocol.as_ptr(), #pyobject_type) == 1 #ok_expr + else #err_expr + } + } + } + .into() +} diff --git a/rapidquery/__init__.py b/rapidquery/__init__.py index 13d5942..444ee14 100644 --- a/rapidquery/__init__.py +++ b/rapidquery/__init__.py @@ -1,60 +1,16 @@ -""" -RapidQuery is a powerful SQL query builder library designed for Python, -combining the simplicity of Python with the raw speed of **Rust**. -Build complex SQL queries effortlessly and efficiently, with a library that -prioritizes both performance and ease of use. -""" +from . import dcl, ddl, dialect, dml, expr, serializers, tcl +from .expr import Asterisk, Ident, Unquote, Value -from ._lib import common as common -from ._lib import mysql as mysql -from ._lib import postgres as postgres -from ._lib import query as query -from ._lib import schema as schema -from ._lib import sqlite as sqlite -from ._lib import sqltypes as sqltypes - -# Export .common -Column = common.Column -ColumnRef = common.ColumnRef -Expr = common.Expr -ForeignKey = common.ForeignKey -Func = common.Func -TableName = common.TableName -Value = common.Value -all = common.all -any = common.any -not_ = common.not_ - -# Export .query -CaseStatement = query.CaseStatement -DeleteStatement = query.DeleteStatement -Frame = query.Frame -InsertStatement = query.InsertStatement -OnConflict = query.OnConflict -Ordering = query.Ordering -QueryStatement = query.QueryStatement -Returning = query.Returning -SelectLabel = query.SelectLabel -SelectStatement = query.SelectStatement -UpdateStatement = query.UpdateStatement -WindowStatement = query.WindowStatement -WithClause = query.WithClause -WithQuery = query.WithQuery - -# Export .schema -AlterTable = schema.AlterTable -AlterTableAddColumnOption = schema.AlterTableAddColumnOption -AlterTableAddForeignKeyOption = schema.AlterTableAddForeignKeyOption -AlterTableBaseOption = schema.AlterTableBaseOption -AlterTableDropColumnOption = schema.AlterTableDropColumnOption -AlterTableDropForeignKeyOption = schema.AlterTableDropForeignKeyOption -AlterTableModifyColumnOption = schema.AlterTableModifyColumnOption -AlterTableRenameColumnOption = schema.AlterTableRenameColumnOption -DropIndex = schema.DropIndex -DropTable = schema.DropTable -Index = schema.Index -IndexColumn = schema.IndexColumn -RenameTable = schema.RenameTable -SchemaStatement = schema.SchemaStatement -Table = schema.Table -TruncateTable = schema.TruncateTable +__all__ = [ + "Asterisk", + "Ident", + "Unquote", + "Value", + "dcl", + "ddl", + "dialect", + "dml", + "expr", + "serializers", + "tcl", +] diff --git a/rapidquery/_lib/__init__.pyi b/rapidquery/_lib/__init__.pyi deleted file mode 100644 index 978b592..0000000 --- a/rapidquery/_lib/__init__.pyi +++ /dev/null @@ -1,23 +0,0 @@ -""" -RapidQuery core which is written in Rust. -""" - -from __future__ import annotations - -from . import common as common -from . import mysql as mysql -from . import postgres as postgres -from . import query as query -from . import schema as schema -from . import sqlite as sqlite -from . import sqltypes as sqltypes - -__all__ = [ - "sqltypes", - "schema", - "query", - "common", - "sqlite", - "postgres", - "mysql", -] diff --git a/rapidquery/_lib/common.pyi b/rapidquery/_lib/common.pyi deleted file mode 100644 index 03688f1..0000000 --- a/rapidquery/_lib/common.pyi +++ /dev/null @@ -1,873 +0,0 @@ -from __future__ import annotations - -import typing - -from .query import SelectLabel, SelectStatement, WindowStatement -from .sqltypes import SQLTypeAbstract - -T = typing.TypeVar("T") -_ForeignKeyActions: typing.TypeAlias = typing.Literal[ - "CASCADE", "RESTRICT", "NO ACTION", "SET DEFAULT", "SET NULL" -] - -class _ColumnRefProperty(typing.Protocol): - __column_ref__: typing.Any - -class _TableNameProperty(typing.Protocol): - __table_name__: typing.Any - -_ColumnRefNew = typing.Union[ - "ColumnRef", - str, - _ColumnRefProperty, - type[_ColumnRefProperty], -] -_TableNameNew = typing.Union[ - "TableName", - str, - _TableNameProperty, - type[_TableNameProperty], -] -_ExprNew = typing.Any - -class Column(typing.Generic[T]): - """ - Defines a table column with its properties and constraints. - - Represents a complete column definition including: - - Column name and data type - - Constraints (primary key, unique, nullable) - - Auto-increment behavior - - Default values and generated columns - - Comments and extra specifications - - This class is used within Table to specify the structure - of table columns. It encapsulates all the properties that define how - a column behaves and what data it can store. - """ - - def __init__( - self, - name: str, - type: SQLTypeAbstract[T], - *, - primary_key: bool = False, - unique_key: bool = False, - nullable: bool | None = None, - auto_increment: bool = False, - extra: str | None = None, - comment: str | None = None, - default: typing.Any = ..., - generated: typing.Any = ..., - stored_generated: bool = False, - ) -> None: - """ - Construct a table column with column type. - - Args: - name: The name of this column as represented in the database. - type: The column's type. - primary_key: If `True`, marks this column as a primary key column. - unique_key: If `True`, marks this column as a unique key column. - nullable: If `False`, marks this column as "NOT NULL". If `True`, marks it as "NULL". - auto_increment: If `True`, marks this column as auto increment. Auto increment phrase is - depended on backend/dialect. - extra: Some extra options in custom string. - comment: (MySQL only) Column comment. - default: Represents default value for the column. - generated: Sets the column as generated. - stored_generated: Works with `generated` parameter together, which marks column as "STORED GENERATED". - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def adapt(self, object: T | None) -> Value[T]: - """Shorthand for `Value(object, self.type)`.""" - ... - - @property - def auto_increment(self) -> bool: - """Whether this is a auto increment column.""" - ... - @auto_increment.setter - def auto_increment(self, value: bool) -> None: ... - @property - def comment(self) -> str | None: - """Comment describing this column.""" - ... - @comment.setter - def comment(self, value: str | None) -> None: ... - @property - def default(self) -> Expr | None: - """Default value for this column.""" - ... - @default.setter - def default(self, value: _ExprNew) -> None: ... - @property - def extra(self) -> str | None: - """Extra SQL specifications for this column.""" - ... - @extra.setter - def extra(self, value: str | None) -> None: ... - @property - def generated(self) -> Expr | None: - """Expression for generated column values.""" - ... - @generated.setter - def generated(self, value: _ExprNew) -> None: ... - @property - def name(self) -> str: - """Column name.""" - ... - @name.setter - def name(self, value: str) -> None: ... - @property - def nullable(self) -> bool | None: - """Whether this is a nullable column.""" - ... - @property - def primary_key(self) -> bool: - """Whether this is a nullable column.""" - ... - @primary_key.setter - def primary_key(self, value: bool) -> None: ... - @property - def stored_generated(self) -> bool: - """Whether this is a stored generated column.""" - ... - @stored_generated.setter - def stored_generated(self, value: bool) -> None: ... - @property - def type(self) -> SQLTypeAbstract[T]: - """Column type.""" - ... - - @property - def unique_key(self) -> bool: - """Whether this is a unique column.""" - ... - @unique_key.setter - def unique_key(self, value: bool) -> None: ... - @property - def __column_ref__(self) -> ColumnRef: ... - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - - def label(self, alias: str, window: WindowStatement | str | None = None) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - -@typing.final -class ColumnRef: - """ - Represents a reference to a database column with optional table and schema qualification. - - This class is used to uniquely identify columns in SQL queries, supporting - schema-qualified and table-qualified column references. - - NOTE: this class is immutable and frozen, But you can use `.copy_with` method to make changes on it. - """ - - def __new__( - cls, - name: str, - table: str | None = ..., - schema: str | None = ..., - ) -> typing.Self: ... - def __copy__(self) -> typing.Self: ... - def __eq__(self, value: object, /) -> bool: ... - def __ne__(self, value: object, /) -> bool: ... - def __repr__(self, /) -> str: ... - def copy_with( - self, - *, - name: str = ..., - table: str | None = ..., - schema: str | None = ..., - ) -> typing.Self: - """ - Returns a copy of this `ColumnRef`, but with the changes you want. - """ - ... - - @property - def name(self) -> str: - """Column reference name.""" - ... - - @classmethod - def parse(cls, string: str) -> typing.Self: - """ - Parse a string representation of a column reference. - - Supports formats like: - - "column_name" - - "table.column_name" - - "schema.table.column_name" - """ - ... - - @property - def schema(self) -> str | None: - """Schema name""" - ... - - @property - def table(self) -> str | None: - """Table name""" - ... - - def __hash__(self) -> int: ... - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - - def label(self, alias: str, window: WindowStatement | str | None = None) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - -@typing.final -class Expr: - """ - Represents a SQL expression that can be built into SQL code. - - This class provides a fluent interface for constructing complex SQL expressions - in a database-agnostic way. It supports arithmetic operations, comparisons, - logical operations, and database-specific functions. - - The class automatically handles SQL injection protection and proper quoting - when building the final SQL statement. - - NOTE: `Expr` is immutable, so by calling each method you will give a new instance - of it which includes new change(s). - """ - - def __new__(cls, value: _ExprNew, /) -> typing.Self: ... - def __add__(self, other: _ExprNew) -> typing.Self: - """Create an addition expression.""" - ... - - def __and__(self, other: _ExprNew) -> typing.Self: - """Create a logical AND expression.""" - ... - - def __eq__(self, other: _ExprNew) -> typing.Self: # type: ignore[override] - """Create an equality comparison expression.""" - ... - - def __ge__(self, other: _ExprNew) -> typing.Self: - """Create a greater-than-or-equal comparison expression.""" - ... - - def __gt__(self, other: _ExprNew) -> typing.Self: - """Create a greater-than comparison expression.""" - ... - - def __le__(self, other: _ExprNew) -> typing.Self: - """Create a less-than-or-equal comparison expression.""" - ... - - def __lshift__(self, other: _ExprNew) -> typing.Self: - """Create a bitwise left-shift expression.""" - ... - - def __lt__(self, other: _ExprNew) -> typing.Self: - """Create a less-than comparison expression.""" - ... - - def __mod__(self, other: _ExprNew) -> typing.Self: - """Create a modulo expression.""" - ... - - def __mul__(self, other: _ExprNew) -> typing.Self: - """Create a multiplication expression.""" - ... - - def __ne__(self, other: _ExprNew) -> typing.Self: # type: ignore[override] - """Create an inequality comparison expression.""" - ... - - def __neg__(self) -> typing.Self: - """Create a negation expression.""" - ... - - def __or__(self, other: _ExprNew) -> typing.Self: - """Create a logical OR expression.""" - ... - - def __radd__(self, other: _ExprNew) -> typing.Self: - """Create an addition expression.""" - ... - - def __rand__(self, other: _ExprNew) -> typing.Self: - """Create a logical AND expression.""" - ... - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - def __rlshift__(self, other: _ExprNew) -> typing.Self: - """Create a bitwise left-shift expression.""" - ... - - def __rmod__(self, other: _ExprNew) -> typing.Self: - """Create a modulo expression.""" - ... - - def __rmul__(self, other: _ExprNew) -> typing.Self: - """Create a multiplication expression.""" - ... - - def __ror__(self, other: _ExprNew) -> typing.Self: - """Create a logical OR expression.""" - ... - - def __rrshift__(self, other: _ExprNew) -> typing.Self: - """Create a bitwise right-shift expression.""" - ... - - def __rshift__(self, other: _ExprNew) -> typing.Self: - """Create a bitwise right-shift expression.""" - ... - - def __rsub__(self, other: _ExprNew) -> typing.Self: - """Create a subtraction expression.""" - ... - - def __rtruediv__(self, other: _ExprNew) -> typing.Self: - """Create a division expression.""" - ... - - def __sub__(self, other: _ExprNew) -> typing.Self: - """Create a subtraction expression.""" - ... - - def __truediv__(self, other: _ExprNew) -> typing.Self: - """Create a division expression.""" - ... - - @classmethod - def all(cls, statement: SelectStatement) -> typing.Self: - """Express a `ALL` sub-query expression.""" - ... - - @classmethod - def any(cls, statement: SelectStatement) -> typing.Self: - """Express a `ANY` sub-query expression.""" - ... - - @classmethod - def asterisk(cls) -> typing.Self: - """Returns asterisk '*' expression.""" - ... - - def between(self, a: _ExprNew, b: _ExprNew) -> typing.Self: - """Create a BETWEEN range comparison expression.""" - ... - - def bit_and(self, other: _ExprNew) -> typing.Self: - """Create a bitwise AND expression.""" - ... - - def bit_or(self, other: _ExprNew) -> typing.Self: - """Create a bitwise AND expression.""" - ... - - def cast_as(self, value: str) -> typing.Self: - """Create a `CAST` expression to convert to a specific SQL type.""" - ... - - @classmethod - def col(cls, value: _ColumnRefNew) -> typing.Self: - """ - Tries to convert the `value` into `ColumnRef`, and then converts it to `Expr`. - """ - ... - - @classmethod - def current_date(cls) -> typing.Self: - """Create an expression for the CURRENT_DATE SQL function.""" - ... - - @classmethod - def current_time(cls) -> typing.Self: - """Create an expression for the CURRENT_TIME SQL function.""" - ... - - @classmethod - def current_timestamp(cls) -> typing.Self: - """Create an expression for the CURRENT_TIMESTAMP SQL function.""" - ... - - @classmethod - def custom(cls, value: str) -> typing.Self: - """ - Create an expression from a custom SQL string. - - Warning: This method does not escape the input, so it should only - be used with trusted strings to avoid SQL injection vulnerabilities. - """ - ... - - @classmethod - def exists(cls, statement: SelectStatement) -> typing.Self: - """Express a `EXISTS` sub-query expression.""" - ... - - def in_(self, other: typing.Iterable[_ExprNew] | SelectStatement) -> typing.Self: - """Express a `IN` expression.""" - ... - - def is_(self, other: _ExprNew) -> typing.Self: - """Create an IS comparison expression.""" - ... - - def is_not(self, other: _ExprNew) -> typing.Self: - """Create an IS NOT comparison expression.""" - ... - - def is_not_null(self) -> typing.Self: - """Create an IS NOT NULL expression.""" - ... - - def is_null(self) -> typing.Self: - """Create an IS NULL expression.""" - ... - - def like(self, pattern: str, escape: str | None = ...) -> typing.Self: - """Create a `LIKE` pattern matching expression.""" - ... - - def not_between(self, a: _ExprNew, b: _ExprNew) -> typing.Self: - """Create a NOT BETWEEN range comparison expression.""" - ... - - def not_in(self, other: typing.Iterable[_ExprNew] | SelectStatement) -> typing.Self: - """Express a `NOT IN` expression.""" - ... - - def not_like(self, pattern: str, escape: str | None = ...) -> typing.Self: - """Create a NOT LIKE pattern matching expression.""" - ... - - def max(self) -> typing.Self: - """ - Create `MAX(self)` function call. - - Shorthand for `Func.max(self).to_expr()`. - """ - ... - - def min(self) -> typing.Self: - """ - Create `MIN(self)` function call. - - Shorthand for `Func.min(self).to_expr()`. - """ - ... - - def abs(self) -> typing.Self: - """ - Create `ABS(self)` function call. - - Shorthand for `Func.abs(self).to_expr()`. - """ - ... - - @classmethod - def null(cls) -> typing.Self: - """Create an expression representing the NULL value.""" - ... - - @classmethod - def some(cls, statement: SelectStatement) -> typing.Self: - """Express a `SOME` sub-query expression.""" - ... - - @classmethod - def val( - cls, - /, - value: T | None, - sql_type: SQLTypeAbstract[T] | None = ..., - ) -> typing.Self: - """Shorthand for `Expr(Value(value, sql_type))`""" - ... - - def label(self, alias: str, window: WindowStatement | str | None = None) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - - def _to_sql(self, backend: str) -> str: ... - - __hash__ = None # type: ignore - -class ForeignKey: - """ - Specifies a foreign key relationship between tables. - - Defines referential integrity constraints including: - - Source columns (in the child table) - - Target columns (in the parent table) - - Actions for updates and deletes (CASCADE, RESTRICT, SET NULL, etc.) - - Optional naming for the constraint - - Foreign keys ensure data consistency by requiring that values in the - child table's columns match existing values in the parent table's columns. - """ - - def __init__( - self, - from_columns: typing.Iterable[_ColumnRefNew], - to_columns: typing.Iterable[_ColumnRefNew], - to_table: _TableNameNew | None = None, - name: str | None = None, - *, - on_delete: _ForeignKeyActions | None = None, - on_update: _ForeignKeyActions | None = None, - ) -> None: - """ - Construct a foreign key constraint. You can use it with `Table` or `AlterTable`. - - Args: - from_columns: An iterable of column references, or column names, or `Column` object. - The columns must defined and presented in the parent table. Cannot be empty. - to_columns: An iterable of column references, or column names, or `Column` object. - The columns must defined and presented in the target table. Cannot be empty. - to_table: The target table. If not specified, tries to detect it from `to_columns` argument. - name: The constraint name. - on_delete: The foreign key action for "ON DELETE". - on_update: The foreign key action for "ON UPDATE". - """ - ... - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def from_columns(self) -> typing.Sequence[str]: - """Key columns.""" - ... - @from_columns.setter - def from_columns(self, value: typing.Iterable[_ColumnRefNew]) -> None: ... - @property - def name(self) -> str | None: - """Foreign key constraint name""" - ... - @name.setter - def name(self, value: str | None) -> None: ... - @property - def on_delete(self) -> _ForeignKeyActions | None: - """ON DELETE action.""" - ... - @on_delete.setter - def on_delete(self, value: _ForeignKeyActions | None) -> None: ... - @property - def on_update(self) -> _ForeignKeyActions | None: - """ON UPDATE action.""" - ... - @on_update.setter - def on_update(self, value: _ForeignKeyActions | None) -> None: ... - @property - def to_columns(self) -> typing.Sequence[str]: - """Referencing columns.""" - ... - @to_columns.setter - def to_columns(self, value: typing.Iterable[_ColumnRefNew]) -> None: ... - @property - def to_table(self) -> TableName: - """Referencing table.""" - ... - @to_table.setter - def to_table(self, value: TableName) -> None: ... - -@typing.final -class Func: - """ - Represents a SQL function call that can be used in expressions. - - This class provides a type-safe way to construct SQL function calls - with proper argument handling and database dialect support. - """ - - def __new__(cls, name: str, *args: _ExprNew) -> typing.Self: - """ - Construct a function expression. You can use ready-to-use classmethods instead of this. - - Args: - name: the function name in string. - args: function arguments. - """ - ... - - def __repr__(self, /) -> str: ... - @classmethod - def abs(cls, /, expr: _ExprNew) -> typing.Self: - """Create a ABS(expr) function call.""" - ... - - @classmethod - def avg(cls, /, expr: _ExprNew) -> typing.Self: - """Create a AVG(expr) function call.""" - ... - - @classmethod - def bit_and(cls, /, expr: _ExprNew) -> typing.Self: - """ - Create a BIT_AND(expr) function call - this is not supported on SQLite. - """ - ... - - @classmethod - def bit_or(cls, /, expr: _ExprNew) -> typing.Self: - """Create a BIT_OR(expr) function call - this is not supported on SQLite.""" - ... - - @classmethod - def char_length(cls, /, expr: _ExprNew) -> typing.Self: - """Create a CHAR_LENGTH(expr) function call.""" - ... - - @classmethod - def coalesce(cls, /, *exprs: _ExprNew) -> typing.Self: - """Create a COALESCE function call.""" - ... - - @classmethod - def count(cls, /, expr: _ExprNew) -> typing.Self: - """Create a COUNT(expr) function call.""" - ... - - @classmethod - def count_distinct(cls, /, expr: _ExprNew) -> typing.Self: - """Create a COUNT(DISTINCT expr) function call.""" - ... - - @classmethod - def dense_rank(cls, /) -> typing.Self: - """Create a DENSE_RANK() function call.""" - ... - - @classmethod - def greatest(cls, /, *exprs: _ExprNew) -> typing.Self: - """Create a GREATEST function call.""" - ... - - @classmethod - def if_null(cls, /, a: _ExprNew, b: _ExprNew) -> typing.Self: - """Create a IF_NULL(a, b) function call.""" - ... - - @classmethod - def least(cls, /, *exprs: _ExprNew) -> typing.Self: - """Create a LEAST function call.""" - ... - - @classmethod - def lower(cls, /, expr: _ExprNew) -> typing.Self: - """Create a LOWER(expr) function call.""" - ... - - @classmethod - def max(cls, /, expr: _ExprNew) -> typing.Self: - """Create a MAX(expr) function call.""" - ... - - @classmethod - def md5(cls, /, expr: _ExprNew) -> typing.Self: - """ - Create a MD5(expr) function call - this is only available in Postgres and MySQL. - """ - ... - - @classmethod - def min(cls, /, expr: _ExprNew) -> typing.Self: - """Create a MIN(expr) function call.""" - ... - - @classmethod - def now(cls) -> typing.Self: - """Create a NOW() function call.""" - ... - - @classmethod - def percent_rank(cls, /) -> typing.Self: - """Create a PERCENT_RANK() function call.""" - ... - - @classmethod - def random(cls, /) -> typing.Self: - """Create a RANDOM() function call.""" - ... - - @classmethod - def rank(cls, /) -> typing.Self: - """Create a RANK() function call.""" - ... - - @classmethod - def round(cls, /, expr: _ExprNew) -> typing.Self: - """Create a ROUND(expr) function call.""" - ... - - @classmethod - def round_with_precision(cls, /, a: _ExprNew, b: _ExprNew) -> typing.Self: - """Create a ROUND(a, b) function call.""" - ... - - @classmethod - def sum(cls, /, expr: _ExprNew) -> typing.Self: - """Create a SUM(expr) function call.""" - ... - - @classmethod - def upper(cls, /, expr: _ExprNew) -> typing.Self: - """Create a UPPER(expr) function call.""" - ... - - @classmethod - def cast_as(cls, /, expr: _ExprNew, alias: str) -> typing.Self: - """Call CAST function with a custom type.""" - ... - - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - - def label(self, alias: str, window: WindowStatement | str | None = None) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - -@typing.final -class TableName: - """ - Represents a table name reference with optional schema, database, and alias. - - This class encapsulates a table name that can include: - - The base table name - - Optional schema/namespace qualification - - Optional database qualification (for systems that support it) - - The class provides parsing capabilities for string representations - and supports comparison operations. - - NOTE: this class is immutable and frozen. - """ - - def __new__( - cls, - name: str, - schema: str | None = None, - database: str | None = None, - alias: str | None = None, - ) -> typing.Self: ... - def __copy__(self) -> typing.Self: ... - def __eq__(self, value: object, /) -> bool: - """Return self==value.""" - ... - - def __ne__(self, value: object, /) -> bool: - """Return self!=value.""" - ... - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def alias(self) -> str | None: ... - def copy_with( - self, - *, - name: str = ..., - schema: str | None = ..., - database: str | None = ..., - alias: str | None = ..., - ) -> typing.Self: - """Create a shallow copy of this TableName.""" - ... - - @property - def database(self) -> str | None: ... - @property - def name(self) -> str: ... - @classmethod - def parse(cls, string: str) -> typing.Self: - """ - Parse a string representation of a table name. - - Supports formats like: - - "table_name" - - "schema.table_name" - - "database.schema.table_name" - """ - ... - - @property - def schema(self) -> str | None: ... - def __hash__(self) -> int: ... - -class Value(typing.Generic[T]): - """ - Bridges Python types, Rust types, and SQL types for seamless data conversion. - - This class handles validation, adaptation, and conversion between different - type systems used in the application stack. - - NOTE: this class is immutable and frozen. - """ - - def __init__( - self, - value: T | None, - sql_type: SQLTypeAbstract[T] | None = ..., - ) -> None: - """ - Construct a new `Value`. - - It can automatically detects the type of your value and selects appropriate Rust and SQL types. - For example: - - Python `int` becomes `BIGINT` SQL type (`BigIntegerType`) - - Python `dict` or `list` becomes `JSON` SQL type (`JsonType`) - - Python `float` becomes `DOUBLE` SQL type (`DoubleType`) - - However, for more accurate type selection, use the `sql_type` parameter. - """ - ... - - def __hash__(self, /) -> int: ... - def __repr__(self, /) -> str: ... - @property - def sql_type(self) -> SQLTypeAbstract[T]: - """The defined or detected SQL type.""" - ... - - @property - def value(self) -> T | None: - """Converts the adapted value back to Python.""" - ... - - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - -def all(arg1: Expr, *args: Expr) -> Expr: - """ - Create a logical AND condition that is true only if all conditions are true. - - This is equivalent to SQL's AND operator applied to multiple expressions. - """ - ... - -def any(arg1: Expr, *args: Expr) -> Expr: - """ - Create a logical OR condition that is true if any condition is true. - - This is equivalent to SQL's OR operator applied to multiple expressions. - """ - ... - -def not_(arg: Expr) -> Expr: - """Create a logical NOT.""" - ... diff --git a/rapidquery/_lib/mysql.pyi b/rapidquery/_lib/mysql.pyi deleted file mode 100644 index 4d21ee8..0000000 --- a/rapidquery/_lib/mysql.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import annotations - -__all__ = [] diff --git a/rapidquery/_lib/postgres.pyi b/rapidquery/_lib/postgres.pyi deleted file mode 100644 index 4d21ee8..0000000 --- a/rapidquery/_lib/postgres.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import annotations - -__all__ = [] diff --git a/rapidquery/_lib/query.pyi b/rapidquery/_lib/query.pyi deleted file mode 100644 index a9c6402..0000000 --- a/rapidquery/_lib/query.pyi +++ /dev/null @@ -1,1836 +0,0 @@ -from __future__ import annotations - -import typing - -from .common import Expr, Func, Value, _ColumnRefNew, _ExprNew, _TableNameNew - -_BackendName: typing.TypeAlias = typing.Literal[ - "sqlite", "postgresql", "postgres", "mysql" -] - -class CaseStatement: - """ - Represents a `CASE WHEN` SQL statement. - """ - - def __init__(self) -> None: - """ - Construct a `CASE WHEN` statement. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement( - rq.SelectLabel( - rq.CaseStatement() - .when(rq.Expr.col("aspect").in_([2, 4]), True) - .else_(False), - "is_even", - ), - ) - .from_table("glyph") - .to_sql("postgres") - ) - # SELECT (CASE WHEN ("aspect" IN (2, 4)) THEN TRUE ELSE FALSE END) AS "is_even" FROM "glyph" - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def else_(self, result: _ExprNew) -> typing.Self: - """ - Ends the case statement with the final ELSE result. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement( - rq.SelectLabel( - rq.CaseStatement() - .when(rq.Expr.col("aspect") > 0, "positive") - .when(rq.Expr.col("aspect") < 0, "negative") - .else_("zero"), - "polarity", - ), - ) - .from_table("glyph") - .to_sql("postgres") - ) - # SELECT (CASE WHEN ("aspect" > 0) THEN 'positive' WHEN ("aspect" < 0) THEN 'negative' ELSE 'zero' END) AS "polarity" FROM "glyph" - ``` - """ - ... - - def when(self, condition: Expr, result: _ExprNew) -> typing.Self: - """ - Adds new `WHEN` to existing case statement. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement( - rq.CaseStatement() - .when( - rq.any( - rq.Expr.col("font_size") > 48, - rq.Expr.col("size_w") > 500, - ), - "large", - ) - .when( - rq.any( - rq.Expr.col("font_size").between(24, 48), - rq.Expr.col("size_w").between(300, 500), - ), - "medium", - ) - .else_("small") - .label("char_size"), - ) - .from_table("characters") - .to_sql("postgres") - ) - # SELECT - # (CASE WHEN ("font_size" > 48 OR "size_w" > 500) THEN 'large' - # WHEN (("font_size" BETWEEN 24 AND 48) OR ("size_w" BETWEEN 300 AND 500)) THEN 'medium' - # ELSE 'small' END) AS "char_size" - # FROM "characters" - ``` - """ - ... - - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - - def label( - self, alias: str, window: WindowStatement | str | None = None - ) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - -class DeleteStatement(QueryStatement): - """ - Builds `DELETE` SQL statements with a fluent interface. - - Provides a chainable API for constructing DELETE queries with support for: - - WHERE conditions for filtering - - LIMIT for restricting deletion count - - ORDER BY for determining deletion order - - RETURNING clauses for getting deleted data - """ - - def __init__(self, table: _TableNameNew) -> None: - """ - Construct a `DELETE` statement. - - Args: - table: The table to delete from. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.DeleteStatement("glyph") - .where(rq.any(rq.Expr.col("id") < 1, rq.Expr.col("id") > 10)) - .to_sql("mysql") - ) - # DELETE FROM `glyph` WHERE `id` < 1 OR `id` > 10 - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def clear_order_by(self) -> typing.Self: - """Remove orders from statement.""" - ... - - def clear_where(self) -> typing.Self: - """Remove where conditions from statement.""" - ... - - def from_table(self, table: _TableNameNew) -> typing.Self: - """ - Override the table to delete from. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.DeleteStatement("name_1") - .where(rq.Expr.col("id") == 1) - .from_table("name_2") - .to_sql("sqlite") - ) - # DELETE FROM "name_2" WHERE "id" = 1 - ``` - """ - ... - - def limit(self, n: int) -> typing.Self: - """Limit the number of rows to delete.""" - ... - - def order_by(self, clause: Ordering) -> typing.Self: - """ - Specify the order in which to delete rows. Typically used with - `.limit` method to delete specific rows. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.DeleteStatement("glyph") - .where(rq.Expr.col("id") < 100) - .limit(20) - .order_by(rq.Ordering("id", "DESC")) - .to_sql("postgres") - ) - # DELETE FROM "glyph" WHERE "id" < 100 ORDER BY "id" DESC LIMIT 20 - ``` - """ - ... - - def returning(self, clause: Returning) -> typing.Self: - """ - Specify columns to return from the inserted rows. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.DeleteStatement("glyph") - .where(rq.Expr.col("id") < 30) - .returning(rq.Returning("id")) - .to_sql("sqlite") - ) - # DELETE FROM "glyph" WHERE "id" < 30 RETURNING "id" - ``` - """ - ... - - def where(self, condition: Expr) -> typing.Self: - """ - Add a `WHERE` condition to filter rows to delete. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.DeleteStatement("glyph") - .where(rq.Expr.col("id") < 100) - .where(rq.Expr.col("name").like("%A")) - .to_sql("postgres") - ) - # DELETE FROM "glyph" WHERE "id" < 100 AND "name" LIKE '%A' - ``` - """ - ... - -@typing.final -class Frame: - """ - Defines the window frame start and end boundaries for window functions. - - A frame allows you to constrain the set of rows used by the window function - to a subset relative to the current row. - - Use the provided classmethods to construct specific frame boundaries. - """ - - @classmethod - def current_row(cls) -> typing.Self: - """The boundary is the current row being processed.""" - ... - - @classmethod - def following(cls, val: int) -> typing.Self: - """ - The boundary is a fixed number of rows/values after the current row. - - Args: - val: The number of rows or the value range following the current row. - """ - ... - - @classmethod - def preceding(cls, val: int) -> typing.Self: - """ - The boundary is a fixed number of rows/values before the current row. - - Args: - val: The number of rows or the value range preceding the current row. - """ - ... - - @classmethod - def unbounded_following(cls) -> typing.Self: - """The boundary is the last row of the partition.""" - ... - - @classmethod - def unbounded_preceding(cls) -> typing.Self: - """The boundary is the first row of the partition.""" - ... - -class InsertStatement(QueryStatement): - """ - Builds `INSERT` SQL statements with a fluent interface. - - Provides a chainable API for constructing INSERT queries with support for: - - Single or multiple row insertion - - Conflict resolution (UPSERT) - - RETURNING clauses - - REPLACE functionality - - Default values - """ - - def __init__(self, table: _TableNameNew) -> None: - """ - Construct a `INSERT` statement. - - Args: - table: The target table for insertion. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=4.21, image="123") - .to_sql("mysql") - ) - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (4.21, '123') - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def columns(self, *args: _ColumnRefNew) -> typing.Self: - """ - Specify (override) the columns for insertion. - - There's no need to use this method when you're specifying column - names in `.values` method. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .columns("aspect", "image") - .values(5.15, "12A") - .values(4.21, "123") - .to_sql("mysql") - ) - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (5.15, '12A'), (4.21, '123') - ``` - """ - ... - - def into(self, table: _TableNameNew) -> typing.Self: - """ - Override the target table for insertion. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("name_1") - .values(aspect=4.21, image="123") - .into("name_2") - .to_sql("sqlite") - ) - # INSERT INTO "name_2" ("aspect", "image") VALUES (4.21, '123') - ``` - """ - ... - - def on_conflict(self, action: OnConflict) -> typing.Self: - """Specify conflict resolution behavior (UPSERT).""" - ... - - def or_default_values(self, rows: int = 1) -> typing.Self: - """ - Use DEFAULT VALUES if no values were specified. The `rows` - Specifies number of rows to insert with default values. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.InsertStatement("glyph").or_default_values().to_sql("postgres") - # INSERT INTO "glyph" VALUES (DEFAULT) - - stmt = ( - rq.InsertStatement("glyph") - .or_default_values() - .values(aspect=6.7) - .to_sql("postgres") - ) - # INSERT INTO "glyph" ("aspect") VALUES (6.7) - ``` - """ - ... - - def replace(self) -> typing.Self: - """ - Convert this INSERT to a REPLACE statement. - - REPLACE will delete existing rows that conflict with the new row - before inserting. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .replace() - .values(aspect=5.15, image="12A") - .to_sql("sqlite") - ) - # REPLACE INTO "glyph" ("aspect", "image") VALUES (5.15, '12A') - ``` - """ - ... - - def returning(self, clause: Returning) -> typing.Self: - """ - Specify columns to return from the inserted rows. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(image="12A") - .returning(rq.Returning.all()) - .to_sql("sqlite") - ) - # INSERT INTO "glyph" ("image") VALUES ('12A') RETURNING * - - stmt = ( - rq.InsertStatement("glyph") - .values(image="12A") - .returning(rq.Returning("id")) - .to_sql("sqlite") - ) - # INSERT INTO "glyph" ("image") VALUES ('12A') RETURNING "id" - ``` - """ - ... - - def select_from(self, statement: SelectStatement) -> typing.Self: - """ - Specify a select query whose values to be inserted. Raises `ValueError` if - `self`s columns length and `statement`s columns length has mismatch. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .columns("aspect", "image") - .select_from( - rq.SelectStatement(rq.Expr.col("aspect"), rq.Expr.col("image")) - .from_table("glyph") - .where(rq.Expr.col("image").like("0%")) - ) - .to_sql("mysql") - ) - # INSERT INTO `glyph` (`aspect`, `image`) - # SELECT `aspect` AS `aspect`, `image` AS `image` FROM `glyph` WHERE `image` LIKE '0%' - - stmt = ( - rq.InsertStatement("glyph") - .columns("image") - .select_from( - rq.SelectStatement("hello").where( - rq.not_(rq.Expr.exists(rq.SelectStatement("world"))) - ) - ) - .to_sql("postgres") - ) - # INSERT INTO "glyph" ("image") SELECT 'hello' WHERE NOT EXISTS(SELECT 'world') - ``` - """ - ... - - @typing.overload - def values(self, *args: _ExprNew) -> typing.Self: - """ - Specify values to insert. Also you can specify columns using keyword arguments. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .columns("aspect", "image") - .values(2, rq.Func.cast_as("2020-02-02 00:00:00", "DATE")) - .to_sql("postgres") - ) - # INSERT INTO "glyph" ("aspect", "image") VALUES (2, CAST('2020-02-02 00:00:00' AS DATE)) - ``` - """ - ... - - @typing.overload - def values(self, **kwds: _ExprNew) -> typing.Self: - """ - Specify values to insert. Also you can specify columns using keyword arguments. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=2, image=rq.Func.cast_as("2020-02-02 00:00:00", "DATE")) - .to_sql("postgres") - ) - # INSERT INTO "glyph" ("aspect", "image") VALUES (2, CAST('2020-02-02 00:00:00' AS DATE)) - ``` - """ - ... - -class OnConflict: - """ - Specifies conflict resolution behavior for `INSERT` statements. - - Handles situations where an `INSERT` would violate a unique constraint - or primary key. - - This corresponds to `INSERT ... ON CONFLICT` in PostgreSQL and - `INSERT ... ON DUPLICATE KEY UPDATE` in MySQL. - """ - - def __init__(self, *targets: _ColumnRefNew) -> None: - """ - Construct a new `OnConflict` instance. - - Args: - targets: Target columns. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_update(image="ex")) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') - # ON CONFLICT ("id") DO UPDATE SET "image" = 'ex' - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def action_where(self, condition: Expr) -> typing.Self: - """ - Add a `WHERE` clause to the conflict action (conditional update). - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict( - rq.OnConflict("id") - .do_update(image="ex") - .action_where(rq.Expr.col("aspect").is_null()) - ) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') - # ON CONFLICT ("id") DO UPDATE SET "image" = 'ex' WHERE "aspect" IS NULL - - stmt.to_sql("mysql") - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, 'abcd') - # ON DUPLICATE KEY UPDATE `image` = 'ex' - ``` - """ - ... - - def do_nothing(self, *keys: _ColumnRefNew) -> typing.Self: - """ - Specify `DO NOTHING` action for conflicts. - - When a conflict occurs, the conflicting row will be skipped. - - `keys` parameter provides primary keys if you are using MySQL, for MySQL specific polyfill. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_nothing()) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') ON CONFLICT ("id") DO NOTHING - - stmt.to_sql("mysql") - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, 'abcd') ON DUPLICATE KEY IGNORE - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_nothing("id")) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') ON CONFLICT ("id") DO NOTHING - - stmt.to_sql("mysql") - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, 'abcd') ON DUPLICATE KEY UPDATE `id` = `id` - ``` - """ - ... - - def do_update(self, *args: _ColumnRefNew, **kwds: _ExprNew) -> typing.Self: - """ - Specify `DO UPDATE` action for conflicts using column names, or with explicit values. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_update("aspect", image=rq.Expr(1) + 2)) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') - # ON CONFLICT ("id") DO UPDATE SET "aspect" = "excluded"."aspect", "image" = 1 + 2 - - stmt.to_sql("mysql") - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, 'abcd') - # ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = 1 + 2 - ``` - """ - ... - - def target_where(self, condition: Expr) -> typing.Self: - """ - Add a `WHERE` clause to the conflict target (partial unique index). - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict( - rq.OnConflict("id") - .do_update(image="ex") - .target_where(rq.Expr.col("aspect").is_null()) - ) - ) - stmt.to_sql("postgres") - # INSERT INTO "glyph" ("aspect", "image") VALUES (3.1415, 'abcd') - # ON CONFLICT ("id") WHERE "aspect" IS NULL DO UPDATE SET "image" = 'ex' - - stmt.to_sql("mysql") - # INSERT INTO `glyph` (`aspect`, `image`) VALUES (3.1415, 'abcd') - # ON DUPLICATE KEY UPDATE `image` = 'ex' - ``` - """ - ... - -@typing.final -class Ordering: - """ - Specifies ordering behavior statements. - - NOTE: this class is immutable and frozen. - """ - - def __new__( - cls, - target: Expr | _ColumnRefNew, - order: typing.Literal["ASC", "DESC"] = "ASC", - null_ordering: typing.Literal["FIRST", "LAST"] | None = None, - ) -> typing.Self: - """ - Construct a new `Ordering` instance. - - Args: - target: Ordering target. Can be an expression (`Expr`) or a column. - order: Ascendant (`"ASC"`) or descendant (`"DESC"`). - null_ordering: Null ordering option. `"FIRST"` means `NULLS FIRST` SQL, and - `"LAST"` means `NULLS LAST` SQL. - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def null_order(self) -> typing.Literal["FIRST", "LAST"] | None: ... - @property - def order(self) -> typing.Literal["ASC", "DESC"]: ... - @property - def target(self) -> Expr: - """Target expression.""" - ... - -class QueryStatement: - """Subclass of query statements.""" - - def build(self, backend: _BackendName, /) -> tuple[str, tuple[Value, ...]]: - """Build the SQL statement with parameter values.""" - ... - - def to_sql(self, backend: _BackendName, /) -> str: - """ - Build a SQL string representation. - - **This method is unsafe and can cause SQL injection.** use `.build()` method instead. - """ - ... - -@typing.final -class Returning: - """ - `RETURNING` clause. - - Works on PostgreSQL and SQLite>=3.35.0. - """ - - def __new__(cls, *args: Expr | _ColumnRefNew) -> typing.Self: - """ - Construct a new `RETURNING` clause. - - Args: - args: Returning expressions. Can be expressions (`Expr`) or column references. - If any `"*"`, or `ColumnRef("*")` found, ignores all others and returns `RETURNING *` clause. - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @classmethod - def all(cls) -> typing.Self: - """Same as `self("*")`.""" - ... - -class SelectLabel: - """ - Represents a column expression with an optional alias in a `SELECT` statement. - - Used to specify both the expression to select and an optional alias name - for the result column. - """ - - def __init__( - self, - expr: _ExprNew, - alias: str | None = None, - window: WindowStatement | str | None = None, - ) -> None: - """ - Construct a new `SelectLabel` instance. - - Args: - expr: Target expression to select. - alias: Target label, i.e. ` AS `. - window: Window statement or window name. - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def alias(self) -> str | None: ... - @property - def expr(self) -> Expr: ... - @property - def window(self) -> WindowStatement | str | None: ... - -class SelectStatement(QueryStatement): - """ - Builds SELECT SQL statements with a fluent interface. - - Provides a chainable API for constructing SELECT queries with support for: - - Column selection with expressions and aliases - - Table and subquery sources - - Filtering with WHERE and HAVING - - Joins (inner, left, right, full, cross, lateral) - - Grouping and aggregation - - Ordering and pagination - - Set operations (UNION, EXCEPT, INTERSECT) - - Row locking for transactions - - DISTINCT queries - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character", "fonts.name") - .from_table("characters") - .join("fonts", rq.Expr.col("characters.font_id") == rq.Expr.col("fonts.id"), "LEFT") - .where(rq.Expr.col("size_w").in_((3, 4))) - .where(rq.Expr.col("character").like("A%")) - .to_sql("postgres") - ) - # SELECT "character" AS "character", "fonts"."name" AS "name" FROM "characters" - # LEFT JOIN "fonts" ON "characters"."font_id" = "fonts"."id" - # WHERE "size_w" IN (3, 4) AND "character" LIKE 'A%' - ``` - """ - - def __init__(self, *exprs: SelectLabel | _ExprNew) -> None: - """ - Construct a new `SELECT` statement. - - Args: - exprs: Select expressions. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.SelectStatement(1, "hello", "font").to_sql("postgres") - # SELECT 1, 'hello', 'font' - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def clear_order_by(self) -> typing.Self: - """Remove orders from statement.""" - ... - - def clear_where(self) -> typing.Self: - """Remove where conditions from statement.""" - ... - - def columns(self, *args: _ColumnRefNew) -> typing.Self: - """ - Add select target expressions. - - Works same as `self.exprs(Expr.col(i) for i in args)`, but much easier and faster. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character", "size_w", "size_h") - .from_table("characters") - .to_sql("mysql") - ) - # SELECT `character` AS `character`, `size_w` AS `size_w`, `size_h` AS `size_h` FROM `characters` - ``` - """ - ... - - def distinct(self, *on: _ColumnRefNew) -> typing.Self: - """ - Changes `SELECT` statement into `SELECT DISTINCT` statement. - - Args: - on: Column references. If specified, uses *Postgres-ONLY* `SELECT DISTINCT ON ...` syntax. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .distinct() - .columns("character", "size_w", "size_h") - .from_table("characters") - .to_sql("mysql") - ) - # SELECT DISTINCT `character` AS `character`, `size_w` AS `size_w`, `size_h` AS `size_h` FROM `characters` - - stmt = ( - rq.SelectStatement() - .distinct("character") - .columns("character", "size_w", "size_h") - .to_sql("postgres") - ) - # SELECT DISTINCT ON ("character") "character" AS "character", "size_w" AS "size_w", "size_h" AS "size_h" - ``` - """ - ... - - def exprs(self, *args: SelectLabel | _ExprNew) -> typing.Self: - """ - Add select target expressions. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .exprs(rq.Func.max(rq.Expr.col("id")), rq.Expr.val(0) + 1 + 2 + 3) - .from_table("characters") - .to_sql("sqlite") - ) - # SELECT MAX("id"), 0 + 1 + 2 + 3 FROM "characters" - ``` - """ - ... - - def from_function(self, function: Expr | Func, alias: str) -> typing.Self: - """ - Select from function call. - - Args: - function: An expression or a function. If it is `Expr`, it should be a function call - and nothing more, or `ValueError` will be raised. - alias: label, i.e. `FROM AS `. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement(rq.Expr.asterisk()) - .from_function(rq.Func.random(), "func") - .to_sql("postgres") - ) - # SELECT * FROM RANDOM() AS "func" - ``` - """ - ... - - def from_subquery(self, subquery: SelectStatement, alias: str) -> typing.Self: - """ - Select from `SELECT` subquery. - - Args: - subquery: A select statement. - alias: label, i.e. `FROM AS `. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("image") - .from_subquery( - rq.SelectStatement().columns("image", "aspect").from_table("glyph"), - "subglyph", - ) - .to_sql("postgres") - ) - # SELECT "image" AS "image" FROM - # (SELECT "image" AS "image", "aspect" AS "aspect" FROM "glyph") AS "subglyph" - ``` - """ - ... - - def from_table(self, table: _TableNameNew) -> typing.Self: - """ - Select from table. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement(rq.Expr.asterisk()) - .from_table("characters") - .from_table("fonts") - .where(rq.Expr.col("fonts.id") == rq.Expr.col("characters.font_id")) - .to_sql("postgres") - ) - # SELECT * FROM "characters", "fonts" WHERE "fonts"."id" = "characters"."font_id" - ``` - """ - ... - - def group_by(self, *groups: Expr | _ColumnRefNew) -> typing.Self: - """ - Add group by expressions or column references. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .group_by(rq.Expr.col("size_w"), rq.Expr.col("size_h")) - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" GROUP BY "size_w", "size_h" - ``` - """ - ... - - def having(self, condition: Expr) -> typing.Self: - """ - Add having condition expression. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement(rq.Func.max(rq.Expr.col("image"))) - .columns("aspect") - .from_table("glyph") - .group_by(rq.Expr.col("aspect")) - .having(rq.Expr.col("aspect") > 2) - .having(rq.Expr.col("aspect") < 8) - .to_sql("postgres") - ) - # SELECT MAX("image"), "aspect" AS "aspect" FROM "glyph" - # GROUP BY "aspect" HAVING "aspect" > 2 AND "aspect" < 8 - ``` - """ - ... - - def join( - self, - table: _TableNameNew, - on: Expr | None = None, - type: typing.Literal["CROSS", "FULL", "INNER", "LEFT", "RIGHT"] | None = None, - ) -> typing.Self: - """ - Join with other table. - - Args: - table: The table to join with. - on: Join condition. - type: Join type. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character", "fonts.name") - .from_table("characters") - .join( - "fonts", - rq.Expr.col("characters.font_id") == rq.Expr.col("fonts.id"), - "RIGHT", - ) - .to_sql("mysql") - ) - # SELECT `character` AS `character`, `fonts`.`name` AS `name` FROM `characters` - # RIGHT JOIN `fonts` ON `characters`.`font_id` = `fonts`.`id` - ``` - """ - ... - - def join_function( - self, - function: Func | Expr, - alias: str, - on: Expr | None = None, - type: typing.Literal["CROSS", "FULL", "INNER", "LEFT", "RIGHT"] | None = None, - ) -> typing.Self: - """ - Join with a function call. - - Args: - function: An expression or a function. If it is `Expr`, it should be a function call - and nothing more, or `ValueError` will be raised. - alias: label. - on: Join condition. - type: Join type. - """ - ... - - def join_subquery( - self, - subquery: SelectStatement, - alias: str, - on: Expr | None = None, - type: typing.Literal["CROSS", "FULL", "INNER", "LEFT", "RIGHT"] | None = None, - lateral: bool = False, - ) -> typing.Self: - """ - Join with other `SELECT` statement. - - Args: - subquery: Select statement. - alias: label. - on: Join condition. - type: Join type. - literal: If `True`, uses `JOIN LATERAL` syntax, which *is not supported by SQLite*. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("name") - .from_table("fonts") - .join_subquery( - rq.SelectStatement().columns("id").from_table("glyph"), - "sub_glyph", - rq.Expr.col("fonts.id") == rq.Expr.col("sub_glyph.id"), - "LEFT", - ) - .to_sql("mysql") - ) - # SELECT `name` AS `name` FROM `fonts` - # LEFT JOIN (SELECT `id` AS `id` FROM `glyph`) AS `sub_glyph` ON `fonts`.`id` = `sub_glyph`.`id` - ``` - """ - ... - - def limit(self, n: int) -> typing.Self: - """Limit the number of rows to select.""" - ... - - def lock( - self, - type: typing.Literal[ - "UPDATE", "NO KEY UPDATE", "SHARE", "KEY SHARE" - ] = "UPDATE", - behavior: typing.Literal["NOWAIT", "SKIP"] | None = None, - tables: typing.Iterable[_TableNameNew] = (), - ) -> typing.Self: - """ - Row locking (if is supported by backend/dialect). - - Args: - type: Row locking type. - behavior: Row locking behavior. - tables: Row locking tables. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .where(rq.Expr.col("id") == 5) - .lock() - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" WHERE "id" = 5 FOR UPDATE - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .where(rq.Expr.col("id") == 5) - .lock(tables=["glyph"]) - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" WHERE "id" = 5 FOR UPDATE OF "glyph" - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .where(rq.Expr.col("id") == 5) - .lock(behavior="NOWAIT") - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" WHERE "id" = 5 FOR UPDATE NOWAIT - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .where(rq.Expr.col("id") == 5) - .lock("SHARE", behavior="SKIP", tables=["glyph"]) - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" WHERE "id" = 5 FOR SHARE OF "glyph" SKIP LOCKED - ``` - """ - ... - - def offset(self, n: int) -> typing.Self: - """Set offset.""" - ... - - def order_by(self, clause: Ordering) -> typing.Self: - """Specify the order in which to delete rows.""" - ... - - def union( - self, - statement: SelectStatement, - type: typing.Literal["ALL", "INTERSECT", "DISTINCT", "EXCEPT"] = "DISTINCT", - ) -> typing.Self: - """ - Union with multiple `SELECT` statement that **must have the same selected fields**. - - Args: - statement: Select statement. - type: Union type. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement("hello") - .union(rq.SelectStatement("world"), "ALL") - .to_sql("postgres") - ) - # SELECT 'hello' UNION ALL (SELECT 'world') - ``` - """ - ... - - def where(self, condition: Expr) -> typing.Self: - """ - Add select `WHERE` condition. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement() - .columns("character") - .from_table("characters") - .where(rq.Expr.col("id") == 5) - .where(rq.Expr.col("name").like("%A")) - .to_sql("postgres") - ) - # SELECT "character" AS "character" FROM "characters" WHERE "id" = 5 AND "name" LIKE '%A' - ``` - """ - ... - - def window(self, name: str, statement: WindowStatement) -> typing.Self: - """ - Add `WINDOW` to statement. - - Args: - name: Window name. - statement: Window statement. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.SelectStatement( - rq.Expr.col("character").label("w", "C") - ) - .window("C", rq.WindowStatement("font_size")) - .from_table("characters") - .to_sql("postgres") - ) - # SELECT "character" OVER "C" AS "w" FROM "characters" WINDOW "C" AS (PARTITION BY "font_size") - ``` - """ - ... - - def to_expr(self) -> Expr: - """Shorthand for `Expr(self)`""" - ... - - def label( - self, alias: str, window: WindowStatement | str | None = None - ) -> SelectLabel: - """Shorthand for `SelectLabel(self, alias, window)`""" - pass - -class UpdateStatement(QueryStatement): - """ - Builds UPDATE SQL statements with a fluent interface. - - Provides a chainable API for constructing UPDATE queries with support for: - - Setting column values - - WHERE conditions for filtering - - LIMIT for restricting update count - - ORDER BY for determining update order - - RETURNING clauses for getting updated data - """ - - def __init__(self, table: _TableNameNew) -> None: - """ - Construct a new `UPDATE` statement. - - Args: - table: The table to update. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .where(rq.Expr.col("id") == 1) - .to_sql("sqlite") - ) - # UPDATE "glyph" SET "aspect" = 1.23, "image" = 123 WHERE "id" = 1 - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def clear_order_by(self) -> typing.Self: - """Remove orders from statement.""" - ... - - def clear_where(self) -> typing.Self: - """Remove where conditions from statement.""" - ... - - def from_table(self, table: _TableNameNew) -> typing.Self: - """ - Update using data from another table (`UPDATE .. FROM ..`). - - MySQL doesn't support the UPDATE FROM syntax. And the current implementation attempt to - tranform it to the UPDATE JOIN syntax, which only works for one join target. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("glyph") - .values(tokens=rq.Expr.col("characters.character")) - .from_table("characters") - .where(rq.Expr.col("glyph.image") == rq.Expr.col("characters.user_data")) - ) - stmt.to_sql("postgres") - # UPDATE "glyph" SET "tokens" = "characters"."character" FROM "characters" WHERE "glyph"."image" = "characters"."user_data" - - stmt.to_sql("mysql") - # UPDATE `glyph` JOIN `characters` ON `glyph`.`image` = `characters`.`user_data` SET `glyph`.`tokens` = `characters`.`character` - ``` - """ - ... - - def limit(self, n: int) -> typing.Self: - """Limit the number of rows to update.""" - ... - - def order_by(self, clause: Ordering) -> typing.Self: - """ - Specify the order in which to delete rows. Typically used with - `.limit` method to delete specific rows. - """ - ... - - def returning(self, clause: Returning) -> typing.Self: - """ - Specify columns to return from the inserted rows. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .returning(rq.Returning.all()) - .to_sql("sqlite") - ) - # UPDATE "glyph" SET "aspect" = 1.23, "image" = 123 RETURNING * - - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .returning(rq.Returning("id")) - .to_sql("sqlite") - ) - # UPDATE "glyph" SET "aspect" = 1.23, "image" = 123 RETURNING "id" - ``` - """ - ... - - def table(self, table: _TableNameNew) -> typing.Self: - """ - Override the table to update. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("name_1") - .values(aspect=1.23, image=123) - .table("name_2") - .to_sql("sqlite") - ) - # UPDATE "name_2" SET "aspect" = 1.23, "image" = 123 - ``` - """ - ... - - def values(self, **kwds: _ExprNew) -> typing.Self: - """ - Specify columns and their new values. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .values(font_id=20) - .to_sql("sqlite") - ) - # UPDATE "name_2" SET "aspect" = 1.23, "image" = 123, "font_id" = 20 - ``` - """ - ... - - def where(self, condition: Expr) -> typing.Self: - """ - Add a WHERE condition to filter rows to update. - - Examples: - ```python - import rapidquery as rq - - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .where(rq.Expr.col("id") > 10) - .where(rq.Expr.col("id") < 20) - .to_sql("sqlite") - ) - # UPDATE "glyph" SET "aspect" = 1.23, "image" = 123 WHERE "id" > 10 AND "id" < 20 - ``` - """ - ... - -class WindowStatement: - """ - Represents an `OVER` clause used with window functions. - - Window functions perform calculations across a set of table rows that are - somehow related to the current row. This class builds the specification - of that set (the window), including partitioning, ordering, and framing. - - # References: - 1. - 2. - 3. - """ - - def __init__(self, *partition_by: Expr | _ColumnRefNew) -> None: - """ - Construct a new `WINDOW` specification. - - Args: - *partition_by: One or more columns or expressions to divide - the result set into partitions. - - Examples: - ```python - import rapidquery as rq - - # Example 1: Defining a simple frame - stmt = ( - rq.SelectStatement( - rq.Expr.col("character").label( - "C", - rq.WindowStatement("font_size").frame( - "ROWS", - rq.Frame.preceding(10), - ) - ) - ) - .from_table("characters") - .to_sql("postgres") - ) - - # Example 2: Defining a frame with start and end - stmt = ( - rq.SelectStatement( - rq.Expr.col("character").label( - "C", - rq.WindowStatement("font_size").frame( - "ROWS", - rq.Frame.unbounded_preceding(), - rq.Frame.unbounded_following(), - ) - ) - ) - .from_table("characters") - .to_sql("postgres") - ) - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def frame( - self, - frame_type: typing.Literal["ROWS", "RANGE"], - frame_start: Frame, - frame_end: Frame | None = None, - ) -> typing.Self: - """ - Set the window frame clause. - - Args: - frame_type: Either "ROWS" (physical rows) or "RANGE" (logical values). - frame_start: The starting boundary of the frame. - frame_end: Optional ending boundary. If not provided, the frame - typically defaults to the current row or the database default. - """ - ... - - def order_by(self, clause: Ordering) -> typing.Self: - """ - Specify the order of rows within each partition. - - Args: - clause: An `Ordering` instance defining the sort key and direction. - """ - ... - - def partition(self, partition_by: Expr | _ColumnRefNew) -> typing.Self: - """ - Add a column or expression to the `PARTITION BY` clause. - - Args: - partition_by: The column or expression to partition by. - """ - ... - -_CommonTableExpressionQuery = ( - SelectStatement | DeleteStatement | UpdateStatement | InsertStatement -) - -class WithClause: - """ - A WITH clause can contain one or multiple common table expressions (CTEs). - - These named queries can act as a "query local table" that are materialized during execution and - then can be used by the query prefixed with the WITH clause. - - A CTE is a name, column names and a query returning data for those columns. - - Some databases (like sqlite) restrict the acceptable kinds of queries inside of the WITH clause - CTEs. These databases only allow `SelectStatement`s to form a CTE. - - Other databases like postgres allow modification queries (UPDATE, DELETE) inside of the WITH - clause but they have to return a table. (They must have a RETURNING clause). - - RapidQuery doesn't check this or restrict the kind of CTE that you can create - in rust. This means that you can put an UPDATE or DELETE queries into WITH clause and RapidQuery - will succeed in generating that kind of sql query but the execution inside the database will - fail because they are invalid. - - It is your responsibility to ensure that the kind of WITH clause that you put together makes - sense and valid for that database that you are using. - - NOTE that for recursive WITH queries (in sql: "WITH RECURSIVE") you can only have a - single CTE inside of the WITH clause. That query must match certain - requirements: - * It is a query of UNION or UNION ALL of two queries. - * The first part of the query (the left side of the UNION) must be executable first in itself. - It must be non-recursive. (Cannot contain self reference) - * The self reference must appear in the right hand side of the UNION. - * The query can only have a single self-reference. - * Recursive data-modifying statements are not supported, but you can use the results of a - recursive SELECT query in a data-modifying statement. (like so: WITH RECURSIVE - cte_name(a,b,c,d) AS (SELECT ... UNION SELECT ... FROM ... JOIN cte_name ON ... WHERE ...) - DELETE FROM table WHERE table.a = cte_name.a) - - Note that this type is not a `QueryStatement`. To generate `WITH` statement, you must convert this type to - `WithQuery` type, using `WithClause.query` method. - """ - - def __init__(self) -> None: - """ - Construct a new `WithClause` instance. - - Note that this type is not a `QueryStatement`. To generate `WITH` statement, you must convert this type to - `WithQuery` type, using `WithClause.query` method. - - Examples: - ```python - import rapidquery as rq - - select = ( - rq.SelectStatement() - .columns("names.id", "names.name") - .from_table("filtered_names AS fil") - .join("names", rq.Expr.col("fil.name_id") == rq.Expr.col("names.id")) - ) - clause = ( - rq.WithClause() - .cte( - "source_filtered_names", - select.__copy__().where(rq.Expr.col("fil.user_id") == 5), - ) - .cte( - "dest_filtered_names", - select.where(rq.Expr.col("fil.user_id") == 8), - ) - .cte( - "missing_names", - rq.SelectStatement() - .columns("dest.name") - .from_table("dest_filtered_names AS dest") - .join( - "source_filtered_names AS source", - rq.Expr.col("dest.id") == rq.Expr.col("source.id"), - ) - .where(rq.Expr.col("source.id").is_null()), - ) - .query(rq.SelectStatement().columns("name").from_table("missing_names")) - ) - clause.to_sql("postgres") - # WITH - # "source_filtered_names" ("id", "name") AS ( - # SELECT "names"."id" AS "id", "names"."name" AS "name" FROM "filtered_names" AS "fil" - # JOIN "names" ON "fil"."name_id" = "names"."id" WHERE "fil"."user_id" = 5 - # ) , - # "dest_filtered_names" ("id", "name") AS ( - # SELECT "names"."id" AS "id", "names"."name" AS "name" FROM "filtered_names" AS "fil" - # JOIN "names" ON "fil"."name_id" = "names"."id" - # WHERE "fil"."user_id" = 8 - # ) , - # "missing_names" ("name") AS ( - # SELECT "dest"."name" AS "name" FROM "dest_filtered_names" AS "dest" - # JOIN "source_filtered_names" AS "source" ON "dest"."id" = "source"."id" - # WHERE "source"."id" IS NULL - # ) - # SELECT "name" AS "name" FROM "missing_names" - ``` - """ - ... - - def recursive(self) -> typing.Self: - """ - Sets whether this clause is a recursive with clause of not. - It will generate a 'WITH RECURSIVE' query. - """ - ... - - def cte( - self, - name: str, - query: _CommonTableExpressionQuery, - columns: typing.Iterable[str] = (), - materialized: bool | None = None, - ) -> typing.Self: - """ - Add a CTE to this with clause. - - Args: - name: The common table expression (CTE) name. - query: The query, which have to return data. - columns: Named columns for the CTE table definition. - If empty, tries to detect them from the `query`. - materialized: Some databases allow you to put “MATERIALIZED” or “NOT MATERIALIZED” in the - CTE definition. This will affect how during the execution of WithQuery the - CTE in the WithClause will be executed. If the database doesn’t support this - syntax this option specified here will be ignored and not appear in the - generated sql. - """ - ... - - def cycle( - self, - expr: _ExprNew | None = None, - set_as: str | None = None, - using: str | None = None, - ) -> typing.Self: - """ - For recursive `WithQuery` `WithClause`s the CYCLE sql clause can be specified to avoid creating - an infinite traversals that loops on graph cycles indefinitely. You specify an expression that - identifies a node in the graph and that will be used to determine during the iteration of - the execution of the query when appending of new values whether the new values are distinct new - nodes or are already visited and therefore they should be added again into the result. - - A query can have both SEARCH and CYCLE clauses. - - This setting is not meaningful if the query is not recursive. - Some databases don’t support this clause. In that case this option will be silently ignored. - - Examples: - ```python - base_query = rq.SelectStatement( - rq.Expr.col("id"), - 1, - rq.Expr.col("next"), - rq.Expr.col("value"), - ).from_table("table") - - cte_referencing = ( - rq.SelectStatement( - rq.Expr.col("id"), - rq.Expr.col("depth") + 1, - rq.Expr.col("next"), - rq.Expr.col("value"), - ) - .from_table("table") - .join( - "cte_traversal", - rq.Expr.col("cte_traversal.next") == rq.Expr.col("table.id"), - "INNER", - ) - ) - - clause = ( - rq.WithClause() - .recursive() - .cte( - "cte_traversal", - base_query.union(cte_referencing, "ALL"), - columns=["id", "depth", "next", "value"], - ) - .cycle(rq.Expr.col("id"), set_as="looped", using="traversal_path") - .query(rq.SelectStatement().columns("*").from_table("cte_traversal")) - ) - clause.to_sql("postgres") - # WITH RECURSIVE "cte_traversal" ("id", "depth", "next", "value") AS ( - # SELECT "id" AS "id", 1, "next" AS "next", "value" AS "value" FROM "table" - # UNION ALL ( - # SELECT "id" AS "id", "depth" + 1, "next" AS "next", "value" AS "value" FROM "table" - # INNER JOIN "cte_traversal" ON "cte_traversal"."next" = "table"."id" - # ) - # ) - # CYCLE "id" SET "looped" USING "traversal_path" SELECT * FROM "cte_traversal" - ``` - """ - ... - - def search( - self, - expr: SelectLabel | None = None, - order: typing.Literal["BREADTH", "DEPTH"] | str | None = None, - ) -> typing.Self: - """ - For recursive `WithQuery` `WithClause`s the traversing order can be specified in some databases - that support this functionality. - - A query can have both SEARCH and CYCLE clauses. - - This setting is not meaningful if the query is not recursive. - Some databases don’t support this clause. In that case this option will be silently ignored. - - The `SelectLabel` used must specify an alias which will be the name that you can use to order - the result of the CTE. - - Examples: - ```python - base_query = ( - rq.SelectStatement() - .columns("id", "parent_id") - .from_table("nodes") - .where(rq.Expr.col("parent_id").is_null()) - ) - reference_query = ( - rq.SelectStatement() - .columns("n.id", "n.parent_id") - .from_table("nodes as n") - .join( - "tree as t", - rq.Expr.col("n.parent_id") == rq.Expr.col("t.id"), - ) - ) - - clause = ( - rq.WithClause() - .recursive() - .cte( - "tree", - base_query.union(reference_query, "ALL"), - columns=["id", "parent_id"], - ) - .search(rq.Expr.col("id").label("traversal_order"), "DEPTH") - .query( - rq.SelectStatement() - .columns("*") - .from_table("tree") - .order_by(rq.Ordering("traversal_order")) - ) - ) - clause.to_sql("postgresql") - # WITH RECURSIVE "tree" ("id", "parent_id") AS ( - # SELECT "id" AS "id", "parent_id" AS "parent_id" FROM "nodes" - # WHERE "parent_id" IS NULL - # UNION ALL ( - # SELECT "n"."id" AS "id", "n"."parent_id" AS "parent_id" - # FROM "nodes" AS "n" - # JOIN "tree" AS "t" ON "n"."parent_id" = "t"."id" - # ) - # ) - # SEARCH DEPTH FIRST BY "id" SET "traversal_order" - # SELECT * FROM "tree" ORDER BY "traversal_order" ASC - ``` - """ - ... - - def query(self, val: _CommonTableExpressionQuery) -> WithQuery: - """ - You can turn this into a `WithQuery` using this function. - The resulting WITH query will execute the argument query with this WITH clause. - """ - ... - -class WithQuery(QueryStatement): - """ - A WITH query. A simple SQL query that has a WITH clause (`WithClause`). - - It's recommended to use `WithClause.query` method instead of directly constructing - this type. - - For full description, see `WithClause`'s documentation. - """ - - def __init__( - self, - clause: WithClause, - query: _CommonTableExpressionQuery, - ) -> None: - """ - Construct a new `WithQuery` instance. - - Args: - clause: The `WITH` clause. - query: The final query. - """ - ... diff --git a/rapidquery/_lib/schema.pyi b/rapidquery/_lib/schema.pyi deleted file mode 100644 index 16d269c..0000000 --- a/rapidquery/_lib/schema.pyi +++ /dev/null @@ -1,857 +0,0 @@ -from __future__ import annotations - -import typing - -from .common import Column, Expr, ForeignKey, TableName, _ColumnRefNew, _TableNameNew - -_IndexColumnValue: typing.TypeAlias = IndexColumn | _ColumnRefNew -_IndexColumnOrder: typing.TypeAlias = typing.Literal["ASC", "DESC"] -_BackendName: typing.TypeAlias = typing.Literal["sqlite", "postgresql", "postgres", "mysql"] - -class AlterTable(SchemaStatement): - """ - Represents an `ALTER TABLE` SQL statement. - - Provides a flexible way to modify existing table structures by applying - one or more alteration operations such as adding/dropping columns, - modifying column definitions, or managing constraints. - - Multiple operations can be batched together in a single ALTER TABLE - statement for efficiency. - """ - - def __init__( - self, - name: _TableNameNew, - options: typing.Iterable[AlterTableBaseOption] = (), - ) -> None: - """ - Construct a new `ALTER TABLE` statement. - - Args: - name: The name of the table to alter. - options: Iterable of alteration operations to apply. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [ - rq.AlterTableDropColumnOption("updated_at"), - rq.AlterTableRenameColumnOption("name", "username"), - ], - ).to_sql("postgres") - # ALTER TABLE "users" DROP COLUMN "updated_at", RENAME COLUMN "name" TO "username" - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - def add_option(self, opt: AlterTableBaseOption) -> typing.Self: - """Add an alteration operation.""" - ... - - @property - def name(self) -> TableName: - """The name of the table to alter.""" - ... - @name.setter - def name(self, value: _TableNameNew) -> None: ... - @property - def options(self) -> typing.Sequence[AlterTableBaseOption]: - """The list of alteration operations to apply.""" - ... - @options.setter - def options(self, value: typing.Iterable[AlterTableBaseOption]) -> None: ... - -class AlterTableAddColumnOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to add a new column. - - Adds a column to an existing table with optional IF NOT EXISTS clause - to prevent errors if the column already exists. - - NOTE: this class is immutable and frozen. - """ - - def __init__(self, column: Column, if_not_exists: bool = False) -> None: - """ - Construct a new `AlterTableAddColumnOption` instance. - - Args: - column: The column definition to add. - if_not_exists: If `True`, uses `IF NOT EXISTS` clause. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [ - rq.AlterTableAddColumnOption( - rq.Column( - "updated_at", - rq.sqltypes.Timestamp(timezone=True), - default=rq.Expr.current_timestamp(), - ) - ) - ], - ).to_sql("postgres") - # ALTER TABLE "users" ADD COLUMN "updated_at" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def column(self) -> Column: - """The column definition to add.""" - ... - - @property - def if_not_exists(self) -> bool: - """Whether to use IF NOT EXISTS clause.""" - ... - -class AlterTableAddForeignKeyOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to add a foreign key constraint. - - Adds referential integrity between tables by creating a foreign key - relationship on an existing table. - """ - - def __init__(self, foreign_key: ForeignKey) -> None: - """ - Construct a new `AlterTableAddForeignKeyOption` instance. - - Args: - foreign_key: The foreign key definition to add. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [ - rq.AlterTableAddForeignKeyOption( - rq.ForeignKey(["font_id"], ["fonts.id"], on_delete="CASCADE") - ) - ], - ).to_sql("postgres") - # ALTER TABLE "users" ADD CONSTRAINT "fk_fonts_font_id__id" FOREIGN KEY ("font_id") REFERENCES "fonts" ("id") ON DELETE CASCADE - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def foreign_key(self) -> ForeignKey: - """The foreign key definition to add.""" - ... - -class AlterTableBaseOption: - """ - This abstract base class represents the different types of modifications - that can be made to an existing table structure, such as adding/dropping - columns, modifying column definitions, or managing foreign keys. - - NOTE: You cannot use this class as a parent (subclass), but you can use its children - as a parent (subclass). e.g. `AlterTableDropColumnOption`. - """ - -class AlterTableDropColumnOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to drop an existing column. - - Removes a column from the table. This operation may fail if the column - is referenced by other database objects. - """ - - def __init__(self, name: _ColumnRefNew) -> None: - """ - Construct a new `AlterTableDropColumnOption` instance. - - Args: - name: The column name/reference to drop. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [rq.AlterTableDropColumnOption("updated_at")], - ).to_sql("postgres") - # ALTER TABLE "users" DROP COLUMN "updated_at" - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def name(self) -> str: - """The column name to drop.""" - ... - -class AlterTableDropForeignKeyOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to drop a foreign key constraint. - - Removes a foreign key relationship by its constraint name. - """ - - def __init__(self, name: str) -> None: - """ - Construct a new `AlterTableDropForeignKeyOption` instance. - - Args: - name: The foreign key constraint name to drop. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [rq.AlterTableDropForeignKeyOption("fk_users_id_fonts_id")], - ).to_sql("postgres") - # ALTER TABLE "users" DROP CONSTRAINT "fk_users_id_fonts_id" - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def name(self) -> str: - """The foreign key constraint name to drop.""" - ... - -class AlterTableModifyColumnOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to modify a column definition. - - Changes properties of an existing column such as type, nullability, - default value, or other constraints. - """ - - def __init__(self, column: Column) -> None: - """ - Construct a new `AlterTableModifyColumnOption` instance. - - Args: - column: The column you want to modify with changes. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [ - rq.AlterTableModifyColumnOption( - rq.Column("updated_at", rq.sqltypes.Date(), default=None) - ) - ], - ).to_sql("postgres") - # ALTER TABLE "users" ALTER COLUMN "updated_at" TYPE date, ALTER COLUMN "updated_at" SET DEFAULT NULL - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def column(self) -> Column: ... - -class AlterTableRenameColumnOption(AlterTableBaseOption): - """ - `ALTER TABLE` operation to rename a column. - - Changes the name of an existing column without modifying its type - or constraints. - """ - - def __init__(self, from_name: _ColumnRefNew, to_name: _ColumnRefNew) -> None: - """ - Construct a new `AlterTableRenameColumnOption` instance. - - Args: - from_name: The column name/reference to rename. - to_name: New column name. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.AlterTable( - "users", - [rq.AlterTableRenameColumnOption("hello", "world")], - ).to_sql("postgres") - # ALTER TABLE "users" RENAME COLUMN "hello" TO "world" - ``` - """ - ... - - def __repr__(self, /) -> str: ... - @property - def from_name(self) -> str: - """The column name/reference to rename.""" - ... - - @property - def to_name(self) -> str: - """New column name.""" - ... - -class DropIndex(SchemaStatement): - """ - Represents a `DROP INDEX` SQL statement. - - Builds index deletion statements with support for: - - Conditional deletion (IF EXISTS) - - Table-specific index dropping - """ - - def __init__( - self, - name: str, - table: _TableNameNew, - if_exists: bool = False, - ) -> None: - """ - Construct a new `DROP INDEX` statement. - - Args: - name: The name of the index to drop. - table: The table from which to drop the index. - if_exists: If `True`, uses `IF EXISTS` clause (is not supported by MySQL). - - Examples: - ```python - import rapidquery as rq - - stmt = rq.DropIndex("idx_glyph_aspect", "glyph").to_sql("mysql") - # DROP INDEX `idx_glyph_aspect` ON `glyph` - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def if_exists(self) -> bool: - """Whether to use IF EXISTS clause to avoid errors.""" - ... - - @if_exists.setter - def if_exists(self, value: bool) -> None: ... - @property - def name(self) -> str: - """The name of the index to drop.""" - ... - @name.setter - def name(self, value: str) -> None: ... - @property - def table(self) -> TableName: - """The table from which to drop the index.""" - ... - @table.setter - def table(self, value: _TableNameNew) -> None: ... - -class DropTable(SchemaStatement): - """ - Represents a `DROP TABLE` SQL statement. - - Builds table deletion statements with support for: - - Conditional deletion (IF EXISTS) to avoid errors - - CASCADE to drop dependent objects - - RESTRICT to prevent deletion if dependencies exist - """ - - def __init__( - self, - name: _TableNameNew, - *, - if_exists: bool = False, - cascade: bool = False, - restrict: bool = False, - ) -> None: - """ - Construct a new `DROP TABLE` statement. - - Args: - name: The table name to drop. - if_exists: If `True`, adds the `IF EXISTS` clause to avoid an error if the table does not exist. - cascade: If `True`, adds the `CASCADE` clause to automatically drop objects that depend on - the table (such as foreign keys or views). - restrict: If `True`, adds the `RESTRICT` clause to refuse to drop the table if any - objects depend on it. This is typically the default behavior in many SQL dialects. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.DropTable("glyph", if_exists=True, cascade=True, restrict=True).to_sql( - "mysql" - ) - # DROP TABLE IF EXISTS `glyph` RESTRICT CASCADE - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def cascade(self) -> bool: - """Whether to automatically drop dependent objects.""" - ... - - @cascade.setter - def cascade(self, value: bool) -> None: ... - @property - def if_exists(self) -> bool: - """Whether to use the `IF EXISTS` clause.""" - ... - - @if_exists.setter - def if_exists(self, value: bool) -> None: ... - @property - def name(self) -> TableName: - """The table name to drop.""" - ... - - @name.setter - def name(self, value: _TableNameNew) -> None: ... - @property - def restrict(self) -> bool: - """Whether to prevent deletion if dependencies exist.""" - ... - - @restrict.setter - def restrict(self, value: bool) -> None: ... - -class Index(SchemaStatement): - """ - Represents a database index specification. - - This class defines the structure and properties of a database index, - including column definitions, uniqueness constraints, index type, - and partial indexing conditions. - - You can use it to generate `CREATE INDEX` SQL expressions. - """ - - def __init__( - self, - name: str | None, - columns: typing.Iterable[_IndexColumnValue], - table: _TableNameNew | None = None, - *, - primary: bool = False, - if_not_exists: bool = False, - nulls_not_distinct: bool = False, - unique: bool = False, - index_type: str | None = None, - where: Expr | None = None, - include: typing.Iterable[str] = (), - ) -> None: - """ - Construct a new database index specification. - Also you can use it to generate `CREATE INDEX` SQL expression. - - Args: - name: The index name. You should always set for indexes that aren't primary or unique, or - you want use them to generate `CREATE INDEX` statement. - columns: Column expressions to include in the index. You can use `IndexColumn` class - if you want to specify prefix or order. - table: The table on which to create the index. You should always set for indexes that - you want use them to generate `CREATE INDEX` statement. - primary: If `True`, means this is a primary key constraint. - if_not_exists: If `True`, uses `IF NOT EXISTS` clause. - nulls_not_distinct: If `True`, NULL values will be considered equal for uniqueness. - unique: If `True`, means this is a unique key constraint. - index_type: The type/algorithm for this index. e.g. `"HASH"`, `"BTREE"`, `"FULL TEXT"`. - where: Condition for partial indexing. - include: Additional columns to include in the index for covering queries. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.Index("idx_glyph_aspect", ["aspect"], "glyph", if_not_exists=True).to_sql( - "sqlite" - ) - # CREATE INDEX IF NOT EXISTS "idx_glyph_aspect" ON "glyph" ("aspect") - - stmt = rq.Index( - "idx_glyph_aspect", [rq.IndexColumn("aspect", "ASC", 128)], "glyph" - ).to_sql("mysql") - # CREATE INDEX `idx_glyph_aspect` ON `glyph` (`aspect` (128) ASC) - - stmt = rq.Index( - "idx_font_name_include_language", - ["name"], - "fonts", - include=["language"], - where=rq.Expr.col("aspect").in_([3, 4]), - ).to_sql("postgresql") - # CREATE INDEX "idx_font_name_include_language" ON "fonts" ("name") INCLUDE ("language") - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def columns(self) -> typing.Sequence[IndexColumn]: - """The columns that make up this index.""" - ... - @columns.setter - def columns(self, value: typing.Iterable[_IndexColumnValue]) -> None: ... - @property - def if_not_exists(self) -> bool: - """Whether to use IF NOT EXISTS clause.""" - ... - @if_not_exists.setter - def if_not_exists(self, value: bool) -> None: ... - @property - def include(self) -> typing.Sequence[str]: - """Additional columns to include in the index for covering queries.""" - ... - @include.setter - def include(self, value: typing.Iterable[str]) -> None: ... - @property - def index_type(self) -> str | None: - """The type/algorithm for this index.""" - ... - @index_type.setter - def index_type(self, value: str | None) -> None: ... - @property - def name(self) -> str | None: - """Index name""" - ... - @name.setter - def name(self, value: str | None) -> None: ... - @property - def nulls_not_distinct(self) -> bool: - """Whether NULL values should be considered equal for uniqueness.""" - ... - @nulls_not_distinct.setter - def nulls_not_distinct(self, value: bool) -> None: ... - @property - def primary(self) -> bool: - """Whether this is a primary key constraint.""" - ... - @primary.setter - def primary(self, value: bool) -> None: ... - @property - def table(self) -> TableName | None: - """The table on which to create the index.""" - ... - @table.setter - def table(self, value: _TableNameNew | None) -> None: ... - @property - def unique(self) -> bool: - """Whether this is a unique constraint.""" - ... - @unique.setter - def unique(self, value: bool) -> None: ... - @property - def where(self) -> Expr | None: - """Condition for partial indexing.""" - ... - @where.setter - def where(self, value: object | None) -> None: ... - -@typing.final -class IndexColumn: - """ - Defines a column within an index specification. - - Represents a single column's participation in an index, including: - - The column name - - Optional prefix length (for partial indexing) - - Sort order (ascending or descending) - - Used within `Index` to specify which columns are indexed - and how they should be ordered. - - NOTE: this class is immutable and frozen. - """ - - def __new__( - cls, - name: str, - order: _IndexColumnOrder | None = None, - prefix: int | None = None, - ) -> typing.Self: - """ - Construct a new `IndexColumn` instance. - - Args: - name: The column name to be indexed. - order: The sort order for the column, typically 'ASC' for ascending - or 'DESC' for descending. If `None`, the database default is used. - prefix: The number of characters to index for string/text columns. - Used for prefix indexing to save space or handle large text fields. - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def name(self) -> str: - """The name of the column to include in the index.""" - ... - - @property - def order(self) -> _IndexColumnOrder | None: - """Sort order for this column (e.g., ASC or DESC).""" - ... - - @property - def prefix(self) -> int | None: - """Number of characters to index for string columns (prefix indexing).""" - ... - -class RenameTable(SchemaStatement): - """ - Represents a `RENAME TABLE` SQL statement. - - Changes the name of an existing table to a new name. Both names can be - schema-qualified if needed. - """ - - def __init__(self, from_name: _TableNameNew, to_name: _TableNameNew) -> None: - """ - Construct a new `RENAME TABLE` statement. - - Args: - from_name: The current name of the table. - to_name: The new name for the table. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.RenameTable("users", "accounts").to_sql("mysql") - # RENAME TABLE `users` TO `accounts` - ``` - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def from_name(self) -> TableName: - """The current name of the table.""" - ... - @from_name.setter - def from_name(self, value: _TableNameNew) -> None: ... - @property - def to_name(self) -> TableName: - """The new name for the table.""" - ... - @to_name.setter - def to_name(self, value: _TableNameNew) -> None: ... - -class SchemaStatement: - """Subclass of schema statements.""" - - def to_sql(self, backend: _BackendName, /) -> str: - """Build a SQL string representation.""" - ... - -class Table(SchemaStatement): - """ - Represents a complete database table definition. - - This class encapsulates all aspects of a table structure including: - - Column definitions with their types and constraints - - Indexes for query optimization - - Foreign key relationships for referential integrity - - Check constraints for data validation - - Table-level options like engine, collation, and character set - - Used to generate `CREATE TABLE` SQL statements with full schema specifications. - """ - - def __init__( - self, - name: TableName | str, - *args: Column | Index | ForeignKey | Expr, - if_not_exists: bool = False, - temporary: bool = False, - comment: str | None = None, - engine: str | None = None, - collate: str | None = None, - character_set: str | None = None, - extra: str | None = None, - ) -> None: - """ - Construct a new database table definition. Also you can use it - to generate `CREATE TABLE` SQL statements with full schema specifications. - - Args: - name: The name of this table as represented in the database. You can also specify database and schema here. - If you're using `TableName`, you can use `database` and `schema` parameters; but you can also do it with `str`. - Format: `"db_name.schema_name.table_name"`. - args: List of columns (`Column`s), indexes (`Index`s), foreign keys (`ForeignKey`s), or - check constraints (`Expr`s). - if_not_exists: If `True`, uses `IF NOT EXISTS` clause. - temporary: If `True`, marks the table as a temporary table. - comment: Comment describing the purpose of this table. - engine: Storage engine for the table (e.g., InnoDB, MyISAM for MySQL). - collate: Collation for string comparisons and sorting in this table. - character_set: Character set encoding for text data in this table. - extra: Additional table-specific options for the statement. - - Examples: - ```python - import rapidquery as rq - - table = rq.Table( - "public.characters", - rq.Column("id", rq.sqltypes.BigInteger(), primary_key=True, auto_increment=True), - rq.Column("character", rq.sqltypes.Char(1)), - rq.Column("font_size", rq.sqltypes.SmallInteger()), - rq.Column("font_id", rq.sqltypes.BigInteger()), - rq.ForeignKey(["font_id"], ["fonts.id"], on_delete="CASCADE"), - rq.Index("ix_characters_fonts_id", ["font_id"]), - rq.Expr(rq.Func.char_length(rq.Expr.col("character"))) == 1, - if_not_exists=True, - ) - - table.to_sql("postgres") - # CREATE TABLE IF NOT EXISTS "public"."characters" ( - # "id" bigserial PRIMARY KEY NOT NULL, - # "character" char(1) NOT NULL, - # "font_size" smallint NOT NULL, - # "font_id" bigint NOT NULL, - # CONSTRAINT "fk_fonts_font_id__id" FOREIGN KEY ("font_id") REFERENCES "fonts" ("id") ON DELETE CASCADE, - # CHECK (CHAR_LENGTH("character") = 1) - # ); - # CREATE INDEX IF NOT EXISTS "ix_characters_fonts_id" ON "public"."characters" ("font_id") - ``` - """ - ... - - @property - def __table_name__(self) -> TableName: ... - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def character_set(self) -> str | None: - """Character set encoding for text data in this table.""" - ... - @character_set.setter - def character_set(self, value: str | None) -> None: ... - @property - def checks(self) -> typing.Sequence[Expr]: - """Table check constraints.""" - ... - @checks.setter - def checks(self, value: typing.Iterable[Expr]) -> None: ... - @property - def collate(self) -> str | None: - """Collation for string comparisons and sorting in this table.""" - ... - @collate.setter - def collate(self, value: str | None) -> None: ... - @property - def columns(self) -> typing.Sequence[Column]: - """Table columns.""" - ... - @columns.setter - def columns(self, value: typing.Iterable[Column]) -> None: ... - @property - def comment(self) -> str | None: - """Comment describing the purpose of this table.""" - ... - @comment.setter - def comment(self, value: str | None) -> None: ... - @property - def engine(self) -> str | None: - """Storage engine for the table (e.g., InnoDB, MyISAM for MySQL).""" - ... - @engine.setter - def engine(self, value: str | None) -> None: ... - @property - def extra(self) -> str | None: - """Additional table-specific options for the CREATE TABLE statement.""" - ... - @extra.setter - def extra(self, value: str | None) -> None: ... - @property - def foreign_keys(self) -> typing.Sequence[ForeignKey]: - """Table foreign keys.""" - ... - @foreign_keys.setter - def foreign_keys(self, value: typing.Iterable[ForeignKey]) -> None: ... - @property - def if_not_exists(self) -> bool: - """Whether to use IF NOT EXISTS clause to avoid errors if table exists.""" - ... - @if_not_exists.setter - def if_not_exists(self, value: bool) -> None: ... - @property - def indexes(self) -> typing.Sequence[Index]: - """Table indexes.""" - ... - @indexes.setter - def indexes(self, value: typing.Iterable[Index]) -> None: ... - @property - def name(self) -> TableName: - """The name of this table.""" - ... - - @property - def temporary(self) -> bool: - """Whether this is a temporary table that exists only for the session.""" - ... - @temporary.setter - def temporary(self, value: bool) -> None: ... - -class TruncateTable(SchemaStatement): - """ - Represents a `TRUNCATE TABLE` SQL statement. - - Quickly removes all rows from a table, typically faster than DELETE - and with different transaction and trigger behavior depending on the - database system. - - NOTE: SQLite doesn't support TRUNCATE statement. - """ - - def __init__(self, name: _TableNameNew) -> None: - """ - Construct a new `TRUNCATE TABLE` statement. - - Args: - name: The name of the table to truncate. - - Examples: - ```python - import rapidquery as rq - - stmt = rq.TruncateTable("users").to_sql("mysql") - # TRUNCATE TABLE `users` - ``` - - NOTE: SQLite doesn't support TRUNCATE statement. - """ - ... - - def __copy__(self) -> typing.Self: ... - def __repr__(self, /) -> str: ... - @property - def name(self) -> TableName: - """The name of the table to truncate.""" - ... - @name.setter - def name(self, value: _TableNameNew) -> None: ... diff --git a/rapidquery/_lib/sqlite.pyi b/rapidquery/_lib/sqlite.pyi deleted file mode 100644 index 4d21ee8..0000000 --- a/rapidquery/_lib/sqlite.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import annotations - -__all__ = [] diff --git a/rapidquery/_lib/sqltypes.pyi b/rapidquery/_lib/sqltypes.pyi deleted file mode 100644 index f6779dd..0000000 --- a/rapidquery/_lib/sqltypes.pyi +++ /dev/null @@ -1,797 +0,0 @@ -from __future__ import annotations - -import datetime -import decimal -import enum -import typing -import uuid - -T = typing.TypeVar("T") - -@typing.final -class Array(SQLTypeAbstract[list[T]]): - """ - Array column type for storing arrays of elements. - - Represents a column that stores arrays of a specified element type. - Useful in databases that support native array types (like PostgreSQL) - for storing lists of values in a single column. - """ - - def __new__(cls, element: SQLTypeAbstract[T]) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def element(self) -> SQLTypeAbstract[T]: ... - -@typing.final -class BigInteger(SQLTypeAbstract[int]): - """ - Large integer column type (BIGINT). - - Stores 64-bit integers for very large numeric values. Essential for - high-volume systems, timestamps, large counters, or when integer - overflow is a concern. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class BigUnsigned(SQLTypeAbstract[int]): - """ - Unsigned big integer column type. - - Stores very large positive integers only. Provides the maximum positive - integer range for high-volume systems or when very large positive - values are required. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Binary(SQLTypeAbstract[bytes]): - """ - Fixed-length binary data column type (BINARY). - - Stores binary data of a fixed length. Values shorter than the specified - length are padded. Useful for storing hashes, keys, or other binary - data with consistent length. - """ - - def __new__(cls, length: int = 255) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int: ... - -@typing.final -class Bit(SQLTypeAbstract[bytes]): - """ - Fixed-length bit string column type (BIT). - - Stores a fixed number of bits. Useful for storing boolean flags efficiently - or binary data where individual bits have meaning. - """ - - def __new__(cls, length: int) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int: ... - -@typing.final -class Blob(SQLTypeAbstract[bytes]): - """ - Binary large object column type (BLOB). - - Stores large binary data such as images, documents, audio files, or - any binary content. Size limits vary by database system. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Boolean(SQLTypeAbstract[bool]): - """ - Boolean column type (BOOLEAN). - - Stores true/false values. The standard way to store boolean data, - though implementation varies by database (some use TINYINT(1) or - similar representations). - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Char(SQLTypeAbstract[str]): - """ - Fixed-length character string column type (CHAR). - - Represents a fixed-length character string. Values shorter than the - specified length are padded with spaces. Suitable for storing data - with consistent, known lengths like country codes or status flags. - """ - - def __new__(cls, length: int | None = ...) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int | None: ... - -@typing.final -class Date(SQLTypeAbstract[datetime.date]): - """ - Date-only column type (DATE). - - Stores date information without time component. Ideal for birth dates, - deadlines, or any date-based data where time precision is not needed. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class DateTime(SQLTypeAbstract[datetime.datetime]): - """ - Date and time column type (DATETIME). - - Stores both date and time information without timezone awareness. - Suitable for recording timestamps, event times, or scheduling information - when timezone handling is managed at the application level. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Decimal(SQLTypeAbstract[decimal.Decimal | int | float | str]): - """ - Exact numeric decimal column type (DECIMAL/NUMERIC). - - Stores exact numeric values with fixed precision and scale. Essential for - financial calculations, currency values, or any situation where exact - decimal representation is required without floating-point approximation. - """ - - def __new__(cls, context: tuple[int, int] | None = None) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def context(self) -> tuple[int, int] | None: ... - -@typing.final -class Double(SQLTypeAbstract[float | int]): - """ - Double-precision floating point column type (DOUBLE). - - Stores approximate numeric values with double precision. Provides higher - precision than FLOAT for scientific calculations or when more accuracy - is required in floating-point operations. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Enum(SQLTypeAbstract[str | enum.Enum]): - """ - Enumeration column type (ENUM). - - Stores one value from a predefined set of allowed string values. - Provides type safety and storage efficiency for categorical data - with a fixed set of possible values. - """ - - def __new__(cls, name: str, variants: typing.Iterable[str]) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def name(self) -> str: ... - @property - def variants(self) -> typing.Sequence[str]: ... - -@typing.final -class Float(SQLTypeAbstract[float | int]): - """ - Single-precision floating point column type (FLOAT). - - Stores approximate numeric values with single precision. Suitable for - scientific calculations, measurements, or any numeric data where some - precision loss is acceptable in exchange for storage efficiency. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class INET(SQLTypeAbstract[str]): - """ - Internet address column type (INET). - - Stores IPv4 or IPv6 addresses, with or without subnet specification. - More flexible than CIDR type, allowing both host addresses and network ranges. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Integer(SQLTypeAbstract[int]): - """ - Standard integer column type (INTEGER/INT). - - The most common integer type, typically storing 32-bit integers in the - range -2,147,483,648 to 2,147,483,647 (signed). Suitable for most - numeric data including IDs, quantities, and counters. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class JSON(SQLTypeAbstract[typing.Any]): - """ - JSON data column type (JSON). - - Stores JSON documents with validation and indexing capabilities. - Allows for flexible schema design and complex nested data structures - while maintaining some query capabilities. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class JSONBinary(SQLTypeAbstract[typing.Any]): - """ - Binary JSON column type (JSONB). - - Stores JSON documents in a binary format for improved performance. - Provides faster query and manipulation operations compared to text-based - JSON storage, with additional indexing capabilities. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class MacAddress(SQLTypeAbstract[str]): - """ - MAC address column type (MACADDR). - - Stores MAC (Media Access Control) addresses for network devices. - Provides validation and formatting for 6-byte MAC addresses. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -class SQLTypeAbstract(typing.Generic[T]): - """ - Base class for all SQL column data types. - - This abstract base class represents SQL data types that can be used in - column definitions. Each subclass implements a specific SQL data type - with its particular characteristics, constraints, and backend-specific - representations. - """ - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class SmallInteger(SQLTypeAbstract[int]): - """ - Small integer column type (SMALLINT). - - Typically stores integers in the range -32,768 to 32,767 (signed) or - 0 to 65,535 (unsigned). Good for moderate-sized counters or numeric codes. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class SmallUnsigned(SQLTypeAbstract[int]): - """ - Unsigned small integer column type. - - Stores moderate positive integers only, typically 0 to 65,535. Good for - larger counters or numeric codes that are always positive. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class String(SQLTypeAbstract[str]): - """ - Variable-length character string column type (VARCHAR). - - Represents a variable-length character string with a maximum length limit. - This is the most common string type for storing text data of varying lengths - like names, descriptions, or user input. - """ - - def __new__(cls, length: int | None = ...) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int | None: ... - -@typing.final -class Text(SQLTypeAbstract[str]): - """ - Large text column type (TEXT). - - Represents a large text field capable of storing long strings without - a predefined length limit. Suitable for storing articles, comments, - descriptions, or any text content that may be very long. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Time(SQLTypeAbstract[datetime.time]): - """ - Time-only column type (TIME). - - Stores time information without date component. Useful for storing - daily schedules, opening hours, or any time-based data that repeats - daily regardless of the specific date. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Timestamp(SQLTypeAbstract[datetime.datetime]): - """ - Timestamp column type (TIMESTAMP). - - Stores timestamp values, often with automatic update capabilities. - Behavior varies by database system. - """ - - def __new__(cls, timezone: bool = False) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def timezone(self) -> bool: ... - -@typing.final -class TinyInteger(SQLTypeAbstract[int]): - """ - Very small integer column type (TINYINT). - - Typically stores integers in the range -128 to 127 (signed) or 0 to 255 - (unsigned). Useful for flags, small counters, or enumerated values. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class TinyUnsigned(SQLTypeAbstract[int]): - """ - Unsigned tiny integer column type. - - Stores small positive integers only, typically 0 to 255. Useful for - small counters, percentages, or enumerated values that are always positive. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class UUID(SQLTypeAbstract[uuid.UUID]): - """ - UUID column type (UUID). - - Stores universally unique identifiers. Ideal for distributed systems, - primary keys, or any situation where globally unique identifiers are - needed without central coordination. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class Unsigned(SQLTypeAbstract[int]): - """ - Unsigned integer column type. - - Stores positive integers only, typically 0 to 4,294,967,295. Doubles the - positive range compared to signed integers, useful for IDs and counters - that will never be negative. - """ - - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - -@typing.final -class VarBinary(SQLTypeAbstract[bytes]): - """ - Variable-length binary data column type (VARBINARY). - - Stores binary data of variable length up to a specified maximum. - More storage-efficient than BINARY for binary data of varying lengths. - """ - - def __new__(cls, length: int | None = None) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int: ... - -@typing.final -class VarBit(SQLTypeAbstract[bytes]): - """ - Variable-length bit string column type (VARBIT). - - Stores a variable number of bits up to a specified maximum. More flexible - than fixed BIT type for bit strings of varying lengths. - """ - - def __new__(cls, length: int) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int: ... - -@typing.final -class Vector(SQLTypeAbstract[list[float]]): - """ - Vector column type for storing mathematical vectors. - - Specialized type for storing vector data, often used in machine learning, - similarity search, or mathematical applications. - """ - - def __new__(cls, length: int | None = None) -> typing.Self: ... - def __repr__(self, /) -> str: - """Return repr(self).""" - ... - - @property - def __type_name__(self) -> str: - """ - Type name. e.g. `'INTEGER'`, `'STRING'` - - It also may be a property. This function must NOT raise any error. - """ - ... - - @property - def length(self) -> int | None: ... diff --git a/rapidquery/dcl.py b/rapidquery/dcl.py new file mode 100644 index 0000000..dcd839e --- /dev/null +++ b/rapidquery/dcl.py @@ -0,0 +1,5 @@ +from ._core.dcl import DataControlLanguage + +__all__ = [ + "DataControlLanguage", +] diff --git a/rapidquery/dcl.pyi b/rapidquery/dcl.pyi new file mode 100644 index 0000000..b950892 --- /dev/null +++ b/rapidquery/dcl.pyi @@ -0,0 +1,6 @@ +"""Data Control Language""" + +class DataControlLanguage: + """Base of every DCL statement. Construct a subclass, not this class.""" + + ... diff --git a/rapidquery/ddl.py b/rapidquery/ddl.py new file mode 100644 index 0000000..6e97d68 --- /dev/null +++ b/rapidquery/ddl.py @@ -0,0 +1,5 @@ +from ._core.ddl import DataDefinitionLanguage + +__all__ = [ + "DataDefinitionLanguage", +] diff --git a/rapidquery/ddl.pyi b/rapidquery/ddl.pyi new file mode 100644 index 0000000..d513aa1 --- /dev/null +++ b/rapidquery/ddl.pyi @@ -0,0 +1,6 @@ +"""Data Definition Language""" + +class DataDefinitionLanguage: + """Base of every DDL statement. Construct a subclass, not this class.""" + + ... diff --git a/rapidquery/dialect.py b/rapidquery/dialect.py new file mode 100644 index 0000000..5d11267 --- /dev/null +++ b/rapidquery/dialect.py @@ -0,0 +1,259 @@ +from ._core.dialect import ( + # Dialect options + OPT_ARRAY_FORMAT_CONSTRUCTOR, + OPT_ARRAY_FORMAT_FUNCTION, + OPT_ARRAY_FORMAT_LIST, + OPT_ARRAY_FORMAT_UNSUPPORTED, + OPT_BIT_FORMAT_POSTFIX_BIG_B, + OPT_BIT_FORMAT_PREFIX_BIG_B, + OPT_BIT_FORMAT_PREFIX_SMALL_B, + OPT_BIT_FORMAT_UNSUPPORTED, + OPT_BITWISE_BIT_AND_ONLY, + OPT_BITWISE_BIT_SHIFT_FUNCTIONS, + OPT_BITWISE_INFIX, + OPT_BITWISE_INFIX_AND_SHIFT, + OPT_BITWISE_INFIX_NO_SHIFT, + OPT_BITWISE_SHIFT_FUNCTIONS, + OPT_BITXOR_BIT_XOR_FUNCTION, + OPT_BITXOR_CARET, + OPT_BITXOR_HASH, + OPT_BITXOR_UNSUPPORTED, + OPT_BITXOR_XOR_FUNCTION, + OPT_BOOL_FORMAT_INTEGER, + OPT_BOOL_FORMAT_KEYWORD, + OPT_BYTES_FORMAT_HEX_TO_RAW, + OPT_BYTES_FORMAT_POSTFIX_XBV, + OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X, + OPT_BYTES_FORMAT_PREFIX_BIG_X, + OPT_BYTES_FORMAT_PREFIX_ZERO_X, + OPT_BYTES_FORMAT_SLASH_X_BLOB, + OPT_BYTES_FORMAT_SLASH_X_BYTEA, + OPT_BYTES_FORMAT_TO_BINARY, + OPT_BYTES_FORMAT_TO_VARBYTE, + OPT_BYTES_FORMAT_UNHEX, + OPT_COLLATE_FUNCTION_STRING, + OPT_COLLATE_INFIX_IDENT, + OPT_COLLATE_UNSUPPORTED, + OPT_DATE_FORMAT_KEYWORD, + OPT_DATE_FORMAT_QUOTED, + OPT_FUNCTION_FILTER_FILTER, + OPT_FUNCTION_FILTER_FILTER_WHERE, + OPT_FUNCTION_FILTER_INSIDE_WHERE, + OPT_FUNCTION_FILTER_UNSUPPORTED, + OPT_IDENTIFIER_ESCAPE_BACKSLASH, + OPT_IDENTIFIER_ESCAPE_DOUBLE, + OPT_IDENTIFIER_FORMAT_BACKTICK, + OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE, + OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET, + OPT_ILIKE_EXPRESSION_STANDARD, + OPT_ILIKE_EXPRESSION_USE_LIKE, + OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER, + OPT_INET_FORMAT_CAST, + OPT_INET_FORMAT_QUOTED, + OPT_INET_FORMAT_UNSUPPORTED, + OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, + OPT_IS_DISTINCT_FROM_STANDARD, + OPT_IS_DISTINCT_FROM_UNSUPPORTED, + OPT_JSON_FORMAT_JSON_KEYWORD, + OPT_JSON_FORMAT_PARSE_JSON, + OPT_JSON_FORMAT_QUOTED, + OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, + OPT_LIKE_ANY_OR_EXPANSION, + OPT_LIKE_ANY_PATTERN_LIST, + OPT_LIKE_ESCAPE_INCLUDING_ANY, + OPT_LIKE_ESCAPE_SCALAR, + OPT_LIKE_ESCAPE_UNSUPPORTED, + OPT_MACADDR_FORMAT_CAST, + OPT_MACADDR_FORMAT_QUOTED, + OPT_MACADDR_FORMAT_UNSUPPORTED, + OPT_MODULO_FUNCTION, + OPT_MODULO_INFIX, + OPT_MODULO_PERCENT, + OPT_NULL_TREATMENT_INSIDE_PARENS, + OPT_NULL_TREATMENT_POSTFIX, + OPT_NULL_TREATMENT_UNSUPPORTED, + OPT_PLACEHOLDER_NUMBERED, + OPT_PLACEHOLDER_SIMPLE, + OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS, + OPT_RLIKE_EXPRESSION_REGEXP_LIKE, + OPT_RLIKE_EXPRESSION_REGEXP_MATCHES, + OPT_RLIKE_EXPRESSION_STANDARD, + OPT_RLIKE_EXPRESSION_TILDE, + OPT_RLIKE_EXPRESSION_UNSUPPORTED, + OPT_STRING_CONCAT_CONCAT_FUNCTION, + OPT_STRING_CONCAT_PIPES, + OPT_STRING_CONCAT_PLUS, + OPT_STRING_ESCAPE_BACKSLASH_HEX, + OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO, + OPT_STRING_ESCAPE_BACKSLASH_MYSQL, + OPT_STRING_ESCAPE_BACKSLASH_UNICODE, + OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE, + OPT_STRING_ESCAPE_QUOTE, + OPT_STRING_FORMAT_PREFIX_BIG_E, + OPT_STRING_FORMAT_PREFIX_BIG_N, + OPT_STRING_FORMAT_SINGLE_QUOTE, + OPT_TIME_FORMAT_KEYWORD, + OPT_TIME_FORMAT_QUOTED, + OPT_TIMESTAMP_FORMAT_QUOTED, + OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ, + OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE, + OPT_TRIM_FUNCTION, + OPT_TRIM_SPACES_ONLY, + OPT_TRIM_SQL_FROM, + OPT_TRY_CAST_SAFE_CAST, + OPT_TRY_CAST_TRY_CAST, + OPT_TRY_CAST_TRY_CAST_WORD, + OPT_TRY_CAST_UNSUPPORTED, + OPT_TUPLE_PARENTHESES, + OPT_TUPLE_ROW, + OPT_TUPLE_ROW_REJECT_EMPTY, + OPT_TUPLE_STRUCT, + OPT_TUPLE_UNSUPPORTED, + OPT_UUID_FORMAT_CAST_UUID, + OPT_UUID_FORMAT_QUOTED, + OPT_UUID_FORMAT_UUID_KEYWORD, + # Dialects + BigQuery, + Databricks, + Dialect, + DuckDb, + Hive, + MsSql, + MySql, + Oracle, + PostgreSql, + Redshift, + Snowflake, + Spark, + Sqlite, + Teradata, +) + +__all__ = [ + "OPT_ARRAY_FORMAT_CONSTRUCTOR", + "OPT_ARRAY_FORMAT_FUNCTION", + "OPT_ARRAY_FORMAT_LIST", + "OPT_ARRAY_FORMAT_UNSUPPORTED", + "OPT_BITWISE_BIT_AND_ONLY", + "OPT_BITWISE_BIT_SHIFT_FUNCTIONS", + "OPT_BITWISE_INFIX", + "OPT_BITWISE_INFIX_AND_SHIFT", + "OPT_BITWISE_INFIX_NO_SHIFT", + "OPT_BITWISE_SHIFT_FUNCTIONS", + "OPT_BITXOR_BIT_XOR_FUNCTION", + "OPT_BITXOR_CARET", + "OPT_BITXOR_HASH", + "OPT_BITXOR_UNSUPPORTED", + "OPT_BITXOR_XOR_FUNCTION", + "OPT_BIT_FORMAT_POSTFIX_BIG_B", + "OPT_BIT_FORMAT_PREFIX_BIG_B", + "OPT_BIT_FORMAT_PREFIX_SMALL_B", + "OPT_BIT_FORMAT_UNSUPPORTED", + "OPT_BOOL_FORMAT_INTEGER", + "OPT_BOOL_FORMAT_KEYWORD", + "OPT_BYTES_FORMAT_HEX_TO_RAW", + "OPT_BYTES_FORMAT_POSTFIX_XBV", + "OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X", + "OPT_BYTES_FORMAT_PREFIX_BIG_X", + "OPT_BYTES_FORMAT_PREFIX_ZERO_X", + "OPT_BYTES_FORMAT_SLASH_X_BLOB", + "OPT_BYTES_FORMAT_SLASH_X_BYTEA", + "OPT_BYTES_FORMAT_TO_BINARY", + "OPT_BYTES_FORMAT_TO_VARBYTE", + "OPT_BYTES_FORMAT_UNHEX", + "OPT_COLLATE_FUNCTION_STRING", + "OPT_COLLATE_INFIX_IDENT", + "OPT_COLLATE_UNSUPPORTED", + "OPT_DATE_FORMAT_KEYWORD", + "OPT_DATE_FORMAT_QUOTED", + "OPT_FUNCTION_FILTER_FILTER", + "OPT_FUNCTION_FILTER_FILTER_WHERE", + "OPT_FUNCTION_FILTER_INSIDE_WHERE", + "OPT_FUNCTION_FILTER_UNSUPPORTED", + "OPT_IDENTIFIER_ESCAPE_BACKSLASH", + "OPT_IDENTIFIER_ESCAPE_DOUBLE", + "OPT_IDENTIFIER_FORMAT_BACKTICK", + "OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE", + "OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET", + "OPT_ILIKE_EXPRESSION_STANDARD", + "OPT_ILIKE_EXPRESSION_USE_LIKE", + "OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER", + "OPT_INET_FORMAT_CAST", + "OPT_INET_FORMAT_QUOTED", + "OPT_INET_FORMAT_UNSUPPORTED", + "OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL", + "OPT_IS_DISTINCT_FROM_STANDARD", + "OPT_IS_DISTINCT_FROM_UNSUPPORTED", + "OPT_JSON_FORMAT_JSON_KEYWORD", + "OPT_JSON_FORMAT_PARSE_JSON", + "OPT_JSON_FORMAT_QUOTED", + "OPT_LIKE_ANY_ARRAY_CONSTRUCTOR", + "OPT_LIKE_ANY_OR_EXPANSION", + "OPT_LIKE_ANY_PATTERN_LIST", + "OPT_LIKE_ESCAPE_INCLUDING_ANY", + "OPT_LIKE_ESCAPE_SCALAR", + "OPT_LIKE_ESCAPE_UNSUPPORTED", + "OPT_MACADDR_FORMAT_CAST", + "OPT_MACADDR_FORMAT_QUOTED", + "OPT_MACADDR_FORMAT_UNSUPPORTED", + "OPT_MODULO_FUNCTION", + "OPT_MODULO_INFIX", + "OPT_MODULO_PERCENT", + "OPT_NULL_TREATMENT_INSIDE_PARENS", + "OPT_NULL_TREATMENT_POSTFIX", + "OPT_NULL_TREATMENT_UNSUPPORTED", + "OPT_PLACEHOLDER_NUMBERED", + "OPT_PLACEHOLDER_SIMPLE", + "OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS", + "OPT_RLIKE_EXPRESSION_REGEXP_LIKE", + "OPT_RLIKE_EXPRESSION_REGEXP_MATCHES", + "OPT_RLIKE_EXPRESSION_STANDARD", + "OPT_RLIKE_EXPRESSION_TILDE", + "OPT_RLIKE_EXPRESSION_UNSUPPORTED", + "OPT_STRING_CONCAT_CONCAT_FUNCTION", + "OPT_STRING_CONCAT_PIPES", + "OPT_STRING_CONCAT_PLUS", + "OPT_STRING_ESCAPE_BACKSLASH_HEX", + "OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO", + "OPT_STRING_ESCAPE_BACKSLASH_MYSQL", + "OPT_STRING_ESCAPE_BACKSLASH_UNICODE", + "OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE", + "OPT_STRING_ESCAPE_QUOTE", + "OPT_STRING_FORMAT_PREFIX_BIG_E", + "OPT_STRING_FORMAT_PREFIX_BIG_N", + "OPT_STRING_FORMAT_SINGLE_QUOTE", + "OPT_TIMESTAMP_FORMAT_QUOTED", + "OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ", + "OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE", + "OPT_TIME_FORMAT_KEYWORD", + "OPT_TIME_FORMAT_QUOTED", + "OPT_TRIM_FUNCTION", + "OPT_TRIM_SPACES_ONLY", + "OPT_TRIM_SQL_FROM", + "OPT_TRY_CAST_SAFE_CAST", + "OPT_TRY_CAST_TRY_CAST", + "OPT_TRY_CAST_TRY_CAST_WORD", + "OPT_TRY_CAST_UNSUPPORTED", + "OPT_TUPLE_PARENTHESES", + "OPT_TUPLE_ROW", + "OPT_TUPLE_ROW_REJECT_EMPTY", + "OPT_TUPLE_STRUCT", + "OPT_TUPLE_UNSUPPORTED", + "OPT_UUID_FORMAT_CAST_UUID", + "OPT_UUID_FORMAT_QUOTED", + "OPT_UUID_FORMAT_UUID_KEYWORD", + "BigQuery", + "Databricks", + "Dialect", + "DuckDb", + "Hive", + "MsSql", + "MySql", + "Oracle", + "PostgreSql", + "Redshift", + "Snowflake", + "Spark", + "Sqlite", + "Teradata", +] diff --git a/rapidquery/dialect.pyi b/rapidquery/dialect.pyi new file mode 100644 index 0000000..df1d110 --- /dev/null +++ b/rapidquery/dialect.pyi @@ -0,0 +1,721 @@ +""" +SQL dialects and the integer flags that configure one. + +Use a builtin (`PostgreSql`, `Sqlite`, `MySql`, and the others) when rendering +an expression. Build a `Dialect` only when you need a different combination of +`OPT_*` flags. Each flag selects a spelling; it does not invent syntax the +dialect lacks. + +Examples: + Quote an identifier and write a boolean the way MySQL does: + + >>> from rapidquery.dialect import ( + ... OPT_BOOL_FORMAT_INTEGER, + ... OPT_IDENTIFIER_FORMAT_BACKTICK, + ... Dialect, + ... ) + >>> dialect = Dialect( + ... bool_format=OPT_BOOL_FORMAT_INTEGER, + ... identifier_format=OPT_IDENTIFIER_FORMAT_BACKTICK, + ... ) + >>> dialect.write_boolean(True) + '1' + >>> dialect.write_identifier("id") + '`id`' +""" + +import typing + +OPT_PLACEHOLDER_SIMPLE: typing.Final[int] +"""Placeholders are a bare `?`.""" + +OPT_PLACEHOLDER_NUMBERED: typing.Final[int] +"""Placeholders are numbered, such as `$1`.""" + +OPT_NULL_TREATMENT_UNSUPPORTED: typing.Final[int] +"""`IGNORE NULLS` and `RESPECT NULLS` are rejected.""" + +OPT_NULL_TREATMENT_INSIDE_PARENS: typing.Final[int] +"""Null treatment is written inside the call parentheses.""" + +OPT_NULL_TREATMENT_POSTFIX: typing.Final[int] +"""Null treatment is written after the call.""" + +OPT_LIKE_ESCAPE_UNSUPPORTED: typing.Final[int] +"""`LIKE` has no `ESCAPE` clause.""" + +OPT_LIKE_ESCAPE_SCALAR: typing.Final[int] +"""`ESCAPE` is legal on a single pattern, not on native `LIKE ANY`.""" + +OPT_LIKE_ESCAPE_INCLUDING_ANY: typing.Final[int] +"""`ESCAPE` is legal on a single pattern and on `LIKE ANY`.""" + +OPT_LIKE_ANY_OR_EXPANSION: typing.Final[int] +"""`LIKE ANY` is expanded to `OR` of scalar `LIKE` predicates.""" + +OPT_LIKE_ANY_ARRAY_CONSTRUCTOR: typing.Final[int] +"""`LIKE ANY` uses `ANY (ARRAY[…])`.""" + +OPT_LIKE_ANY_PATTERN_LIST: typing.Final[int] +"""`LIKE ANY` uses a parenthesized pattern list.""" + +OPT_ILIKE_EXPRESSION_STANDARD: typing.Final[int] +"""Case-insensitive match is the `ILIKE` operator.""" + +OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER: typing.Final[int] +"""Case-insensitive match lowers both sides and uses `LIKE`.""" + +OPT_ILIKE_EXPRESSION_USE_LIKE: typing.Final[int] +"""Case-insensitive match falls back to `LIKE`.""" + +OPT_RLIKE_EXPRESSION_UNSUPPORTED: typing.Final[int] +"""Regex predicates are rejected.""" + +OPT_RLIKE_EXPRESSION_STANDARD: typing.Final[int] +"""Regex match is the `RLIKE` operator.""" + +OPT_RLIKE_EXPRESSION_TILDE: typing.Final[int] +"""Regex match is `~`.""" + +OPT_RLIKE_EXPRESSION_REGEXP_LIKE: typing.Final[int] +"""Regex match is `REGEXP_LIKE`.""" + +OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS: typing.Final[int] +"""Regex match is `REGEXP_CONTAINS`.""" + +OPT_RLIKE_EXPRESSION_REGEXP_MATCHES: typing.Final[int] +"""Regex match is `regexp_matches`.""" + +OPT_STRING_CONCAT_PIPES: typing.Final[int] +"""String concatenation is `||`.""" + +OPT_STRING_CONCAT_PLUS: typing.Final[int] +"""String concatenation is `+`.""" + +OPT_STRING_CONCAT_CONCAT_FUNCTION: typing.Final[int] +"""String concatenation is `CONCAT`.""" + +OPT_BITXOR_UNSUPPORTED: typing.Final[int] +"""Bitwise XOR is rejected.""" + +OPT_BITXOR_CARET: typing.Final[int] +"""Bitwise XOR is `^`.""" + +OPT_BITXOR_HASH: typing.Final[int] +"""Bitwise XOR is `#`.""" + +OPT_BITXOR_XOR_FUNCTION: typing.Final[int] +"""Bitwise XOR is `XOR`.""" + +OPT_BITXOR_BIT_XOR_FUNCTION: typing.Final[int] +"""Bitwise XOR is `BITXOR`.""" + +OPT_MODULO_PERCENT: typing.Final[int] +"""Modulo is `%`.""" + +OPT_MODULO_FUNCTION: typing.Final[int] +"""Modulo is a function call.""" + +OPT_MODULO_INFIX: typing.Final[int] +"""Modulo is the dialect's infix operator.""" + +OPT_IS_DISTINCT_FROM_UNSUPPORTED: typing.Final[int] +"""`IS DISTINCT FROM` is rejected.""" + +OPT_IS_DISTINCT_FROM_STANDARD: typing.Final[int] +"""`IS DISTINCT FROM` uses the SQL standard spelling.""" + +OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL: typing.Final[int] +"""`IS DISTINCT FROM` uses the dialect's null-safe equality operator.""" + +OPT_TRY_CAST_UNSUPPORTED: typing.Final[int] +"""`TRY_CAST` is rejected.""" + +OPT_TRY_CAST_TRY_CAST: typing.Final[int] +"""`TRY_CAST` is written `TRY_CAST`.""" + +OPT_TRY_CAST_TRY_CAST_WORD: typing.Final[int] +"""`TRY_CAST` is written as one word, `TRYCAST`.""" + +OPT_TRY_CAST_SAFE_CAST: typing.Final[int] +"""`TRY_CAST` is written `SAFE_CAST`.""" + +OPT_TRIM_SQL_FROM: typing.Final[int] +"""`TRIM` uses the SQL `TRIM([side] chars FROM expr)` form.""" + +OPT_TRIM_FUNCTION: typing.Final[int] +"""`TRIM` is a function, including `LTRIM` and `RTRIM`.""" + +OPT_TRIM_SPACES_ONLY: typing.Final[int] +"""`TRIM` can remove spaces only.""" + +OPT_COLLATE_UNSUPPORTED: typing.Final[int] +"""`COLLATE` is rejected.""" + +OPT_COLLATE_INFIX_IDENT: typing.Final[int] +"""`COLLATE` is an infix identifier.""" + +OPT_COLLATE_FUNCTION_STRING: typing.Final[int] +"""`COLLATE` is a function and the name is a string.""" + +OPT_BITWISE_INFIX: typing.Final[int] +"""Bitwise AND, OR, NOT, and shifts are infix operators.""" + +OPT_BITWISE_INFIX_AND_SHIFT: typing.Final[int] +"""Bitwise operators and shifts are both infix.""" + +OPT_BITWISE_INFIX_NO_SHIFT: typing.Final[int] +"""Bitwise AND, OR, and NOT are infix. Shifts are rejected.""" + +OPT_BITWISE_BIT_SHIFT_FUNCTIONS: typing.Final[int] +"""Shifts are `BITSHIFTLEFT` and `BITSHIFTRIGHT`.""" + +OPT_BITWISE_SHIFT_FUNCTIONS: typing.Final[int] +"""Shifts are `SHIFTLEFT` and `SHIFTRIGHT`.""" + +OPT_BITWISE_BIT_AND_ONLY: typing.Final[int] +"""Only bitwise AND is available, as a function.""" + +OPT_TUPLE_UNSUPPORTED: typing.Final[int] +"""Tuple values are rejected.""" + +OPT_TUPLE_ROW: typing.Final[int] +"""Tuples are `ROW(…)`.""" + +OPT_TUPLE_ROW_REJECT_EMPTY: typing.Final[int] +"""Tuples are `ROW(…)`. An empty tuple is rejected.""" + +OPT_TUPLE_STRUCT: typing.Final[int] +"""Tuples are `struct(…)`.""" + +OPT_TUPLE_PARENTHESES: typing.Final[int] +"""Tuples are `(…)`. Fewer than two elements is rejected.""" + +OPT_BIT_FORMAT_UNSUPPORTED: typing.Final[int] +"""Bit-string literals are rejected.""" + +OPT_BIT_FORMAT_PREFIX_BIG_B: typing.Final[int] +"""Bit strings are `B'…'`.""" + +OPT_BIT_FORMAT_PREFIX_SMALL_B: typing.Final[int] +"""Bit strings are `b'…'`.""" + +OPT_BIT_FORMAT_POSTFIX_BIG_B: typing.Final[int] +"""Bit strings are `'…'B`.""" + +OPT_BOOL_FORMAT_KEYWORD: typing.Final[int] +"""Booleans are `TRUE` and `FALSE`.""" + +OPT_BOOL_FORMAT_INTEGER: typing.Final[int] +"""Booleans are `1` and `0`.""" + +OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE: typing.Final[int] +"""Quoted identifiers use double quotes.""" + +OPT_IDENTIFIER_FORMAT_BACKTICK: typing.Final[int] +"""Quoted identifiers use backticks.""" + +OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET: typing.Final[int] +"""Quoted identifiers use square brackets.""" + +OPT_IDENTIFIER_ESCAPE_DOUBLE: typing.Final[int] +"""A quote inside an identifier is written twice.""" + +OPT_IDENTIFIER_ESCAPE_BACKSLASH: typing.Final[int] +"""A quote inside an identifier is escaped with a backslash.""" + +OPT_STRING_FORMAT_SINGLE_QUOTE: typing.Final[int] +"""String literals use single quotes.""" + +OPT_STRING_FORMAT_PREFIX_BIG_E: typing.Final[int] +"""String literals use the `E'…'` escape prefix.""" + +OPT_STRING_FORMAT_PREFIX_BIG_N: typing.Final[int] +"""String literals use the national `N'…'` prefix.""" + +OPT_STRING_ESCAPE_QUOTE: typing.Final[int] +"""A quote inside a string is written twice.""" + +OPT_STRING_ESCAPE_BACKSLASH_HEX: typing.Final[int] +"""String escapes use backslash, with non-ASCII bytes in hex.""" + +OPT_STRING_ESCAPE_BACKSLASH_MYSQL: typing.Final[int] +"""String escapes use the MySQL backslash table.""" + +OPT_STRING_ESCAPE_BACKSLASH_UNICODE: typing.Final[int] +"""String escapes use backslash Unicode sequences.""" + +OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO: typing.Final[int] +"""String escapes use backslash hex, and NUL is rejected.""" + +OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE: typing.Final[int] +"""String escapes use backslash Unicode, and quotes are escaped too.""" + +OPT_BYTES_FORMAT_PREFIX_BIG_X: typing.Final[int] +"""Bytes are `X'…'`.""" + +OPT_BYTES_FORMAT_PREFIX_ZERO_X: typing.Final[int] +"""Bytes are `0x…`.""" + +OPT_BYTES_FORMAT_SLASH_X_BYTEA: typing.Final[int] +"""Bytes are `'\\x…'::bytea`.""" + +OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X: typing.Final[int] +"""Bytes are `b'\\x…'`.""" + +OPT_BYTES_FORMAT_HEX_TO_RAW: typing.Final[int] +"""Bytes are `HEXTORAW('…')`.""" + +OPT_BYTES_FORMAT_UNHEX: typing.Final[int] +"""Bytes are `UNHEX('…')`.""" + +OPT_BYTES_FORMAT_TO_VARBYTE: typing.Final[int] +"""Bytes are `TO_VARBYTE(…)`.""" + +OPT_BYTES_FORMAT_POSTFIX_XBV: typing.Final[int] +"""Bytes use the dialect's postfix binary literal.""" + +OPT_BYTES_FORMAT_TO_BINARY: typing.Final[int] +"""Bytes are `TO_BINARY('…')`.""" + +OPT_BYTES_FORMAT_SLASH_X_BLOB: typing.Final[int] +"""Bytes are `'\\x…'::BLOB`.""" + +OPT_FUNCTION_FILTER_UNSUPPORTED: typing.Final[int] +"""Aggregate `FILTER` is rejected.""" + +OPT_FUNCTION_FILTER_FILTER: typing.Final[int] +"""`FILTER` omits the `WHERE` keyword.""" + +OPT_FUNCTION_FILTER_FILTER_WHERE: typing.Final[int] +"""`FILTER` is `FILTER (WHERE …)`.""" + +OPT_FUNCTION_FILTER_INSIDE_WHERE: typing.Final[int] +"""The filter is a `WHERE` clause inside the call.""" + +OPT_ARRAY_FORMAT_UNSUPPORTED: typing.Final[int] +"""Array literals are rejected.""" + +OPT_ARRAY_FORMAT_CONSTRUCTOR: typing.Final[int] +"""Arrays are `ARRAY[…]`.""" + +OPT_ARRAY_FORMAT_LIST: typing.Final[int] +"""Arrays are a bracket list.""" + +OPT_ARRAY_FORMAT_FUNCTION: typing.Final[int] +"""Arrays are a function call.""" + +OPT_DATE_FORMAT_QUOTED: typing.Final[int] +"""Dates are quoted `YYYY-MM-DD` strings.""" + +OPT_DATE_FORMAT_KEYWORD: typing.Final[int] +"""Dates are `DATE 'YYYY-MM-DD'`.""" + +OPT_TIME_FORMAT_QUOTED: typing.Final[int] +"""Times are quoted strings.""" + +OPT_TIME_FORMAT_KEYWORD: typing.Final[int] +"""Times are `TIME '…'`.""" + +OPT_TIMESTAMP_FORMAT_QUOTED: typing.Final[int] +"""Timestamps are quoted strings.""" + +OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ: typing.Final[int] +"""Aware timestamps use the `TIMESTAMPTZ` keyword.""" + +OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE: typing.Final[int] +"""Aware timestamps use `TIMESTAMP WITH TIMEZONE`.""" + +OPT_INET_FORMAT_UNSUPPORTED: typing.Final[int] +"""Inet literals are rejected.""" + +OPT_INET_FORMAT_QUOTED: typing.Final[int] +"""Inet values are quoted strings.""" + +OPT_INET_FORMAT_CAST: typing.Final[int] +"""Inet values are `'…'::inet`.""" + +OPT_MACADDR_FORMAT_UNSUPPORTED: typing.Final[int] +"""MAC address literals are rejected.""" + +OPT_MACADDR_FORMAT_QUOTED: typing.Final[int] +"""MAC addresses are quoted strings.""" + +OPT_MACADDR_FORMAT_CAST: typing.Final[int] +"""MAC addresses are `'…'::macaddr`.""" + +OPT_UUID_FORMAT_QUOTED: typing.Final[int] +"""UUIDs are quoted strings.""" + +OPT_UUID_FORMAT_UUID_KEYWORD: typing.Final[int] +"""UUIDs are `UUID '…'`.""" + +OPT_UUID_FORMAT_CAST_UUID: typing.Final[int] +"""UUIDs are `'…'::uuid`.""" + +OPT_JSON_FORMAT_QUOTED: typing.Final[int] +"""JSON is a quoted string.""" + +OPT_JSON_FORMAT_JSON_KEYWORD: typing.Final[int] +"""JSON is `JSON '…'`.""" + +OPT_JSON_FORMAT_PARSE_JSON: typing.Final[int] +"""JSON is `PARSE_JSON('…')`.""" + +class Dialect: + """Quoting, literal spelling, and which SQL constructs are legal. + + Use a builtin dialect when rendering an expression. Construct `Dialect` + when you need a different mix of `OPT_*` flags. Flags select a spelling + the renderer already knows. They do not add syntax that has no writer. + + Examples: + Write MySQL-style booleans and identifiers: + + >>> from rapidquery.dialect import ( + ... OPT_BOOL_FORMAT_INTEGER, + ... OPT_IDENTIFIER_FORMAT_BACKTICK, + ... Dialect, + ... ) + >>> dialect = Dialect( + ... bool_format=OPT_BOOL_FORMAT_INTEGER, + ... identifier_format=OPT_IDENTIFIER_FORMAT_BACKTICK, + ... ) + >>> dialect.write_boolean(True) + '1' + >>> dialect.write_identifier("id") + '`id`' + """ + + def __new__( + cls, + *, + placeholder_character: str = "?", + placeholder: int = 0, + null_treatment: int = 0, + like_escape: int = 0, + like_any: int = 0, + ilike_expression: int = 0, + rlike_expression: int = 0, + string_concat: int = 0, + bitxor: int = 0, + modulo: int = 0, + is_distinct_from: int = 0, + try_cast: int = 0, + trim: int = 0, + collate: int = 0, + bitwise: int = 0, + tuple: int = 0, + bit_format: int = 0, + bool_format: int = 0, + identifier_format: int = 0, + identifier_escape: int = 0, + string_format: int = 0, + string_escape: int = 0, + bytes_format: int = 0, + function_filter: int = 0, + array_format: int = 0, + date_format: int = 0, + time_format: int = 0, + timestamp_format: int = 0, + inet_format: int = 0, + macaddr_format: int = 0, + uuid_format: int = 0, + json_format: int = 0, + supports_similar_to: bool = False, + parenthesize_in_between: bool = False, + supports_double_colon: bool = False, + ) -> typing.Self: + """ + Build a dialect from flags. + + Each integer argument is one `OPT_*` constant. Omitted flags use the + zero value, which is that option's fallback spelling. + + Args: + placeholder_character: One character written for a parameter. + placeholder: `OPT_PLACEHOLDER_SIMPLE` or `OPT_PLACEHOLDER_NUMBERED`. + null_treatment: Where `{IGNORE|RESPECT} NULLS` is written. + like_escape: Where `LIKE … ESCAPE` is legal. + like_any: How `LIKE ANY` is spelled. + ilike_expression: How a case-insensitive `LIKE` is spelled. + rlike_expression: How a regex predicate is spelled. + string_concat: How `||` is spelled. + bitxor: How `^` is spelled. + modulo: How `%` is spelled. + is_distinct_from: How `IS DISTINCT FROM` is spelled. + try_cast: How `TRY_CAST` is spelled. + trim: How `TRIM` is spelled. + collate: How `COLLATE` is spelled. + bitwise: How `&`, `|`, `~`, and shifts are spelled. + tuple: How a row value is spelled. + bit_format: How a bit-string literal is spelled. + bool_format: `TRUE`/`FALSE` or `1`/`0`. + identifier_format: Delimiter around a quoted identifier. + identifier_escape: How an identifier quotes its delimiter. + string_format: Prefix and quotes of a string literal. + string_escape: How special characters in a string are escaped. + bytes_format: How a byte literal is spelled. + function_filter: How aggregate `FILTER` is spelled. + array_format: How an array literal is spelled. + date_format: How a date literal is spelled. + time_format: How a time literal is spelled. + timestamp_format: How a timestamp literal is spelled. + inet_format: How an inet literal is spelled. + macaddr_format: How a MAC address literal is spelled. + uuid_format: How a UUID literal is spelled. + json_format: How a JSON literal is spelled. + supports_similar_to: Whether `SIMILAR TO` is legal. + parenthesize_in_between: Wrap `IN` and `BETWEEN` in parentheses. + supports_double_colon: Whether `expr::type` is legal. + + Raises: + TypeError: `placeholder_character` is not one character, or a flag + is not an `int`. + """ + + def write_boolean(self, value: bool) -> str: + """ + Write `value` as this dialect's boolean literal. + + Args: + value: Python boolean. + + Returns: + `TRUE`, `FALSE`, `1`, or `0`. + """ + + def write_identifier(self, value: str) -> str: + """Quote `value` with this dialect's identifier delimiters. + + Args: + value: Raw identifier body. + + Returns: + The delimited identifier. + """ + + def write_string_literal(self, value: str) -> str: + """ + Escape `value` and wrap it as a complete string literal. + + Args: + value: Text to embed. A NUL character is rejected. + + Returns: + The SQL string literal. + + Raises: + ValueError: `value` contains a NUL character. + """ + + def write_bits(self, value: str) -> str: + """ + Write a bit string of `0` and `1` characters. + + Args: + value: Bit characters. Empty is legal. Any other character is not. + + Returns: + The dialect's bit literal. + + Raises: + ValueError: `value` contains a character other than `0` or `1`. + NotImplementedError: This dialect has no bit-string literal. + """ + + def write_bytes(self, value: bytes) -> str: + """Write `value` as this dialect's blob or bytea literal. + + Args: + value: Raw bytes. + + Returns: + The SQL literal. + + Raises: + NotImplementedError: This dialect has no byte literal of the + configured form. + """ + + @property + def placeholder_character(self) -> str: + """The one character used in placeholders.""" + + @property + def placeholder(self) -> int: + """`OPT_PLACEHOLDER_SIMPLE` or `OPT_PLACEHOLDER_NUMBERED`.""" + + @property + def null_treatment(self) -> int: + """Where `{IGNORE|RESPECT} NULLS` is written.""" + + @property + def like_escape(self) -> int: + """Where `LIKE … ESCAPE` is legal.""" + + @property + def like_any(self) -> int: + """How `LIKE ANY` is spelled.""" + + @property + def ilike_expression(self) -> int: + """How a case-insensitive match is spelled.""" + + @property + def rlike_expression(self) -> int: + """How a regex predicate is spelled.""" + + @property + def string_concat(self) -> int: + """How string concatenation is spelled.""" + + @property + def bitxor(self) -> int: + """How bitwise XOR is spelled.""" + + @property + def modulo(self) -> int: + """How modulo is spelled.""" + + @property + def is_distinct_from(self) -> int: + """How `IS DISTINCT FROM` is spelled.""" + + @property + def try_cast(self) -> int: + """How `TRY_CAST` is spelled.""" + + @property + def trim(self) -> int: + """How `TRIM` is spelled.""" + + @property + def collate(self) -> int: + """How `COLLATE` is spelled.""" + + @property + def bitwise(self) -> int: + """How bitwise operators are spelled.""" + + @property + def tuple(self) -> int: + """How a row value is spelled.""" + + @property + def bit_format(self) -> int: + """How a bit-string literal is spelled.""" + + @property + def bool_format(self) -> int: + """How a boolean literal is spelled.""" + + @property + def identifier_format(self) -> int: + """Which delimiter wraps a quoted identifier.""" + + @property + def identifier_escape(self) -> int: + """How an identifier quotes its own delimiter.""" + + @property + def string_format(self) -> int: + """Which prefix and quotes a string literal uses.""" + + @property + def string_escape(self) -> int: + """How special characters in a string are escaped.""" + + @property + def bytes_format(self) -> int: + """How a byte literal is spelled.""" + + @property + def function_filter(self) -> int: + """How aggregate `FILTER` is spelled.""" + + @property + def array_format(self) -> int: + """How an array literal is spelled.""" + + @property + def date_format(self) -> int: + """How a date literal is spelled.""" + + @property + def time_format(self) -> int: + """How a time literal is spelled.""" + + @property + def timestamp_format(self) -> int: + """How a timestamp literal is spelled.""" + + @property + def inet_format(self) -> int: + """How an inet literal is spelled.""" + + @property + def macaddr_format(self) -> int: + """How a MAC address literal is spelled.""" + + @property + def uuid_format(self) -> int: + """How a UUID literal is spelled.""" + + @property + def json_format(self) -> int: + """How a JSON literal is spelled.""" + + @property + def supports_similar_to(self) -> bool: + """Whether `SIMILAR TO` can be rendered.""" + + @property + def parenthesize_in_between(self) -> bool: + """Whether `IN` and `BETWEEN` are wrapped in parentheses.""" + + @property + def supports_double_colon(self) -> bool: + """Whether `expr::type` can be rendered.""" + + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +BigQuery: typing.Final[Dialect] = ... +"""BigQuery standard SQL.""" + +Databricks: typing.Final[Dialect] = ... +"""Databricks SQL.""" + +DuckDb: typing.Final[Dialect] = ... +"""DuckDB SQL.""" + +Hive: typing.Final[Dialect] = ... +"""HiveQL.""" + +MsSql: typing.Final[Dialect] = ... +"""Microsoft SQL Server.""" + +MySql: typing.Final[Dialect] = ... +"""MySQL.""" + +Oracle: typing.Final[Dialect] = ... +"""Oracle SQL.""" + +PostgreSql: typing.Final[Dialect] = ... +"""PostgreSQL.""" + +Redshift: typing.Final[Dialect] = ... +"""Amazon Redshift.""" + +Snowflake: typing.Final[Dialect] = ... +"""Snowflake SQL.""" + +Spark: typing.Final[Dialect] = ... +"""Spark SQL.""" + +Sqlite: typing.Final[Dialect] = ... +"""SQLite.""" + +Teradata: typing.Final[Dialect] = ... +"""Teradata SQL.""" diff --git a/rapidquery/dml.py b/rapidquery/dml.py new file mode 100644 index 0000000..a49e5f3 --- /dev/null +++ b/rapidquery/dml.py @@ -0,0 +1,5 @@ +from ._core.dml import DataManipulationLanguage + +__all__ = [ + "DataManipulationLanguage", +] diff --git a/rapidquery/dml.pyi b/rapidquery/dml.pyi new file mode 100644 index 0000000..1e66ce2 --- /dev/null +++ b/rapidquery/dml.pyi @@ -0,0 +1,6 @@ +"""Data Manipulation Language""" + +class DataManipulationLanguage: + """Base of every DML statement. Construct a subclass, not this class.""" + + ... diff --git a/rapidquery/expr.py b/rapidquery/expr.py new file mode 100644 index 0000000..9b35dc7 --- /dev/null +++ b/rapidquery/expr.py @@ -0,0 +1,47 @@ +from ._core.expr import ( + Asterisk, + BetweenExpr, + BinaryExpr, + CaseExpr, + CastExpr, + CollateExpr, + CustomExpr, + Expr, + Function, + Ident, + InListExpr, + IsDistinctFromExpr, + IsExpr, + LikeExpr, + RLikeExpr, + SimilarToExpr, + TrimExpr, + TupleExpr, + UnaryExpr, + Unquote, + Value, +) + +__all__ = [ + "Asterisk", + "BetweenExpr", + "BinaryExpr", + "CaseExpr", + "CastExpr", + "CollateExpr", + "CustomExpr", + "Expr", + "Function", + "Ident", + "InListExpr", + "IsDistinctFromExpr", + "IsExpr", + "LikeExpr", + "RLikeExpr", + "SimilarToExpr", + "TrimExpr", + "TupleExpr", + "UnaryExpr", + "Unquote", + "Value", +] diff --git a/rapidquery/expr.pyi b/rapidquery/expr.pyi new file mode 100644 index 0000000..44ca754 --- /dev/null +++ b/rapidquery/expr.pyi @@ -0,0 +1,1176 @@ +import typing +from collections.abc import Iterable +from typing import Self, final, overload + +from rapidquery.dialect import Dialect +from rapidquery.serializers import Serializer + +class _IdentClassVarProtocol(typing.Protocol): + __ident__: Ident + +class _IdentPropertyProtocol(typing.Protocol): + @property + def __ident__(self) -> Ident: ... + +_IdentExtractor = str | Unquote | Ident | Asterisk | type[Asterisk] | _IdentClassVarProtocol | _IdentPropertyProtocol + +class Expr: + """ + Base of every SQL expression node. + + Do not construct `Expr` itself. Build a subclass (`Ident`, `Value`, + `BinaryExpr`, and the rest), then render it. `to_sql` inlines literals. + `prepare` binds them and returns the SQL plus the bound objects. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident + >>> (Ident("age") > 18).to_sql(Sqlite) + '"age" > 18' + """ + + def to_sql(self, dialect: Dialect) -> str: + """ + Render this node with values written as SQL literals. + + Use this to inspect SQL. Prefer `prepare` when a driver will bind + the parameters. + + Args: + dialect: Dialect that chooses quoting and literal spelling. + + Returns: + The SQL text. + + Raises: + RuntimeError: The dialect rejects this construct. + NotImplementedError: The dialect has no spelling for this construct. + ValueError: A `CustomExpr` template has a different number of `{}` + slots than arguments. + """ + + def prepare(self, dialect: Dialect) -> tuple[str, tuple[typing.Any, ...]]: + """ + Render this node and bind each `Value` as a placeholder. + + Identifiers and keywords are still written as SQL. Only values become + parameters, in left-to-right order. The placeholder character comes + from the dialect (`?` or `$1`). + + Args: + dialect: Dialect that chooses quoting and placeholders. + + Returns: + The SQL text and the bound Python objects, in placeholder order. + + Raises: + RuntimeError: The dialect rejects this construct. + NotImplementedError: The dialect has no spelling for this construct. + ValueError: A `CustomExpr` template has a different number of `{}` + slots than arguments. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Value + >>> Value(1).prepare(PostgreSql) + ('$1', (1,)) + """ + + def __add__(self, other: object, /) -> BinaryExpr: + """SQL ``self + other``.""" + + def __radd__(self, other: object, /) -> BinaryExpr: + """SQL ``other + self``, when the left operand is not an expression.""" + + def __sub__(self, other: object, /) -> BinaryExpr: + """SQL ``self - other``.""" + + def __rsub__(self, other: object, /) -> BinaryExpr: + """SQL ``other - self``, when the left operand is not an expression.""" + + def __mul__(self, other: object, /) -> BinaryExpr: + """SQL ``self * other``.""" + + def __rmul__(self, other: object, /) -> BinaryExpr: + """SQL ``other * self``, when the left operand is not an expression.""" + + def __truediv__(self, other: object, /) -> BinaryExpr: + """SQL ``self / other``.""" + + def __rtruediv__(self, other: object, /) -> BinaryExpr: + """SQL ``other / self``, when the left operand is not an expression.""" + + def __mod__(self, other: object, /) -> BinaryExpr: + """SQL ``self % other``.""" + + def __rmod__(self, other: object, /) -> BinaryExpr: + """SQL ``other % self``, when the left operand is not an expression.""" + + def __eq__(self, other: object, /) -> BinaryExpr: # pyright: ignore[reportIncompatibleMethodOverride] + """SQL ``self = other``.""" + + def __ne__(self, other: object, /) -> BinaryExpr: # pyright: ignore[reportIncompatibleMethodOverride] + """SQL ``self <> other``.""" + + def __lt__(self, other: object, /) -> BinaryExpr: + """SQL ``self < other``.""" + + def __le__(self, other: object, /) -> BinaryExpr: + """SQL ``self <= other``.""" + + def __gt__(self, other: object, /) -> BinaryExpr: + """SQL ``self > other``.""" + + def __ge__(self, other: object, /) -> BinaryExpr: + """SQL ``self >= other``.""" + + def __and__(self, other: object, /) -> BinaryExpr: + """SQL ``self AND other``. Python ``&`` is logical AND, not bitwise.""" + + def __rand__(self, other: object, /) -> BinaryExpr: + """SQL ``other AND self``. Python ``&`` is logical AND, not bitwise.""" + + def __or__(self, other: object, /) -> BinaryExpr: + """SQL ``self OR other``. Python ``|`` is logical OR, not bitwise.""" + + def __ror__(self, other: object, /) -> BinaryExpr: + """SQL ``other OR self``. Python ``|`` is logical OR, not bitwise.""" + + def bit_and(self, other: object, /) -> BinaryExpr: + """SQL bitwise ``self & other``.""" + + def bit_or(self, other: object, /) -> BinaryExpr: + """ + SQL bitwise ``self | other``. + + Is not supported by Redshift, Oracle. + """ + + def bit_xor(self, other: object, /) -> BinaryExpr: + """ + SQL bitwise ``self ^ other``. + + Is not supported by Oracle, Redshift, SQLite. + """ + + def bit_not(self) -> UnaryExpr: + """ + SQL ``~self``. This is not bound to Python ``~``. + + Is not supported by Redshift, Oracle. + """ + + def __lshift__(self, other: object, /) -> BinaryExpr: + """ + SQL ``self << other``. + + Is not supported by MsSQL, Oracle. + """ + + def __rlshift__(self, other: object, /) -> BinaryExpr: + """ + SQL ``other << self``, when the left operand is not an expression. + + Is not supported by MsSQL, Oracle. + """ + + def __rshift__(self, other: object, /) -> BinaryExpr: + """ + SQL ``self >> other``. + + Is not supported by MsSQL, Oracle. + """ + + def __rrshift__(self, other: object, /) -> BinaryExpr: + """ + SQL ``other >> self``, when the left operand is not an expression. + + Is not supported by MsSQL, Oracle. + """ + + def concat(self, other: object, /) -> BinaryExpr: + """SQL ``self || other``. Dialects may rewrite ``||`` as a function or as ``+``.""" + + def __neg__(self) -> UnaryExpr: + """SQL ``-self``.""" + + def __pos__(self) -> UnaryExpr: + """SQL ``+self``.""" + + def not_(self) -> UnaryExpr: + """SQL ``NOT self``.""" + + def between(self, low: object, high: object, /) -> BetweenExpr: + """SQL ``self BETWEEN low AND high``.""" + + def not_between(self, low: object, high: object, /) -> BetweenExpr: + """SQL ``self NOT BETWEEN low AND high``.""" + + def is_(self, kind: None | bool | str, /) -> IsExpr: + """ + SQL ``self IS kind``. + + ``None`` is ``IS NULL``. ``True`` and ``False`` are ``IS TRUE`` and ``IS FALSE``. + A string is ``NULL``, ``TRUE``, ``FALSE``, or ``UNKNOWN``. + + Raises: + TypeError: ``kind`` is not ``None``, ``bool``, or ``str``. + ValueError: ``kind`` is a string and not one of those four words. + """ + + def is_not(self, kind: None | bool | str, /) -> IsExpr: + """ + SQL ``self IS NOT kind``. + + ``None`` is ``IS NOT NULL``. ``True`` and ``False`` are ``IS NOT TRUE`` and + ``IS NOT FALSE``. A string is ``NULL``, ``TRUE``, ``FALSE``, or ``UNKNOWN``. + + Raises: + TypeError: ``kind`` is not ``None``, ``bool``, or ``str``. + ValueError: ``kind`` is a string and not one of those four words. + """ + + def is_distinct_from(self, other: object, /) -> IsDistinctFromExpr: + """SQL ``self IS DISTINCT FROM other``.""" + + def is_not_distinct_from(self, other: object, /) -> IsDistinctFromExpr: + """SQL ``self IS NOT DISTINCT FROM other``.""" + + def in_(self, list: Iterable[object], /) -> InListExpr: + """SQL ``self IN (list)``.""" + + def not_in(self, list: Iterable[object], /) -> InListExpr: + """SQL ``self NOT IN (list)``.""" + + def like(self, pattern: object, /, *, escape: str | None = None) -> LikeExpr: + """ + SQL ``self LIKE pattern``. + + A list or tuple of patterns is ``LIKE ANY``. + + Args: + pattern: One pattern, or a list or tuple for ``ANY``. + escape: One escape character, or ``None``. + + Raises: + TypeError: ``escape`` is not one character. + """ + + def not_like(self, pattern: object, /, *, escape: str | None = None) -> LikeExpr: + """ + SQL ``self NOT LIKE pattern``. + + A list or tuple of patterns is ``NOT LIKE ANY``. + + Args: + pattern: One pattern, or a list or tuple for ``ANY``. + escape: One escape character, or ``None``. + + Raises: + TypeError: ``escape`` is not one character. + """ + + def ilike(self, pattern: object, /, *, escape: str | None = None) -> LikeExpr: + """ + SQL ``self ILIKE pattern``. + + A list or tuple of patterns is ``ILIKE ANY``. + + Args: + pattern: One pattern, or a list or tuple for ``ANY``. + escape: One escape character, or ``None``. + + Raises: + TypeError: ``escape`` is not one character. + """ + + def not_ilike(self, pattern: object, /, *, escape: str | None = None) -> LikeExpr: + """ + SQL ``self NOT ILIKE pattern``. + + A list or tuple of patterns is ``NOT ILIKE ANY``. + + Args: + pattern: One pattern, or a list or tuple for ``ANY``. + escape: One escape character, or ``None``. + + Raises: + TypeError: ``escape`` is not one character. + """ + + def similar_to(self, pattern: object, /) -> SimilarToExpr: + """SQL ``self SIMILAR TO pattern``.""" + + def not_similar_to(self, pattern: object, /) -> SimilarToExpr: + """SQL ``self NOT SIMILAR TO pattern``.""" + + def rlike(self, pattern: object, /) -> RLikeExpr: + """SQL regex match of ``self`` against ``pattern``.""" + + def not_rlike(self, pattern: object, /) -> RLikeExpr: + """Negated SQL regex match of ``self`` against ``pattern``.""" + + def cast_as(self, data_type_name: str, /) -> CastExpr: + """SQL ``CAST(self AS data_type_name)``.""" + + def try_cast_as(self, data_type_name: str, /) -> CastExpr: + """SQL ``TRY_CAST(self AS data_type_name)``.""" + + def collate(self, collation: _IdentExtractor, /) -> CollateExpr: + """SQL ``self COLLATE collation``. The collation is an identifier.""" + + def trim( + self, + *, + where: typing.Literal["BOTH", "LEADING", "TRAILING"] | str = "BOTH", + characters: object | None = None, + ) -> TrimExpr: + """ + SQL ``TRIM``, ``LTRIM``, or ``RTRIM`` of this expression. + + Args: + where: ``BOTH``, ``LEADING``, or ``TRAILING``. + characters: Characters to strip, or ``None`` for spaces. + + Raises: + ValueError: ``where`` is not one of those three words. + """ + + def __repr__(self) -> str: + """Constructor-style text, such as ``Ident("id")``.""" + +@final +class Unquote: + """ + Identifier body that is written without quote characters. + + Use this for a keyword-like name that must stay bare, such as + `CURRENT_TIMESTAMP`. The name must match `[A-Za-z_][A-Za-z0-9_]*`. + This is not a raw SQL fragment. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Ident, Unquote + >>> Ident(Unquote("COUNT")).to_sql(PostgreSql) + 'COUNT' + """ + + def __new__(cls, name: str, /) -> Self: + """ + Store `name` as an unquoted identifier part. + + Args: + name: ASCII identifier body. A digit cannot be first. + + Raises: + ValueError: `name` is empty or is not a simple ASCII identifier. + """ + + @property + def name(self) -> str: + """The identifier body, without quotes.""" + + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +@final +class Asterisk: + """ + The SQL wildcard `*`. + + Pass the class or an instance as the last part of an `Ident` (`t.*`). + `Ident("*")` is a quoted column whose name is a star, not this wildcard. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Asterisk, Ident + >>> Ident("t", Asterisk).to_sql(PostgreSql) + '"t".*' + """ + + def __new__(cls) -> Self: + """Return the wildcard marker.""" + + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +@final +class Ident(Expr): + """ + Dotted SQL name: schema, table, column, or a final wildcard. + + Use `Ident` wherever a name must be quoted for the dialect. Pass one + argument per part. A `str` is always quoted, including `"*"`. Flatten a + nested `Ident` by passing it as one argument. At most 10 parts. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Ident + >>> Ident("users", "id").to_sql(PostgreSql) + '"users"."id"' + """ + + def __new__(cls, *parts: _IdentExtractor) -> Self: + """ + Build an identifier from parts. + + Args: + parts: One part each. A `str` is quoted. `Unquote` stays bare. + `Asterisk` is legal only as the last part. + + Raises: + TypeError: A part is not a string, `Unquote`, `Asterisk`, or `Ident`. + ValueError: There are no parts, a part is empty, `Asterisk` is not + last, or there are more than 10 parts. + """ + + def __len__(self) -> int: + """Number of parts. At least one.""" + + def __getitem__(self, index: int) -> str | Unquote | Asterisk: + """ + Return the part at `index`. + + A quoted part comes back as `str`. Negative indexes count from the end. + + Raises: + IndexError: `index` is outside the parts. + """ + + def to_list(self) -> list[str | Unquote | Asterisk]: + """ + Copy the parts into a list. + + Quoted parts are `str`. Unquoted parts are `Unquote`. A wildcard is + `Asterisk`. + """ + +_T = typing.TypeVar("_T") + +@final +class Value(Expr, typing.Generic[_T]): + """ + A Python object kept until SQL is rendered. + + Use `Value` for literals and for driver parameters. `to_sql` runs the + serializer (`None` becomes `NULL`). `prepare` binds the object and does + not call the serializer. Omit the serializer to infer one from the + object's type. Passing an existing `Value` reuses its object. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Value + >>> Value("hi").to_sql(PostgreSql) + "E'hi'" + """ + + @overload + def __new__(cls, object: _T | None, /) -> Self: + """Wrap `object` and infer a serializer from its type.""" + + @overload + def __new__(cls, object: _T | None, serializer: Serializer[_T] | None, /) -> Self: + """ + Wrap `object` with an explicit serializer. + + Args: + object: Value to bind or serialize. An existing `Value` contributes + its object. + serializer: How `to_sql` spells `object`. `None` infers one, or + keeps the serializer when `object` is already a `Value`. + + Raises: + TypeError: `serializer` is not a `Serializer`. + """ + + def get_object(self) -> _T | None: + """The wrapped Python object. `None` renders as `NULL`.""" + + def get_serializer(self) -> Serializer[_T]: + """Serializer used by `to_sql`. `prepare` does not call it.""" + +@final +class Function(Expr): + """ + SQL function call, including a bare name with no arguments. + + Use this for `count`, `sum`, and dialect functions that are not their own + node. The name is an identifier: a string is quoted, so a niladic keyword + such as `CURRENT_TIMESTAMP` must be an `Unquote`. `filter` adds + `FILTER (WHERE …)` where the dialect allows it. Null treatment is a + separate clause, placed where that dialect requires it. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Function, Ident + >>> Function("count", Ident("id")).to_sql(PostgreSql) + '"count"("id")' + """ + + def __new__(cls, name: _IdentExtractor, /, *args: object, distinct: bool = False) -> Self: + """ + Build a call. + + Args: + name: Function name. A `str` is a quoted identifier, and a dotted + string is split into parts. + args: Arguments. A `str` becomes a `Value`, not an identifier. + distinct: When true, write `DISTINCT` before the arguments. + + Raises: + TypeError: `name` is not an identifier. + ValueError: `name` is not a legal identifier. + """ + + def filter(self, predicate: object, /) -> Self: + """ + Return a copy with `FILTER (WHERE predicate)`. + + The predicate is any expression. A string becomes a literal. Rendering + raises when the dialect has no `FILTER` syntax. + + Args: + predicate: Boolean expression placed in the filter. + + This method is not supported by Hive, MsSQL, MySQL, Redshift, Snowflake, Teradata. + """ + + def ignore_nulls(self) -> Self: + """ + Return a copy that writes `IGNORE NULLS`. + + Is not supported by Hive, MySQL, PostgreSql, Sqlite. + """ + + def respect_nulls(self) -> Self: + """ + Return a copy that writes `RESPECT NULLS`. + + Is not supported by Hive, MySQL, PostgreSql, Sqlite. + """ + + def get_name(self) -> Ident: + """The function name.""" + + def get_distinct(self) -> bool: + """Whether the call writes `DISTINCT`.""" + + def get_args(self) -> tuple[Expr, ...]: + """The argument expressions, in order.""" + + def get_filter(self) -> Expr | None: + """The `FILTER` predicate, or `None`.""" + + def get_null_treatment(self) -> typing.Literal["IGNORE NULLS", "RESPECT NULLS"] | None: + """`IGNORE NULLS`, `RESPECT NULLS`, or `None`.""" + +@final +class TupleExpr(Expr): + """ + Parenthesized row value. + + Use this for `ROW(a, b)` and the other tuple spellings a dialect allows. + A string argument is a literal, not a column name. Rendering raises when + the dialect rejects tuples or rejects this arity. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import TupleExpr + >>> TupleExpr(1, 2).to_sql(PostgreSql) + 'ROW(1, 2)' + """ + + def __new__(cls, *items: object) -> Self: + """ + Build a row from expressions. + + Args: + items: One expression per element. Strings become literals. + """ + + def get_items(self) -> tuple[Expr, ...]: + """The element expressions, in order.""" + +@final +class UnaryExpr(Expr): + """Prefix operator: `NOT`, `+`, `-`, or `~`. + + Use this for negation and bitwise complement. `NOT` is written with a + space. The others are written against the operand. `~` becomes a function + on dialects that have no prefix complement. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident, UnaryExpr + >>> UnaryExpr("NOT", Ident("active")).to_sql(Sqlite) + 'NOT "active"' + """ + + def __new__(cls, operator: typing.Literal["NOT", "+", "-", "~"] | str, expr: object, /) -> Self: + """ + Build a prefix operation. + + Args: + operator: `NOT`, `+`, `-`, or `~`. Case is ignored for `NOT`. + expr: The operand. A string becomes a literal. + + Raises: + ValueError: `operator` is not one of those four spellings. + """ + + def get_operator(self) -> typing.Literal["NOT", "+", "-", "~"]: + """The operator spelling.""" + + def get_expr(self) -> Expr: + """The operand.""" + +@final +class BinaryExpr(Expr): + """ + Infix operator between two expressions. + + Use this for arithmetic, comparison, `AND` / `OR`, concatenation, and + bitwise operators. `!=` is stored and written as `<>`. A string on either + side is a literal. Dialects may rewrite `%`, `^`, `||`, and shifts into + function calls. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import BinaryExpr, Ident + >>> BinaryExpr(Ident("age"), ">", 18).to_sql(Sqlite) + '"age" > 18' + """ + + def __new__( + cls, + left: object, + operator: typing.Literal[ + "+", + "-", + "*", + "/", + "%", + "=", + "<>", + "!=", + "<", + "<=", + ">=", + ">", + "AND", + "OR", + "&", + "|", + "^", + "<<", + ">>", + "||", + ] + | str, + right: object, + /, + ) -> Self: + """ + Build an infix operation. + + Args: + left: Left operand. + operator: One of the spellings listed on this method. `!=` is stored + as `<>`. Any other spelling is kept as a custom operator. + right: Right operand. + """ + + def get_left(self) -> Expr: + """The left operand.""" + + def get_operator(self) -> str: + """The operator spelling. `!=` is returned as `<>`.""" + + def get_right(self) -> Expr: + """The right operand.""" + +@final +class IsExpr(Expr): + """`expr IS [NOT] NULL`, `TRUE`, `FALSE`, or `UNKNOWN`. + + Use this for null tests and boolean state tests. Pass the kind as a + string. Set `negated` for `IS NOT`. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident, IsExpr + >>> IsExpr(Ident("name"), "NULL").to_sql(Sqlite) + '"name" IS NULL' + """ + + def __new__( + cls, + expr: object, + kind: typing.Literal["NULL", "TRUE", "FALSE", "UNKNOWN"] | str, + /, + *, + negated: bool = False, + ) -> Self: + """ + Build an `IS` test. + + Args: + expr: Tested expression. + kind: `NULL`, `TRUE`, `FALSE`, or `UNKNOWN`. + negated: Write `IS NOT` instead of `IS`. + + Raises: + ValueError: `kind` is not one of those four words. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_kind(self) -> typing.Literal["NULL", "TRUE", "FALSE", "UNKNOWN"]: + """`NULL`, `TRUE`, `FALSE`, or `UNKNOWN`.""" + + def get_negated(self) -> bool: + """Whether the test is `IS NOT`.""" + +@final +class IsDistinctFromExpr(Expr): + """`left IS [NOT] DISTINCT FROM right`. + + Use this when `NULL` on either side must not make the comparison unknown. + Rendering follows the dialect: standard syntax, a null-safe operator, or + an error when the dialect has neither. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Ident, IsDistinctFromExpr + >>> IsDistinctFromExpr(Ident("a"), Ident("b")).to_sql(PostgreSql) + '"a" IS DISTINCT FROM "b"' + """ + + def __new__(cls, left: object, right: object, /, *, negated: bool = False) -> Self: + """ + Build an `IS DISTINCT FROM` comparison. + + Args: + left: Left expression. + right: Right expression. + negated: Write `IS NOT DISTINCT FROM`. + + ``IS DISTINCT FROM`` is not supported by Oracle, Redshift, Teradata. + """ + + def get_left(self) -> Expr: + """The left expression.""" + + def get_right(self) -> Expr: + """The right expression.""" + + def get_negated(self) -> bool: + """Whether the predicate is negated.""" + +@final +class InListExpr(Expr): + """`expr [NOT] IN (item, …)`. + + Use this for a fixed list of expressions. A string item is a literal. + An empty list renders as a boolean literal and does not write `expr`. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident, InListExpr + >>> InListExpr(Ident("id"), [1, 2]).to_sql(Sqlite) + '"id" IN (1, 2)' + """ + + def __new__(cls, expr: object, list: Iterable[object], /, *, negated: bool = False) -> Self: + """ + Build an `IN` list. + + Args: + expr: Tested expression. + list: Membership expressions. + negated: Write `NOT IN`. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_list(self) -> tuple[Expr, ...]: + """The membership expressions.""" + + def get_negated(self) -> bool: + """Whether the predicate is `NOT IN`.""" + +@final +class BetweenExpr(Expr): + """`expr [NOT] BETWEEN low AND high`. + + Use this for an inclusive range. Both bounds are expressions. There is + no `SYMMETRIC` clause. Some dialects parenthesize the whole predicate. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import BetweenExpr, Ident + >>> BetweenExpr(Ident("n"), 1, 3).to_sql(Sqlite) + '"n" BETWEEN 1 AND 3' + """ + + def __new__(cls, expr: object, low: object, high: object, /, *, negated: bool = False) -> Self: + """ + Build a `BETWEEN` predicate. + + Args: + expr: Tested expression. + low: Inclusive lower bound. + high: Inclusive upper bound. + negated: Write `NOT BETWEEN`. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_low(self) -> Expr: + """The lower bound.""" + + def get_high(self) -> Expr: + """The upper bound.""" + + def get_negated(self) -> bool: + """Whether the predicate is `NOT BETWEEN`.""" + +@final +class LikeExpr(Expr): + """`LIKE`, `ILIKE`, or `LIKE ANY`, with an optional `ESCAPE` character. + + Use this for pattern predicates. A string pattern is one pattern. A list + or tuple of patterns is `LIKE ANY`, spelled the way the dialect allows. + `case_sensitive=False` selects `ILIKE` or the dialect's fallback. `escape` + must be one character and is inlined, never bound. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident, LikeExpr + >>> LikeExpr(Ident("name"), "A%").to_sql(Sqlite) + '"name" LIKE \\'A%\\'' + """ + + def __new__( + cls, + expr: object, + pattern: object, + /, + *, + negated: bool = False, + case_sensitive: bool = True, + escape: str | None = None, + ) -> Self: + """ + Build a pattern predicate. + + Args: + expr: Tested expression. + pattern: One pattern, or a list or tuple of patterns for `ANY`. + negated: Negate the match. + case_sensitive: When false, render the dialect's case-insensitive form. + escape: One escape character, or `None` to omit ``ESCAPE``. ``ESCAPE`` is + not supported by BigQuery, Hive. + + Raises: + TypeError: `escape` is not one character, or `pattern` is not an + expression, list, or tuple. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_pattern(self) -> Expr | tuple[Expr, ...]: + """One pattern, or a tuple of patterns when this is `LIKE ANY`.""" + + def get_negated(self) -> bool: + """Whether the match is negated.""" + + def get_case_sensitive(self) -> bool: + """Whether the match keeps case.""" + + def get_escape(self) -> str | None: + """The escape character, or `None`.""" + +@final +class SimilarToExpr(Expr): + """`expr [NOT] SIMILAR TO pattern`. + + Use this only for dialects that implement SQL `SIMILAR TO` (PostgreSQL, + Redshift, DuckDB). Other dialects raise at render time. There is no + `ESCAPE` clause. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Ident, SimilarToExpr + >>> SimilarToExpr(Ident("name"), "a%").to_sql(PostgreSql) + '"name" SIMILAR TO E\\'a%\\'' + """ + + def __new__(cls, expr: object, pattern: object, /, *, negated: bool = False) -> Self: + """ + Build a `SIMILAR TO` predicate. + + Args: + expr: Tested expression. + pattern: SQL regular pattern. + negated: Write `NOT SIMILAR TO`. + + ``SIMILAR TO`` is supported by DuckDB, PostgreSql, Redshift. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_pattern(self) -> Expr: + """The pattern.""" + + def get_negated(self) -> bool: + """Whether the predicate is negated.""" + +@final +class RLikeExpr(Expr): + """Regex match, spelled the way the dialect spells it. + + Use this instead of writing `RLIKE`, `~`, or `REGEXP_LIKE` yourself. + PostgreSQL and Redshift use `~`. Other dialects use `RLIKE` or a function, + or raise when they have no regex predicate. There is no `ESCAPE` clause. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Ident, RLikeExpr + >>> RLikeExpr(Ident("name"), "a.*").to_sql(PostgreSql) + '"name" ~ E\\'a.*\\'' + """ + + def __new__(cls, expr: object, pattern: object, /, *, negated: bool = False) -> Self: + """ + Build a regex predicate. + + Args: + expr: Tested expression. + pattern: Regular expression. + negated: Negate the match. + + ``RLIKE`` is not supported by MsSQL, Sqlite, Teradata. + """ + + def get_expr(self) -> Expr: + """The tested expression.""" + + def get_pattern(self) -> Expr: + """The regular expression.""" + + def get_negated(self) -> bool: + """Whether the match is negated.""" + +@final +class CastExpr(Expr): + """`CAST`, `TRY_CAST`, or PostgreSQL-style `::`. + + Use this to name the result type. The type string is a SQL type name, not + a quoted identifier. `TRY_CAST` is rewritten to the dialect's spelling, + such as `SAFE_CAST`, or rejected when the dialect has no try-cast. + `kind="::"` renders only when the dialect allows that operator. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import CastExpr, Ident + >>> CastExpr(Ident("id"), "int").to_sql(PostgreSql) + 'CAST("id" AS INT)' + """ + + def __new__( + cls, + expr: object, + data_type_name: str, + /, + *, + kind: typing.Literal["CAST", "TRY_CAST", "::"] | str = "CAST", + ) -> Self: + """ + Build a cast. + + Args: + expr: Input expression. + data_type_name: SQL type name, such as `"int"` or `"varchar(16)"`. + kind: `CAST`, `TRY_CAST`, or `::`. ``TRY_CAST`` is not supported by + Hive, MySQL, Oracle, PostgreSql, Sqlite. ``::`` is supported + by Databricks, DuckDB, PostgreSql, Redshift, Snowflake, Spark. + + Raises: + ValueError: `kind` is not one of those three spellings. + """ + + def get_expr(self) -> Expr: + """The input expression.""" + + def get_data_type_name(self) -> str: + """The SQL type name, in the spelling that will be written.""" + + def get_kind(self) -> typing.Literal["CAST", "TRY_CAST", "::"]: + """`CAST`, `TRY_CAST`, or `::`.""" + +@final +class TrimExpr(Expr): + """`TRIM` of an expression, from one side, of an optional character set. + + Use this for `TRIM`, `LTRIM`, and `RTRIM`. `where` selects both ends, + the leading end, or the trailing end. `characters` is the set to strip. + Omit it to strip spaces. The written function depends on the dialect. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import Ident, TrimExpr + >>> TrimExpr(Ident("s")).to_sql(Sqlite) + 'TRIM("s")' + """ + + def __new__( + cls, + expr: object, + /, + *, + where: typing.Literal["BOTH", "LEADING", "TRAILING"] | str = "BOTH", + characters: object | None = None, + ) -> Self: + """ + Build a trim call. + + Args: + expr: Input expression. + where: `BOTH`, `LEADING`, or `TRAILING`. + characters: Characters to strip, or `None` for spaces. + + Raises: + ValueError: `where` is not one of those three words. + """ + + def get_expr(self) -> Expr: + """The input expression.""" + + def get_where(self) -> typing.Literal["BOTH", "LEADING", "TRAILING"]: + """Which side is trimmed.""" + + def get_characters(self) -> Expr | None: + """The character set, or `None` when trimming spaces.""" + +@final +class CollateExpr(Expr): + """ + `expr COLLATE collation`. + + Use this to attach a collation name. The collation is an identifier, not + a string literal, except on dialects that spell collation as a function + and then quote the name as text. Rendering raises when the dialect has + no collation syntax. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import CollateExpr, Ident + >>> CollateExpr(Ident("s"), "en_US").to_sql(PostgreSql) + '"s" COLLATE "en_US"' + """ + + def __new__(cls, expr: object, collation: str | Unquote | Ident | Asterisk | type[Asterisk], /) -> Self: + """ + Attach a collation. + + Args: + expr: Input expression. + collation: Collation name, as an identifier. + + ``COLLATE`` is not supported by Hive, Spark, Teradata. + """ + + def get_expr(self) -> Expr: + """The input expression.""" + + def get_collation(self) -> Ident: + """The collation name.""" + +@final +class CaseExpr(Expr): + """`CASE` expression, searched or with an operand. + + Use this for conditional results. Omit the operand for `CASE WHEN`. + Pass an operand for `CASE operand WHEN`. `when` and `else_` return a new + node. A `CASE` with no branches is still constructible; render it only + after adding a branch. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import CaseExpr, Ident + >>> CaseExpr().when(Ident("a"), 1).else_(0).to_sql(Sqlite) + 'CASE WHEN "a" THEN 1 ELSE 0 END' + """ + + def __new__(cls, operand: object | None = None, /) -> Self: + """ + Start a `CASE`. + + Args: + operand: The `CASE operand` expression, or `None` for a searched + `CASE`. + """ + + def when(self, when: object, then: object) -> Self: + """Return a copy with one more `WHEN` / `THEN` pair. + + Args: + when: Comparison value, or a condition when there is no operand. + then: Result when this branch matches. + """ + + def else_(self, result: object) -> Self: + """ + Return a copy whose `ELSE` result is `result`. + + Args: + result: Value written after `ELSE`. + """ + + def get_operand(self) -> Expr | None: + """The `CASE` operand, or `None` for a searched `CASE`.""" + + def get_when_list(self) -> tuple[tuple[Expr, Expr], ...]: + """`(WHEN, THEN)` pairs, in order.""" + + def get_else(self) -> Expr | None: + """The `ELSE` expression, or `None`.""" + +@final +class CustomExpr(Expr): + """SQL template with `{}` slots filled by expressions. + + Use this when no node spells the fragment yet. Each `{}` consumes one + argument, rendered with the same dialect. `{{` and `}}` write a literal + brace. The template is not escaped. Keep it to keywords and punctuation + you trust, and put data in the arguments. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.expr import CustomExpr, Ident + >>> CustomExpr("COALESCE({}, 0)", Ident("n")).to_sql(Sqlite) + 'COALESCE("n", 0)' + """ + + def __new__(cls, sql: str, *args: object) -> Self: + """ + Build a template. + + Args: + sql: SQL text. `{}` marks an argument. `{{` and `}}` are braces. + args: One expression per `{}` slot. + + The slot count is checked when the expression is rendered, not here. + """ + + def get_sql(self) -> str: + """The template, including `{}` slots.""" + + def get_args(self) -> tuple[Expr, ...]: + """The expressions that fill `{}` slots, in order.""" diff --git a/rapidquery/serializers.py b/rapidquery/serializers.py new file mode 100644 index 0000000..73825ac --- /dev/null +++ b/rapidquery/serializers.py @@ -0,0 +1,56 @@ +"""Serializers that turn a Python object into a SQL literal. + +Pass one to `Value` when inference is wrong, or call `serialize` to render a +single literal. Each builtin covers one Python type. `ArraySerializer` and +`CustomSerializer` cover lists and caller-defined spellings. The dialect +still chooses quotes, keywords, and whether the form exists. + +Examples: + Force a string serializer instead of inferring one from the object: + + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.expr import Value + >>> from rapidquery.serializers import StringSerializer + >>> Value("hi", StringSerializer()).to_sql(PostgreSql) + "E'hi'" +""" + +from ._core.serializers import ( + ArraySerializer, + BitSerializer, + BooleanSerializer, + BytesSerializer, + CustomSerializer, + DateSerializer, + DatetimeSerializer, + FloatSerializer, + InetSerializer, + IntegerSerializer, + JSONSerializer, + MacAddressSerializer, + Serializer, + StringSerializer, + TimeSerializer, + UnspecifiedSerializer, + UuidSerializer, +) + +__all__ = [ + "ArraySerializer", + "BitSerializer", + "BooleanSerializer", + "BytesSerializer", + "CustomSerializer", + "DateSerializer", + "DatetimeSerializer", + "FloatSerializer", + "InetSerializer", + "IntegerSerializer", + "JSONSerializer", + "MacAddressSerializer", + "Serializer", + "StringSerializer", + "TimeSerializer", + "UnspecifiedSerializer", + "UuidSerializer", +] diff --git a/rapidquery/serializers.pyi b/rapidquery/serializers.pyi new file mode 100644 index 0000000..e2fbc6a --- /dev/null +++ b/rapidquery/serializers.pyi @@ -0,0 +1,366 @@ +import datetime +import decimal +import enum +import typing +import uuid +from collections.abc import Callable + +from rapidquery.dialect import Dialect + +_T = typing.TypeVar("_T") + +class Serializer(typing.Generic[_T]): + """ + Turns one Python object into a SQL literal for a dialect. + + Use a subclass when `Value` should not infer the spelling, or call + `serialize` to render a literal without building an expression. Subclasses + are frozen singletons except `ArraySerializer` and `CustomSerializer`. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import StringSerializer + >>> StringSerializer().serialize(PostgreSql, "hi") + "E'hi'" + """ + + def serialize(self, dialect: Dialect, object: _T) -> str: + """ + Render `object` as a SQL literal. + + Args: + dialect: Dialect that chooses quotes, keywords, and hex form. + object: Value this serializer accepts. + + Returns: + The SQL literal, with no trailing separator. + + Raises: + TypeError: `object` is not a type this serializer accepts. + ValueError: `object` is the right type but not a legal value. + NotImplementedError: The dialect has no literal of this kind. + """ + + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +@typing.final +class IntegerSerializer(Serializer[int]): + """ + Bare SQL integer for a Python `int`. + + Use this for integers of any width, including values outside 64-bit. + `bool` is rejected so `True` is not written as `1` by this serializer. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import IntegerSerializer + >>> IntegerSerializer().serialize(Sqlite, 7) + '7' + """ + + def __new__(cls) -> typing.Self: + """Return the integer serializer.""" + +@typing.final +class FloatSerializer(Serializer[int | float | decimal.Decimal]): + """ + Bare SQL number for `float`, `int`, or `decimal.Decimal`. + + Use this when a whole number must stay a decimal (`1.0`, not `1`). + Non-finite floats are rejected. Strings are not accepted. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import FloatSerializer + >>> FloatSerializer().serialize(Sqlite, 1.0) + '1.0' + """ + + def __new__(cls) -> typing.Self: + """Return the float serializer.""" + +@typing.final +class BooleanSerializer(Serializer[bool]): + """ + Dialect boolean literal for a Python `bool`. + + Use this for `TRUE` / `FALSE` or `1` / `0`, whichever the dialect writes. + Only `bool` is accepted. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import BooleanSerializer + >>> BooleanSerializer().serialize(PostgreSql, True) + 'TRUE' + """ + + def __new__(cls) -> typing.Self: + """Return the boolean serializer.""" + +@typing.final +class StringSerializer(Serializer[str | enum.Enum]): + """ + Quoted, escaped SQL string. + + Use this for text. An `enum.Enum` member is accepted when its value is a + `str`. The dialect chooses the quotes and the escape rules. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import StringSerializer + >>> StringSerializer().serialize(Sqlite, "hi") + "'hi'" + """ + + def __new__(cls) -> typing.Self: + """Return the string serializer.""" + +@typing.final +class UuidSerializer(Serializer[uuid.UUID | str]): + """ + UUID literal, spelled with the dialect's `uuid_format`. + + Use this for `uuid.UUID` or a UUID string. The result is a quoted literal, + `UUID '…'`, or `'…'::uuid`. + + Examples: + >>> import uuid + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import UuidSerializer + >>> UuidSerializer().serialize(PostgreSql, uuid.UUID(int=0)) + "E'00000000-0000-0000-0000-000000000000'::uuid" + """ + + def __new__(cls) -> typing.Self: + """Return the UUID serializer.""" + +@typing.final +class BytesSerializer(Serializer[bytes]): + """ + Dialect blob or bytea literal for exact `bytes`. + + Use this for binary payloads. `bytearray` and `memoryview` are rejected. + The dialect chooses `X'…'`, `\\x…::bytea`, or a conversion function. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import BytesSerializer + >>> BytesSerializer().serialize(PostgreSql, b"A") + "'\\\\x41'::bytea" + """ + + def __new__(cls) -> typing.Self: + """Return the bytes serializer.""" + +@typing.final +class BitSerializer(Serializer[str]): + """ + Bit-string literal for a string of `0` and `1` characters. + + Use this on PostgreSQL, DuckDB, MySQL, and Teradata. Other dialects raise. + The string must be non-empty. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import BitSerializer + >>> BitSerializer().serialize(PostgreSql, "101") + "B'101'" + """ + + def __new__(cls) -> typing.Self: + """Return the bit-string serializer.""" + +@typing.final +class JSONSerializer(Serializer[typing.Any]): + """ + JSON literal, spelled with the dialect's `json_format`. + + Use this for objects the JSON encoder can dump. `orjson` is used when it + is installed; otherwise the stdlib `json` module is used. The JSON text + is then wrapped as a quoted literal, `JSON '…'`, or `PARSE_JSON('…')`. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import JSONSerializer + >>> JSONSerializer().serialize(Sqlite, {"a": 1}) + '\\'{"a":1}\\'' + """ + + def __new__(cls) -> typing.Self: + """Return the JSON serializer.""" + +@typing.final +class DateSerializer(Serializer[datetime.date]): + """ + Date literal `YYYY-MM-DD`, quoted or after the `DATE` keyword. + + Use this for `datetime.date`. A `datetime.datetime` is rejected so a + timestamp is not silently truncated. + + Examples: + >>> import datetime + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import DateSerializer + >>> DateSerializer().serialize(Sqlite, datetime.date(2020, 1, 2)) + "'2020-01-02'" + """ + + def __new__(cls) -> typing.Self: + """Return the date serializer.""" + +@typing.final +class TimeSerializer(Serializer[datetime.time]): + """ + Time literal `HH:MM:SS[.ffffff]`, quoted or after the `TIME` keyword. + + Use this for `datetime.time`. + + Examples: + >>> import datetime + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import TimeSerializer + >>> TimeSerializer().serialize(Sqlite, datetime.time(1, 2, 3)) + "'01:02:03'" + """ + + def __new__(cls) -> typing.Self: + """Return the time serializer.""" + +@typing.final +class DatetimeSerializer(Serializer[datetime.datetime | int | float]): + """ + Timestamp literal for a `datetime` or a UTC epoch. + + Use this for naive or aware datetimes, or for `int` / `float` seconds + since the Unix epoch. Naive values keep their wall time. Aware values + keep their offset. Epoch numbers are treated as UTC. The dialect chooses + a quoted literal or a `TIMESTAMP` / `TIMESTAMPTZ` keyword. + + Examples: + >>> import datetime + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import DatetimeSerializer + >>> value = datetime.datetime(2020, 1, 2, 3, 4, 5) + >>> DatetimeSerializer().serialize(Sqlite, value) + "'2020-01-02 03:04:05'" + """ + + def __new__(cls) -> typing.Self: + """Return the timestamp serializer.""" + +@typing.final +class InetSerializer(Serializer[typing.Any]): + """ + IP address or network literal for PostgreSQL and DuckDB. + + Use this for `ipaddress` objects or for a string that is an address or + CIDR. Other dialects raise. The result is a quoted literal or `'…'::inet`. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import InetSerializer + >>> InetSerializer().serialize(PostgreSql, "127.0.0.1") + "E'127.0.0.1/32'::inet" + """ + + def __new__(cls) -> typing.Self: + """Return the inet serializer.""" + +@typing.final +class MacAddressSerializer(Serializer[str]): + """ + MAC address literal. + + Use this for a `str` that uses `:` or `-` as the separator. PostgreSQL + writes a native MAC literal. Other dialects raise. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import MacAddressSerializer + >>> MacAddressSerializer().serialize(PostgreSql, "00:11:22:33:44:55") + "E'00:11:22:33:44:55'::macaddr" + """ + + def __new__(cls) -> typing.Self: + """Return the MAC address serializer.""" + +@typing.final +class UnspecifiedSerializer(Serializer[typing.Any]): + """ + Inserts `str(object)` with no quotes and no type check. + + Use this only when no builtin serializer matches and you already hold SQL + text. The result is not escaped. Prefer an explicit serializer for values + that come from outside the program. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import UnspecifiedSerializer + >>> UnspecifiedSerializer().serialize(Sqlite, 1) + '1' + """ + + def __new__(cls) -> typing.Self: + """Return the unspecified serializer.""" + +@typing.final +class ArraySerializer(Serializer[list[_T] | tuple[_T]]): + """ + `ARRAY[elem, …]` built by serializing each item. + + Use this for a list or tuple on PostgreSQL, DuckDB, and BigQuery. Pass + another `ArraySerializer` as `element` for a nested array. Other dialects + raise. + + Examples: + >>> from rapidquery.dialect import PostgreSql + >>> from rapidquery.serializers import ArraySerializer, IntegerSerializer + >>> ArraySerializer(IntegerSerializer()).serialize(PostgreSql, [1, 2]) + 'ARRAY[1, 2]' + """ + + def __new__(cls, element: Serializer[_T]) -> typing.Self: + """ + Build an array serializer. + + Args: + element: Serializer applied to every item. + """ + + @property + def element(self) -> Serializer[_T]: + """Serializer applied to every item.""" + +@typing.final +class CustomSerializer(Serializer[_T]): + """ + Inserts the text returned by a Python callable. + + Use this for a literal spelling the builtins do not cover. The callable + receives the dialect name and the object, and must return SQL text. That + text is inserted as-is, with no extra quoting. + + Examples: + >>> from rapidquery.dialect import Sqlite + >>> from rapidquery.serializers import CustomSerializer + >>> serializer = CustomSerializer(lambda _dialect, value: str(value)) + >>> serializer.serialize(Sqlite, 3) + '3' + """ + + def __new__(cls, function: Callable[[Dialect, _T], str]) -> typing.Self: + """ + Build a serializer around `function`. + + Args: + function: `fn(dialect, object) -> str`. `dialect` is the `Dialect` + value, not its name. + + Raises: + TypeError: `function` is not callable. + """ + + @property + def function(self) -> Callable[[Dialect, _T], str]: + """The callable passed to the constructor.""" diff --git a/rapidquery/tcl.py b/rapidquery/tcl.py new file mode 100644 index 0000000..39039f9 --- /dev/null +++ b/rapidquery/tcl.py @@ -0,0 +1,5 @@ +from ._core.tcl import TransactionControlLanguage + +__all__ = [ + "TransactionControlLanguage", +] diff --git a/rapidquery/tcl.pyi b/rapidquery/tcl.pyi new file mode 100644 index 0000000..4c9f8b8 --- /dev/null +++ b/rapidquery/tcl.pyi @@ -0,0 +1,6 @@ +"""Transaction Control Language""" + +class TransactionControlLanguage: + """Base of every TCL statement. Construct a subclass, not this class.""" + + ... diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 89b7108..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,4 +0,0 @@ -format_strings = true -format_macro_matchers = true -format_macro_bodies = true -imports_granularity = "Module" diff --git a/src/common/column.rs b/src/common/column.rs deleted file mode 100644 index fd71ef5..0000000 --- a/src/common/column.rs +++ /dev/null @@ -1,427 +0,0 @@ -use sea_query::IntoIden; - -use crate::common::expression::PyExpr; -use crate::internal::parameters::OptionalParam; -use crate::internal::repr::ReprFormatter; -use crate::internal::type_engine::TypeEngine; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; -use crate::sqltypes::SQLTypeTrait; - -pub const OPT_PRIMARY_KEY: u8 = 1 << 0; -pub const OPT_UNIQUE_KEY: u8 = 1 << 1; -pub const OPT_NOT_NULL: u8 = 1 << 2; -pub const OPT_NULL: u8 = 1 << 3; -pub const OPT_AUTO_INCREMENT: u8 = 1 << 4; -pub const OPT_STORED_GENERATED: u8 = 1 << 5; - -crate::implement_pyclass! { - /// Defines a table column with its properties and constraints. - /// - /// Represents a complete column definition including: - /// - Column name and data type - /// - Constraints (primary key, unique, nullable) - /// - Auto-increment behavior - /// - Default values and generated columns - /// - Comments and extra specifications - /// - /// This class is used within Table to specify the structure - /// of table columns. It encapsulates all the properties that define how - /// a column behaves and what data it can store. - #[derive(Debug, Clone)] - mutable [subclass, generic] PyColumn(ColumnState) as "Column" { - pub name: String, - pub r#type: TypeEngine, - pub options: u8, - pub default: Option, - pub generated: Option, - pub extra: Option, - pub comment: Option, - } -} - -impl ToSeaQuery for ColumnState { - #[inline] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::ColumnRef { - sea_query::ColumnRef::Column(sea_query::Alias::new(&self.name).into_iden()) - } -} - -impl ToSeaQuery for ColumnState { - #[inline] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::ColumnDef { - let mut column_def = sea_query::ColumnDef::new_with_type( - sea_query::Alias::new(self.name.clone()), - self.r#type.to_sea_query_column_type(), - ); - - if self.options & OPT_PRIMARY_KEY > 0 { - column_def.primary_key(); - } - if self.options & OPT_AUTO_INCREMENT > 0 { - column_def.auto_increment(); - } - if self.options & OPT_NULL > 0 { - column_def.null(); - } - if self.options & OPT_NOT_NULL > 0 { - column_def.not_null(); - } - if self.options & OPT_UNIQUE_KEY > 0 { - column_def.unique_key(); - } - - if let Some(x) = &self.default { - column_def.default(x.0.clone()); - } - if let Some(x) = &self.generated { - column_def.generated(x.0.clone(), self.options & OPT_STORED_GENERATED > 0); - } - - if let Some(x) = &self.extra { - column_def.extra(x); - } - if let Some(x) = &self.comment { - column_def.comment(x); - } - - column_def - } -} - -#[pyo3::pymethods] -impl PyColumn { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[ - pyo3( - signature=( - name, - r#type, - *, - primary_key=false, - unique_key=false, - nullable=None, - auto_increment=false, - extra=None, - comment=None, - default=OptionalParam::Undefined, - generated=OptionalParam::Undefined, - stored_generated=false, - ) - ) - ] - fn __init__( - &self, - name: String, - r#type: RefBoundObject<'_>, - primary_key: bool, - unique_key: bool, - nullable: Option, - auto_increment: bool, - extra: Option, - comment: Option, - default: OptionalParam, - generated: OptionalParam, - stored_generated: bool, - ) -> pyo3::PyResult<()> { - let sql_type = TypeEngine::new(r#type)?; - - let default_expr = match default { - OptionalParam::Defined(x) => Some(super::expression::PyExpr::try_from_specific_type( - &x, - Some(sql_type.clone()), - )?), - OptionalParam::Undefined => None, - }; - let generated_expr = match generated { - OptionalParam::Defined(x) => Some(super::expression::PyExpr::try_from(&x)?), - OptionalParam::Undefined => None, - }; - - let mut options = 0u8; - if primary_key { - options |= OPT_PRIMARY_KEY; - } - if unique_key { - options |= OPT_UNIQUE_KEY; - } - - match nullable { - Some(true) => options |= OPT_NULL, - Some(false) => options |= OPT_NOT_NULL, - None => (), - } - - if auto_increment { - options |= OPT_AUTO_INCREMENT; - } - if stored_generated { - options |= OPT_STORED_GENERATED; - } - - let state = ColumnState { - name, - r#type: sql_type, - options, - default: default_expr, - generated: generated_expr, - extra, - comment, - }; - self.0.set(state); - - Ok(()) - } - - /// Column name. - #[getter] - fn name(&self) -> String { - self.0.lock().name.clone() - } - - #[setter] - fn set_name(&self, value: String) { - self.0.lock().name = value; - } - - /// Column type. - #[getter] - fn r#type(&self, py: pyo3::Python) -> PyObject { - let lock = self.0.lock(); - lock.r#type.as_pyobject(py).unbind() - } - - #[getter] - fn primary_key(&self) -> bool { - self.0.lock().options & OPT_PRIMARY_KEY > 0 - } - - #[setter] - fn set_primary_key(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_PRIMARY_KEY; - } else { - lock.options &= !OPT_PRIMARY_KEY; - } - } - - #[getter] - fn unique_key(&self) -> bool { - self.0.lock().options & OPT_UNIQUE_KEY > 0 - } - - #[setter] - fn set_unique_key(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_UNIQUE_KEY; - } else { - lock.options &= !OPT_UNIQUE_KEY; - } - } - - #[getter] - fn auto_increment(&self) -> bool { - self.0.lock().options & OPT_AUTO_INCREMENT > 0 - } - - #[setter] - fn set_auto_increment(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_AUTO_INCREMENT; - } else { - lock.options &= !OPT_AUTO_INCREMENT; - } - } - - #[getter] - fn nullable(&self) -> Option { - let options = self.0.lock().options; - - if options & OPT_NOT_NULL > 0 { - Some(false) - } else if options & OPT_NULL > 0 { - Some(true) - } else { - None - } - } - - #[setter] - fn set_nullable(&self, value: Option) { - let mut lock = self.0.lock(); - - match value { - Some(true) => { - lock.options |= OPT_NULL; - lock.options &= !OPT_NOT_NULL; - } - Some(false) => { - lock.options |= OPT_NOT_NULL; - lock.options &= !OPT_NULL; - } - None => { - lock.options &= !OPT_NOT_NULL; - lock.options &= !OPT_NULL; - } - } - } - - #[getter] - fn stored_generated(&self) -> bool { - self.0.lock().options & OPT_STORED_GENERATED > 0 - } - - #[setter] - fn set_stored_generated(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_STORED_GENERATED; - } else { - lock.options &= !OPT_STORED_GENERATED; - } - } - - /// Extra SQL specifications for this column. - #[getter] - fn extra(&self) -> Option { - self.0.lock().extra.clone() - } - - #[setter] - fn set_extra(&self, val: Option) { - let mut lock = self.0.lock(); - lock.extra = val; - } - - /// Comment describing this column. - #[getter] - fn comment(&self) -> Option { - self.0.lock().comment.clone() - } - - #[setter] - fn set_comment(&self, val: Option) { - let mut lock = self.0.lock(); - lock.comment = val; - } - - /// Default value for this column. - #[getter] - fn default(&self) -> Option { - let lock = self.0.lock(); - lock.default.clone() - } - - #[setter] - fn set_default(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - let sql_type = lock.r#type.clone(); - - let default = super::expression::PyExpr::try_from_specific_type(val, Some(sql_type))?; - lock.default = Some(default); - Ok(()) - } - - /// Expression for generated column values. - #[getter] - fn generated(&self) -> Option { - let lock = self.0.lock(); - lock.generated.clone() - } - - #[setter] - fn set_generated(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - - let generated = super::expression::PyExpr::try_from_specific_type(val, None)?; - lock.generated = Some(generated); - Ok(()) - } - - /// Shorthand for `Value(object, self.type)`. - fn adapt(&self, object: BoundObject<'_>) -> pyo3::PyResult { - let lock = self.0.lock(); - let sql_type = lock.r#type.clone(); - - let result = super::value::ValueState::from_pyobject(sql_type, object)?; - Ok(result.into()) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = self.to_expr(); - - let state = crate::query::select::SelectLabelState { - expr, - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self) -> PyExpr { - let col_ref = Self::__column_ref__(self); - PyExpr(sea_query::Expr::column(col_ref)) - } - - #[getter] - fn __column_ref__(&self) -> super::column_ref::PyColumnRef { - let lock = self.0.lock(); - - super::column_ref::PyColumnRef { - name: Some(sea_query::Alias::new(&lock.name).into_iden()), - table: None, - schema: None, - } - } - - fn __copy__(&self) -> Self { - let lock = self.0.lock(); - lock.clone().into() - } - - pub fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .quote("", &lock.name) - .display("type", &lock.r#type) - .optional_boolean("primary_key", lock.options & OPT_PRIMARY_KEY > 0) - .optional_boolean("unique_key", lock.options & OPT_UNIQUE_KEY > 0) - .optional_boolean("auto_increment", lock.options & OPT_AUTO_INCREMENT > 0) - .optional_display( - "nullable", - if lock.options & OPT_NOT_NULL > 0 { - Some(false) - } else if lock.options & OPT_NULL > 0 { - Some(true) - } else { - None - }, - ) - .optional_boolean("stored_generated", lock.options & OPT_STORED_GENERATED > 0) - .optional_quote("extra", lock.extra.as_ref()) - .optional_quote("comment", lock.comment.as_ref()) - .optional_map("default", lock.default.as_ref(), |x| x.__repr__()) - .optional_map("generated", lock.generated.as_ref(), |x| x.__repr__()) - .finish() - } -} diff --git a/src/common/column_ref.rs b/src/common/column_ref.rs deleted file mode 100644 index 59cdd71..0000000 --- a/src/common/column_ref.rs +++ /dev/null @@ -1,340 +0,0 @@ -use std::str::FromStr; - -use pyo3::types::{PyAnyMethods, PyStringMethods}; -use sea_query::IntoIden; - -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundKwargs, RefBoundObject}; - -crate::implement_pyclass! { - // NOTE: SQLTypes, PyExpr, PyFunc, PyTableName & PyColumnRef could never mark as subclass. - // these should be immutable and final types. - - /// Represents a reference to a database column with optional table and schema qualification. - /// - /// This class is used to uniquely identify columns in SQL queries, supporting - /// schema-qualified and table-qualified column references. - /// - /// NOTE: this class is immutable and frozen. - #[derive(Debug, Clone)] - [] PyColumnRef as "ColumnRef" { - /// Name of the referenced column. [`Option::None`] means '*'. - pub name: Option, - pub table: Option, - pub schema: Option, - } -} - -impl sea_query::IntoColumnRef for PyColumnRef { - fn into_column_ref(self) -> sea_query::ColumnRef { - if let Some(name) = self.name { - match (self.table, self.schema) { - (Some(table), Some(schema)) => { - sea_query::ColumnRef::SchemaTableColumn(schema, table, name) - } - (Some(table), None) => sea_query::ColumnRef::TableColumn(table, name), - _ => sea_query::ColumnRef::Column(name), - } - } else if let Some(table) = self.table { - sea_query::ColumnRef::TableAsterisk(table) - } else { - sea_query::ColumnRef::Asterisk - } - } -} - -impl FromStr for PyColumnRef { - type Err = pyo3::PyErr; - - fn from_str(string: &str) -> Result { - let string = string.trim().to_owned(); - - if string.is_empty() { - return Err(pyo3::exceptions::PyValueError::new_err( - "cannot parse an empty string", - )); - } - - // Possible formats: - // name - // table.name - // schema.table.name - let mut string: Vec = string.split('.').map(String::from).collect(); - - if string.len() > 3 { - return Err(pyo3::exceptions::PyValueError::new_err("invalid format")); - } - - let name = string.pop().unwrap(); - let table = string.pop().map(|x| sea_query::Alias::new(x).into_iden()); - let schema = string.pop().map(|x| sea_query::Alias::new(x).into_iden()); - - Ok(Self { - name: if name == "*" { - None - } else { - Some(sea_query::Alias::new(name).into_iden()) - }, - table, - schema, - }) - } -} - -impl TryFrom> for PyColumnRef { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject<'_>) -> Result { - unsafe { - if pyo3::ffi::Py_TYPE(value.as_ptr()) == crate::typeref::COLUMN_REF_TYPE { - let casted_value = value.cast_unchecked::(); - return Ok(casted_value.get().clone()); - } - - if let Ok(x) = value.cast_exact::() { - return Self::from_str(x.to_str()?); - } - - if let Some(result) = Self::try_from_property(value)? { - return Ok(result); - } - - crate::new_error!( - PyTypeError, - "expected ColumnRef, str, or object.to_column_ref property, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ) - } - } -} - -impl PyColumnRef { - #[inline] - pub fn try_from_property(value: RefBoundObject) -> pyo3::PyResult> { - const PROPERTY_NAME: &std::ffi::CStr = c"__column_ref__"; - - let property = match value.getattr(PROPERTY_NAME) { - Ok(x) => Ok(Some(x)), - Err(err) if err.is_instance_of::(value.py()) => { - Ok(None) - } - Err(err) => Err(err), - }; - let property = property?; - - if property.is_none() { - return Ok(None); - } - - unsafe { - let property = property.unwrap_unchecked(); - - if pyo3::ffi::Py_TYPE(property.as_ptr()) == crate::typeref::COLUMN_REF_TYPE { - let casted_value = property.cast_unchecked::(); - return Ok(Some(casted_value.get().clone())); - } - - if let Ok(x) = property.extract::() { - return Self::from_str(&x).map(Some); - } - - crate::new_error!( - PyTypeError, - "__column_ref__ property returns something other than ColumnRef or str; returns {}", - crate::internal::get_type_name(property.py(), property.as_ptr()) - ) - } - } -} - -#[pyo3::pymethods] -impl PyColumnRef { - #[new] - #[pyo3(signature=(name, table=None, schema=None))] - fn __new__( - name: String, - table: Option, - schema: Option, - ) -> pyo3::PyResult { - let name = unsafe { - if name == "*" { - None - } else { - Some(name) - } - }; - - Ok(Self { - name: name.map(|x| sea_query::Alias::new(x).into_iden()), - table: table.map(|x| sea_query::Alias::new(x).into_iden()), - schema: schema.map(|x| sea_query::Alias::new(x).into_iden()), - }) - } - - #[getter] - fn name(&self) -> String { - match &self.name { - None => String::from("*"), - Some(x) => x.to_string(), - } - } - - #[getter] - fn table(&self) -> Option { - self.table.as_ref().map(|x| x.to_string()) - } - - #[getter] - fn schema(&self) -> Option { - self.schema.as_ref().map(|x| x.to_string()) - } - - /// Parse a string representation of a column reference. - /// - /// Supports formats like: - /// - "column_name" - /// - "table.column_name" - /// - "schema.table.column_name" - #[classmethod] - fn parse(_cls: &pyo3::Bound<'_, pyo3::types::PyType>, string: String) -> pyo3::PyResult { - Self::from_str(&string) - } - - #[pyo3(signature=(**kwds))] - fn copy_with(&self, kwds: Option>) -> pyo3::PyResult { - use pyo3::types::PyDictMethods; - - let mut cloned = self.clone(); - if kwds.is_none() { - return Ok(cloned); - } - - let kwds = unsafe { kwds.unwrap_unchecked() }; - - for (key, val) in kwds.iter() { - unsafe { - let key = key.extract::().unwrap_unchecked(); - - let val: Option = unsafe { - if pyo3::ffi::Py_IsNone(val.as_ptr()) == 1 { - None - } else if pyo3::ffi::PyUnicode_CheckExact(val.as_ptr()) == 1 { - Some(val.extract::().unwrap_unchecked()) - } else { - return crate::new_error!( - PyTypeError, - "expected str or None, got {}", - crate::internal::get_type_name(val.py(), val.as_ptr()) - ); - } - }; - - if key == "name" { - if val.is_none() { - return crate::new_error!(PyTypeError, "expected str for name, got None"); - } - - let val_str = unsafe { val.unwrap_unchecked() }; - if val_str == "*" { - cloned.name = None; - } else { - cloned.name = Some(sea_query::Alias::new(val_str).into_iden()); - } - } else if key == "table" { - cloned.table = val.map(|x| sea_query::Alias::new(x).into_iden()); - } else if key == "schema" { - cloned.schema = val.map(|x| sea_query::Alias::new(x).into_iden()); - } else { - return crate::new_error!( - PyTypeError, - "got an unexpected keyword argument '{}'", - key - ); - } - } - } - - Ok(cloned) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = self.to_expr(); - - let state = crate::query::select::SelectLabelState { - expr, - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - fn __eq__(slf: pyo3::PyRef<'_, Self>, other: pyo3::PyRef<'_, Self>) -> bool { - if slf.as_ptr() == other.as_ptr() { - return true; - } - - slf.name == other.name && slf.schema == other.schema && slf.table == other.table - } - - fn __ne__(slf: pyo3::PyRef<'_, Self>, other: pyo3::PyRef<'_, Self>) -> bool { - if slf.as_ptr() == other.as_ptr() { - return false; - } - - slf.name != other.name || slf.schema != other.schema || slf.table != other.table - } - - fn __copy__(&self) -> Self { - self.clone() - } - - fn __hash__(&self) -> u64 { - use std::hash::{Hash, Hasher}; - - let mut state = std::hash::DefaultHasher::new(); - - if let Some(x) = &self.name { - x.to_string().hash(&mut state); - } - if let Some(x) = &self.table { - x.to_string().hash(&mut state); - } - if let Some(x) = &self.schema { - x.to_string().hash(&mut state); - } - - state.finish() - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self) -> super::expression::PyExpr { - super::expression::PyExpr(sea_query::Expr::column(self.clone())) - } - - pub fn __repr__(&self) -> String { - let mut fmt = ReprFormatter::new("ColumnRef"); - - match &self.name { - Some(x) => { - fmt.iden("name", x); - } - None => { - fmt.pair("name", "*"); - } - } - fmt.optional_iden("table", self.table.as_ref()); - fmt.optional_iden("schema", self.schema.as_ref()); - - fmt.finish() - } -} diff --git a/src/common/expression/expr.rs b/src/common/expression/expr.rs deleted file mode 100644 index 9ec08ae..0000000 --- a/src/common/expression/expr.rs +++ /dev/null @@ -1,700 +0,0 @@ -use pyo3::types::PyAnyMethods; - -use crate::internal::repr::ReprFormatter; -use crate::internal::type_engine::TypeEngine; -use crate::internal::{BoundObject, RefBoundObject, ToSeaQuery}; - -crate::implement_pyclass! { - // NOTE: SQLTypes, PyExpr, PyFunc, PyTableName & PyColumnRef could never mark as subclass. - // these should be immutable and final types. - - /// Represents a SQL expression that can be built into SQL code. - /// - /// This class provides a fluent interface for constructing complex SQL expressions - /// in a database-agnostic way. It supports arithmetic operations, comparisons, - /// logical operations, and database-specific functions. - /// - /// The class automatically handles SQL injection protection and proper quoting - /// when building the final SQL statement. - /// - /// NOTE: `Expr` is immutable, so by calling each method you will give a new instance - /// of it which includes new change(s). - #[derive(Debug, Clone)] - [] PyExpr as "Expr" (pub sea_query::SimpleExpr); -} - -impl PyExpr { - pub fn try_from_specific_type( - value: RefBoundObject<'_>, - type_engine: Option, - ) -> pyo3::PyResult { - unsafe { - let py = value.py(); - let type_ptr = pyo3::ffi::Py_TYPE(value.as_ptr()); - - if type_ptr == crate::typeref::EXPR_TYPE { - let casted_value = value.cast_unchecked::(); - - return Ok(casted_value.get().clone()); - } - - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::VALUE_TYPE) == 1 { - let casted_value = value.cast_unchecked::(); - let unbound = casted_value.get(); - - return Ok(Self(unbound.0.lock().simple_expr(py)?)); - } - - if type_ptr == crate::typeref::COLUMN_REF_TYPE { - let casted_value = value.cast_unchecked::(); - let cloned = casted_value.get().clone(); - - return Ok(Self(sea_query::Expr::column(cloned))); - } - - if type_ptr == crate::typeref::FUNC_TYPE { - let casted_value = value.cast_unchecked::(); - let cloned = casted_value.get().clone(); - - return Ok(Self(sea_query::SimpleExpr::FunctionCall(cloned.0))); - } - - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::SELECT_STATEMENT_TYPE) - == 1 - { - let casted_value = - value.cast_unchecked::(); - - let inner_value = casted_value.get(); - let result = sea_query::SimpleExpr::SubQuery( - None, - Box::new(sea_query::SubQueryStatement::SelectStatement( - inner_value.0.lock().to_sea_query(py), - )), - ); - - return Ok(Self(result)); - } - - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::CASE_STATEMENT_TYPE) - == 1 - { - let casted_value = value.cast_unchecked::(); - - let inner_value = casted_value.get(); - let result = - sea_query::SimpleExpr::Case(Box::new(inner_value.0.lock().to_sea_query(py))); - - return Ok(Self(result)); - } - - if pyo3::ffi::PyTuple_Check(value.as_ptr()) == 1 { - use pyo3::types::PyTupleMethods; - - let casted_value = value.cast_unchecked::(); - let mut arr = Vec::with_capacity(casted_value.len()); - - for item in casted_value.iter() { - arr.push(Self::try_from(&item)?); - } - - let result = sea_query::Expr::tuple(arr.into_iter().map(|x| x.0)); - return Ok(Self(result.into())); - } - - if let Some(result) = Self::try_from_property(value)? { - return Ok(result); - } - - if let Some(result) = crate::common::column_ref::PyColumnRef::try_from_property(value)? - { - return Ok(Self(sea_query::Expr::column(result))); - } - - let type_engine = match type_engine { - Some(x) => x, - None => TypeEngine::infer_pyobject(value)?, - }; - - let result = - crate::common::value::ValueState::from_pyobject(type_engine, value.clone())? - .simple_expr(py)?; - - Ok(Self(result)) - } - } - - #[inline] - pub fn try_from_property(value: RefBoundObject) -> pyo3::PyResult> { - const PROPERTY_NAME: &std::ffi::CStr = c"__expr__"; - - let property = match value.getattr(PROPERTY_NAME) { - Ok(x) => Ok(Some(x)), - Err(err) if err.is_instance_of::(value.py()) => { - Ok(None) - } - Err(err) => Err(err), - }; - let property = property?; - - if property.is_none() { - return Ok(None); - } - - unsafe { - let property = property.unwrap_unchecked(); - - if pyo3::ffi::Py_TYPE(property.as_ptr()) == crate::typeref::EXPR_TYPE { - let casted_value = property.cast_into_unchecked::(); - return Ok(Some(casted_value.get().clone())); - } - - crate::new_error!( - PyTypeError, - "__expr__ property returns something other than Expr; returns {}", - crate::internal::get_type_name(property.py(), property.as_ptr()) - ) - } - } -} - -impl From for PyExpr { - fn from(value: sea_query::SimpleExpr) -> Self { - Self(value) - } -} - -impl TryFrom> for PyExpr { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject<'_>) -> Result { - Self::try_from_specific_type(value, None) - } -} - -#[pyo3::pymethods] -impl PyExpr { - #[new] - #[pyo3(signature = (value, /))] - fn __new__(value: RefBoundObject<'_>) -> pyo3::PyResult { - Self::try_from(value) - } - - /// Shorthand for `Expr(Value(value, sql_type))` - #[classmethod] - #[pyo3(signature=(value, sql_type=None))] - fn val( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - value: RefBoundObject<'_>, - sql_type: Option>, - ) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::Py_TYPE(value.as_ptr()) == crate::typeref::VALUE_TYPE { - let casted_value = value.cast_unchecked::(); - let unbound = casted_value.get(); - - return Ok(Self(unbound.0.lock().simple_expr(value.py())?)); - } - - let type_engine = { - if let Some(sql_type) = sql_type { - TypeEngine::new(&sql_type)? - } else { - TypeEngine::infer_pyobject(value)? - } - }; - - let result = - crate::common::value::ValueState::from_pyobject(type_engine, value.clone())? - .simple_expr(value.py())?; - - Ok(Self(result)) - } - } - - /// Tries to convert the `value` into `ColumnRef`, and then converts it to `Expr`. - #[classmethod] - fn col( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - value: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let column_ref = crate::common::column_ref::PyColumnRef::try_from(value)?; - Ok(Self(sea_query::Expr::column(column_ref))) - } - - /// Returns asterisk '*' expression. - #[classmethod] - fn asterisk(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - sea_query::Expr::column(sea_query::Asterisk).into() - } - - /// Create an expression from a custom SQL string. - /// - /// Warning: This method does not escape the input, so it should only - /// be used with trusted strings to avoid SQL injection vulnerabilities. - #[classmethod] - fn custom(_cls: &pyo3::Bound<'_, pyo3::types::PyType>, value: String) -> Self { - sea_query::SimpleExpr::Custom(value).into() - } - - /// Create an expression for the CURRENT_DATE SQL function. - #[classmethod] - fn current_date(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - sea_query::SimpleExpr::Keyword(sea_query::Keyword::CurrentDate).into() - } - - /// Create an expression for the CURRENT_TIME SQL function. - #[classmethod] - fn current_time(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - sea_query::SimpleExpr::Keyword(sea_query::Keyword::CurrentTime).into() - } - - /// Create an expression for the CURRENT_TIMESTAMP SQL function. - #[classmethod] - fn current_timestamp(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - sea_query::SimpleExpr::Keyword(sea_query::Keyword::CurrentTimestamp).into() - } - - /// Create an expression representing the NULL value. - #[classmethod] - fn null(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - sea_query::SimpleExpr::Keyword(sea_query::Keyword::Null).into() - } - - /// Express a `EXISTS` sub-query expression. - #[classmethod] - fn exists( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - statement: RefBoundObject<'_>, - ) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) != 1 - { - return crate::new_error!( - PyTypeError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - - let casted_statement = - statement.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - - let result = inner_statement.0.lock().to_sea_query(_cls.py()); - - Ok(Self(sea_query::Expr::exists(result))) - } - } - - /// Express a `ANY` sub-query expression. - #[classmethod] - fn any( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - statement: RefBoundObject<'_>, - ) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) != 1 - { - return crate::new_error!( - PyTypeError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - - let casted_statement = - statement.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - - let result = inner_statement.0.lock().to_sea_query(_cls.py()); - - Ok(Self(sea_query::Expr::any(result))) - } - } - - /// Express a `SOME` sub-query expression. - #[classmethod] - fn some( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - statement: RefBoundObject<'_>, - ) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) != 1 - { - return crate::new_error!( - PyTypeError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - - let casted_statement = - statement.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - - let result = inner_statement.0.lock().to_sea_query(_cls.py()); - - Ok(Self(sea_query::Expr::some(result))) - } - } - - /// Express a `ALL` sub-query expression. - #[classmethod] - fn all( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - statement: RefBoundObject<'_>, - ) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) != 1 - { - return crate::new_error!( - PyTypeError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - - let casted_statement = - statement.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - let result = inner_statement.0.lock().to_sea_query(_cls.py()); - - Ok(Self(sea_query::Expr::all(result))) - } - } - - /// Express a `IN` expression. - fn in_(slf: pyo3::PyRef<'_, Self>, other: RefBoundObject<'_>) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(other.as_ptr(), crate::typeref::SELECT_STATEMENT_TYPE) - == 1 - { - let casted_statement = - other.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - let result = inner_statement.0.lock().to_sea_query(slf.py()); - let result = sea_query::ExprTrait::in_subquery(slf.0.clone(), result); - - return Ok(result.into()); - } - } - - let mut exprs: Vec = Vec::new(); - - for item in other.try_iter()? { - let item = item?; - let item = Self::try_from(&item)?; - exprs.push(item.0); - } - - Ok(sea_query::ExprTrait::is_in(slf.0.clone(), exprs).into()) - } - - /// Express a `NOT IN` expression. - fn not_in(slf: pyo3::PyRef<'_, Self>, other: RefBoundObject<'_>) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(other.as_ptr(), crate::typeref::SELECT_STATEMENT_TYPE) - == 1 - { - let casted_statement = - other.cast_unchecked::(); - - let inner_statement = casted_statement.get(); - let result = inner_statement.0.lock().to_sea_query(slf.py()); - let result = sea_query::ExprTrait::not_in_subquery(slf.0.clone(), result); - - return Ok(result.into()); - } - } - - let mut exprs: Vec = Vec::new(); - - for item in other.try_iter()? { - let item = item?; - let item = Self::try_from(&item)?; - exprs.push(item.0); - } - - Ok(sea_query::ExprTrait::is_not_in(slf.0.clone(), exprs).into()) - } - - /// Create a `CAST` expression to convert to a specific SQL type. - fn cast_as(slf: pyo3::PyRef<'_, Self>, value: String) -> Self { - slf.0.clone().cast_as(sea_query::Alias::new(value)).into() - } - - /// Create a `LIKE` pattern matching expression. - #[pyo3(signature=(pattern, escape=None))] - fn like(slf: pyo3::PyRef<'_, Self>, pattern: String, escape: Option) -> Self { - let e = sea_query::LikeExpr::new(pattern); - - if let Some(x) = escape { - sea_query::ExprTrait::like(slf.0.clone(), e.escape(x)).into() - } else { - sea_query::ExprTrait::like(slf.0.clone(), e).into() - } - } - - /// Create a NOT LIKE pattern matching expression. - #[pyo3(signature=(pattern, escape=None))] - fn not_like(slf: pyo3::PyRef<'_, Self>, pattern: String, escape: Option) -> Self { - let e = sea_query::LikeExpr::new(pattern); - - if let Some(x) = escape { - sea_query::ExprTrait::not_like(slf.0.clone(), e.escape(x)).into() - } else { - sea_query::ExprTrait::not_like(slf.0.clone(), e).into() - } - } - - /// Create an equality comparison expression. - fn __eq__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::eq(slf.0.clone(), other.0).into()) - } - - /// Create an inequality comparison expression. - fn __ne__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::ne(slf.0.clone(), other.0).into()) - } - - /// Create a greater-than comparison expression. - fn __gt__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::gt(slf.0.clone(), other.0).into()) - } - - /// Create a greater-than-or-equal comparison expression. - fn __ge__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::gte(slf.0.clone(), other.0).into()) - } - - /// Create a less-than comparison expression. - fn __lt__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::lt(slf.0.clone(), other.0).into()) - } - - /// Create a less-than-or-equal comparison expression. - fn __le__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::lte(slf.0.clone(), other.0).into()) - } - - /// Create an addition expression. - fn __add__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::add(slf.0.clone(), other.0).into()) - } - - /// Create an subtraction expression. - fn __sub__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::sub(slf.0.clone(), other.0).into()) - } - - /// Create a logical AND expression. - fn __and__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::and(slf.0.clone(), other.0).into()) - } - - fn __neg__<'a>(slf: pyo3::PyRef<'a, Self>) -> pyo3::PyResult { - Ok(sea_query::ExprTrait::mul(slf.0.clone(), -1).into()) - } - - /// Create a logical OR expression. - fn __or__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::or(slf.0.clone(), other.0).into()) - } - - fn bit_and<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::bit_and(slf.0.clone(), other.0).into()) - } - - fn bit_or<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::bit_or(slf.0.clone(), other.0).into()) - } - - /// Create a division expression. - fn __truediv__<'a>( - slf: pyo3::PyRef<'a, Self>, - other: RefBoundObject<'a>, - ) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::div(slf.0.clone(), other.0).into()) - } - - /// Create an IS comparison expression. - fn is_<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::is(slf.0.clone(), other.0).into()) - } - - /// Create an IS NOT comparison expression. - fn is_not<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::is_not(slf.0.clone(), other.0).into()) - } - - /// Create an IS NULL expression. - fn is_null(slf: pyo3::PyRef<'_, Self>) -> Self { - sea_query::ExprTrait::is_null(slf.0.clone()).into() - } - - /// Create an IS NOT NULL expression. - fn is_not_null(slf: pyo3::PyRef<'_, Self>) -> Self { - sea_query::ExprTrait::is_not_null(slf.0.clone()).into() - } - - /// Create a bitwise left shift expression. - fn __lshift__<'a>( - slf: pyo3::PyRef<'a, Self>, - other: RefBoundObject<'a>, - ) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::left_shift(slf.0.clone(), other.0).into()) - } - - /// Create a bitwise right shift expression. - fn __rshift__<'a>( - slf: pyo3::PyRef<'a, Self>, - other: RefBoundObject<'a>, - ) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::right_shift(slf.0.clone(), other.0).into()) - } - - /// Create a modulo expression. - fn __mod__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::modulo(slf.0.clone(), other.0).into()) - } - - /// Create a multiplication expression. - fn __mul__<'a>(slf: pyo3::PyRef<'a, Self>, other: RefBoundObject<'a>) -> pyo3::PyResult { - let other = Self::try_from(other)?; - Ok(sea_query::ExprTrait::mul(slf.0.clone(), other.0).into()) - } - - /// Create a BETWEEN range comparison expression. - fn between<'a>( - slf: pyo3::PyRef<'a, Self>, - a: RefBoundObject<'a>, - b: RefBoundObject<'a>, - ) -> pyo3::PyResult { - let a = Self::try_from(a)?; - let b = Self::try_from(b)?; - - Ok(sea_query::ExprTrait::between(slf.0.clone(), a.0, b.0).into()) - } - - /// Create a NOT BETWEEN range comparison expression. - fn not_between<'a>( - slf: pyo3::PyRef<'a, Self>, - a: RefBoundObject<'a>, - b: RefBoundObject<'a>, - ) -> pyo3::PyResult { - let a = Self::try_from(a)?; - let b = Self::try_from(b)?; - - Ok(sea_query::ExprTrait::not_between(slf.0.clone(), a.0, b.0).into()) - } - - /// Create `MAX(self)` function call. - /// - /// Shorthand for `Func.max(self).to_expr()` - fn max(&self) -> Self { - let expr: sea_query::SimpleExpr = sea_query::Func::max(self.0.clone()).into(); - Self(expr) - } - - /// Create `MIN(self)` function call. - /// - /// Shorthand for `Func.min(self).to_expr()` - fn min(&self) -> Self { - let expr: sea_query::SimpleExpr = sea_query::Func::min(self.0.clone()).into(); - Self(expr) - } - - /// Create `ABS(self)` function call. - /// - /// Shorthand for `Func.abs(self).to_expr()` - fn abs(&self) -> Self { - let expr: sea_query::SimpleExpr = sea_query::Func::abs(self.0.clone()).into(); - Self(expr) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - - let state = crate::query::select::SelectLabelState { - expr: self.clone(), - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn _to_sql(&self, _py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let builder = crate::internal::get_schema_builder(backend)?; - let mut sql = String::new(); - - let assert_unwind = std::panic::AssertUnwindSafe(|| { - sea_query::QueryBuilder::prepare_simple_expr(&*builder, &self.0, &mut sql) - }); - - std::panic::catch_unwind(assert_unwind) - .map_err(|_| pyo3::PyErr::new::("build failed"))?; - - Ok(sql) - } - - // TODO: sqlite_*, pg_*, mysql_* - - pub fn __repr__(&self) -> String { - #[cfg(not(debug_assertions))] - { - ReprFormatter::new("Expr").pair("", "...").finish() - } - - #[cfg(debug_assertions)] - { - ReprFormatter::new("Expr").debug("", &self.0).finish() - } - } -} diff --git a/src/common/expression/func.rs b/src/common/expression/func.rs deleted file mode 100644 index 5cd209e..0000000 --- a/src/common/expression/func.rs +++ /dev/null @@ -1,331 +0,0 @@ -use pyo3::types::PyTupleMethods; -use sea_query::IntoIden; - -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, RefBoundObject}; - -crate::implement_pyclass! { - // NOTE: SQLTypes, PyExpr, PyFunc, PyTableName & PyColumnRef could never mark as subclass. - // these should be immutable and final types. - - /// Represents a SQL function call that can be used in expressions. - /// - /// This class provides a type-safe way to construct SQL function calls - /// with proper argument handling and database dialect support. - #[derive(Debug, Clone)] - [] PyFunc as "Func" (pub sea_query::FunctionCall); -} - -#[pyo3::pymethods] -impl PyFunc { - #[new] - #[pyo3(signature=(name, *args))] - pub fn __new__(name: String, args: BoundArgs<'_>) -> pyo3::PyResult { - let mut function_call = sea_query::Func::cust(sea_query::Alias::new(name)); - - for item in args.iter() { - let expr = super::expr::PyExpr::try_from(&item)?; - function_call = function_call.arg(expr.0); - } - - Ok(Self(function_call)) - } - - /// Create a NOW() function call. - #[classmethod] - fn now(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Func::cust(sea_query::Alias::new("NOW"))) - } - - /// Create a SUM(expr) function call. - #[classmethod] - fn sum( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::sum(expr.0))) - } - - /// Create a MIN(expr) function call. - #[classmethod] - fn min( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::min(expr.0))) - } - - /// Create a MAX(expr) function call. - #[classmethod] - fn max( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::max(expr.0))) - } - - /// Create a ABS(expr) function call. - #[classmethod] - fn abs( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::abs(expr.0))) - } - - /// Create a AVG(expr) function call. - #[classmethod] - fn avg( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::avg(expr.0))) - } - - /// Create a COUNT(expr) function call. - #[classmethod] - fn count( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::count(expr.0))) - } - - /// Create a COUNT(DISTINCT expr) function call. - #[classmethod] - fn count_distinct( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::count_distinct(expr.0))) - } - - /// Create a IF_NULL(a, b) function call. - #[classmethod] - fn if_null( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - a: RefBoundObject<'_>, - b: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let a = super::expr::PyExpr::try_from(a)?; - let b = super::expr::PyExpr::try_from(b)?; - - Ok(Self(sea_query::Func::if_null(a.0, b.0))) - } - - /// Create a GREATEST function call. - #[classmethod] - #[pyo3(signature=(*exprs))] - fn greatest( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - exprs: BoundArgs<'_>, - ) -> pyo3::PyResult { - let mut simple_exprs = Vec::with_capacity(exprs.len()); - - for expr in exprs.iter() { - let expr = super::expr::PyExpr::try_from(&expr)?; - simple_exprs.push(expr.0); - } - - Ok(Self(sea_query::Func::greatest(simple_exprs))) - } - - /// Create a LEAST function call. - #[classmethod] - #[pyo3(signature=(*exprs))] - fn least( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - exprs: BoundArgs<'_>, - ) -> pyo3::PyResult { - let mut simple_exprs = Vec::with_capacity(exprs.len()); - - for expr in exprs.iter() { - let expr = super::expr::PyExpr::try_from(&expr)?; - simple_exprs.push(expr.0); - } - - Ok(Self(sea_query::Func::least(simple_exprs))) - } - - /// Create a CHAR_LENGTH(expr) function call. - #[classmethod] - fn char_length( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::char_length(expr.0))) - } - - /// Create a COALESCE function call. - #[classmethod] - #[pyo3(signature=(*exprs))] - fn coalesce( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - exprs: BoundArgs<'_>, - ) -> pyo3::PyResult { - let mut simple_exprs = Vec::with_capacity(exprs.len()); - - for expr in exprs.iter() { - let expr = super::expr::PyExpr::try_from(&expr)?; - simple_exprs.push(expr.0); - } - - Ok(Self(sea_query::Func::coalesce(simple_exprs))) - } - - /// Create a LOWER(expr) function call. - #[classmethod] - fn lower( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::lower(expr.0))) - } - - /// Create a UPPER(expr) function call. - #[classmethod] - fn upper( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::upper(expr.0))) - } - - /// Create a BIT_AND(expr) function call - this is not supported on SQLite. - #[classmethod] - fn bit_and( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::bit_and(expr.0))) - } - - /// Create a BIT_OR(expr) function call - this is not supported on SQLite. - #[classmethod] - fn bit_or( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::bit_or(expr.0))) - } - - /// Create a RANDOM() function call. - #[classmethod] - fn random(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Func::random()) - } - - /// Create a RANK() function call. - #[classmethod] - fn rank(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Func::cust("RANK")) - } - - /// Create a DENSE_RANK() function call. - #[classmethod] - fn dense_rank(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Func::cust("DENSE_RANK")) - } - - /// Create a PERCENT_RANK() function call. - #[classmethod] - fn percent_rank(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Func::cust("PERCENT_RANK")) - } - - /// Create a ROUND(expr) function call. - #[classmethod] - fn round( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::round(expr.0))) - } - - /// Create a ROUND(a, b) function call. - #[classmethod] - fn round_with_precision( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - a: RefBoundObject<'_>, - b: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let a = super::expr::PyExpr::try_from(a)?; - let b = super::expr::PyExpr::try_from(b)?; - - Ok(Self(sea_query::Func::round_with_precision(a.0, b.0))) - } - - /// Create a MD5(expr) function call - this is only available in Postgres and MySQL. - #[classmethod] - fn md5( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::md5(expr.0))) - } - - #[classmethod] - fn cast_as( - _cls: &pyo3::Bound<'_, pyo3::types::PyType>, - expr: RefBoundObject<'_>, - alias: String, - ) -> pyo3::PyResult { - let expr = super::expr::PyExpr::try_from(expr)?; - Ok(Self(sea_query::Func::cast_as( - expr.0, - sea_query::Alias::new(alias).into_iden(), - ))) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = self.to_expr(); - - let state = crate::query::select::SelectLabelState { - expr, - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self) -> super::expr::PyExpr { - super::expr::PyExpr(sea_query::SimpleExpr::FunctionCall(self.0.clone())) - } - - pub fn __repr__(&self) -> String { - #[cfg(not(debug_assertions))] - { - ReprFormatter::new("Func").pair("", "...").finish() - } - - #[cfg(debug_assertions)] - { - ReprFormatter::new("Func").debug("", &self.0).finish() - } - } -} diff --git a/src/common/expression/mod.rs b/src/common/expression/mod.rs deleted file mode 100644 index 82c61f1..0000000 --- a/src/common/expression/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -mod expr; -mod func; - -pub use expr::*; -pub use func::*; - -use crate::internal::{BoundArgs, PyObject}; - -/// Create a logical AND condition that is true only if all conditions are true. -/// -/// This is equivalent to SQL's AND operator applied to multiple expressions. -/// -/// @signature (arg1: Expr, *args: Expr) -> Expr -#[pyo3::pyfunction] -#[pyo3(signature=(arg1, *args))] -pub fn all(arg1: pyo3::Bound<'_, PyExpr>, args: BoundArgs<'_>) -> pyo3::PyResult { - let py = arg1.py(); - let mut expr = arg1.unbind(); - - for m in args { - let m = m.cast_into_exact::()?; - - let result = sea_query::ExprTrait::and(expr.get().0.clone(), m.get().0.clone()); - expr = pyo3::Py::new(py, PyExpr(result))?; - } - - Ok(expr.into_any()) -} - -/// Create a logical OR condition that is true if any condition is true. -/// -/// This is equivalent to SQL's OR operator applied to multiple expressions. -/// -/// @signature (arg1: Expr, *args: Expr) -> Expr -#[pyo3::pyfunction] -#[pyo3(signature=(arg1, *args))] -pub fn any(arg1: pyo3::Bound<'_, PyExpr>, args: BoundArgs<'_>) -> pyo3::PyResult { - let py = arg1.py(); - let mut expr = arg1.unbind(); - - for m in args { - let m = m.cast_into_exact::()?; - - let result = sea_query::ExprTrait::or(expr.get().0.clone(), m.get().0.clone()); - expr = pyo3::Py::new(py, PyExpr(result))?; - } - - Ok(expr.into_any()) -} - -/// Create a logical NOT. -/// -/// @signature (arg: Expr) -> Expr -#[pyo3::pyfunction] -pub fn not_(arg: &pyo3::Bound<'_, PyExpr>) -> PyExpr { - sea_query::ExprTrait::not(arg.get().0.clone()).into() -} diff --git a/src/common/foreign_key.rs b/src/common/foreign_key.rs deleted file mode 100644 index ba72806..0000000 --- a/src/common/foreign_key.rs +++ /dev/null @@ -1,434 +0,0 @@ -use super::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, RefBoundObject, ToSeaQuery}; - -#[inline] -fn map_str_to_foreign_key_action(value: String) -> pyo3::PyResult { - match value.to_lowercase().as_str() { - "cascade" => Ok(sea_query::ForeignKeyAction::Cascade), - "no action" => Ok(sea_query::ForeignKeyAction::NoAction), - "restrict" => Ok(sea_query::ForeignKeyAction::Restrict), - "set default" => Ok(sea_query::ForeignKeyAction::SetDefault), - "set null" => Ok(sea_query::ForeignKeyAction::SetNull), - _ => Err(pyo3::exceptions::PyValueError::new_err(format!( - "unknown foreign key action: {value}", - ))), - } -} - -#[inline] -fn map_foreign_key_action_to_str(value: sea_query::ForeignKeyAction) -> String { - match value { - sea_query::ForeignKeyAction::Cascade => String::from("CASCADE"), - sea_query::ForeignKeyAction::NoAction => String::from("NO ACTION"), - sea_query::ForeignKeyAction::Restrict => String::from("RESTRICT"), - sea_query::ForeignKeyAction::SetDefault => String::from("SET DEFAULT"), - sea_query::ForeignKeyAction::SetNull => String::from("SET NULL"), - } -} - -crate::implement_pyclass! { - /// Specifies a foreign key relationship between tables. - /// - /// Defines referential integrity constraints including: - /// - Source columns (in the child table) - /// - Target columns (in the parent table) - /// - Actions for updates and deletes (CASCADE, RESTRICT, SET NULL, etc.) - /// - Optional naming for the constraint - /// - /// Foreign keys ensure data consistency by requiring that values in the - /// child table's columns match existing values in the parent table's columns. - #[derive(Debug, Clone)] - mutable [subclass] PyForeignKey(ForeignKeyState) as "ForeignKey" { - /// Foreign key constraint name - pub name: Option, - - /// To table - pub to_table: PyTableName, - - /// To columns - pub to_columns: Vec, - - // /// From table - // pub from_table: Option, - - /// From columns - pub from_columns: Vec, - - /// On delete action - pub on_delete: Option, - - /// On update action - pub on_update: Option, - } -} - -impl ToSeaQuery for ForeignKeyState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::ForeignKeyCreateStatement { - let mut stmt = sea_query::ForeignKeyCreateStatement::new(); - stmt.to_tbl(self.to_table.clone()); - - if let Some(x) = &self.name { - stmt.name(x); - } - - for c in &self.from_columns { - stmt.from_col(sea_query::Alias::new(c)); - } - for c in &self.to_columns { - stmt.to_col(sea_query::Alias::new(c)); - } - - if let Some(x) = self.on_delete { - stmt.on_delete(x); - } - if let Some(x) = self.on_update { - stmt.on_update(x); - } - - stmt - } -} - -impl ToSeaQuery for ForeignKeyState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::TableForeignKey { - let mut stmt = sea_query::TableForeignKey::new(); - stmt.to_tbl(self.to_table.clone()); - - if let Some(x) = &self.name { - stmt.name(x); - } - - for c in &self.from_columns { - stmt.from_col(sea_query::Alias::new(c)); - } - for c in &self.to_columns { - stmt.to_col(sea_query::Alias::new(c)); - } - - if let Some(x) = self.on_delete { - stmt.on_delete(x); - } - if let Some(x) = self.on_update { - stmt.on_update(x); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyForeignKey { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[ - pyo3( - signature=( - from_columns, - to_columns, - to_table=None, - name=None, - *, - on_delete=None, - on_update=None - ) - ) - ] - fn __init__( - &self, - from_columns: Vec>, - to_columns: Vec>, - to_table: Option>, - name: Option, - on_delete: Option, - on_update: Option, - ) -> pyo3::PyResult<()> { - // Validate & convert actions - let on_delete = match on_delete { - None => None, - Some(x) => Some(map_str_to_foreign_key_action(x)?), - }; - let on_update = match on_update { - None => None, - Some(x) => Some(map_str_to_foreign_key_action(x)?), - }; - - let mut to_table = match to_table { - Some(x) => Some(PyTableName::try_from(x)?), - None => None, - }; - - // Validate from/to_columns - if from_columns.is_empty() { - return crate::new_error!(PyValueError, "from_columns cannot be empty."); - } - if to_columns.is_empty() { - return crate::new_error!(PyValueError, "to_columns cannot be empty."); - } - if from_columns.len() != to_columns.len() { - return crate::new_error!( - PyValueError, - "from_columns and to_columns must have same length ({} != {})", - from_columns.len(), - to_columns.len() - ); - } - - // Convert from_columns - let mut from_columns_str = Vec::with_capacity(from_columns.len()); - - for col in from_columns.into_iter() { - let col_ref = super::column_ref::PyColumnRef::try_from(&col)?; - - match col_ref.name { - Some(x) => from_columns_str.push(x.to_string()), - None => { - return crate::new_error!( - PyValueError, - "ForeignKey cannot accept asterisk '*' as column" - ); - } - } - } - - // Convert to_columns - let mut to_columns_str = Vec::with_capacity(to_columns.len()); - - for col in to_columns.into_iter() { - let col_ref = super::column_ref::PyColumnRef::try_from(&col)?; - - match &col_ref.name { - Some(x) => to_columns_str.push(x.to_string()), - None => { - return crate::new_error!( - PyValueError, - "ForeignKey cannot accept asterisk '*' as column" - ); - } - } - - if let Some(detected_table_name) = col_ref.table { - let detected_table_name = PyTableName { - name: detected_table_name, - schema: col_ref.schema, - database: None, - alias: None, - }; - - if let Some(last) = &to_table { - if last.ne(&detected_table_name) { - return crate::new_error!(PyValueError, "multiple target table found"); - } - } - - to_table = Some(detected_table_name); - } - } - - if to_table.is_none() { - return crate::new_error!( - PyValueError, - "No target table found. You should specify the target table with `to_table`, or \ - `to_columns` parameter." - ); - } - let to_table = unsafe { to_table.unwrap_unchecked() }; - - // let name = match name { - // Some(x) => x, - // None => { - // let mut s = format!("fk_{}", to_table.name.to_string()); - - // for col in from_columns_str.iter() { - // s.push('_'); - // s += col; - // } - - // s.push('_'); - - // for col in to_columns_str.iter() { - // s.push('_'); - // s += col; - // } - - // s - // } - // }; - - let result = ForeignKeyState { - name, - to_table, - to_columns: to_columns_str, - from_columns: from_columns_str, - on_delete, - on_update, - }; - self.0.set(result); - Ok(()) - } - - /// Foreign key constraint name - #[getter] - fn name(&self) -> Option { - self.0.lock().name.clone() - } - - #[setter] - fn set_name(&self, val: Option) { - let mut lock = self.0.lock(); - lock.name = val; - } - - /// Referencing table. - #[getter] - fn to_table(&self) -> PyTableName { - self.0.lock().to_table.clone() - } - - #[setter] - fn set_to_table(&self, value: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - lock.to_table = super::table_ref::PyTableName::try_from(value)?; - Ok(()) - } - - /// Key columns. - #[getter] - #[allow(clippy::wrong_self_convention)] - fn from_columns(&self) -> Vec { - self.0.lock().from_columns.clone() - } - - #[setter] - fn set_from_columns(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - - if val.is_empty() { - return Err(pyo3::exceptions::PyValueError::new_err( - "from_columns is empty", - )); - } - if val.len() != lock.to_columns.len() { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "from_columns and to_columns must have same length ({} != {})", - val.len(), - lock.to_columns.len() - ))); - } - - let mut from_columns_str = Vec::with_capacity(val.len()); - for col in val.into_iter() { - let col_ref = super::column_ref::PyColumnRef::try_from(&col)?; - - match col_ref.name { - Some(x) => from_columns_str.push(x.to_string()), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "ForeignKey cannot accept asterisk '*' as column", - )) - } - } - } - - lock.from_columns = from_columns_str; - Ok(()) - } - - /// Referencing columns. - #[getter] - fn to_columns(&self) -> Vec { - self.0.lock().to_columns.clone() - } - - #[setter] - fn set_to_columns(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - - if val.is_empty() { - return Err(pyo3::exceptions::PyValueError::new_err( - "to_columns is empty", - )); - } - if val.len() != lock.from_columns.len() { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "to_columns and to_columns must have same length ({} != {})", - val.len(), - lock.to_columns.len() - ))); - } - - let mut to_columns_str = Vec::with_capacity(val.len()); - for col in val.into_iter() { - let col_ref = super::column_ref::PyColumnRef::try_from(&col)?; - - match col_ref.name { - Some(x) => to_columns_str.push(x.to_string()), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "ForeignKey cannot accept asterisk '*' as column", - )) - } - } - } - - lock.to_columns = to_columns_str; - Ok(()) - } - - /// ON DELETE action. - #[getter] - fn on_delete(&self) -> Option { - self.0.lock().on_delete.map(map_foreign_key_action_to_str) - } - - #[setter] - fn set_on_delete(&self, value: Option) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - lock.on_delete = match value { - None => None, - Some(x) => Some(map_str_to_foreign_key_action(x)?), - }; - Ok(()) - } - - /// ON UPDATE action. - #[getter] - fn on_update(&self) -> Option { - self.0.lock().on_update.map(map_foreign_key_action_to_str) - } - - #[setter] - fn set_on_update(&self, value: Option) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - lock.on_update = match value { - None => None, - Some(x) => Some(map_str_to_foreign_key_action(x)?), - }; - Ok(()) - } - - fn __copy__(&self) -> Self { - let lock = self.0.lock(); - lock.clone().into() - } - - pub fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .optional_quote("name", lock.name.as_ref()) - .map("to_table", &lock.to_table, |x| x.__repr__()) - .debug("to_columns", &lock.to_columns) - .debug("from_columns", &lock.from_columns) - .optional_map("on_delete", lock.on_delete, map_foreign_key_action_to_str) - .optional_map("on_update", lock.on_update, map_foreign_key_action_to_str) - .finish() - } -} diff --git a/src/common/mod.rs b/src/common/mod.rs deleted file mode 100644 index 0789a7b..0000000 --- a/src/common/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub mod column; -pub mod column_ref; -pub mod expression; -pub mod foreign_key; -pub mod table_ref; -pub mod value; - -#[pyo3::pymodule(name = "common")] -pub mod common_module { - #[pymodule_export] - use super::value::PyValue; - - #[pymodule_export] - use super::column_ref::PyColumnRef; - - #[pymodule_export] - use super::table_ref::PyTableName; - - #[pymodule_export] - use super::expression::all; - #[pymodule_export] - use super::expression::any; - #[pymodule_export] - use super::expression::not_; - #[pymodule_export] - use super::expression::PyExpr; - #[pymodule_export] - use super::expression::PyFunc; - - #[pymodule_export] - use super::column::PyColumn; - - #[pymodule_export] - use super::foreign_key::PyForeignKey; -} diff --git a/src/common/table_ref.rs b/src/common/table_ref.rs deleted file mode 100644 index ec0ee9f..0000000 --- a/src/common/table_ref.rs +++ /dev/null @@ -1,319 +0,0 @@ -use pyo3::types::PyAnyMethods; -use sea_query::IntoIden; -use std::str::FromStr; - -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundKwargs, RefBoundObject}; - -crate::implement_pyclass! { - // NOTE: SQLTypes, PyExpr, PyFunc, PyTableName & PyColumnRef could never mark as subclass. - // these should be immutable and final types. - - /// Represents a table name reference with optional schema, database, and alias. - /// - /// This class encapsulates a table name that can include: - /// - The base table name - /// - Optional schema/namespace qualification - /// - Optional database qualification (for systems that support it) - /// - /// The class provides parsing capabilities for string representations - /// and supports comparison operations. - /// - /// NOTE: this class is immutable and frozen. - #[derive(Debug, Clone, PartialEq)] - [] PyTableName as "TableName" { - pub name: sea_query::DynIden, - pub schema: Option, - pub database: Option, - pub alias: Option, - } -} - -impl sea_query::IntoTableRef for PyTableName { - fn into_table_ref(self) -> sea_query::TableRef { - match (self.schema, self.database, self.alias) { - (Some(schema), Some(database), Some(alias)) => { - sea_query::TableRef::DatabaseSchemaTableAlias(database, schema, self.name, alias) - } - (Some(schema), None, Some(alias)) => { - sea_query::TableRef::SchemaTableAlias(schema, self.name, alias) - } - (Some(schema), Some(database), None) => { - sea_query::TableRef::DatabaseSchemaTable(database, schema, self.name) - } - (Some(schema), None, None) => sea_query::TableRef::SchemaTable(schema, self.name), - (None, None, Some(alias)) => sea_query::TableRef::TableAlias(self.name, alias), - _ => sea_query::TableRef::Table(self.name), - } - } -} - -impl TryFrom> for PyTableName { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject<'_>) -> Result { - unsafe { - if pyo3::ffi::Py_TYPE(value.as_ptr()) == crate::typeref::TABLE_NAME_TYPE { - let casted_value = value.cast_unchecked::(); - return Ok(casted_value.get().clone()); - } - - if let Ok(x) = value.extract::() { - return Self::from_str(&x); - } - - if let Some(result) = Self::try_from_property(value)? { - return Ok(result); - } - - crate::new_error!( - PyTypeError, - "expected TableName or str or object.__table_name__ property, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ) - } - } -} - -impl FromStr for PyTableName { - type Err = pyo3::PyErr; - - fn from_str(s: &str) -> Result { - let s = s.trim(); - if s.is_empty() { - return Err(pyo3::PyErr::new::( - "cannot parse an empty string", - )); - } - - // Possible formats: - // name - // schema.name - // database.schema.name - // database.schema.name AS alias - let mut s: Vec = s.split('.').map(String::from).collect(); - - if s.len() > 3 { - return Err(pyo3::PyErr::new::( - "invalid format", - )); - } - - let name = s.pop().unwrap(); - let schema = s.pop().map(|x| sea_query::Alias::new(x).into_iden()); - let database = s.pop().map(|x| sea_query::Alias::new(x).into_iden()); - - let (name, alias) = match name.to_ascii_lowercase().find("as") { - None => (name, None), - Some(position) => ( - name[..position].trim().to_string(), - Some(name[position + 2..].trim().to_string()), - ), - }; - - Ok(Self { - name: sea_query::Alias::new(name).into_iden(), - schema, - database, - alias: alias.map(|x| sea_query::Alias::new(x).into_iden()), - }) - } -} - -impl PyTableName { - #[inline] - pub fn try_from_property(value: RefBoundObject) -> pyo3::PyResult> { - const PROPERTY_NAME: &std::ffi::CStr = c"__table_name__"; - - let property = match value.getattr(PROPERTY_NAME) { - Ok(x) => Ok(Some(x)), - Err(err) if err.is_instance_of::(value.py()) => { - Ok(None) - } - Err(err) => Err(err), - }; - let property = property?; - - if property.is_none() { - return Ok(None); - } - - unsafe { - let property = property.unwrap_unchecked(); - - if pyo3::ffi::Py_TYPE(property.as_ptr()) == crate::typeref::TABLE_NAME_TYPE { - let casted_property = property.cast_unchecked::(); - return Ok(Some(casted_property.get().clone())); - } - - if let Ok(x) = property.extract::() { - return Self::from_str(&x).map(Some); - } - - crate::new_error!( - PyTypeError, - "__table_name__ property returns something other than TableName or str; returns {}", - crate::internal::get_type_name(property.py(), property.as_ptr()) - ) - } - } -} - -#[pyo3::pymethods] -impl PyTableName { - #[new] - #[pyo3(signature=(name, schema=None, database=None, alias=None))] - fn new( - name: String, - schema: Option, - database: Option, - alias: Option, - ) -> Self { - Self { - name: sea_query::Alias::new(name).into_iden(), - schema: schema.map(|x| sea_query::Alias::new(x).into_iden()), - database: database.map(|x| sea_query::Alias::new(x).into_iden()), - alias: alias.map(|x| sea_query::Alias::new(x).into_iden()), - } - } - - /// Parse a string representation of a table name. - /// - /// Supports formats like: - /// - "table_name" - /// - "schema.table_name" - /// - "database.schema.table_name" - /// - "database.schema.table_name as alias" - #[classmethod] - fn parse(_cls: &pyo3::Bound<'_, pyo3::types::PyType>, string: String) -> pyo3::PyResult { - Self::from_str(&string) - } - - #[getter] - fn name(&self) -> String { - self.name.to_string() - } - - #[getter] - fn schema(&self) -> Option { - self.schema.as_ref().map(|x| x.to_string()) - } - - #[getter] - fn database(&self) -> Option { - self.database.as_ref().map(|x| x.to_string()) - } - - #[getter] - fn alias(&self) -> Option { - self.alias.as_ref().map(|x| x.to_string()) - } - - /// Create a shallow copy of this TableName. - #[pyo3(signature=(**kwds))] - fn copy_with(&self, kwds: Option>) -> pyo3::PyResult { - use pyo3::types::PyDictMethods; - - let mut cloned = self.clone(); - if kwds.is_none() { - return Ok(cloned); - } - - let kwds = unsafe { kwds.unwrap_unchecked() }; - - for (key, val) in kwds.iter() { - #[cfg(debug_assertions)] - let key = key.extract::().unwrap(); - - #[cfg(not(debug_assertions))] - let key = unsafe { key.extract::().unwrap_unchecked() }; - - // All of values are Option - let val = unsafe { - if pyo3::ffi::Py_IsNone(val.as_ptr()) == 1 { - None - } else if pyo3::ffi::PyUnicode_CheckExact(val.as_ptr()) == 1 { - Some(val.extract::().unwrap_unchecked()) - } else { - return crate::new_error!( - PyTypeError, - "expected str or None, got {}", - crate::internal::get_type_name(val.py(), val.as_ptr()) - ); - } - }; - - if key == "name" { - if let Some(x) = val { - // Ignore name=None - cloned.name = sea_query::Alias::new(x).into_iden(); - } - } else if key == "database" { - cloned.database = val.map(|x| sea_query::Alias::new(x).into_iden()); - } else if key == "schema" { - cloned.schema = val.map(|x| sea_query::Alias::new(x).into_iden()); - } else if key == "alias" { - cloned.alias = val.map(|x| sea_query::Alias::new(x).into_iden()); - } else { - return crate::new_error!( - PyTypeError, - "got an unexpected keyword argument '{}'", - key - ); - } - } - - Ok(cloned) - } - - fn __eq__(slf: pyo3::PyRef<'_, Self>, other: &pyo3::Bound<'_, Self>) -> pyo3::PyResult { - if slf.as_ptr() == other.as_ptr() { - return Ok(true); - } - - let other = other.get(); - Ok(slf.eq(other)) - } - - fn __ne__(slf: pyo3::PyRef<'_, Self>, other: &pyo3::Bound<'_, Self>) -> pyo3::PyResult { - if slf.as_ptr() == other.as_ptr() { - return Ok(false); - } - - let other = other.get(); - Ok(slf.ne(other)) - } - - fn __copy__(&self) -> Self { - self.clone() - } - - fn __hash__(&self) -> u64 { - use std::hash::{Hash, Hasher}; - - let mut state = std::hash::DefaultHasher::new(); - - self.name.to_string().hash(&mut state); - - if let Some(x) = &self.database { - x.to_string().hash(&mut state); - } - if let Some(x) = &self.schema { - x.to_string().hash(&mut state); - } - if let Some(x) = &self.alias { - x.to_string().hash(&mut state); - } - - state.finish() - } - - pub fn __repr__(&self) -> String { - ReprFormatter::new("TableName") - .iden("name", &self.name) - .optional_iden("schema", self.schema.as_ref()) - .optional_iden("database", self.database.as_ref()) - .optional_iden("alias", self.alias.as_ref()) - .finish() - } -} diff --git a/src/common/value.rs b/src/common/value.rs deleted file mode 100644 index 450f38c..0000000 --- a/src/common/value.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::internal::repr::ReprFormatter; -use crate::internal::type_engine::TypeEngine; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject}; -use crate::sqltypes::SQLTypeTrait; - -crate::implement_pyclass! { - /// Bridges Python types, Rust types, and SQL types for seamless data conversion. - /// - /// This class handles validation, adaptation, and conversion between different - /// type systems used in the application stack. - /// - /// It can automatically detects the type of your value and selects appropriate Rust and SQL types. - /// For example: - /// - Python `int` becomes `BIGINT` SQL type (`BigIntegerType`) - /// - Python `dict` or `list` becomes `JSON` SQL type (`JsonType`) - /// - Python `float` becomes `DOUBLE` SQL type (`DoubleType`) - /// - /// However, for more accurate type selection, it's recommended to use the `sql_type` parameter. - /// - /// NOTE: this class is immutable and frozen. - mutable [subclass] PyValue(ValueState) as "Value" { - sql_type: TypeEngine, - serialized: Option, - deserialized: Option, - } -} - -impl ValueState { - #[inline(always)] - pub unsafe fn new_unchecked( - sql_type: TypeEngine, - serialized: Option, - deserialized: Option, - ) -> Self { - Self { - sql_type, - serialized, - deserialized, - } - } - - pub fn from_pyobject(sql_type: TypeEngine, object: BoundObject<'_>) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::Py_IsNone(object.as_ptr()) == 0 { - sql_type.validate(object.py(), object.as_ptr())?; - } else { - // There's no need to do validation. - // The `ValueState` will handle `NoneType`s itself - } - - Ok(Self::new_unchecked(sql_type, None, Some(object.unbind()))) - } - } - - pub fn from_sea_query_value(py: pyo3::Python, value: sea_query::Value) -> Self { - let sql_type = TypeEngine::infer_value(py, &value); - - unsafe { Self::new_unchecked(sql_type, Some(value), None) } - } - - pub fn simple_expr(&mut self, py: pyo3::Python) -> pyo3::PyResult { - if let Some(x) = &self.serialized { - return Ok(sea_query::SimpleExpr::Value(x.clone())); - } - - let result = unsafe { - let deserialized = self.deserialized.as_ref().unwrap_unchecked(); - - if pyo3::ffi::Py_IsNone(deserialized.as_ptr()) == 1 { - sea_query::Value::Bool(None) - } else { - self.sql_type.serialize(py, deserialized.as_ptr())? - } - }; - - self.serialized = Some(result.clone()); - Ok(sea_query::SimpleExpr::Value(result)) - } - - pub fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - sql_type: self.sql_type.clone(), - serialized: self.serialized.clone(), - deserialized: self.deserialized.as_ref().map(|x| x.clone_ref(py)), - } - } -} - -#[pyo3::pymethods] -impl PyValue { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[pyo3(signature=(value, sql_type=None))] - fn __init__( - &self, - value: BoundObject<'_>, - sql_type: Option>, - ) -> pyo3::PyResult<()> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::VALUE_TYPE) == 1 { - let py = value.py(); - - let casted_value = value.cast_into_unchecked::(); - let state = casted_value.get().0.lock().clone_ref(py); - - self.0.set(state); - return Ok(()); - } - } - - let type_engine = { - if let Some(sql_type) = sql_type { - TypeEngine::new(&sql_type)? - } else { - TypeEngine::infer_pyobject(&value)? - } - }; - - let result = ValueState::from_pyobject(type_engine, value)?; - self.0.set(result); - Ok(()) - } - - #[getter] - fn sql_type<'a>(&self, py: pyo3::Python<'a>) -> BoundObject<'a> { - let lock = self.0.lock(); - lock.sql_type.as_pyobject(py) - } - - /// Converts the adapted value back to a Python type. - #[getter] - fn value<'py>(&self, py: pyo3::Python<'py>) -> pyo3::PyResult> { - let mut lock = self.0.lock(); - - if let Some(x) = &lock.deserialized { - return Ok(x.bind(py).clone()); - } - - let deserialized = unsafe { - lock.sql_type - .deserialize(py, lock.serialized.as_ref().unwrap()) - .map(|x| pyo3::Bound::from_owned_ptr(py, x))? - }; - lock.deserialized = Some(deserialized.clone().unbind()); - - Ok(deserialized) - } - - fn __hash__(&self, py: pyo3::Python) -> pyo3::PyResult { - self.value(py) - .map(|x| unsafe { pyo3::ffi::PyObject_Hash(x.as_ptr()) }) - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self, py: pyo3::Python) -> pyo3::PyResult { - let mut lock = self.0.lock(); - lock.simple_expr(py).map(super::expression::PyExpr) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { - let value = slf.value(slf.py())?; - - Ok(ReprFormatter::new_with_pyref(&slf) - .debug("", &value) - .finish()) - } -} diff --git a/src/data_type/mod.rs b/src/data_type/mod.rs new file mode 100644 index 0000000..9d6b5fb --- /dev/null +++ b/src/data_type/mod.rs @@ -0,0 +1,198 @@ +use crate::internal::sql; + +#[cfg(test)] +mod tests; + +/// SQL type name, interned when it is one of the known spellings. +/// +/// Known names fold to their traditional uppercase form. Unknown names and +/// parameterized names such as `varchar(16)` keep the caller's spelling. +/// +/// ``` +/// use rapidquery::data_type::DataTypeName; +/// +/// assert_eq!(DataTypeName::from("varchar").as_str(), "VARCHAR"); +/// assert_eq!(DataTypeName::from("varchar(16)").as_str(), "varchar(16)"); +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[repr(transparent)] +pub struct DataTypeName(arcstr::ArcStr); + +impl DataTypeName { + #[inline] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +// Longest interned name is `"timestamp without time zone"` (27 bytes). +const LOOKUP_BUF: usize = 32; + +fn interned(value: &str) -> Option { + if value.len() > LOOKUP_BUF || !value.is_ascii() { + return None; + } + let mut buf = [0u8; LOOKUP_BUF]; + for (i, b) in value.bytes().enumerate() { + buf[i] = b.to_ascii_lowercase(); + } + // SAFETY: `value` is ASCII, so the lowercased copy is valid UTF-8. + let lower = unsafe { std::str::from_utf8_unchecked(&buf[..value.len()]) }; + lookup_alias(lower) +} + +fn custom_name>(value: T) -> DataTypeName { + DataTypeName(value.into()) +} + +// Interned no-arg types. `arcstr::literal!` shares one allocation per spelling. +macro_rules! simple_data_types { + ( + $( + $(#[$docs:meta])* + ($upcase:ident => $realname:literal, $($possible_names:literal)|+); + )+ + ) => { + $( + $(#[$docs])* + pub(super) const $upcase: DataTypeName = DataTypeName(arcstr::literal!($realname)); + )+ + + fn lookup_alias(lowercase: &str) -> Option { + match lowercase { + $( + $($possible_names)|+ => Some($upcase), + )+ + _ => None, + } + } + }; +} + +simple_data_types! { + // --- String --- + (CHARACTER_DATA_TYPE => "CHARACTER", "character"); + (CHAR_DATA_TYPE => "CHAR", "char"); + (CHARACTER_VARYING_DATA_TYPE => "CHARACTER VARYING", "character varying"); + (CHAR_VARYING_DATA_TYPE => "CHAR VARYING", "char varying"); + (VARCHAR_DATA_TYPE => "VARCHAR", "varchar"); + (NVARCHAR_DATA_TYPE => "NVARCHAR", "nvarchar"); + (TEXT_DATA_TYPE => "TEXT", "text"); + + // --- Uuid --- + (UUID_DATA_TYPE => "UUID", "uuid"); + + // --- Bytes --- + (BYTES_DATA_TYPE => "BYTES", "bytes"); + (BINARY_DATA_TYPE => "BINARY", "binary"); + (VARBINARY_DATA_TYPE => "VARBINARY", "varbinary"); + (BLOB_DATA_TYPE => "BLOB", "blob"); + (BYTEA_DATA_TYPE => "BYTEA", "bytea"); + + // --- Bit --- + (BIT_DATA_TYPE => "BIT", "bit"); + (VARBIT_DATA_TYPE => "VARBIT", "varbit"); + (BIT_VARYING_DATA_TYPE => "BIT VARYING", "bit varying"); + + // --- Float (includes decimal/numeric) --- + (NUMERIC_DATA_TYPE => "NUMERIC", "numeric"); + (BIGNUMERIC_DATA_TYPE => "BIGNUMERIC", "bignumeric"); + (DECIMAL_DATA_TYPE => "DECIMAL", "decimal"); + (BIGDECIMAL_DATA_TYPE => "BIGDECIMAL", "bigdecimal"); + (FLOAT_DATA_TYPE => "FLOAT", "float"); + (REAL_DATA_TYPE => "REAL", "real"); + (DOUBLE_DATA_TYPE => "DOUBLE", "double"); + (DOUBLE_PRECISION_DATA_TYPE => "DOUBLE PRECISION", "double precision"); + + // --- Integer --- + (TINYINT_DATA_TYPE => "TINYINT", "tinyint"); + (SMALLINT_DATA_TYPE => "SMALLINT", "smallint"); + (INT_DATA_TYPE => "INT", "int"); + (INTEGER_DATA_TYPE => "INTEGER", "integer"); + (BIGINT_DATA_TYPE => "BIGINT", "bigint"); + + // --- Boolean --- + (BOOLEAN_DATA_TYPE => "BOOLEAN", "boolean" | "bool"); + + // --- Date --- + (DATE_DATA_TYPE => "DATE", "date"); + + // --- Time --- + (TIME_DATA_TYPE => "TIME", "time" | "time without timezone" | "time without time zone"); + (TIME_WITH_TIMEZONE_DATA_TYPE => "TIME WITH TIMEZONE", "time with timezone"); + (TIME_WITH_TIME_ZONE_DATA_TYPE => "TIME WITH TIME ZONE", "time with time zone"); + + // --- Datetime --- + (DATETIME_DATA_TYPE => "DATETIME", "datetime"); + (TIMESTAMP_DATA_TYPE => "TIMESTAMP", "timestamp" | "timestamp without timezone" | "timestamp without time zone"); + (TIMESTAMP_WITH_TIMEZONE_DATA_TYPE => "TIMESTAMP WITH TIMEZONE", "timestamp with timezone"); + (TIMESTAMP_WITH_TIME_ZONE_DATA_TYPE => "TIMESTAMP WITH TIME ZONE", "timestamp with time zone"); + (TIMESTAMPTZ_DATA_TYPE => "TIMESTAMPTZ", "timestamptz"); + + // --- Json --- + (JSON_DATA_TYPE => "JSON", "json"); + (JSONB_DATA_TYPE => "JSONB", "jsonb"); + + // --- Inet --- + (INET_DATA_TYPE => "INET", "inet"); + + // --- MacAddress --- + (MACADDRESS_DATA_TYPE => "MACADDRESS", "macaddress" | "macaddr"); +} + +fn is_parameterized(value: &str) -> bool { + value.as_bytes().contains(&b'(') +} + +impl From<&str> for DataTypeName { + fn from(value: &str) -> Self { + if is_parameterized(value) { + return custom_name(value); + } + interned(value).unwrap_or_else(|| custom_name(value)) + } +} + +impl From for DataTypeName { + fn from(value: String) -> Self { + if is_parameterized(&value) { + return custom_name(value); + } + if let Some(dt) = interned(&value) { + return dt; + } + custom_name(value) + } +} + +impl AsRef for DataTypeName { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl From for String { + #[inline] + fn from(val: DataTypeName) -> Self { + val.0.to_string() + } +} + +impl std::fmt::Display for DataTypeName { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl sql::WriteSql for DataTypeName { + fn write_sql( + &self, + _dialect: &crate::dialect::PyDialect, + out: &mut W, + ) -> sql::PrepareResult<()> { + out.push_str(self.as_str()); + Ok(()) + } +} diff --git a/src/data_type/tests.rs b/src/data_type/tests.rs new file mode 100644 index 0000000..b7541f5 --- /dev/null +++ b/src/data_type/tests.rs @@ -0,0 +1,163 @@ +use super::*; + +#[test] +fn all_interned_names_canonicalize() { + let cases = [ + // --- String --- + ("character", "CHARACTER"), + ("char", "CHAR"), + ("character varying", "CHARACTER VARYING"), + ("char varying", "CHAR VARYING"), + ("varchar", "VARCHAR"), + ("nvarchar", "NVARCHAR"), + ("text", "TEXT"), + // --- Uuid --- + ("uuid", "UUID"), + // --- Bytes --- + ("binary", "BINARY"), + ("varbinary", "VARBINARY"), + ("blob", "BLOB"), + ("bytea", "BYTEA"), + // --- Bit --- + ("bit", "BIT"), + ("bit varying", "BIT VARYING"), + // --- Float (includes decimal/numeric) --- + ("numeric", "NUMERIC"), + ("decimal", "DECIMAL"), + ("float", "FLOAT"), + ("real", "REAL"), + ("double", "DOUBLE"), + ("double precision", "DOUBLE PRECISION"), + // --- Integer --- + ("tinyint", "TINYINT"), + ("smallint", "SMALLINT"), + ("int", "INT"), + ("integer", "INTEGER"), + ("bigint", "BIGINT"), + // --- Boolean --- + ("boolean", "BOOLEAN"), + // --- Date --- + ("date", "DATE"), + // --- Time --- + ("time", "TIME"), + ("time with timezone", "TIME WITH TIMEZONE"), + // --- Datetime --- + ("datetime", "DATETIME"), + ("timestamp", "TIMESTAMP"), + ("timestamp without timezone", "TIMESTAMP"), + ("timestamp with timezone", "TIMESTAMP WITH TIMEZONE"), + ("timestamp with time zone", "TIMESTAMP WITH TIME ZONE"), + ("timestamptz", "TIMESTAMPTZ"), + // --- Json / Jsonb stay distinct spellings --- + ("json", "JSON"), + ("jsonb", "JSONB"), + // --- Inet --- + ("inet", "INET"), + // --- MacAddress (alias collapses onto the interned spelling) --- + ("macaddress", "MACADDRESS"), + ("macaddr", "MACADDRESS"), + ]; + + for (input, canonical) in cases { + let from_str = DataTypeName::from(input); + assert_eq!(from_str.as_str(), canonical, "input {input:?}"); + + let upper = input.to_ascii_uppercase(); + let from_upper = DataTypeName::from(upper.as_str()); + assert_eq!(from_upper.as_str(), canonical, "input {upper:?}"); + assert_eq!(from_str, from_upper, "case fold of {input:?}"); + + let from_string = DataTypeName::from(input.to_string()); + assert_eq!(from_string, from_str, "From of {input:?}"); + } +} + +#[test] +fn interned_synonyms_compare_equal() { + assert_eq!( + DataTypeName::from("timestamp"), + DataTypeName::from("timestamp without timezone"), + ); + assert_eq!(DataTypeName::from("macaddress"), DataTypeName::from("MACADDR"),); + // Distinct interned spellings stay distinct. + assert_ne!( + DataTypeName::from("timestamp with timezone"), + DataTypeName::from("timestamp with time zone"), + ); + assert_ne!(DataTypeName::from("json"), DataTypeName::from("jsonb")); + assert_ne!(DataTypeName::from("int"), DataTypeName::from("integer")); +} + +#[test] +fn mixed_case_known_names_are_canonicalized() { + let cases = [ + ("Int", "INT"), + ("InTeGeR", "INTEGER"), + ("VarChar", "VARCHAR"), + ("tExT", "TEXT"), + ("UuId", "UUID"), + ("Boolean", "BOOLEAN"), + ("TimeStampTz", "TIMESTAMPTZ"), + ("Timestamp Without Timezone", "TIMESTAMP"), + ("MacAddr", "MACADDRESS"), + ("JsonB", "JSONB"), + ("Double Precision", "DOUBLE PRECISION"), + ("ByteA", "BYTEA"), + ]; + for (input, canonical) in cases { + let dt = DataTypeName::from(input); + assert_eq!(dt.as_str(), canonical, "input {input:?}"); + } +} + +#[test] +fn parameterized_name_is_custom_and_preserved() { + for input in ["varchar(255)", "VARCHAR(255)", "decimal(10, 2)", "int(11)"] { + let dt = DataTypeName::from(input); + assert_eq!(dt.as_str(), input); + } +} + +#[test] +fn unknown_name_is_custom_and_preserved() { + let dt = DataTypeName::from("some_totally_unknown_type"); + assert_eq!(dt.as_str(), "some_totally_unknown_type"); +} + +#[test] +fn from_string_reuses_unknown_allocation_path() { + let dt = DataTypeName::from(String::from("not_a_real_type")); + assert_eq!(dt.as_str(), "not_a_real_type"); +} + +/// Non-ASCII never hits the interned table: the lookup bails before matching, +/// and the caller's spelling (including unprintable scalars) is kept. +#[test] +fn unicode_and_unprintable_names_are_custom() { + let cases = [ + "тип", + "文字", + "int🔥", + // Cyrillic 'е' (U+0435), so this is not the interned "integer". + "intеger", + "varchar\u{200b}", + "\u{0000}", + "int\u{0000}", + "\t", + "\n", + "\r\n", + "\u{0001}", + "\u{0007}", + "\u{001b}", + "\u{007f}", + "bit\u{0008}varying", + ]; + for input in cases { + let from_str = DataTypeName::from(input); + assert_eq!(from_str.as_str(), input, "input {input:?}"); + + let from_string = DataTypeName::from(input.to_string()); + assert_eq!(from_string.as_str(), input, "From {input:?}"); + assert_eq!(from_str, from_string); + } +} diff --git a/src/dcl/mod.rs b/src/dcl/mod.rs new file mode 100644 index 0000000..85c397e --- /dev/null +++ b/src/dcl/mod.rs @@ -0,0 +1,12 @@ +//! Data Control Language + +/// Base of every DCL statement. Construct a subclass, not this class. +#[rapidquery_macros::pyclass("rapidquery.dcl.DataControlLanguage", subclass, skip_from_py_object)] +#[derive(Debug, Clone, Copy)] +pub struct PyDataControlLanguage; + +#[pyo3::pymodule(name = "dcl", gil_used = false)] +pub mod dcl_mod { + #[pymodule_export] + use super::PyDataControlLanguage; +} diff --git a/src/ddl/mod.rs b/src/ddl/mod.rs new file mode 100644 index 0000000..a213695 --- /dev/null +++ b/src/ddl/mod.rs @@ -0,0 +1,12 @@ +//! Data Definition Language + +/// Base of every DDL statement. Construct a subclass, not this class. +#[rapidquery_macros::pyclass("rapidquery.ddl.DataDefinitionLanguage", subclass, skip_from_py_object)] +#[derive(Debug, Clone, Copy)] +pub struct PyDataDefinitionLanguage; + +#[pyo3::pymodule(name = "ddl", gil_used = false)] +pub mod ddl_mod { + #[pymodule_export] + use super::PyDataDefinitionLanguage; +} diff --git a/src/dialect/base.rs b/src/dialect/base.rs new file mode 100644 index 0000000..a8c47b5 --- /dev/null +++ b/src/dialect/base.rs @@ -0,0 +1,302 @@ +use crate::internal::utils; + +use super::flags; + +/// SQL dialect: quoting, literals, and which syntax is legal. +/// +/// Use a builtin such as `PostgreSql`, or build one from `OPT_*` flags. +#[rapidquery_macros::pyclass("rapidquery.dialect.Dialect", skip_from_py_object)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PyDialect(pub flags::DialectFlags); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + unsafe { + // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a + // live `PyObject*`. + if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { + let cast = obj.cast_unchecked::(); + return Ok(Self(cast.get().0)); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected Dialect, got {}", + utils::get_type_name(obj.py(), obj.as_ptr()) + ) + } + } +} + +// One item is one constructor argument and one getter. +// Enum items also export `OPT_*` constants. +// Adding a flag is a new item here, plus the enum and the bitfield field. +rapidquery_macros::dialect_options! { + char placeholder_character = '?'; + + enum Placeholder placeholder { + Simple => OPT_PLACEHOLDER_SIMPLE, + Numbered => OPT_PLACEHOLDER_NUMBERED, + } + + enum NullTreatment null_treatment { + Unsupported => OPT_NULL_TREATMENT_UNSUPPORTED, + InsideParens => OPT_NULL_TREATMENT_INSIDE_PARENS, + Postfix => OPT_NULL_TREATMENT_POSTFIX, + } + + enum LikeEscape like_escape { + Unsupported => OPT_LIKE_ESCAPE_UNSUPPORTED, + Scalar => OPT_LIKE_ESCAPE_SCALAR, + IncludingAny => OPT_LIKE_ESCAPE_INCLUDING_ANY, + } + + enum LikeAny like_any { + OrExpansion => OPT_LIKE_ANY_OR_EXPANSION, + ArrayConstructor => OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, + PatternList => OPT_LIKE_ANY_PATTERN_LIST, + } + + enum ILikeExpression ilike_expression { + Standard => OPT_ILIKE_EXPRESSION_STANDARD, + UseLikeLower => OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER, + UseLike => OPT_ILIKE_EXPRESSION_USE_LIKE, + } + + enum RLikeExpression rlike_expression { + Unsupported => OPT_RLIKE_EXPRESSION_UNSUPPORTED, + Standard => OPT_RLIKE_EXPRESSION_STANDARD, + Tilde => OPT_RLIKE_EXPRESSION_TILDE, + RegexpLike => OPT_RLIKE_EXPRESSION_REGEXP_LIKE, + RegexpContains => OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS, + RegexpMatches => OPT_RLIKE_EXPRESSION_REGEXP_MATCHES, + } + + enum StringConcat string_concat { + Pipes => OPT_STRING_CONCAT_PIPES, + Plus => OPT_STRING_CONCAT_PLUS, + ConcatFunction => OPT_STRING_CONCAT_CONCAT_FUNCTION, + } + + enum BitXor bitxor { + Unsupported => OPT_BITXOR_UNSUPPORTED, + Caret => OPT_BITXOR_CARET, + Hash => OPT_BITXOR_HASH, + XORFunction => OPT_BITXOR_XOR_FUNCTION, + BitXORFunction => OPT_BITXOR_BIT_XOR_FUNCTION, + } + + enum Modulo modulo { + Percent => OPT_MODULO_PERCENT, + Function => OPT_MODULO_FUNCTION, + Infix => OPT_MODULO_INFIX, + } + + enum IsDistinctFrom is_distinct_from { + Unsupported => OPT_IS_DISTINCT_FROM_UNSUPPORTED, + Standard => OPT_IS_DISTINCT_FROM_STANDARD, + NullSafeEqual => OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, + } + + enum TryCast try_cast { + Unsupported => OPT_TRY_CAST_UNSUPPORTED, + TryCast => OPT_TRY_CAST_TRY_CAST, + TryCastWord => OPT_TRY_CAST_TRY_CAST_WORD, + SafeCast => OPT_TRY_CAST_SAFE_CAST, + } + + enum Trim trim { + SqlFrom => OPT_TRIM_SQL_FROM, + Function => OPT_TRIM_FUNCTION, + SpacesOnly => OPT_TRIM_SPACES_ONLY, + } + + enum Collate collate { + Unsupported => OPT_COLLATE_UNSUPPORTED, + InfixIdent => OPT_COLLATE_INFIX_IDENT, + FunctionString => OPT_COLLATE_FUNCTION_STRING, + } + + enum Bitwise bitwise { + Infix => OPT_BITWISE_INFIX, + InfixAndShift => OPT_BITWISE_INFIX_AND_SHIFT, + InfixNoShift => OPT_BITWISE_INFIX_NO_SHIFT, + BitShiftFunctions => OPT_BITWISE_BIT_SHIFT_FUNCTIONS, + ShiftFunctions => OPT_BITWISE_SHIFT_FUNCTIONS, + BitAndOnly => OPT_BITWISE_BIT_AND_ONLY, + } + + enum Tuple tuple { + Unsupported => OPT_TUPLE_UNSUPPORTED, + Row => OPT_TUPLE_ROW, + RowRejectEmpty => OPT_TUPLE_ROW_REJECT_EMPTY, + Struct => OPT_TUPLE_STRUCT, + Parentheses => OPT_TUPLE_PARENTHESES, + } + + enum BitFormat bit_format { + Unsupported => OPT_BIT_FORMAT_UNSUPPORTED, + PrefixBigB => OPT_BIT_FORMAT_PREFIX_BIG_B, + PrefixSmallB => OPT_BIT_FORMAT_PREFIX_SMALL_B, + PostfixBigB => OPT_BIT_FORMAT_POSTFIX_BIG_B, + } + + enum BoolFormat bool_format { + Keyword => OPT_BOOL_FORMAT_KEYWORD, + Integer => OPT_BOOL_FORMAT_INTEGER, + } + + enum IdentifierFormat identifier_format { + DoubleQuote => OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE, + Backtick => OPT_IDENTIFIER_FORMAT_BACKTICK, + SquareBracket => OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET, + } + + enum IdentifierEscape identifier_escape { + Double => OPT_IDENTIFIER_ESCAPE_DOUBLE, + Backslash => OPT_IDENTIFIER_ESCAPE_BACKSLASH, + } + + enum StringFormat string_format { + SingleQuote => OPT_STRING_FORMAT_SINGLE_QUOTE, + PrefixBigE => OPT_STRING_FORMAT_PREFIX_BIG_E, + PrefixBigN => OPT_STRING_FORMAT_PREFIX_BIG_N, + } + + enum StringEscape string_escape { + Quote => OPT_STRING_ESCAPE_QUOTE, + BackslashHex => OPT_STRING_ESCAPE_BACKSLASH_HEX, + BackslashMysql => OPT_STRING_ESCAPE_BACKSLASH_MYSQL, + BackslashUnicode => OPT_STRING_ESCAPE_BACKSLASH_UNICODE, + BackslashHexZero => OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO, + BackslashUnicodeQuote => OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE, + } + + enum BytesFormat bytes_format { + PrefixBigX => OPT_BYTES_FORMAT_PREFIX_BIG_X, + PrefixZeroX => OPT_BYTES_FORMAT_PREFIX_ZERO_X, + SlashXBytea => OPT_BYTES_FORMAT_SLASH_X_BYTEA, + PrefixBigBSlashX => OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X, + HexToRaw => OPT_BYTES_FORMAT_HEX_TO_RAW, + Unhex => OPT_BYTES_FORMAT_UNHEX, + ToVarbyte => OPT_BYTES_FORMAT_TO_VARBYTE, + PostfixXbv => OPT_BYTES_FORMAT_POSTFIX_XBV, + ToBinary => OPT_BYTES_FORMAT_TO_BINARY, + SlashXBlob => OPT_BYTES_FORMAT_SLASH_X_BLOB, + } + + enum FunctionFilter function_filter { + Unsupported => OPT_FUNCTION_FILTER_UNSUPPORTED, + Filter => OPT_FUNCTION_FILTER_FILTER, + FilterWhere => OPT_FUNCTION_FILTER_FILTER_WHERE, + InsideWhere => OPT_FUNCTION_FILTER_INSIDE_WHERE, + } + + enum ArrayFormat array_format { + Unsupported => OPT_ARRAY_FORMAT_UNSUPPORTED, + Constructor => OPT_ARRAY_FORMAT_CONSTRUCTOR, + List => OPT_ARRAY_FORMAT_LIST, + Function => OPT_ARRAY_FORMAT_FUNCTION, + } + + enum DateFormat date_format { + Quoted => OPT_DATE_FORMAT_QUOTED, + Keyword => OPT_DATE_FORMAT_KEYWORD, + } + + enum TimeFormat time_format { + Quoted => OPT_TIME_FORMAT_QUOTED, + Keyword => OPT_TIME_FORMAT_KEYWORD, + } + + enum TimestampFormat timestamp_format { + Quoted => OPT_TIMESTAMP_FORMAT_QUOTED, + Timestamptz => OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ, + WithTimezone => OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE, + } + + enum InetFormat inet_format { + Unsupported => OPT_INET_FORMAT_UNSUPPORTED, + Quoted => OPT_INET_FORMAT_QUOTED, + Cast => OPT_INET_FORMAT_CAST, + } + + enum MacAddrFormat macaddr_format { + Unsupported => OPT_MACADDR_FORMAT_UNSUPPORTED, + Quoted => OPT_MACADDR_FORMAT_QUOTED, + Cast => OPT_MACADDR_FORMAT_CAST, + } + + enum UuidFormat uuid_format { + Quoted => OPT_UUID_FORMAT_QUOTED, + UuidKeyword => OPT_UUID_FORMAT_UUID_KEYWORD, + CastUuid => OPT_UUID_FORMAT_CAST_UUID, + } + + enum JsonFormat json_format { + Quoted => OPT_JSON_FORMAT_QUOTED, + JsonKeyword => OPT_JSON_FORMAT_JSON_KEYWORD, + ParseJson => OPT_JSON_FORMAT_PARSE_JSON, + } + + bool supports_similar_to = false; + bool parenthesize_in_between = false; + bool supports_double_colon = false; +} + +#[pyo3::pymethods] +impl PyDialect { + /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). + fn write_boolean(&self, value: bool) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_boolean(&mut buf, value)?; + Ok(buf) + } + + /// Escape `ident` and wrap it in this dialect’s identifier delimiters. + fn write_identifier(&self, value: &str) -> pyo3::PyResult { + let mut buf = String::new(); + let _ = self.0.write_identifier(&mut buf, value); + Ok(buf) + } + + /// Escape `value` and wrap it as a complete string literal. + fn write_string_literal(&self, value: &str) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_string_literal(&mut buf, value)?; + Ok(buf) + } + + /// Write a bit-string ('0'/'1' characters) as a native literal. + fn write_bits(&self, value: &str) -> pyo3::PyResult { + if value.chars().any(|c| c != '0' && c != '1') { + return rapidquery_macros::pyerr!(Err(PyValueError), "invalid input: {}", value); + } + + let mut buf = String::new(); + self.0.write_bits(&mut buf, value)?; + Ok(buf) + } + + /// Write bytes as a native blob/bytea/varbyte literal. + fn write_bytes(&self, value: &[u8]) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_bytes(&mut buf, value)?; + Ok(buf) + } + + fn __eq__(&self, other: &pyo3::Bound<'_, Self>) -> bool { + self.0 == other.get().0 + } + + fn __hash__(&self) -> u64 { + use std::hash::Hash; + use std::hash::Hasher; + + let mut hasher = std::hash::DefaultHasher::default(); + self.0.hash(&mut hasher); + hasher.finish() + } +} diff --git a/src/dialect/builtin.rs b/src/dialect/builtin.rs new file mode 100644 index 0000000..072d8dd --- /dev/null +++ b/src/dialect/builtin.rs @@ -0,0 +1,522 @@ +use super::base; +use super::flags; + +/// BigQuery SQL dialect. +pub const BIGQUERY_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::InsideParens) + .with_like_escape(flags::LikeEscape::Unsupported) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLikeLower) + .with_rlike_expression(flags::RLikeExpression::RegexpContains) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::SafeCast) + .with_trim(flags::Trim::Function) + .with_collate(flags::Collate::FunctionString) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::Backtick) + .with_identifier_escape(flags::IdentifierEscape::Backslash) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashHex) + .with_bytes_format(flags::BytesFormat::PrefixBigBSlashX) + .with_function_filter(flags::FunctionFilter::InsideWhere) + .with_array_format(flags::ArrayFormat::Constructor) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Keyword) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::JsonKeyword) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); + +/// Databricks SQL dialect. +pub const DATABRICKS_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::PatternList) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::Standard) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Struct) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::Backtick) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashUnicodeQuote) + .with_bytes_format(flags::BytesFormat::PrefixBigX) + .with_function_filter(flags::FunctionFilter::FilterWhere) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// DuckDB SQL dialect. +pub const DUCKDB_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('$' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::InsideParens) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::RegexpMatches) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::XORFunction) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::Function) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Row) + .with_bit_format(flags::BitFormat::PrefixBigB) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::SlashXBlob) + .with_function_filter(flags::FunctionFilter::Filter) + .with_array_format(flags::ArrayFormat::List) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Keyword) + .with_timestamp_format(flags::TimestampFormat::Timestamptz) + .with_inet_format(flags::InetFormat::Cast) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::UuidKeyword) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(true) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// Hive SQL dialect. +pub const HIVE_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::Unsupported) + .with_like_escape(flags::LikeEscape::Unsupported) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLikeLower) + .with_rlike_expression(flags::RLikeExpression::Standard) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::NullSafeEqual) + .with_try_cast(flags::TryCast::Unsupported) + .with_trim(flags::Trim::SpacesOnly) + .with_collate(flags::Collate::Unsupported) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Struct) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::Backtick) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashUnicode) + .with_bytes_format(flags::BytesFormat::Unhex) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Function) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); + +/// Microsoft SQL Server dialect. +pub const MSSQL_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLike) + .with_rlike_expression(flags::RLikeExpression::Unsupported) + .with_string_concat(flags::StringConcat::Plus) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::InfixNoShift) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Integer) + .with_identifier_format(flags::IdentifierFormat::SquareBracket) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::PrefixBigN) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::PrefixZeroX) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Quoted) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::Quoted) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(true) + .with_supports_double_colon(false), +); + +/// MySQL dialect. +pub const MYSQL_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::Unsupported) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLike) + .with_rlike_expression(flags::RLikeExpression::Standard) + .with_string_concat(flags::StringConcat::ConcatFunction) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::NullSafeEqual) + .with_try_cast(flags::TryCast::Unsupported) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::RowRejectEmpty) + .with_bit_format(flags::BitFormat::PrefixSmallB) + .with_bool_format(flags::BoolFormat::Integer) + .with_identifier_format(flags::IdentifierFormat::Backtick) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashMysql) + .with_bytes_format(flags::BytesFormat::PrefixBigX) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Quoted) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::Quoted) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); + +/// Oracle SQL dialect. +pub const ORACLE_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character(':' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLikeLower) + .with_rlike_expression(flags::RLikeExpression::RegexpLike) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Unsupported) + .with_modulo(flags::Modulo::Function) + .with_is_distinct_from(flags::IsDistinctFrom::Unsupported) + .with_try_cast(flags::TryCast::Unsupported) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::BitAndOnly) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Integer) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::HexToRaw) + .with_function_filter(flags::FunctionFilter::FilterWhere) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); + +/// PostgreSQL dialect. +pub const POSTGRESQL_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('$' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::Unsupported) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::ArrayConstructor) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::Tilde) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Hash) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::Unsupported) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Row) + .with_bit_format(flags::BitFormat::PrefixBigB) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::PrefixBigE) + .with_string_escape(flags::StringEscape::BackslashHex) + .with_bytes_format(flags::BytesFormat::SlashXBytea) + .with_function_filter(flags::FunctionFilter::FilterWhere) + .with_array_format(flags::ArrayFormat::Constructor) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Keyword) + .with_timestamp_format(flags::TimestampFormat::Timestamptz) + .with_inet_format(flags::InetFormat::Cast) + .with_macaddr_format(flags::MacAddrFormat::Cast) + .with_uuid_format(flags::UuidFormat::CastUuid) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(true) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// Amazon Redshift dialect. +pub const REDSHIFT_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('$' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::Tilde) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Unsupported) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Unsupported) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::FunctionString) + .with_bitwise(flags::Bitwise::InfixAndShift) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::ToVarbyte) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Quoted) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::Quoted) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(true) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// Snowflake SQL dialect. +pub const SNOWFLAKE_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character(':' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::IncludingAny) + .with_like_any(flags::LikeAny::PatternList) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::Standard) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::BitXORFunction) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::Function) + .with_collate(flags::Collate::FunctionString) + .with_bitwise(flags::Bitwise::BitShiftFunctions) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashHexZero) + .with_bytes_format(flags::BytesFormat::ToBinary) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::Timestamptz) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::ParseJson) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// Apache Spark SQL dialect. +pub const SPARK_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::Postfix) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::PatternList) + .with_ilike_expression(flags::ILikeExpression::Standard) + .with_rlike_expression(flags::RLikeExpression::Standard) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Caret) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::TryCast) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::Unsupported) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Struct) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::Backtick) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::BackslashUnicodeQuote) + .with_bytes_format(flags::BytesFormat::PrefixBigX) + .with_function_filter(flags::FunctionFilter::FilterWhere) + .with_array_format(flags::ArrayFormat::Function) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(true), +); + +/// SQLite dialect. +pub const SQLITE_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Numbered) + .with_null_treatment(flags::NullTreatment::Unsupported) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLike) + .with_rlike_expression(flags::RLikeExpression::Unsupported) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::Unsupported) + .with_modulo(flags::Modulo::Percent) + .with_is_distinct_from(flags::IsDistinctFrom::Standard) + .with_try_cast(flags::TryCast::Unsupported) + .with_trim(flags::Trim::Function) + .with_collate(flags::Collate::InfixIdent) + .with_bitwise(flags::Bitwise::Infix) + .with_tuple(flags::Tuple::Parentheses) + .with_bit_format(flags::BitFormat::Unsupported) + .with_bool_format(flags::BoolFormat::Integer) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::PrefixBigX) + .with_function_filter(flags::FunctionFilter::FilterWhere) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Quoted) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::Quoted) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); + +/// Teradata SQL dialect. +pub const TERADATA_DIALECT: base::PyDialect = base::PyDialect( + flags::DialectFlags::new() + .with_placeholder_character('?' as u32) + .with_placeholder(flags::Placeholder::Simple) + .with_null_treatment(flags::NullTreatment::InsideParens) + .with_like_escape(flags::LikeEscape::Scalar) + .with_like_any(flags::LikeAny::OrExpansion) + .with_ilike_expression(flags::ILikeExpression::UseLikeLower) + .with_rlike_expression(flags::RLikeExpression::Unsupported) + .with_string_concat(flags::StringConcat::Pipes) + .with_bitxor(flags::BitXor::BitXORFunction) + .with_modulo(flags::Modulo::Infix) + .with_is_distinct_from(flags::IsDistinctFrom::Unsupported) + .with_try_cast(flags::TryCast::TryCastWord) + .with_trim(flags::Trim::SqlFrom) + .with_collate(flags::Collate::Unsupported) + .with_bitwise(flags::Bitwise::ShiftFunctions) + .with_tuple(flags::Tuple::Unsupported) + .with_bit_format(flags::BitFormat::PostfixBigB) + .with_bool_format(flags::BoolFormat::Keyword) + .with_identifier_format(flags::IdentifierFormat::DoubleQuote) + .with_identifier_escape(flags::IdentifierEscape::Double) + .with_string_format(flags::StringFormat::SingleQuote) + .with_string_escape(flags::StringEscape::Quote) + .with_bytes_format(flags::BytesFormat::PostfixXbv) + .with_function_filter(flags::FunctionFilter::Unsupported) + .with_array_format(flags::ArrayFormat::Unsupported) + .with_date_format(flags::DateFormat::Keyword) + .with_time_format(flags::TimeFormat::Quoted) + .with_timestamp_format(flags::TimestampFormat::WithTimezone) + .with_inet_format(flags::InetFormat::Unsupported) + .with_macaddr_format(flags::MacAddrFormat::Unsupported) + .with_uuid_format(flags::UuidFormat::Quoted) + .with_json_format(flags::JsonFormat::Quoted) + .with_supports_similar_to(false) + .with_parenthesize_in_between(false) + .with_supports_double_colon(false), +); diff --git a/src/dialect/errors.rs b/src/dialect/errors.rs new file mode 100644 index 0000000..aab5729 --- /dev/null +++ b/src/dialect/errors.rs @@ -0,0 +1,31 @@ +use std::fmt; + +/// Formatting / serializing errors +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatError { + /// Equals to [`fmt::Write`] + Write, + /// Means a `NUL` character has found in string during escaping/serializing a string value. + NulInString, + /// Means something is not supported by a dialect + Unsupported(&'static str), +} + +impl From for FormatError { + #[inline] + fn from(_: fmt::Error) -> Self { + Self::Write + } +} + +impl fmt::Display for FormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Write => f.write_str("failed to write SQL to memory"), + Self::NulInString => f.write_str("NUL is not allowed in a string literal"), + Self::Unsupported(name) => write!(f, "this dialect does not support {name}"), + } + } +} + +impl std::error::Error for FormatError {} diff --git a/src/dialect/flags.rs b/src/dialect/flags.rs new file mode 100644 index 0000000..d514121 --- /dev/null +++ b/src/dialect/flags.rs @@ -0,0 +1,980 @@ +use bitfield_struct::bitenum; +use bitfield_struct::bitfield; + +use crate::internal::utils; + +use super::errors::FormatError; + +/// Positional parameter syntax +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Placeholder { + /// e.g. `'?'` + #[fallback] + Simple, + /// e.g. `'?1'` + Numbered, +} + +/// The way dialect supports `{IGNORE|RESPECT} NULLS` expressions in `Function`s. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NullTreatment { + /// Is not supported + #[fallback] + Unsupported, + /// e.g. `FIRST_VALUE(x IGNORE NULLS)` + InsideParens, + /// e.g. `FIRST_VALUE(x) IGNORE NULLS` + Postfix, +} + +/// Where an inlined `LIKE` `ESCAPE` clause is legal. +/// +/// `escape: None` writes nothing. A set escape character on a form this +/// variant does not allow raises. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LikeEscape { + /// No `ESCAPE` clause. + Unsupported, + /// Scalar `LIKE` and an `OR` expansion. Native `LIKE ANY` has no slot. + Scalar, + /// Scalar, `OR` expansion, and `LIKE ANY (p, …) ESCAPE`. + #[fallback] + IncludingAny, +} + +/// How `LIKE ANY` / `ILIKE ANY` is rendered. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LikeAny { + /// `(x LIKE p1 OR x LIKE p2)`. + /// Useful when it isn't supported natively by dialect + #[fallback] + OrExpansion, + /// `LIKE ANY (ARRAY[p, …])`. + ArrayConstructor, + /// `LIKE ANY (p, …)`. + PatternList, +} + +/// Does dialect support `ILIKE` expression or we should fallback to another options? +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ILikeExpression { + #[fallback] + Standard, + /// Use `LIKE` expression and `LOWER()` function + UseLikeLower, + /// Fallback to `LIKE` expression + UseLike, +} + +/// How regex `RLike` is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RLikeExpression { + /// Is not supported. + #[fallback] + Unsupported, + /// `expr [NOT] RLIKE pattern` + Standard, + /// `expr ~ pattern` / `expr !~ pattern` + Tilde, + /// `REGEXP_LIKE(expr, pattern)` + RegexpLike, + /// `REGEXP_CONTAINS(expr, pattern)` + RegexpContains, + /// `regexp_matches(expr, pattern)` + RegexpMatches, +} + +/// How binary operation StringConcat (`||`) is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StringConcat { + /// `left || right`. + #[fallback] + Pipes, + /// `left + right`. SQL Server `||` is 2025+; classic concat is `+`. + Plus, + /// `CONCAT(left, right)`. MySQL `||` is logical OR unless `PIPES_AS_CONCAT`. + ConcatFunction, +} + +/// How binary operation bit xor (`^`) is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BitXor { + /// Is not supported + Unsupported, + /// `left ^ right`. + #[fallback] + Caret, + /// `left # right`. PostgreSQL `^` is exponent. + Hash, + /// `xor(left, right)`. + XORFunction, + /// `BITXOR(left, right)`. + BitXORFunction, +} + +/// How binary operation mod (`%`) is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Modulo { + /// `left % right`. + #[fallback] + Percent, + /// `MOD(left, right)`. + Function, + /// `left MOD right`. + Infix, +} + +/// How `IS [NOT] DISTINCT FROM` is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IsDistinctFrom { + /// Is not supported + Unsupported, + /// `left IS [NOT] DISTINCT FROM right`. + #[fallback] + Standard, + /// `<=>` is null-safe equal (`IS NOT DISTINCT FROM`). + /// `IS DISTINCT FROM` is `NOT (left <=> right)`. + NullSafeEqual, +} + +/// How `TRY_CAST` is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TryCast { + /// Is not supported + Unsupported, + /// `TRY_CAST(expr AS type)`. + #[fallback] + TryCast, + /// `TRYCAST(expr AS type)`. + TryCastWord, + /// `SAFE_CAST(expr AS type)`. BigQuery's spelling of a null-on-failure cast. + SafeCast, +} + +/// How `TRIM` is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Trim { + /// `TRIM([LEADING|TRAILING] [chars FROM] expr)`. + #[fallback] + SqlFrom, + /// `TRIM` / `LTRIM` / `RTRIM` `(expr[, chars])`. + Function, + /// `TRIM` / `LTRIM` / `RTRIM` `(expr)`. A character set raises. + SpacesOnly, +} + +/// How `COLLATE` is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Collate { + /// Is not supported + Unsupported, + /// `expr COLLATE ident`. + #[fallback] + InfixIdent, + /// `COLLATE(expr, 'body')`. The identifier body is a string literal. + FunctionString, +} + +/// How `&`, `|`, `~`, `<<`, and `>>` are spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Bitwise { + /// `& | ~ << >>`. + #[fallback] + Infix, + /// `& << >>`. `|` and `~` raise. + /// + /// Redshift documents those three and not bitwise OR or NOT + /// (`^` is exponentiation and stays on [`BitXorSyntax`]). + InfixAndShift, + /// `& | ~`. Shifts raise. + InfixNoShift, + /// `BITAND` / `BITOR` / `BITNOT` / `BITSHIFTLEFT` / `BITSHIFTRIGHT`. + BitShiftFunctions, + /// `BITAND` / `BITOR` / `BITNOT` / `SHIFTLEFT` / `SHIFTRIGHT`. + ShiftFunctions, + /// `BITAND` only. `|`, `~`, `<<`, and `>>` raise. + BitAndOnly, +} + +/// How a tuple / row value is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Tuple { + /// Is not supported. + Unsupported, + /// `ROW(...)`. `empty_ok` allows arity 0 (`ROW()`). + Row, + /// `ROW(...)`. Rejects empty (`ROW()`). + RowRejectEmpty, + /// `struct(...)`. Arity 0 is unsupported. + Struct, + /// `(a, b, ...)`. Arity 0 and 1 are unsupported. + #[fallback] + Parentheses, +} + +/// The way dialect accepts literal bit values +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BitFormat { + /// Is not supported. + Unsupported, + /// e.g. `B'101'` + #[fallback] + PrefixBigB, + /// e.g. `b'101'` + PrefixSmallB, + /// e.g. `'101'B` + PostfixBigB, +} + +/// How a boolean literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BoolFormat { + /// `TRUE` / `FALSE` + #[fallback] + Keyword, + /// `1` / `0` + Integer, +} + +/// Delimiters wrapped around a quoted identifier. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IdentifierFormat { + /// `"name"` + #[fallback] + DoubleQuote, + /// `` `name` `` + Backtick, + /// `[name]` + SquareBracket, +} + +/// How to escape identifiers. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IdentifierEscape { + /// Escape identifier delimiters with doubling. + #[fallback] + Double, + /// Escape identifier delimiters with backslash. + Backslash, +} + +/// Delimiters wrapped around a literal string. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StringFormat { + /// `'string'` + #[fallback] + SingleQuote, + /// `E'string'` + PrefixBigE, + /// `N'string'` + PrefixBigN, +} + +/// How to escape literal strings. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StringEscape { + /// `'` → `''`. Other characters are unchanged. + #[fallback] + Quote, + /// `\\`, `\'`, `\n`, `\r`, `\t`; other C0/DEL as `\xNN`. + BackslashHex, + /// Also `\"` and `\Z` for 0x1A; tab and other controls pass through. + BackslashMysql, + /// Other C0/DEL as `\uNNNN`. No `\"`. + BackslashUnicode, + /// Other C0/DEL as `\xNN`. + BackslashHexZero, + /// [`Self::BackslashUnicode`] plus `\"`. + BackslashUnicodeQuote, +} + +/// The way dialect accepts literal bytes values. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BytesFormat { + /// `X'DEADBEEF'` + #[fallback] + PrefixBigX, + /// `0xDEADBEEF` + PrefixZeroX, + /// `'\xDEADBEEF'::bytea` + SlashXBytea, + /// `B'\xNN…'` (each byte as `\xNN`) + PrefixBigBSlashX, + /// `HEXTORAW('DEADBEEF')` + HexToRaw, + /// `UNHEX('DEADBEEF')` + Unhex, + /// `TO_VARBYTE('DEADBEEF', 'hex')` + ToVarbyte, + /// `'DEADBEEF'XBV` + PostfixXbv, + /// `TO_BINARY('DEADBEEF', 'HEX')` + ToBinary, + /// `'\xHH…'::BLOB`; empty is `''::BLOB` + SlashXBlob, +} + +/// Function `FILTER` syntax. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FunctionFilter { + /// Is not supported. + Unsupported, + /// e.g. FIRST_VALUE(x) FILTER (id > 10) + Filter, + /// e.g. FIRST_VALUE(x) FILTER (WHERE id > 10) + #[fallback] + FilterWhere, + /// e.g. FIRST_VALUE(x WHERE id > 10) + InsideWhere, +} + +/// The way dialect accepts array values. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ArrayFormat { + /// Is not supported. + #[fallback] + Unsupported, + /// `ARRAY[…]` + Constructor, + /// `[…]` + List, + /// `array(…)` + Function, +} + +/// How a date literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DateFormat { + /// Quoted string. + #[fallback] + Quoted, + /// `DATE`, a space, then a quoted literal. + Keyword, +} + +/// How a time literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TimeFormat { + /// Quoted string. + #[fallback] + Quoted, + /// `TIME`, a space, then a quoted literal. + Keyword, +} + +/// How a timestamp literal is spelled. +/// +/// Naive values use `TIMESTAMP` when a keyword is configured. Aware values +/// (`datetime` with a tzinfo, or a UTC epoch number) use the keyword this +/// variant names. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TimestampFormat { + /// Both naive and aware values are a quoted string. No keyword. + Quoted, + /// Naive: `TIMESTAMP '…'`. Aware: `TIMESTAMPTZ '…'`. + Timestamptz, + /// Naive: `TIMESTAMP '…'`. Aware: `TIMESTAMP WITH TIMEZONE '…'`. + #[fallback] + WithTimezone, +} + +/// How an inet literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InetFormat { + /// Is not supported. + #[fallback] + Unsupported, + /// `'192.168.1.1/32'` + Quoted, + /// `'192.168.1.1/32'::inet` + Cast, +} + +/// How a MAC address literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MacAddrFormat { + /// Is not supported. + #[fallback] + Unsupported, + /// `'01:23:45:67:89:AB'` + Quoted, + /// `'01:23:45:67:89:AB'::macaddr` + Cast, +} + +/// How a UUID literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UuidFormat { + /// Quoted hyphenated text. + #[fallback] + Quoted, + /// `UUID '…'` + UuidKeyword, + /// `'…'::uuid` + CastUuid, +} + +/// How a JSON literal is spelled. +#[bitenum] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum JsonFormat { + /// Quoted JSON text. + #[fallback] + Quoted, + /// `JSON '…'` + JsonKeyword, + /// `PARSE_JSON('…')` + ParseJson, +} + +/// Packed options that decide how one dialect spells SQL. +/// +/// Each field is a small enum or a bool. Builtin dialects set every field. +/// A flag the writer does not implement makes rendering fail. +/// +/// ``` +/// use rapidquery::dialect::flags::BoolFormat; +/// use rapidquery::dialect::flags::DialectFlags; +/// +/// let flags = DialectFlags::new().with_bool_format(BoolFormat::Integer); +/// let mut out = String::new(); +/// flags.write_boolean(&mut out, true).unwrap(); +/// assert_eq!(out, "1"); +/// ``` +#[bitfield(u128)] +#[derive(PartialEq, Eq, Hash)] +pub struct DialectFlags { + /// Placeholder character as u32 + /// + /// NOTE: Caller is responsible to set a valid character. + #[bits(32)] + pub placeholder_character: u32, + /// Positional parameter syntax + #[bits(1)] + pub placeholder: Placeholder, + /// The way dialect supports `{IGNORE|RESPECT} NULLS` expressions in `Function`s. + #[bits(2)] + pub null_treatment: NullTreatment, + /// Where an inlined `LIKE` `ESCAPE` clause is legal. + #[bits(2)] + pub like_escape: LikeEscape, + /// How `LIKE ANY` / `ILIKE ANY` is rendered. + #[bits(2)] + pub like_any: LikeAny, + /// Does dialect support `ILIKE` expression or we should fallback to another options? + #[bits(2)] + pub ilike_expression: ILikeExpression, + /// How regex `RLike` is spelled. + #[bits(3)] + pub rlike_expression: RLikeExpression, + /// How binary operation StringConcat (`||`) is spelled. + #[bits(2)] + pub string_concat: StringConcat, + /// How binary operation bit xor (`^`) is spelled. + #[bits(3)] + pub bitxor: BitXor, + /// How binary operation mod (`%`) is spelled. + #[bits(2)] + pub modulo: Modulo, + /// How `IS [NOT] DISTINCT FROM` is spelled. + #[bits(2)] + pub is_distinct_from: IsDistinctFrom, + /// How `TRY_CAST` is spelled. + #[bits(2)] + pub try_cast: TryCast, + /// How TRIM is spelled. + #[bits(2)] + pub trim: Trim, + /// How `COLLATE` is spelled. + #[bits(2)] + pub collate: Collate, + /// How `&`, `|`, `~`, `<<`, and `>>` are spelled. + #[bits(3)] + pub bitwise: Bitwise, + /// How a tuple / row value is spelled. + #[bits(3)] + pub tuple: Tuple, + /// The way dialect accepts literal bit values. + #[bits(2)] + pub bit_format: BitFormat, + /// How a boolean literal is spelled. + #[bits(1)] + pub bool_format: BoolFormat, + /// Delimiters wrapped around a quoted identifier. + #[bits(2)] + pub identifier_format: IdentifierFormat, + /// How to escape identifiers. + #[bits(1)] + pub identifier_escape: IdentifierEscape, + /// Delimiters wrapped around a literal string. + #[bits(2)] + pub string_format: StringFormat, + /// How to escape literal strings. + #[bits(3)] + pub string_escape: StringEscape, + /// The way dialect accepts literal bytes values. + #[bits(4)] + pub bytes_format: BytesFormat, + /// Function `FILTER` syntax. + #[bits(3)] + pub function_filter: FunctionFilter, + /// The way dialect accepts array values. + #[bits(3)] + pub array_format: ArrayFormat, + /// How a date literal is spelled. + #[bits(3)] + pub date_format: DateFormat, + /// How a time literal is spelled. + #[bits(3)] + pub time_format: TimeFormat, + /// How a timestamp literal is spelled. + #[bits(2)] + pub timestamp_format: TimestampFormat, + /// How an inet literal is spelled. + #[bits(2)] + pub inet_format: InetFormat, + /// How a MAC address literal is spelled. + #[bits(2)] + pub macaddr_format: MacAddrFormat, + /// How a UUID literal is spelled. + #[bits(2)] + pub uuid_format: UuidFormat, + /// How a JSON literal is spelled. + #[bits(2)] + pub json_format: JsonFormat, + /// Whether `SIMILAR TO` is supported. + #[bits(1)] + pub supports_similar_to: bool, + /// Wrap `IN` / `BETWEEN` in parentheses. + /// + /// SQL Server ranks `AND` above `IN`/`BETWEEN`/`OR`, and those four share + /// a level evaluated left to right. The parentheses keep the predicate + /// from being parsed as part of the left operand. + #[bits(1)] + pub parenthesize_in_between: bool, + /// Whether `expr::type` is supported. + #[bits(1)] + pub supports_double_colon: bool, + + // Padding + #[bits(23)] + __: u32, +} + +impl DialectFlags { + /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). + #[inline] + pub fn write_boolean(&self, writer: &mut W, value: bool) -> Result<(), FormatError> { + match (self.bool_format(), value) { + (BoolFormat::Integer, false) => writer.write_str("0")?, + (BoolFormat::Integer, true) => writer.write_str("1")?, + (BoolFormat::Keyword, false) => writer.write_str("FALSE")?, + (BoolFormat::Keyword, true) => writer.write_str("TRUE")?, + } + Ok(()) + } + + /// Write `string` with special characters neutralized, without wrapping it. + #[inline] + pub(super) fn escape_string(&self, writer: &mut W, string: &str) -> Result<(), FormatError> { + let result = match self.string_escape() { + StringEscape::Quote => utils::replace_to(string, '\'', "''", writer), + StringEscape::BackslashHex => write_backslash_hex(writer, string), + StringEscape::BackslashMysql => write_backslash_mysql(writer, string), + StringEscape::BackslashUnicode => write_backslash_unicode(writer, string, false), + StringEscape::BackslashHexZero => write_backslash_hex_zero(writer, string), + StringEscape::BackslashUnicodeQuote => write_backslash_unicode(writer, string, true), + }; + result.map_err(Into::into) + } + + /// Escape `ident` and wrap it in this dialect's identifier delimiters. + #[inline] + pub fn write_identifier(&self, writer: &mut W, ident: &str) -> std::fmt::Result { + let (start, end) = match self.identifier_format() { + IdentifierFormat::DoubleQuote => ('"', '"'), + IdentifierFormat::Backtick => ('`', '`'), + IdentifierFormat::SquareBracket => ('[', ']'), + }; + writer.write_char(start)?; + + match self.identifier_escape() { + IdentifierEscape::Double => match self.identifier_format() { + IdentifierFormat::DoubleQuote => { + utils::replace_to(ident, '"', "\"\"", writer)?; + } + IdentifierFormat::Backtick => { + utils::replace_to(ident, '`', "``", writer)?; + } + IdentifierFormat::SquareBracket => { + utils::replace_to(ident, ']', "]]", writer)?; + } + }, + IdentifierEscape::Backslash => match self.identifier_format() { + IdentifierFormat::DoubleQuote => { + utils::replace_to(ident, '"', "\\\"", writer)?; + } + IdentifierFormat::Backtick => { + write_identifier_backtick_escape(writer, ident)?; + } + IdentifierFormat::SquareBracket => { + utils::replace_to(ident, ']', "\\]", writer)?; + } + }, + } + + writer.write_char(end) + } + + /// Escape `raw` and wrap it as a complete string literal. + #[inline] + pub fn write_string_literal(&self, writer: &mut W, raw: &str) -> Result<(), FormatError> { + if raw.contains('\0') { + return Err(FormatError::NulInString); + } + + match self.string_format() { + StringFormat::PrefixBigE => writer.write_str("E'")?, + StringFormat::PrefixBigN => writer.write_str("N'")?, + StringFormat::SingleQuote => writer.write_char('\'')?, + } + self.escape_string(writer, raw)?; + writer.write_char('\'')?; + Ok(()) + } + + /// Write a bit-string (`'0'`/`'1'` characters) as a native literal. + /// + /// Native formats embed `bits` as-is. Callers must pass only `'0'` and `'1'`; + /// that is checked with `debug_assert!`. + #[inline] + pub fn write_bits(&self, writer: &mut W, bits: &str) -> Result<(), FormatError> { + debug_assert!( + bits.chars().all(|c| c == '0' || c == '1'), + "write_bits: invalid input: {bits}" + ); + match self.bit_format() { + BitFormat::Unsupported => Err(FormatError::Unsupported("BitSerializer")), + BitFormat::PrefixBigB => write!(writer, "B'{bits}'").map_err(Into::into), + BitFormat::PrefixSmallB => write!(writer, "b'{bits}'").map_err(Into::into), + BitFormat::PostfixBigB => write!(writer, "'{bits}'B").map_err(Into::into), + } + } + + /// Write `bytes` as a native blob/bytea/varbyte literal. + #[inline] + pub fn write_bytes(&self, writer: &mut W, bytes: &[u8]) -> Result<(), FormatError> { + let result = match self.bytes_format() { + BytesFormat::PrefixBigX => { + writer.write_str("X'")?; + bytes_to_hex(writer, bytes)?; + writer.write_char('\'') + } + BytesFormat::PrefixZeroX => { + writer.write_str("0x")?; + bytes_to_hex(writer, bytes) + } + BytesFormat::SlashXBytea => { + writer.write_str("'\\x")?; + bytes_to_hex(writer, bytes)?; + writer.write_str("'::bytea") + } + BytesFormat::PrefixBigBSlashX => { + writer.write_str("B'")?; + write_bytes_slash_x(writer, bytes)?; + writer.write_char('\'') + } + BytesFormat::HexToRaw => { + writer.write_str("HEXTORAW('")?; + bytes_to_hex(writer, bytes)?; + writer.write_str("')") + } + BytesFormat::Unhex => { + writer.write_str("UNHEX('")?; + bytes_to_hex(writer, bytes)?; + writer.write_str("')") + } + BytesFormat::ToVarbyte => { + writer.write_str("TO_VARBYTE('")?; + bytes_to_hex(writer, bytes)?; + writer.write_str("', 'hex')") + } + BytesFormat::PostfixXbv => { + writer.write_char('\'')?; + bytes_to_hex(writer, bytes)?; + writer.write_str("'XBV") + } + BytesFormat::ToBinary => { + writer.write_str("TO_BINARY('")?; + bytes_to_hex(writer, bytes)?; + writer.write_str("', 'HEX')") + } + BytesFormat::SlashXBlob => { + if bytes.is_empty() { + writer.write_str("''::BLOB") + } else { + writer.write_char('\'')?; + write_bytes_slash_x(writer, bytes)?; + writer.write_str("'::BLOB") + } + } + }; + result.map_err(Into::into) + } +} + +const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF"; + +/// Write `byte` as two uppercase hex digits. +#[inline] +fn write_hex2(writer: &mut W, byte: u8) -> std::fmt::Result { + let buf = [HEX_DIGITS[(byte >> 4) as usize], HEX_DIGITS[(byte & 0x0F) as usize]]; + // SAFETY: HEX_DIGITS is ASCII, so `buf` is valid UTF-8. + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf) }) +} + +/// Write `bytes` as a contiguous uppercase hex run (`DEADBEEF`). +/// +/// 64 raw bytes fill a 128-byte stack buffer and are written once. +#[inline] +fn bytes_to_hex(writer: &mut W, bytes: &[u8]) -> std::fmt::Result { + let mut buf = [0u8; 128]; + for chunk in bytes.chunks(64) { + for (i, &byte) in chunk.iter().enumerate() { + let o = i * 2; + buf[o] = HEX_DIGITS[(byte >> 4) as usize]; + buf[o + 1] = HEX_DIGITS[(byte & 0x0F) as usize]; + } + let n = chunk.len() * 2; + // SAFETY: `buf[..n]` is filled only from `HEX_DIGITS` (ASCII). + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) })?; + } + Ok(()) +} + +/// Write each byte as `\xNN` (uppercase hex). +/// +/// 32 raw bytes fill a 128-byte stack buffer and are written once. +#[inline] +fn write_bytes_slash_x(writer: &mut W, bytes: &[u8]) -> std::fmt::Result { + let mut buf = [0u8; 128]; + for chunk in bytes.chunks(32) { + for (i, &byte) in chunk.iter().enumerate() { + let o = i * 4; + buf[o] = b'\\'; + buf[o + 1] = b'x'; + buf[o + 2] = HEX_DIGITS[(byte >> 4) as usize]; + buf[o + 3] = HEX_DIGITS[(byte & 0x0F) as usize]; + } + let n = chunk.len() * 4; + // SAFETY: `buf[..n]` is ASCII (`\`, `x`, and hex digits). + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) })?; + } + Ok(()) +} + +/// Write `\xNN` for a single byte (uppercase hex). +#[inline] +fn write_byte_hex_escape(writer: &mut W, byte: u8) -> std::fmt::Result { + writer.write_str("\\x")?; + write_hex2(writer, byte) +} + +/// Write `\uNNNN` for a codepoint that fits in 16 bits. +#[inline] +fn write_unicode4_escape(writer: &mut W, code: u32) -> std::fmt::Result { + debug_assert!(code <= 0xFFFF); + writer.write_str("\\u")?; + write_hex2(writer, (code >> 8) as u8)?; + write_hex2(writer, code as u8) +} + +/// Use the string escape table, plus `\``. +/// +/// `'` is left as itself. Only known escapes are emitted. +fn write_identifier_backtick_escape(writer: &mut W, ident: &str) -> std::fmt::Result { + write_span_escapes(writer, ident, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'`' => SpanEscape::Literal("\\`"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} + +/// Write `bytes[from..to]` as a string. +/// +/// # Safety invariant +/// `bytes` is the raw view of a `&str`. Callers split only on ASCII bytes, +/// which are char boundaries, or at `bytes.len()`. +#[inline] +fn write_byte_span(writer: &mut W, bytes: &[u8], from: usize, to: usize) -> std::fmt::Result { + if from == to { + return Ok(()); + } + // SAFETY: see invariant above. + let chunk = unsafe { bytes.get_unchecked(from..to) }; + writer.write_str(unsafe { std::str::from_utf8_unchecked(chunk) }) +} + +enum SpanEscape { + Keep, + Literal(&'static str), + HexByte, + Unicode4, +} + +/// Copy each plain span with one `write_str`. Special bytes are ASCII. +#[cfg_attr(feature = "nightly", optimize(speed))] +fn write_span_escapes( + writer: &mut W, + string: &str, + mut classify: impl FnMut(u8) -> SpanEscape, +) -> std::fmt::Result { + let bytes = string.as_bytes(); + let mut start = 0; + let mut i = 0; + while i < bytes.len() { + match classify(bytes[i]) { + SpanEscape::Keep => i += 1, + SpanEscape::Literal(repl) => { + write_byte_span(writer, bytes, start, i)?; + writer.write_str(repl)?; + i += 1; + start = i; + } + SpanEscape::HexByte => { + write_byte_span(writer, bytes, start, i)?; + write_byte_hex_escape(writer, bytes[i])?; + i += 1; + start = i; + } + SpanEscape::Unicode4 => { + write_byte_span(writer, bytes, start, i)?; + write_unicode4_escape(writer, bytes[i] as u32)?; + i += 1; + start = i; + } + } + } + write_byte_span(writer, bytes, start, bytes.len()) +} + +fn write_backslash_hex(writer: &mut W, string: &str) -> std::fmt::Result { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_mysql(writer: &mut W, string: &str) -> std::fmt::Result { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'"' => SpanEscape::Literal("\\\""), + 0 => SpanEscape::Literal("\\0"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + 0x1a => SpanEscape::Literal("\\Z"), + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_unicode( + writer: &mut W, + string: &str, + escape_double_quote: bool, +) -> std::fmt::Result { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'"' if escape_double_quote => SpanEscape::Literal("\\\""), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + 0 => SpanEscape::Literal("\\0"), + b if b < 32 || b == 127 => SpanEscape::Unicode4, + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_hex_zero(writer: &mut W, string: &str) -> std::fmt::Result { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + 0 => SpanEscape::Literal("\\0"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs new file mode 100644 index 0000000..0c12063 --- /dev/null +++ b/src/dialect/mod.rs @@ -0,0 +1,86 @@ +pub mod errors; +pub mod flags; + +mod base; +mod builtin; + +#[cfg(test)] +mod tests; + +pub use base::PyDialect; +pub use builtin::BIGQUERY_DIALECT; +pub use builtin::DATABRICKS_DIALECT; +pub use builtin::DUCKDB_DIALECT; +pub use builtin::HIVE_DIALECT; +pub use builtin::MSSQL_DIALECT; +pub use builtin::MYSQL_DIALECT; +pub use builtin::ORACLE_DIALECT; +pub use builtin::POSTGRESQL_DIALECT; +pub use builtin::REDSHIFT_DIALECT; +pub use builtin::SNOWFLAKE_DIALECT; +pub use builtin::SPARK_DIALECT; +pub use builtin::SQLITE_DIALECT; +pub use builtin::TERADATA_DIALECT; + +#[pyo3::pymodule(name = "dialect", gil_used = false)] +pub mod dialect_mod { + #[pymodule_export] + use super::PyDialect; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const BigQuery: super::PyDialect = super::BIGQUERY_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Databricks: super::PyDialect = super::DATABRICKS_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const DuckDb: super::PyDialect = super::DUCKDB_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Hive: super::PyDialect = super::HIVE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const MsSql: super::PyDialect = super::MSSQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const MySql: super::PyDialect = super::MYSQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Oracle: super::PyDialect = super::ORACLE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const PostgreSql: super::PyDialect = super::POSTGRESQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Redshift: super::PyDialect = super::REDSHIFT_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Snowflake: super::PyDialect = super::SNOWFLAKE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Spark: super::PyDialect = super::SPARK_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Sqlite: super::PyDialect = super::SQLITE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Teradata: super::PyDialect = super::TERADATA_DIALECT; + + #[pymodule_init] + fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + super::base::export_options(m) + } +} diff --git a/src/dialect/tests.rs b/src/dialect/tests.rs new file mode 100644 index 0000000..28b71f5 --- /dev/null +++ b/src/dialect/tests.rs @@ -0,0 +1,733 @@ +use super::*; + +#[inline] +fn bool_flags(format: flags::BoolFormat) -> flags::DialectFlags { + flags::DialectFlags::new().with_bool_format(format) +} + +#[inline] +fn string_flags(format: flags::StringFormat, escape: flags::StringEscape) -> flags::DialectFlags { + flags::DialectFlags::new() + .with_string_format(format) + .with_string_escape(escape) +} + +#[inline] +fn ident_flags(format: flags::IdentifierFormat, escape: flags::IdentifierEscape) -> flags::DialectFlags { + flags::DialectFlags::new() + .with_identifier_format(format) + .with_identifier_escape(escape) +} + +fn write_str(flags: &flags::DialectFlags, raw: &str) -> Result { + let mut out = String::new(); + flags.write_string_literal(&mut out, raw)?; + Ok(out) +} + +fn escape(flags: &flags::DialectFlags, raw: &str) -> String { + let mut out = String::new(); + flags.escape_string(&mut out, raw).unwrap(); + out +} + +fn quote_ident(flags: &flags::DialectFlags, ident: &str) -> String { + let mut out = String::new(); + flags.write_identifier(&mut out, ident).unwrap(); + out +} + +/// A writer that fails on the Nth `write_str` / `write_char` (1-based). +struct FailAfter { + remaining: usize, +} + +impl std::fmt::Write for FailAfter { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + if s.is_empty() { + return Ok(()); + } + if self.remaining == 0 { + return Err(std::fmt::Error); + } + self.remaining -= 1; + Ok(()) + } +} + +/// Standard-SQL string lexer: quotes doubled, no backslash escapes. +/// Returns the unescaped body if `sql` is exactly one literal. +fn parse_quote_doubled(sql: &str) -> Option { + let rest = sql.strip_prefix('\'')?; + let mut body = String::new(); + let mut chars = rest.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\'' { + if chars.peek() == Some(&'\'') { + chars.next(); + body.push('\''); + } else { + return chars.next().is_none().then_some(body); + } + } else { + body.push(ch); + } + } + None +} + +/// MySQL-style backslash lexer (NO_BACKSLASH_ESCAPES off). Unknown escapes +/// keep the escaped byte. Returns the body if the literal closes exactly once. +fn parse_backslash(sql: &str) -> Option { + let rest = sql.strip_prefix('\'')?; + let bytes = rest.as_bytes(); + let mut i = 0; + let mut body = Vec::new(); + while i < bytes.len() { + match bytes[i] { + b'\'' => { + i += 1; + return (i == bytes.len()).then(|| String::from_utf8(body).ok()).flatten(); + } + b'\\' => { + if i + 1 >= bytes.len() { + return None; + } + body.push(bytes[i + 1]); + i += 2; + } + b => { + body.push(b); + i += 1; + } + } + } + None +} + +fn assert_no_early_close_quote(escaped: &str) { + let mut chars = escaped.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\'' { + assert_eq!( + chars.next(), + Some('\''), + "odd quote in quote-doubled escape: {escaped:?}" + ); + } + } +} + +const BREAKOUTS: &[&str] = &[ + "'", + "''", + "' OR '1'='1", + "' OR 1=1 --", + "'; DROP TABLE users; --", + "\\", + "\\'", + "\\\\", + "'''", + "admin'--", + "1' AND SLEEP(1)--", + "\" OR \"1\"=\"1", + "') OR ('1'='1", + "\n' OR 1=1 --", + "\r\n", + "foo\tbar", + "%' OR 1=1 --", + "_", + "/*", + "*/", + "--", + "#", + "\u{1a}", + "\u{7f}", + "\u{01}", + "café", + "I ❤️ SQL", + "\u{202e}1=1 RO", + "’", // U+2019, not a SQL quote + "'", // U+FF0C fullwidth is not the quote; fullwidth apostrophe is U+FF07 + "'", + "\u{ff07}", +]; + +#[test] +fn write_boolean_keyword_and_integer() { + let keyword = bool_flags(flags::BoolFormat::Keyword); + let integer = bool_flags(flags::BoolFormat::Integer); + + let mut out = String::new(); + keyword.write_boolean(&mut out, true).unwrap(); + keyword.write_boolean(&mut out, false).unwrap(); + assert_eq!(out, "TRUEFALSE"); + + out.clear(); + integer.write_boolean(&mut out, true).unwrap(); + integer.write_boolean(&mut out, false).unwrap(); + assert_eq!(out, "10"); +} + +#[test] +fn write_boolean_does_not_emit_quotes_or_lowercase() { + // MySQL may accept the integers; neither spelling is a string, so a + // caller that interpolates them into a quoted context would be wrong. + for value in [false, true] { + let mut keyword = String::new(); + let mut integer = String::new(); + bool_flags(flags::BoolFormat::Keyword) + .write_boolean(&mut keyword, value) + .unwrap(); + + bool_flags(flags::BoolFormat::Integer) + .write_boolean(&mut integer, value) + .unwrap(); + + assert!(!keyword.contains('\'')); + assert!(!integer.contains('\'')); + assert_eq!(keyword, keyword.to_ascii_uppercase()); + assert!(integer == "0" || integer == "1"); + } +} + +#[test] +fn write_boolean_propagates_writer_errors() { + let mut writer = FailAfter { remaining: 0 }; + let err = bool_flags(flags::BoolFormat::Keyword) + .write_boolean(&mut writer, true) + .unwrap_err(); + + assert_eq!(err, errors::FormatError::Write); +} + +#[test] +fn escape_string_quote_doubles_and_leaves_the_rest() { + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::Quote); + assert_eq!(escape(&flags, ""), ""); + assert_eq!(escape(&flags, "plain"), "plain"); + assert_eq!(escape(&flags, "'"), "''"); + assert_eq!(escape(&flags, "''"), "''''"); + assert_eq!(escape(&flags, "a'b'c"), "a''b''c"); + // Backslash is data under standard SQL. Leaving it alone is correct for + // Postgres and wrong for MySQL with backslash escapes enabled. + assert_eq!(escape(&flags, "\\'"), "\\''"); + assert_eq!(escape(&flags, "\0"), "\0"); + assert_eq!(escape(&flags, "\n\r\t"), "\n\r\t"); + assert_eq!(escape(&flags, "100%_"), "100%_"); + assert_eq!(escape(&flags, "雪's"), "雪''s"); +} + +#[test] +fn escape_string_backslash_hex_covers_c0_and_del() { + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::BackslashHex); + assert_eq!(escape(&flags, "\\"), "\\\\"); + assert_eq!(escape(&flags, "'"), "\\'"); + assert_eq!(escape(&flags, "\n\r\t"), "\\n\\r\\t"); + assert_eq!(escape(&flags, "\0"), "\\x00"); + assert_eq!(escape(&flags, "\u{1}"), "\\x01"); + assert_eq!(escape(&flags, "\u{1a}"), "\\x1A"); + assert_eq!(escape(&flags, "\u{1f}"), "\\x1F"); + assert_eq!(escape(&flags, "\u{7f}"), "\\x7F"); + // Space, DEL-adjacent printable, and non-ASCII stay raw. Splitting a + // multibyte char here would corrupt the literal (and the unsafe span + // writer assumes ASCII split points). + assert_eq!(escape(&flags, " ~"), " ~"); + assert_eq!(escape(&flags, "雪's\\"), "雪\\'s\\\\"); + assert_eq!(escape(&flags, "\"quote"), "\"quote"); +} + +#[test] +fn escape_string_mysql_keeps_tab_and_uses_z_for_sub() { + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::BackslashMysql); + assert_eq!(escape(&flags, "\\'\""), "\\\\\\'\\\""); + assert_eq!(escape(&flags, "\0"), "\\0"); + assert_eq!(escape(&flags, "\n\r"), "\\n\\r"); + assert_eq!(escape(&flags, "\t"), "\t"); + assert_eq!(escape(&flags, "\u{1a}"), "\\Z"); + // Other C0 bytes pass through. A strict ANSI client may reject them; + // MySQL's NO_BACKSLASH_ESCAPES-off lexer does not treat them as quotes. + assert_eq!(escape(&flags, "\u{01}"), "\u{01}"); + assert_eq!(escape(&flags, "\u{7f}"), "\u{7f}"); + assert_eq!(escape(&flags, "it\'s"), "it\\'s"); +} + +#[test] +fn escape_string_unicode_and_hex_zero_disagree_on_nul_and_quotes() { + let unicode = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::BackslashUnicode); + let unicode_quote = string_flags( + flags::StringFormat::SingleQuote, + flags::StringEscape::BackslashUnicodeQuote, + ); + let hex_zero = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::BackslashHexZero); + + assert_eq!(escape(&unicode, "\0"), "\\0"); + assert_eq!(escape(&unicode, "\u{01}"), "\\u0001"); + assert_eq!(escape(&unicode, "\u{1a}"), "\\u001A"); + assert_eq!(escape(&unicode, "\u{7f}"), "\\u007F"); + assert_eq!(escape(&unicode, "\"'"), "\"\\'"); + assert_eq!(escape(&unicode_quote, "\"'"), "\\\"\\'"); + assert_eq!(escape(&hex_zero, "\0"), "\\0"); + assert_eq!(escape(&hex_zero, "\u{01}"), "\\x01"); + assert_eq!(escape(&hex_zero, "\u{7f}"), "\\x7F"); + assert_eq!(escape(&hex_zero, "\t\n"), "\\t\\n"); +} + +#[test] +fn escape_string_does_not_wrap_or_reject_nul() { + // NUL rejection belongs to `write_string_literal`. `escape_string` is the + // inner transform and must stay usable for already-open literals. + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::Quote); + let escaped = escape(&flags, "a\0b"); + assert_eq!(escaped, "a\0b"); + assert!(!escaped.starts_with('\'')); +} + +#[test] +fn write_string_literal_prefixes_and_empty() { + let cases = [ + (flags::StringFormat::SingleQuote, "''"), + (flags::StringFormat::PrefixBigE, "E''"), + (flags::StringFormat::PrefixBigN, "N''"), + ]; + for (format, expected) in cases { + let flags = string_flags(format, flags::StringEscape::Quote); + assert_eq!(write_str(&flags, "").unwrap(), expected); + assert_eq!(write_str(&flags, "a").unwrap(), expected.replace("''", "'a'")); + } +} + +#[test] +fn write_string_literal_rejects_nul_before_writing() { + let mut out = String::from("kept"); + let flags = string_flags(flags::StringFormat::PrefixBigE, flags::StringEscape::BackslashHex); + let err = flags.write_string_literal(&mut out, "a\0b").unwrap_err(); + assert_eq!(err, errors::FormatError::NulInString); + // C drivers (historical libmysql) truncate at NUL. Refusing before any + // write avoids a half-emitted literal that a caller might still send. + assert_eq!(out, "kept"); + assert!(flags.write_string_literal(&mut String::new(), "\0").is_err()); +} + +#[test] +fn write_string_literal_quote_mode_survives_classic_payloads() { + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::Quote); + for payload in BREAKOUTS { + if payload.contains('\0') { + continue; + } + let sql = write_str(&flags, payload).unwrap(); + let body = parse_quote_doubled(&sql).unwrap_or_else(|| panic!("quote breakout for {payload:?} -> {sql:?}")); + assert_eq!(body, *payload); + assert_no_early_close_quote(&sql[1..sql.len() - 1]); + } + assert_eq!(write_str(&flags, "' OR '1'='1").unwrap(), "''' OR ''1''=''1'"); + assert_eq!( + write_str(&flags, "'; DROP TABLE users; --").unwrap(), + "'''; DROP TABLE users; --'" + ); +} + +#[test] +fn write_string_literal_backslash_modes_do_not_end_on_backslash_quote() { + // The MySQL bug: escaping only `'` as `\'` (or not escaping `\`) lets + // `\'` close the string. Every backslash mode must escape `\` first. + let modes = [ + flags::StringEscape::BackslashHex, + flags::StringEscape::BackslashMysql, + flags::StringEscape::BackslashUnicode, + flags::StringEscape::BackslashHexZero, + flags::StringEscape::BackslashUnicodeQuote, + ]; + for mode in modes { + let flags = string_flags(flags::StringFormat::SingleQuote, mode); + for payload in ["\\'", "\\\\'", "'", "\\", "a\\'b", "\n' OR 1=1 --"] { + let sql = write_str(&flags, payload).unwrap(); + assert!( + parse_backslash(&sql).is_some(), + "{mode:?} breakout for {payload:?} -> {sql:?}" + ); + // `\'` must not be a closer. The backslash is escaped, then the quote. + if payload.contains('\\') { + assert!(sql.contains("\\\\"), "{mode:?} left a raw backslash in {sql:?}"); + } + } + } +} + +#[test] +fn write_string_literal_quote_mode_is_not_safe_for_mysql_backslash() { + // Documents the mismatch. Under a MySQL lexer, quote-doubling alone does + // not neutralize a leading backslash: `\'` becomes `\''`, which is an + // escaped quote plus a closer, i.e. an empty string and trailing junk. + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::Quote); + let sql = write_str(&flags, "\\' OR 1=1 --").unwrap(); + assert_eq!(sql, "'\\'' OR 1=1 --'"); + assert!(parse_backslash(&sql).is_none()); +} + +#[test] +fn write_string_literal_national_and_escape_prefixes_still_close() { + let e = string_flags(flags::StringFormat::PrefixBigE, flags::StringEscape::BackslashHex); + let n = string_flags(flags::StringFormat::PrefixBigN, flags::StringEscape::Quote); + assert_eq!(write_str(&e, "a'b\\").unwrap(), "E'a\\'b\\\\'"); + assert_eq!(write_str(&n, "a'b").unwrap(), "N'a''b'"); + // Prefix is outside the quotes. A payload must not be able to eat it. + assert!(write_str(&e, "x").unwrap().starts_with("E'")); + assert!(write_str(&n, "x").unwrap().starts_with("N'")); +} + +#[test] +fn write_string_literal_passes_unicode_quotes_and_like_wildcards() { + let flags = string_flags(flags::StringFormat::SingleQuote, flags::StringEscape::Quote); + // U+2019 / U+FF07 are not the ASCII delimiter. Treating them as quotes + // would corrupt data; not treating ASCII `'` as a quote would be SQLi. + assert_eq!(write_str(&flags, "it’s").unwrap(), "'it’s'"); + assert_eq!(write_str(&flags, "\u{ff07}").unwrap(), "'\u{ff07}'"); + assert_eq!(write_str(&flags, "100%_").unwrap(), "'100%_'"); + // U+202E (right-to-left override) can spoof a log line. It is still data. + let spoof = write_str(&flags, "\u{202e}--").unwrap(); + assert_eq!(spoof, "'\u{202e}--'"); +} + +#[test] +fn write_identifier_doubles_only_its_own_closer() { + let dq = ident_flags(flags::IdentifierFormat::DoubleQuote, flags::IdentifierEscape::Double); + let bt = ident_flags(flags::IdentifierFormat::Backtick, flags::IdentifierEscape::Double); + let br = ident_flags(flags::IdentifierFormat::SquareBracket, flags::IdentifierEscape::Double); + + assert_eq!(quote_ident(&dq, ""), "\"\""); + assert_eq!(quote_ident(&dq, "users"), "\"users\""); + assert_eq!(quote_ident(&dq, "a\"b"), "\"a\"\"b\""); + assert_eq!(quote_ident(&dq, "\""), "\"\"\"\""); + // The other delimiters are data. This is the Postgres / ANSI contract. + assert_eq!(quote_ident(&dq, "a`b]c'"), "\"a`b]c'\""); + + assert_eq!(quote_ident(&bt, "a`b"), "`a``b`"); + assert_eq!(quote_ident(&bt, "`"), "````"); + assert_eq!(quote_ident(&bt, "a\"b"), "`a\"b`"); + + assert_eq!(quote_ident(&br, "a]b"), "[a]]b]"); + assert_eq!(quote_ident(&br, "]"), "[]]]"); + assert_eq!(quote_ident(&br, "a[b"), "[a[b]"); + // SQL Server does not treat `[` as special inside brackets. Escaping it + // would change the identifier. + assert_eq!(quote_ident(&br, "dbo.users"), "[dbo.users]"); +} + +#[test] +fn write_identifier_rejects_delimiter_breakout() { + let samples = [ + ( + ident_flags(flags::IdentifierFormat::DoubleQuote, flags::IdentifierEscape::Double), + "\"", + "\"", + '"', + ), + ( + ident_flags(flags::IdentifierFormat::Backtick, flags::IdentifierEscape::Double), + "`", + "`", + '`', + ), + ( + ident_flags(flags::IdentifierFormat::SquareBracket, flags::IdentifierEscape::Double), + "]", + "[", + ']', + ), + ]; + for (flags, closer, start, end) in samples { + for payload in [ + closer.to_string(), + format!("{closer}{closer}"), + format!("x{closer} OR 1=1 --"), + format!("{closer}; DROP TABLE users; --"), + ] { + let sql = quote_ident(&flags, &payload); + assert!( + sql.starts_with(start) && sql.ends_with(end), + "delimiters lost in {sql:?}" + ); + // Doubled closer never leaves a raw closer at an odd position. + let inner = &sql[start.len()..sql.len() - end.len_utf8()]; + let mut rest = inner; + let needle = closer.to_string(); + while let Some(at) = rest.find(&needle) { + assert!( + rest[at + needle.len()..].starts_with(&needle), + "undoubled closer in {sql:?}" + ); + rest = &rest[at + needle.len() * 2..]; + } + } + } +} + +#[test] +fn write_identifier_backslash_modes() { + let dq = ident_flags(flags::IdentifierFormat::DoubleQuote, flags::IdentifierEscape::Backslash); + let bt = ident_flags(flags::IdentifierFormat::Backtick, flags::IdentifierEscape::Backslash); + let br = ident_flags( + flags::IdentifierFormat::SquareBracket, + flags::IdentifierEscape::Backslash, + ); + + assert_eq!(quote_ident(&dq, "a\"b"), "\"a\\\"b\""); + assert_eq!(quote_ident(&dq, "\\"), "\"\\\""); + // Standard SQL does not treat `\` as an identifier escape. `\"` still + // contains a raw closer, so this mode is only safe on engines that + // consume the backslash inside quoted identifiers (MySQL ANSI_QUOTES). + assert_eq!(quote_ident(&dq, "\""), "\"\\\"\""); + + assert_eq!(quote_ident(&bt, "a`b"), "`a\\`b`"); + assert_eq!(quote_ident(&bt, "\\"), "`\\\\`"); + assert_eq!(quote_ident(&bt, "\n\r\t"), "`\\n\\r\\t`"); + assert_eq!(quote_ident(&bt, "\u{01}"), "`\\x01`"); + assert_eq!(quote_ident(&bt, "\u{7f}"), "`\\x7F`"); + // Apostrophe is not the closer. Escaping it would change the name. + assert_eq!(quote_ident(&bt, "it's"), "`it's`"); + + assert_eq!(quote_ident(&br, "a]b"), "[a\\]b]"); + assert_eq!(quote_ident(&br, "]"), "[\\]]"); +} + +#[test] +fn write_identifier_trailing_backslash_can_eat_a_backslash_closer() { + // If the engine treats `\` as an escape, a trailing backslash escapes the + // closing delimiter and the next source token is absorbed into the name. + // Doubling mode does not have this hole. Callers must not pair + // `flags::IdentifierEscape::Backslash` with an engine that ignores backslashes, + // and must reject or double a trailing `\` on engines that honor it. + let bt = ident_flags(flags::IdentifierFormat::Backtick, flags::IdentifierEscape::Backslash); + let br = ident_flags( + flags::IdentifierFormat::SquareBracket, + flags::IdentifierEscape::Backslash, + ); + assert_eq!(quote_ident(&bt, "foo\\"), "`foo\\\\`"); + assert_eq!(quote_ident(&br, "foo\\"), "[foo\\]"); + assert!(quote_ident(&br, "foo\\").ends_with("\\]")); +} + +#[test] +fn write_identifier_comments_and_dots_stay_inside() { + let dq = ident_flags(flags::IdentifierFormat::DoubleQuote, flags::IdentifierEscape::Double); + // A newline inside a quoted identifier is legal in some engines and a + // line-comment introducer in others if the closer is lost. Doubling keeps + // the closer, so `--` cannot comment out the rest of the statement. + assert_eq!(quote_ident(&dq, "a\n--\nb"), "\"a\n--\nb\""); + assert_eq!(quote_ident(&dq, "a.b"), "\"a.b\""); + assert_eq!(quote_ident(&dq, "users"), "\"users\""); +} + +#[test] +fn write_bits_spellings_and_unsupported() { + let mut out = String::new(); + let err = flags::DialectFlags::new() + .with_bit_format(flags::BitFormat::Unsupported) + .write_bits(&mut out, "101") + .unwrap_err(); + + assert_eq!(err, errors::FormatError::Unsupported("BitSerializer")); + assert!(out.is_empty(), "unsupported must not write a partial literal"); + + let cases = [ + (flags::BitFormat::PrefixBigB, "B'101'"), + (flags::BitFormat::PrefixSmallB, "b'101'"), + (flags::BitFormat::PostfixBigB, "'101'B"), + ]; + for (format, expected) in cases { + out.clear(); + flags::DialectFlags::new() + .with_bit_format(format) + .write_bits(&mut out, "101") + .unwrap(); + assert_eq!(out, expected); + out.clear(); + flags::DialectFlags::new() + .with_bit_format(format) + .write_bits(&mut out, "") + .unwrap(); + assert_eq!(out, expected.replace("101", "")); + } +} + +#[test] +fn write_bits_embeds_input_unchanged() { + // Release builds do not re-check the alphabet. A `'` in `bits` would + // close the literal (`B'''; DROP`), so the debug assert is the guard. + let mut out = String::new(); + flags::DialectFlags::new() + .with_bit_format(flags::BitFormat::PrefixBigB) + .write_bits(&mut out, "0") + .unwrap(); + assert_eq!(out, "B'0'"); + out.clear(); + flags::DialectFlags::new() + .with_bit_format(flags::BitFormat::PostfixBigB) + .write_bits(&mut out, "1") + .unwrap(); + assert_eq!(out, "'1'B"); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "write_bits: invalid input")] +fn write_bits_panics_on_quote_in_debug() { + let mut out = String::new(); + let _ = flags::DialectFlags::new() + .with_bit_format(flags::BitFormat::PrefixBigB) + .write_bits(&mut out, "1'; DROP TABLE t; --"); +} + +#[test] +fn write_bytes_every_spelling_including_empty_and_quote_byte() { + // 0x27 is ASCII `'`. Hex spellings must emit `27`, never a raw quote. + let quote_byte = [0x27]; + let dead = [0xDE, 0xAD, 0xBE, 0xEF]; + let cases: [(flags::BytesFormat, &str, &str); 10] = [ + (flags::BytesFormat::PrefixBigX, "X''", "X'27'"), + (flags::BytesFormat::PrefixZeroX, "0x", "0x27"), + (flags::BytesFormat::SlashXBytea, "'\\x'::bytea", "'\\x27'::bytea"), + (flags::BytesFormat::PrefixBigBSlashX, "B''", "B'\\x27'"), + (flags::BytesFormat::HexToRaw, "HEXTORAW('')", "HEXTORAW('27')"), + (flags::BytesFormat::Unhex, "UNHEX('')", "UNHEX('27')"), + ( + flags::BytesFormat::ToVarbyte, + "TO_VARBYTE('', 'hex')", + "TO_VARBYTE('27', 'hex')", + ), + (flags::BytesFormat::PostfixXbv, "''XBV", "'27'XBV"), + ( + flags::BytesFormat::ToBinary, + "TO_BINARY('', 'HEX')", + "TO_BINARY('27', 'HEX')", + ), + (flags::BytesFormat::SlashXBlob, "''::BLOB", "'\\x27'::BLOB"), + ]; + for (format, empty, quoted) in cases { + let flags = flags::DialectFlags::new().with_bytes_format(format); + let mut out = String::new(); + flags.write_bytes(&mut out, &[]).unwrap(); + assert_eq!(out, empty, "{format:?} empty"); + out.clear(); + flags.write_bytes(&mut out, "e_byte).unwrap(); + assert_eq!(out, quoted, "{format:?} quote byte"); + assert!( + !out.contains("''") || format == flags::BytesFormat::SlashXBlob || out.starts_with("''"), + "raw quote leaked into {out}" + ); + out.clear(); + flags.write_bytes(&mut out, &dead).unwrap(); + assert!( + out.contains("DEADBEEF") || out.contains("\\xDE\\xAD\\xBE\\xEF"), + "{out}" + ); + assert!(!out.contains("deadbeef"), "hex must be uppercase: {out}"); + } +} + +#[test] +fn write_bytes_slash_x_blob_empty_is_not_a_slash_escape() { + // Empty `'\x'::BLOB` would be a one-byte escape, not an empty blob. + let flags = flags::DialectFlags::new().with_bytes_format(flags::BytesFormat::SlashXBlob); + let mut out = String::new(); + flags.write_bytes(&mut out, &[]).unwrap(); + assert_eq!(out, "''::BLOB"); + out.clear(); + flags.write_bytes(&mut out, &[0x00]).unwrap(); + assert_eq!(out, "'\\x00'::BLOB"); +} + +#[test] +fn write_bytes_crosses_hex_and_slash_chunk_boundaries() { + let sixty_five = vec![0xABu8; 65]; + let thirty_three = vec![0xFFu8; 33]; + + let mut hex = String::new(); + flags::DialectFlags::new() + .with_bytes_format(flags::BytesFormat::PrefixBigX) + .write_bytes(&mut hex, &sixty_five) + .unwrap(); + + assert_eq!(hex.len(), 2 + 65 * 2 + 1); + assert!(hex.starts_with("X'") && hex.ends_with('\'')); + assert_eq!(&hex[2..hex.len() - 1], &"AB".repeat(65)); + + let mut slash = String::new(); + flags::DialectFlags::new() + .with_bytes_format(flags::BytesFormat::PrefixBigBSlashX) + .write_bytes(&mut slash, &thirty_three) + .unwrap(); + + assert_eq!(slash, format!("B'{}'", "\\xFF".repeat(33))); + + let mut blob = String::new(); + flags::DialectFlags::new() + .with_bytes_format(flags::BytesFormat::SlashXBlob) + .write_bytes(&mut blob, &thirty_three) + .unwrap(); + assert_eq!(blob, format!("'{}'::BLOB", "\\xFF".repeat(33))); +} + +#[test] +fn write_bytes_zero_prefix_empty_is_not_a_closed_literal() { + // `0x` with no digits is rejected by most engines. Locked so a caller + // that needs an empty blob picks another spelling. + let mut out = String::new(); + flags::DialectFlags::new() + .with_bytes_format(flags::BytesFormat::PrefixZeroX) + .write_bytes(&mut out, &[]) + .unwrap(); + + assert_eq!(out, "0x"); +} + +#[test] +fn write_bytes_function_wrappers_cannot_be_closed_by_payload() { + // The hex alphabet is generated, not copied, so a byte cannot inject a + // quote into HEXTORAW / UNHEX / TO_BINARY / TO_VARBYTE. + let nasty = [0x27, 0x5C, 0x00, 0x22, 0x3B]; + for format in [ + flags::BytesFormat::HexToRaw, + flags::BytesFormat::Unhex, + flags::BytesFormat::ToVarbyte, + flags::BytesFormat::ToBinary, + ] { + let mut out = String::new(); + flags::DialectFlags::new() + .with_bytes_format(format) + .write_bytes(&mut out, &nasty) + .unwrap(); + + assert!(out.contains("275C00223B"), "{format:?} -> {out}"); + let hex = out + .split('\'') + .nth(1) + .unwrap_or_else(|| panic!("missing hex literal in {out}")); + + assert!( + hex.chars().all(|c| c.is_ascii_hexdigit()), + "{format:?} leaked non-hex into {out}" + ); + assert_eq!(hex, "275C00223B"); + } +} + +#[test] +fn writer_errors_surface_as_format_error() { + let flags = string_flags(flags::StringFormat::PrefixBigN, flags::StringEscape::Quote); + let mut writer = FailAfter { remaining: 0 }; + assert_eq!( + flags.write_string_literal(&mut writer, "x").unwrap_err(), + errors::FormatError::Write + ); + + let mut writer = FailAfter { remaining: 0 }; + assert!( + flags + .with_bit_format(flags::BitFormat::PrefixBigB) + .write_bits(&mut writer, "1") + .is_err() + ); +} diff --git a/src/dml/mod.rs b/src/dml/mod.rs new file mode 100644 index 0000000..e5a6710 --- /dev/null +++ b/src/dml/mod.rs @@ -0,0 +1,12 @@ +//! Data Manipulation Language + +/// Base of every DML statement. Construct a subclass, not this class. +#[rapidquery_macros::pyclass("rapidquery.dml.DataManipulationLanguage", subclass, skip_from_py_object)] +#[derive(Debug, Clone, Copy)] +pub struct PyDataManipulationLanguage; + +#[pyo3::pymodule(name = "dml", gil_used = false)] +pub mod dml_mod { + #[pymodule_export] + use super::PyDataManipulationLanguage; +} diff --git a/src/expr/base.rs b/src/expr/base.rs new file mode 100644 index 0000000..332d070 --- /dev/null +++ b/src/expr/base.rs @@ -0,0 +1,606 @@ +//! SQL expressions. +//! +//! [`PyExpr`] holds [`ExprInner`]. Concrete nodes (`Ident`, `Binary`, …) are +//! empty frozen subclasses. Shared methods live on the base. + +use crate::internal::alias; +use crate::internal::sql; +use crate::internal::utils; + +use super::enum_; + +/// Base of every SQL expression. Construct a subclass, not this class. +#[rapidquery_macros::pyclass("rapidquery.expr.Expr", subclass, skip_from_py_object)] +#[derive(Debug, Clone)] +pub struct PyExpr(pub enum_::ExprInner); + +#[inline(always)] +fn wrap<'py, T>(py: pyo3::Python<'py>, base: PyExpr) -> pyo3::PyResult> +where + T: pyo3::PyClass + Default, +{ + pyo3::Bound::new(py, pyo3::PyClassInitializer::from(base).add_subclass(T::default())).map(|x| x.into_any()) +} + +/// Wrap `expr` as the matching Python subclass. +/// +/// Shorthand for `PyExpr(expr).into_subclasses(py)` +#[inline(always)] +pub(super) fn into_subclasses<'py>( + py: pyo3::Python<'py>, + expr: enum_::ExprInner, +) -> pyo3::PyResult> { + PyExpr(expr).into_subclasses(py) +} + +impl PyExpr { + /// Wrap `self` as the matching Python subclass. + #[inline] + pub fn into_subclasses<'py>(self, py: pyo3::Python<'py>) -> pyo3::PyResult> { + match &self.0 { + enum_::ExprInner::Ident(_) => wrap::(py, self), + enum_::ExprInner::Value(_) => wrap::(py, self), + enum_::ExprInner::Tuple(_) => wrap::(py, self), + enum_::ExprInner::Unary(_) => wrap::(py, self), + enum_::ExprInner::Binary(_) => wrap::(py, self), + enum_::ExprInner::Function(_) => wrap::(py, self), + enum_::ExprInner::Is(_) => wrap::(py, self), + enum_::ExprInner::IsDistinctFrom(_) => wrap::(py, self), + enum_::ExprInner::InList(_) => wrap::(py, self), + enum_::ExprInner::Between(_) => wrap::(py, self), + enum_::ExprInner::Like(_) => wrap::(py, self), + enum_::ExprInner::SimilarTo(_) => wrap::(py, self), + enum_::ExprInner::RLike(_) => wrap::(py, self), + enum_::ExprInner::Cast(_) => wrap::(py, self), + enum_::ExprInner::Trim(_) => wrap::(py, self), + enum_::ExprInner::Collate(_) => wrap::(py, self), + enum_::ExprInner::Case(_) => wrap::(py, self), + enum_::ExprInner::Custom(_) => wrap::(py, self), + } + } +} + +/// `None`, exact `bool`, or exact `str` → an `IS` expression. +fn is_from_kind<'py>(expr: enum_::ExprInner, kind: &alias::BoundObject<'py>, negated: bool) -> pyo3::PyResult { + let ptr = kind.as_ptr(); + + // SAFETY: `ptr` is a live borrowed object and this runs with the GIL held. + // The checks only read the type tag. The string borrow lasts until `new_base` returns. + unsafe { + if pyo3::ffi::Py_IsNone(ptr) == 1 { + return super::nodes::PyIsExpr::new_base(expr, "NULL", negated); + } + + if pyo3::ffi::PyBool_Check(ptr) == 1 { + let name = if ptr == pyo3::ffi::Py_True() { "TRUE" } else { "FALSE" }; + return super::nodes::PyIsExpr::new_base(expr, name, negated); + } + + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + let text = utils::borrow_py_str_buffer(kind.py(), ptr)?; + return super::nodes::PyIsExpr::new_base(expr, text, negated); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected None, bool, or str, got {}", + utils::get_type_name(kind.py(), ptr) + ) + } +} + +#[pyo3::pymethods] +impl PyExpr { + // ===== Binary Expressions ===== + + /// e.g. `a + b` + fn __add__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Add, other.0).into_subclasses(py) + } + + /// e.g. `a - b` + fn __sub__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Sub, other.0).into_subclasses(py) + } + + /// e.g. `a * b` + fn __mul__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Mul, other.0).into_subclasses(py) + } + + /// e.g. `a / b` + fn __truediv__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Div, other.0).into_subclasses(py) + } + + /// e.g. `a % b` + fn __mod__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Mod, other.0).into_subclasses(py) + } + + /// e.g. `a = b` + fn __eq__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Eq, other.0).into_subclasses(py) + } + + /// e.g. `a <> b` + fn __ne__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::NotEq, other.0).into_subclasses(py) + } + + /// e.g. `a < b` + fn __lt__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Lt, other.0).into_subclasses(py) + } + + /// e.g. `a <= b` + fn __le__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::LtEq, other.0).into_subclasses(py) + } + + /// e.g. `a > b` + fn __gt__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Gt, other.0).into_subclasses(py) + } + + /// e.g. `a >= b` + fn __ge__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::GtEq, other.0).into_subclasses(py) + } + + /// e.g. `a AND b` + fn __and__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::And, other.0).into_subclasses(py) + } + + /// e.g. `a OR b` + fn __or__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::Or, other.0).into_subclasses(py) + } + + /// e.g. `a & b` + fn bit_and<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::BitAnd, other.0).into_subclasses(py) + } + + /// e.g. `a | b` + fn bit_or<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::BitOr, other.0).into_subclasses(py) + } + + /// e.g. `a ^ b` + fn bit_xor<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::BitXor, other.0).into_subclasses(py) + } + + /// e.g. `a << b` + fn __lshift__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::LShift, other.0).into_subclasses(py) + } + + /// e.g. `a >> b` + fn __rshift__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::RShift, other.0).into_subclasses(py) + } + + /// e.g. `a || b` + fn concat<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(self.0.clone(), enum_::BinaryOp::StringConcat, other.0).into_subclasses(py) + } + + /// e.g. `b + a` + fn __radd__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Add, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b - a` + fn __rsub__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Sub, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b * a` + fn __rmul__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Mul, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b / a` + fn __rtruediv__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Div, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b % a` + fn __rmod__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Mod, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b AND a` + fn __rand__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::And, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b OR a` + fn __ror__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::Or, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b << a` + fn __rlshift__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::LShift, self.0.clone()).into_subclasses(py) + } + + /// e.g. `b >> a` + fn __rrshift__<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBinaryExpr::new_base(other.0, enum_::BinaryOp::RShift, self.0.clone()).into_subclasses(py) + } + + // ===== Unary ===== + + /// e.g. `-a` + fn __neg__<'py>(&self, py: pyo3::Python<'py>) -> pyo3::PyResult> { + super::nodes::PyUnaryExpr::new_base(enum_::UnaryOp::Minus, self.0.clone()).into_subclasses(py) + } + + /// e.g. `+a` + fn __pos__<'py>(&self, py: pyo3::Python<'py>) -> pyo3::PyResult> { + super::nodes::PyUnaryExpr::new_base(enum_::UnaryOp::Plus, self.0.clone()).into_subclasses(py) + } + + /// e.g. `NOT a` + fn not_<'py>(&self, py: pyo3::Python<'py>) -> pyo3::PyResult> { + super::nodes::PyUnaryExpr::new_base(enum_::UnaryOp::Not, self.0.clone()).into_subclasses(py) + } + + /// e.g. `~a` + fn bit_not<'py>(&self, py: pyo3::Python<'py>) -> pyo3::PyResult> { + super::nodes::PyUnaryExpr::new_base(enum_::UnaryOp::BitNot, self.0.clone()).into_subclasses(py) + } + + // ===== Between ====== + + /// e.g. `a BETWEEN b AND c` + fn between<'py>( + &self, + py: pyo3::Python<'py>, + low: super::extractors::LiteralExprExtractor, + high: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBetweenExpr::new_base(self.0.clone(), low.0, high.0, false).into_subclasses(py) + } + + /// e.g. `a NOT BETWEEN b AND c` + fn not_between<'py>( + &self, + py: pyo3::Python<'py>, + low: super::extractors::LiteralExprExtractor, + high: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyBetweenExpr::new_base(self.0.clone(), low.0, high.0, true).into_subclasses(py) + } + + // ===== Is ===== + + /// e.g. `a IS [kind]` + fn is_<'py>( + &self, + py: pyo3::Python<'py>, + kind: alias::BoundObject<'py>, + ) -> pyo3::PyResult> { + is_from_kind(self.0.clone(), &kind, false).and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `a IS NOT [kind]` + fn is_not<'py>( + &self, + py: pyo3::Python<'py>, + kind: alias::BoundObject<'py>, + ) -> pyo3::PyResult> { + is_from_kind(self.0.clone(), &kind, true).and_then(|x| x.into_subclasses(py)) + } + + // ===== IsDistinctFrom ===== + + /// e.g. `a IS DISTINCT FROM b` + fn is_distinct_from<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyIsDistinctFromExpr::new_base(self.0.clone(), other.0, false).into_subclasses(py) + } + + /// e.g. `a IS NOT DISTINCT FROM b` + fn is_not_distinct_from<'py>( + &self, + py: pyo3::Python<'py>, + other: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyIsDistinctFromExpr::new_base(self.0.clone(), other.0, true).into_subclasses(py) + } + + // ===== InList ===== + + /// e.g. `a IN (b, c, ...)` + fn in_<'py>(&self, list: alias::BoundObject<'py>) -> pyo3::PyResult> { + let py = list.py(); + + super::nodes::PyInListExpr::new_base(self.0.clone(), list, false).and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `a NOT IN (b, c, ...)` + fn not_in<'py>(&self, list: alias::BoundObject<'py>) -> pyo3::PyResult> { + let py = list.py(); + + super::nodes::PyInListExpr::new_base(self.0.clone(), list, true).and_then(|x| x.into_subclasses(py)) + } + + // // ===== Like ===== + + /// e.g. `a LIKE 'a%'` + #[pyo3(signature = (pattern, /, *, escape = None))] + fn like<'py>( + &self, + pattern: alias::BoundObject<'py>, + escape: Option, + ) -> pyo3::PyResult> { + let py = pattern.py(); + + super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, false, true, escape) + .and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `a NOT LIKE 'a%'` + #[pyo3(signature = (pattern, /, *, escape = None))] + fn not_like<'py>( + &self, + pattern: alias::BoundObject<'py>, + escape: Option, + ) -> pyo3::PyResult> { + let py = pattern.py(); + + super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, true, true, escape) + .and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `a ILIKE 'a%'` + #[pyo3(signature = (pattern, /, *, escape = None))] + fn ilike<'py>( + &self, + pattern: alias::BoundObject<'py>, + escape: Option, + ) -> pyo3::PyResult> { + let py = pattern.py(); + + super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, false, false, escape) + .and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `a NOT ILIKE 'a%'` + #[pyo3(signature = (pattern, /, *, escape = None))] + fn not_ilike<'py>( + &self, + pattern: alias::BoundObject<'py>, + escape: Option, + ) -> pyo3::PyResult> { + let py = pattern.py(); + + super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, true, false, escape) + .and_then(|x| x.into_subclasses(py)) + } + + // ===== SimilarTo / RLike ===== + + /// e.g. `a SIMILAR TO b` + fn similar_to<'py>( + &self, + py: pyo3::Python<'py>, + pattern: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PySimilarToExpr::new_base(self.0.clone(), pattern.0, false).into_subclasses(py) + } + + /// e.g. `a NOT SIMILAR TO b` + fn not_similar_to<'py>( + &self, + py: pyo3::Python<'py>, + pattern: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PySimilarToExpr::new_base(self.0.clone(), pattern.0, true).into_subclasses(py) + } + + /// e.g. `a RLIKE b` + fn rlike<'py>( + &self, + py: pyo3::Python<'py>, + pattern: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyRLikeExpr::new_base(self.0.clone(), pattern.0, false).into_subclasses(py) + } + + /// e.g. `a NOT RLIKE b` + fn not_rlike<'py>( + &self, + py: pyo3::Python<'py>, + pattern: super::extractors::LiteralExprExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyRLikeExpr::new_base(self.0.clone(), pattern.0, true).into_subclasses(py) + } + + // ===== Cast ===== + + /// e.g. `CAST(a AS int)` + fn cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> pyo3::PyResult> { + super::nodes::PyCastExpr::new_base(self.0.clone(), data_type_name, "CAST").and_then(|x| x.into_subclasses(py)) + } + + /// e.g. `TRY_CAST(a AS int)` + fn try_cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> pyo3::PyResult> { + super::nodes::PyCastExpr::new_base(self.0.clone(), data_type_name, "TRY_CAST") + .and_then(|x| x.into_subclasses(py)) + } + + // ===== Collate / Trim ===== + + /// e.g. `a COLLATE coll` + fn collate<'py>( + &self, + py: pyo3::Python<'py>, + collation: super::extractors::IdentExtractor, + ) -> pyo3::PyResult> { + super::nodes::PyCollateExpr::new_base(self.0.clone(), collation.0).into_subclasses(py) + } + + /// e.g. `TRIM(a)` + #[pyo3(signature = (*, r#where = "BOTH", characters = None))] + fn trim<'py>( + &self, + py: pyo3::Python<'py>, + r#where: &str, + characters: Option, + ) -> pyo3::PyResult> { + super::nodes::PyTrimExpr::new_base(self.0.clone(), r#where, characters.map(|x| x.0)) + .and_then(|x| x.into_subclasses(py)) + } + + /// Render this expression as a parameterized statement. + fn prepare<'py>( + slf: pyo3::PyRef<'py, Self>, + py: pyo3::Python<'py>, + dialect: crate::dialect::PyDialect, + ) -> pyo3::PyResult<(String, alias::BoundObject<'py>)> { + let (sql, params) = sql::prepare(py, &slf.0, &dialect)?; + + let params = unsafe { + utils::iterator_to_tuple( + slf.py(), + params.len() as isize, + params.into_iter().map(|x| Ok(x.into_ptr())), + )? + }; + Ok((sql, params)) + } + + /// Render this expression as raw SQL (values inlined as literals). + fn to_sql( + slf: pyo3::PyRef<'_, Self>, + py: pyo3::Python<'_>, + dialect: crate::dialect::PyDialect, + ) -> pyo3::PyResult { + sql::write_sql(py, &slf.0, &dialect).map_err(Into::into) + } + + fn __repr__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + Ok(self.0.python_repr(py)?.into_string()) + } +} diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs new file mode 100644 index 0000000..7555ef8 --- /dev/null +++ b/src/expr/enum_/func.rs @@ -0,0 +1,173 @@ +use super::ExprInner; +use super::ident::Ident; + +use crate::dialect::flags; +use crate::internal::repr; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; +use crate::internal::sql::write_separated; + +/// How `NULL`s are treated in some window/aggregate functions. +/// +/// Placement is dialect-specific: postfix `LAG(x) IGNORE NULLS`, or +/// inside the argument list on BigQuery/DuckDB (`LAG(x IGNORE NULLS)`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NullTreatment { + /// `IGNORE NULLS` + IgnoreNulls, + /// `RESPECT NULLS` + RespectNulls, +} + +impl NullTreatment { + pub fn as_sql(self) -> &'static str { + match self { + Self::IgnoreNulls => "IGNORE NULLS", + Self::RespectNulls => "RESPECT NULLS", + } + } +} + +/// Function call: name, arguments, `DISTINCT`, filter, and null treatment. +/// +/// The name is an [`Ident`]. Arguments are expression nodes. `filter` and +/// `null_treatment` are omitted from the SQL when they are `None`. +/// +/// ``` +/// use std::str::FromStr; +/// +/// use rapidquery::expr::enum_::Function; +/// use rapidquery::expr::enum_::Ident; +/// +/// let func = Function { +/// name: Ident::from_str("count").unwrap(), +/// distinct: false, +/// args: Vec::new(), +/// filter: None, +/// null_treatment: None, +/// }; +/// assert_eq!(func.name.len(), 1); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Function { + /// The function name + pub name: Ident, + /// `DISTINCT` keyword + pub distinct: bool, + /// The arguments to the function, including any options specified within the + /// delimiting parentheses. + pub args: Vec, + /// e.g. `x > 5` in `COUNT(x) FILTER (WHERE x > 5)` + pub filter: Option>, + /// `IGNORE NULLS` / `RESPECT NULLS`. Inside the parens on BigQuery + /// and DuckDB; after the call on other dialects. + pub null_treatment: Option, + // TODO: over, within_group +} + +impl Function { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("Function"); + + fmt.positional(self.name.python_repr()); + + for arg in &self.args { + fmt.positional(arg.python_repr(py)?); + } + + fmt.keyword_true("distinct", self.distinct).close(); + + match self.null_treatment { + Some(NullTreatment::IgnoreNulls) => { + fmt.append_suffix(".ignore_nulls()"); + } + Some(NullTreatment::RespectNulls) => { + fmt.append_suffix(".respect_nulls()"); + } + None => {} + } + + if let Some(filter) = &self.filter { + fmt.append_call("filter").positional(filter.python_repr(py)?); + } + + Ok(fmt) + } +} + +impl WriteSql for Function { + fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + // "name"([DISTINCT] args [IGNORE|RESPECT NULLS] [WHERE expr]) + // [FILTER ([WHERE] expr)] [IGNORE|RESPECT NULLS] + self.name.write_sql(dialect, out)?; + + let args_available = !self.args.is_empty(); + + out.push_char('('); + out.push_str_if(self.distinct && args_available, "DISTINCT "); + + write_separated(dialect, out, &self.args, ", ")?; + + let null_treatment_syntax = dialect.0.null_treatment(); + let function_filter_syntax = dialect.0.function_filter(); + + if self.null_treatment.is_some() && matches!(null_treatment_syntax, flags::NullTreatment::Unsupported) { + return Err( + rapidquery_macros::pyerr!(PyRuntimeError, "null_treatment is not supported by this dialect",).into(), + ); + } + if self.filter.is_some() && matches!(function_filter_syntax, flags::FunctionFilter::Unsupported) { + return Err( + rapidquery_macros::pyerr!(PyRuntimeError, "function filter is not supported by this dialect",).into(), + ); + } + + // Inside parens IGNORE/RESPECT NULLS + if let Some(x) = self.null_treatment + && matches!(null_treatment_syntax, flags::NullTreatment::InsideParens) + { + out.push_char_if(args_available, ' '); + out.push_str(x.as_sql()); + } + + // Inside parens WHERE filter + if let Some(x) = &self.filter + && matches!(function_filter_syntax, flags::FunctionFilter::InsideWhere) + { + out.push_char_if(args_available, ' '); + out.push_str("WHERE "); + x.write_sql(dialect, out)?; + } + + out.push_char(')'); + + // FILTER, then postfix IGNORE/RESPECT NULLS (immediately before OVER). + if let Some(x) = &self.filter { + match function_filter_syntax { + flags::FunctionFilter::FilterWhere => { + out.push_char(' '); + out.push_str("FILTER (WHERE "); + x.write_sql(dialect, out)?; + out.push_char(')'); + } + flags::FunctionFilter::Filter => { + out.push_char(' '); + out.push_str("FILTER ("); + x.write_sql(dialect, out)?; + out.push_char(')'); + } + _ => {} + } + } + + if let Some(x) = self.null_treatment + && matches!(null_treatment_syntax, flags::NullTreatment::Postfix) + { + out.push_char(' '); + out.push_str(x.as_sql()); + } + + Ok(()) + } +} diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs new file mode 100644 index 0000000..11ab9b4 --- /dev/null +++ b/src/expr/enum_/ident.rs @@ -0,0 +1,264 @@ +//! SQL identifiers. + +use crate::dialect::PyDialect; +use crate::internal::repr; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; +use crate::internal::sql::write_separated; + +/// One segment of a dotted identifier. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IdentPart { + /// Identifier that should be quoted and escaped for the dialect. + Quote(arcstr::ArcStr), + /// Identifier emitted without delimiters. Must be a simple ASCII name. + Unquote(arcstr::ArcStr), + /// SQL wildcard `*`. + Asterisk, +} + +impl IdentPart { + /// Quoted part whose body is `s`. + /// + /// `"*"` is a quoted identifier named star, not [`IdentPart::Asterisk`]. + pub fn quoted(s: impl AsRef) -> Result { + let s = s.as_ref(); + if s.is_empty() { + return Err(ParseIdentError::EmptyChunk); + } + Ok(Self::Quote(arcstr::ArcStr::from(s))) + } + + /// Unquoted part whose body is `s`. + /// + /// Rejects anything that is not `[A-Za-z_][A-Za-z0-9_]*`. + pub fn unquoted(s: impl AsRef) -> Result { + let s = s.as_ref(); + if s.is_empty() { + return Err(ParseIdentError::EmptyChunk); + } + if !is_safe_unquoted(s) { + return Err(ParseIdentError::InvalidUnquoted); + } + Ok(Self::Unquote(arcstr::ArcStr::from(s))) + } +} + +impl> From for IdentPart { + /// `"*"` becomes [`IdentPart::Asterisk`]. Any other string is quoted. + /// + /// Empty chunks are not rejected here. [`Ident::from_str`] checks those first. + fn from(value: S) -> Self { + let s = value.as_ref(); + if s == "*" { + IdentPart::Asterisk + } else { + IdentPart::Quote(arcstr::ArcStr::from(s)) + } + } +} + +impl WriteSql for IdentPart { + #[inline] + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match self { + Self::Quote(name) => { + dialect.0.write_identifier(out, name.as_str())?; + } + Self::Unquote(name) => { + out.push_str(name.as_str()); + } + Self::Asterisk => { + out.push_char('*'); + } + } + Ok(()) + } +} + +/// One or more identifier parts, joined with `.` when rendered. +/// +/// A validated ident is never empty and holds at most [`Ident::MAX_PARTS`] parts. +/// [`IdentPart::Asterisk`] is legal only as the last part. +/// +/// ``` +/// use std::str::FromStr; +/// +/// use rapidquery::expr::enum_::Ident; +/// +/// let ident = Ident::from_str("users.id").unwrap(); +/// assert_eq!(ident.len(), 2); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Ident(smallvec::SmallVec<[IdentPart; 4]>); + +/// Why building an [`Ident`] failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParseIdentError { + /// No parts (empty string, empty constructor). + Empty, + /// A quoted or unquoted part was the empty string (`"a..b"`, `""` as a part). + EmptyChunk, + /// More than [`Ident::MAX_PARTS`] parts. + TooManyParts, + /// `*` appeared before the last part (`*.id`). + AsteriskNotLast, + /// Unquoted body was not a simple ASCII identifier. + InvalidUnquoted, +} + +impl std::fmt::Display for ParseIdentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Empty => "identifier is empty", + Self::EmptyChunk => "identifier has an empty part", + Self::TooManyParts => "identifier has too many parts", + Self::AsteriskNotLast => "asterisk must be the last identifier part", + Self::InvalidUnquoted => "unquoted identifier is not a simple ASCII name", + }) + } +} + +impl std::error::Error for ParseIdentError {} + +/// `[A-Za-z_][A-Za-z0-9_]*`. Empty string is not safe. +fn is_safe_unquoted(s: &str) -> bool { + let mut bytes = s.bytes(); + let Some(first) = bytes.next() else { + return false; + }; + if !first.is_ascii_alphabetic() && first != b'_' { + return false; + } + bytes.all(|c| c.is_ascii_alphanumeric() || c == b'_') +} + +impl Ident { + /// Maximum number of dotted parts. + pub const MAX_PARTS: usize = 10; + + /// Borrowed parts in order. + #[inline] + pub fn parts(&self) -> &[IdentPart] { + &self.0 + } + + /// Number of parts. Always ≥ 1 for a successfully built `Ident`. + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// `true` when there are no parts. Unreachable for a validated `Ident`. + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Consume and yield the parts. + #[inline] + pub fn into_parts(self) -> smallvec::SmallVec<[IdentPart; 4]> { + self.0 + } + + pub fn python_repr(&self) -> repr::ReprFormatter { + let mut fmt = repr::ReprFormatter::new_call("Ident"); + + for item in self.parts() { + match item { + IdentPart::Quote(name) => { + fmt.positional(name.as_str()); + } + IdentPart::Unquote(name) => { + let mut unquote = repr::ReprFormatter::new_call("Unquote"); + unquote.positional(name.as_str()); + fmt.positional(unquote); + } + IdentPart::Asterisk => { + fmt.positional_raw("Asterisk"); + } + } + } + + fmt.close(); + fmt + } + + /// One already-validated part. Prefer this over [`from_parts`] for a + /// single `str` / `Unquote` / `Asterisk`. + #[inline] + pub fn from_one(part: IdentPart) -> Self { + Self(smallvec::smallvec![part]) + } + + /// Build from parts. Composition only: empty ident, empty chunk, + /// [`MAX_PARTS`](Self::MAX_PARTS), asterisk-not-last. + /// + /// Unquoted charset is enforced by [`IdentPart::unquoted`], not here. + #[cfg_attr(feature = "nightly", optimize(speed))] + pub fn from_parts(iter: impl IntoIterator) -> Result { + let mut parts = smallvec::SmallVec::<[IdentPart; 4]>::new(); + + for part in iter { + if let Some(IdentPart::Asterisk) = parts.last() { + return Err(ParseIdentError::AsteriskNotLast); + } + if parts.len() >= Self::MAX_PARTS { + return Err(ParseIdentError::TooManyParts); + } + match &part { + IdentPart::Quote(name) | IdentPart::Unquote(name) if name.is_empty() => { + return Err(ParseIdentError::EmptyChunk); + } + IdentPart::Quote(_) | IdentPart::Unquote(_) | IdentPart::Asterisk => {} + } + parts.push(part); + } + + if parts.is_empty() { + return Err(ParseIdentError::Empty); + } + + Ok(Self(parts)) + } +} + +impl std::str::FromStr for Ident { + type Err = ParseIdentError; + + #[cfg_attr(feature = "nightly", optimize(speed))] + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(ParseIdentError::Empty); + } + + let mut parts = smallvec::SmallVec::<[IdentPart; 4]>::new(); + + for chunk in s.splitn(Self::MAX_PARTS + 1, '.') { + if chunk.is_empty() { + return Err(ParseIdentError::EmptyChunk); + } + if parts.len() >= Self::MAX_PARTS { + return Err(ParseIdentError::TooManyParts); + } + // A bare `*` only makes sense as the final part (e.g. `table.*`). + // If another part follows, demote the previous `*` to a quoted star. + if let Some(last) = parts.last_mut() + && matches!(last, IdentPart::Asterisk) + { + *last = IdentPart::Quote(arcstr::ArcStr::from("*")); + } + parts.push(chunk.into()); + } + + Ok(Self(parts)) + } +} + +impl WriteSql for Ident { + #[inline] + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + write_separated(dialect, out, &self.0, ".") + } +} diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs new file mode 100644 index 0000000..3e38083 --- /dev/null +++ b/src/expr/enum_/like.rs @@ -0,0 +1,416 @@ +//! `LIKE` / `ILIKE` / `LIKE ANY` / `SIMILAR TO` / `RLIKE` AST nodes. + +use super::ExprInner; +use crate::dialect::PyDialect; +use crate::dialect::flags; +use crate::internal::repr; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; +use crate::internal::sql::write_separated; + +pub(super) fn write_escape(dialect: &PyDialect, out: &mut W, escape: Option) -> PrepareResult<()> { + let Some(c) = escape else { + return Ok(()); + }; + + out.push_str(" ESCAPE "); + let mut buf = [0u8; 4]; + dialect.0.write_string_literal(out, c.encode_utf8(&mut buf))?; + + Ok(()) +} + +/// Pattern side of [`Like`]: one expression, or a non-empty ANY list. +#[derive(Clone, Debug, PartialEq)] +pub enum LikePattern { + /// `col LIKE p` + One(Box), + /// `col LIKE ANY …` — never empty. + Any(Vec), +} + +impl LikePattern { + fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + match self { + Self::One(pattern) => pattern.python_repr(py), + Self::Any(patterns) => { + let mut fmt = repr::ReprFormatter::new_list(); + + for item in patterns { + fmt.positional(item.python_repr(py)?); + } + + fmt.close(); + Ok(fmt) + } + } + } +} + +/// Pattern predicate: `LIKE`, `ILIKE`, or `LIKE ANY`, plus an optional escape. +/// +/// One pattern is a scalar match. [`LikePattern::Any`] is `LIKE ANY`, spelled +/// as the dialect allows. The escape character is inlined, never bound. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::Like; +/// use rapidquery::expr::enum_::LikePattern; +/// use std::str::FromStr; +/// +/// let expr = Like { +/// case_sensitive: true, +/// negated: false, +/// expr: Box::new(ExprInner::Ident(Ident::from_str("name").unwrap())), +/// pattern: LikePattern::One(Box::new(ExprInner::Ident(Ident::from_str("pat").unwrap()))), +/// escape: None, +/// }; +/// assert!(expr.escape.is_none()); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Like { + pub case_sensitive: bool, + pub negated: bool, + pub expr: Box, + pub pattern: LikePattern, + pub escape: Option, +} + +impl Like { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("LikeExpr"); + + fmt.positional(self.expr.python_repr(py)?) + .positional(self.pattern.python_repr(py)?) + .keyword_false("case_sensitive", self.case_sensitive) + .keyword_true("negated", self.negated) + .keyword("escape", self.escape) + .close(); + + Ok(fmt) + } +} + +fn write_like_operand( + dialect: &PyDialect, + out: &mut W, + expr: &ExprInner, + wrap_lower: bool, + paren_if_equal: bool, +) -> PrepareResult<()> { + if wrap_lower { + out.push_str("LOWER("); + expr.write_sql(dialect, out)?; + out.push_char(')'); + Ok(()) + } else { + expr.write_operand(dialect, out, Like::PRECEDENCE, paren_if_equal) + } +} + +fn write_like_keyword(out: &mut W, ilike: bool) { + out.push_str(if ilike { "ILIKE" } else { "LIKE" }); +} + +fn write_like_infix(out: &mut W, negated: bool, ilike: bool) { + out.push_char(' '); + if negated { + out.push_str("NOT "); + } + write_like_keyword(out, ilike); + out.push_char(' '); +} + +fn write_like_pattern_list( + dialect: &PyDialect, + out: &mut W, + patterns: &[ExprInner], + wrap_lower: bool, +) -> PrepareResult<()> { + if wrap_lower { + for (i, pattern) in patterns.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + write_like_operand(dialect, out, pattern, true, false)?; + } + Ok(()) + } else { + write_separated(dialect, out, patterns, ", ") + } +} + +/// `ESCAPE` is legal on this form. `native_any` is `LIKE ANY` that is not an +/// `OR` expansion. +#[inline] +fn like_escape_allowed(syntax: flags::LikeEscape, native_any: bool) -> bool { + match syntax { + flags::LikeEscape::Unsupported => false, + flags::LikeEscape::Scalar => !native_any, + flags::LikeEscape::IncludingAny => true, + } +} + +#[inline] +fn ensure_like_escape_is_supported( + syntax: flags::LikeEscape, + escape: Option, + native_any: bool, +) -> PrepareResult<()> { + if escape.is_none() || like_escape_allowed(syntax, native_any) { + return Ok(()); + } + + Err(rapidquery_macros::pyerr!( + PyRuntimeError, + if native_any { + "LIKE ANY ESCAPE is not supported by this dialect" + } else { + "LIKE ESCAPE is not supported by this dialect" + } + ) + .into()) +} + +/// `[NOT] (x LIKE|ILIKE ANY (ARRAY?[…]) [ESCAPE char])` +fn write_like_any_native( + dialect: &PyDialect, + out: &mut W, + like: &Like, + patterns: &[ExprInner], + ilike: bool, + wrap_lower: bool, + array: bool, +) -> PrepareResult<()> { + if like.negated { + out.push_str("NOT ("); + } + write_like_operand(dialect, out, &like.expr, wrap_lower, false)?; + out.push_char(' '); + write_like_keyword(out, ilike); + if array { + out.push_str(" ANY (ARRAY["); + } else { + out.push_str(" ANY ("); + } + write_like_pattern_list(dialect, out, patterns, wrap_lower)?; + if array { + out.push_str("])"); + } else { + out.push_char(')'); + } + write_escape(dialect, out, like.escape)?; + if like.negated { + out.push_char(')'); + } + Ok(()) +} + +impl WriteSql for Like { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + let (ilike, wrap_lower) = if self.case_sensitive { + (false, false) + } else { + match dialect.0.ilike_expression() { + flags::ILikeExpression::Standard => (true, false), + flags::ILikeExpression::UseLike => (false, false), + flags::ILikeExpression::UseLikeLower => (false, true), + } + }; + + ensure_like_escape_is_supported( + dialect.0.like_escape(), + self.escape, + matches!(self.pattern, LikePattern::Any(_)) && !matches!(dialect.0.like_any(), flags::LikeAny::OrExpansion), + )?; + + match &self.pattern { + LikePattern::One(pattern) => { + write_like_operand(dialect, out, &self.expr, wrap_lower, false)?; + write_like_infix(out, self.negated, ilike); + write_like_operand(dialect, out, pattern, wrap_lower, true)?; + write_escape(dialect, out, self.escape) + } + LikePattern::Any(patterns) => match dialect.0.like_any() { + flags::LikeAny::ArrayConstructor => { + write_like_any_native(dialect, out, self, patterns, ilike, wrap_lower, true) + } + flags::LikeAny::PatternList => { + write_like_any_native(dialect, out, self, patterns, ilike, wrap_lower, false) + } + flags::LikeAny::OrExpansion => { + let sep = if self.negated { " AND " } else { " OR " }; + out.push_char('('); + for (i, pattern) in patterns.iter().enumerate() { + if i > 0 { + out.push_str(sep); + } + write_like_operand(dialect, out, &self.expr, wrap_lower, false)?; + write_like_infix(out, self.negated, ilike); + write_like_operand(dialect, out, pattern, wrap_lower, true)?; + write_escape(dialect, out, self.escape)?; + } + out.push_char(')'); + Ok(()) + } + }, + } + } +} + +/// `expr [NOT] SIMILAR TO pattern`. +/// +/// Rendered only when the dialect sets `supports_similar_to`. There is no +/// `ESCAPE` clause. Other dialects fail at render time. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::SimilarTo; +/// use std::str::FromStr; +/// +/// let expr = SimilarTo { +/// negated: false, +/// expr: Box::new(ExprInner::Ident(Ident::from_str("name").unwrap())), +/// pattern: Box::new(ExprInner::Ident(Ident::from_str("pat").unwrap())), +/// }; +/// assert!(!expr.negated); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct SimilarTo { + pub negated: bool, + pub expr: Box, + pub pattern: Box, +} + +impl SimilarTo { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("SimilarToExpr"); + + fmt.positional(self.expr.python_repr(py)?) + .positional(self.pattern.python_repr(py)?) + .keyword_true("negated", self.negated) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for SimilarTo { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + if !dialect.0.supports_similar_to() { + return Err( + rapidquery_macros::pyerr!(PyRuntimeError, "SIMILAR TO is not supported by this dialect",).into(), + ); + } + + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + out.push_char(' '); + if self.negated { + out.push_str("NOT SIMILAR TO "); + } else { + out.push_str("SIMILAR TO "); + } + self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) + } +} + +/// Regex predicate. The dialect chooses `RLIKE`, `~`, or a function. +/// +/// Dialects with no regex predicate fail at render time. There is no `ESCAPE` +/// clause. Function names written for this node stay unquoted. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::RLike; +/// use std::str::FromStr; +/// +/// let expr = RLike { +/// negated: false, +/// expr: Box::new(ExprInner::Ident(Ident::from_str("name").unwrap())), +/// pattern: Box::new(ExprInner::Ident(Ident::from_str("pat").unwrap())), +/// }; +/// assert!(!expr.negated); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct RLike { + pub negated: bool, + pub expr: Box, + pub pattern: Box, +} + +impl RLike { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("RLikeExpr"); + + fmt.positional(self.expr.python_repr(py)?) + .positional(self.pattern.python_repr(py)?) + .keyword_true("negated", self.negated) + .close(); + + Ok(fmt) + } +} + +/// `NOT? name(expr, pattern)` — unquoted function rewrite. +fn write_regex_call( + dialect: &PyDialect, + out: &mut W, + name: &str, + negated: bool, + expr: &ExprInner, + pattern: &ExprInner, +) -> PrepareResult<()> { + if negated { + out.push_str("NOT "); + } + out.push_str(name); + out.push_char('('); + expr.write_sql(dialect, out)?; + out.push_str(", "); + pattern.write_sql(dialect, out)?; + out.push_char(')'); + Ok(()) +} + +impl WriteSql for RLike { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.0.rlike_expression() { + flags::RLikeExpression::Unsupported => { + Err(rapidquery_macros::pyerr!(PyRuntimeError, "RLIKE is not supported by this dialect",).into()) + } + flags::RLikeExpression::Standard => { + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + if self.negated { + out.push_str(" NOT RLIKE "); + } else { + out.push_str(" RLIKE "); + } + self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) + } + flags::RLikeExpression::Tilde => { + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + out.push_str(if self.negated { " !~ " } else { " ~ " }); + self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) + } + flags::RLikeExpression::RegexpLike => { + write_regex_call(dialect, out, "REGEXP_LIKE", self.negated, &self.expr, &self.pattern) + } + flags::RLikeExpression::RegexpContains => { + write_regex_call(dialect, out, "REGEXP_CONTAINS", self.negated, &self.expr, &self.pattern) + } + flags::RLikeExpression::RegexpMatches => { + write_regex_call(dialect, out, "regexp_matches", self.negated, &self.expr, &self.pattern) + } + } + } +} diff --git a/src/expr/enum_/mod.rs b/src/expr/enum_/mod.rs new file mode 100644 index 0000000..b422133 --- /dev/null +++ b/src/expr/enum_/mod.rs @@ -0,0 +1,193 @@ +mod func; +mod ident; +mod like; +mod nodes; +mod operator; +mod value; + +pub use func::*; +pub use ident::*; +pub use like::*; +pub use nodes::*; +pub use operator::*; +pub use value::*; + +use crate::dialect::PyDialect; +use crate::internal::repr::ReprFormatter; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; + +/// SQL expression tree. +/// +/// Concrete variants wrap the node structs in [`nodes`]. [`Ident`], [`Value`], +/// and [`Function`] live in sibling modules and already implement `python_repr` +/// / [`WriteSql`]; this enum only dispatches. +/// +/// TODO: `Exists`, `InSubquery`, `Subquery`, and `Quantified` (`ANY` / +/// `ALL` / `SOME`) once `Query` exists. +#[derive(Clone, Debug, PartialEq)] +pub enum ExprInner { + /// Identifier (`schema.table.column`, `*`). + Ident(Ident), + /// Bound or inlined SQL value plus its serializer. + Value(Value), + /// Parenthesized row `(a, b, …)`. + Tuple(Tuple), + /// Unary operation (`NOT x`, `-x`, postfix `x!`). + Unary(Unary), + /// Binary operation (`a + b`, `a AND b`). + Binary(Binary), + /// Function call. + Function(Function), + /// `expr IS [NOT] {NULL|TRUE|FALSE|UNKNOWN}`. + Is(Is), + /// `left IS [NOT] DISTINCT FROM right`. + IsDistinctFrom(IsDistinctFrom), + /// `expr [NOT] IN (list…)`. + InList(InList), + /// `expr [NOT] BETWEEN low AND high`. + Between(Between), + Like(Like), + SimilarTo(SimilarTo), + RLike(RLike), + Cast(Cast), + Trim(Trim), + Collate(Collate), + Case(Case), + Custom(Custom), +} + +macro_rules! impl_from_expr_inner { + ($($ty:ty => $variant:ident),+ $(,)?) => { + $( + impl From<$ty> for ExprInner { + #[inline] + fn from(value: $ty) -> Self { + Self::$variant(value) + } + } + )+ + }; +} + +impl_from_expr_inner! { + Ident => Ident, + Value => Value, + Tuple => Tuple, + Unary => Unary, + Binary => Binary, + Function => Function, + Is => Is, + IsDistinctFrom => IsDistinctFrom, + InList => InList, + Between => Between, + Like => Like, + SimilarTo => SimilarTo, + RLike => RLike, + Cast => Cast, + Trim => Trim, + Collate => Collate, + Case => Case, + Custom => Custom, +} + +impl ExprInner { + /// Lower is tighter binding. + #[inline] + fn precedence(&self) -> u8 { + match self { + Self::Ident(_) | Self::Value(_) | Self::Function(_) => 0, + Self::Tuple(_) => Tuple::PRECEDENCE, + Self::Unary(u) => u.op.precedence(), + Self::Binary(b) => b.op.precedence(), + Self::Is(_) => Is::PRECEDENCE, + Self::InList(_) => InList::PRECEDENCE, + Self::IsDistinctFrom(_) => IsDistinctFrom::PRECEDENCE, + Self::Between(_) => Between::PRECEDENCE, + Self::Like(_) => Like::PRECEDENCE, + Self::SimilarTo(_) => SimilarTo::PRECEDENCE, + Self::RLike(_) => RLike::PRECEDENCE, + Self::Cast(_) => Cast::PRECEDENCE, + Self::Trim(_) => Trim::PRECEDENCE, + Self::Collate(_) => Collate::PRECEDENCE, + Self::Case(_) => Case::PRECEDENCE, + Self::Custom(_) => Custom::PRECEDENCE, + } + } + + /// Write `self` as an operand of a parent at `parent_prec`. + /// + /// Parenthesize when this node binds more loosely than the parent, or + /// when `paren_if_equal` and the precedences match (right-associativity + /// / postfix nesting). + #[inline] + pub(crate) fn write_operand( + &self, + dialect: &PyDialect, + out: &mut W, + parent_prec: u8, + paren_if_equal: bool, + ) -> PrepareResult<()> { + let child_prec = self.precedence(); + let paren = child_prec > parent_prec || (paren_if_equal && child_prec == parent_prec); + if paren { + out.push_char('('); + } + self.write_sql(dialect, out)?; + if paren { + out.push_char(')'); + } + Ok(()) + } + + #[inline] + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + match self { + Self::Ident(x) => Ok(x.python_repr()), + Self::Value(x) => x.python_repr(py), + Self::Tuple(x) => x.python_repr(py), + Self::Unary(x) => x.python_repr(py), + Self::Binary(x) => x.python_repr(py), + Self::Function(x) => x.python_repr(py), + Self::Is(x) => x.python_repr(py), + Self::IsDistinctFrom(x) => x.python_repr(py), + Self::InList(x) => x.python_repr(py), + Self::Between(x) => x.python_repr(py), + Self::Like(x) => x.python_repr(py), + Self::SimilarTo(x) => x.python_repr(py), + Self::RLike(x) => x.python_repr(py), + Self::Cast(x) => x.python_repr(py), + Self::Trim(x) => x.python_repr(py), + Self::Collate(x) => x.python_repr(py), + Self::Case(x) => x.python_repr(py), + Self::Custom(x) => x.python_repr(py), + } + } +} + +impl WriteSql for ExprInner { + #[cfg_attr(feature = "nightly", optimize(speed))] + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match self { + Self::Ident(x) => x.write_sql(dialect, out), + Self::Value(x) => x.write_sql(dialect, out), + Self::Tuple(x) => x.write_sql(dialect, out), + Self::Unary(x) => x.write_sql(dialect, out), + Self::Binary(x) => x.write_sql(dialect, out), + Self::Function(x) => x.write_sql(dialect, out), + Self::Is(x) => x.write_sql(dialect, out), + Self::IsDistinctFrom(x) => x.write_sql(dialect, out), + Self::InList(x) => x.write_sql(dialect, out), + Self::Between(x) => x.write_sql(dialect, out), + Self::Like(x) => x.write_sql(dialect, out), + Self::SimilarTo(x) => x.write_sql(dialect, out), + Self::RLike(x) => x.write_sql(dialect, out), + Self::Cast(x) => x.write_sql(dialect, out), + Self::Trim(x) => x.write_sql(dialect, out), + Self::Collate(x) => x.write_sql(dialect, out), + Self::Case(x) => x.write_sql(dialect, out), + Self::Custom(x) => x.write_sql(dialect, out), + } + } +} diff --git a/src/expr/enum_/nodes.rs b/src/expr/enum_/nodes.rs new file mode 100644 index 0000000..7cbd80e --- /dev/null +++ b/src/expr/enum_/nodes.rs @@ -0,0 +1,1005 @@ +use super::BinaryOp; +use super::CastKind; +use super::ExprInner; +use super::Ident; +use super::IsKind; +use super::TrimWhere; +use super::UnaryOp; +use super::UnarySpacing; +use crate::dialect::PyDialect; +use crate::dialect::flags; +use crate::internal::repr; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; +use crate::internal::sql::write_separated; + +#[inline] +fn unsupported(message: &'static str) -> crate::internal::sql::PrepareError { + rapidquery_macros::pyerr!(PyRuntimeError, message).into() +} + +/// Row value. The dialect chooses `ROW`, `struct`, or parentheses. +/// +/// An empty row is legal only for dialects that accept it. A string stored as +/// an item is a value expression, not a column name. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::Tuple; +/// use std::str::FromStr; +/// +/// let row = Tuple { +/// items: vec![ExprInner::Ident(Ident::from_str("id").unwrap())], +/// }; +/// assert_eq!(row.items.len(), 1); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Tuple { + pub items: Vec, +} + +impl Tuple { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("TupleExpr"); + + for item in &self.items { + fmt.positional(item.python_repr(py)?); + } + fmt.close(); + + Ok(fmt) + } +} + +impl WriteSql for Tuple { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + let arity = self.items.len(); + match dialect.0.tuple() { + flags::Tuple::Unsupported => { + return Err(unsupported("tuple values are not supported by this dialect")); + } + flags::Tuple::Row => { + out.push_str("ROW("); + } + flags::Tuple::RowRejectEmpty => { + if arity == 0 { + return Err(unsupported("empty tuple is not supported by this dialect")); + } + out.push_str("ROW("); + } + flags::Tuple::Struct => { + if arity == 0 { + return Err(unsupported("empty tuple is not supported by this dialect")); + } + out.push_str("struct("); + } + flags::Tuple::Parentheses => { + if arity < 2 { + return Err(unsupported("a tuple needs at least two elements on this dialect")); + } + out.push_char('('); + } + } + write_separated(dialect, out, &self.items, ", ")?; + out.push_char(')'); + Ok(()) + } +} + +/// Prefix operator: `NOT`, `+`, `-`, or `~`. +/// +/// `NOT` is written with a space. The others sit against the operand. Dialects +/// that have no prefix `~` render it as a function. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::Unary; +/// use rapidquery::expr::enum_::UnaryOp; +/// use std::str::FromStr; +/// +/// let expr = Unary { +/// op: UnaryOp::Not, +/// expr: Box::new(ExprInner::Ident(Ident::from_str("active").unwrap())), +/// }; +/// assert_eq!(expr.op.as_sql(), "NOT"); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Unary { + pub op: UnaryOp, + pub expr: Box, +} + +impl Unary { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("UnaryExpr"); + + fmt.positional(self.op.as_sql()) + .positional(self.expr.python_repr(py)?) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for Unary { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + if self.op == UnaryOp::BitNot { + match dialect.0.bitwise() { + flags::Bitwise::Infix | flags::Bitwise::InfixNoShift => { + out.push_char('~'); + return self + .expr + .write_operand(dialect, out, UnaryOp::BitNot.precedence(), true); + } + flags::Bitwise::BitShiftFunctions | flags::Bitwise::ShiftFunctions => { + out.push_str("BITNOT("); + self.expr.write_sql(dialect, out)?; + out.push_char(')'); + return Ok(()); + } + flags::Bitwise::BitAndOnly | flags::Bitwise::InfixAndShift => { + return Err(unsupported("bitwise NOT is not supported by this dialect")); + } + } + } + let prec = self.op.precedence(); + match self.op.spacing() { + UnarySpacing::SpacedPrefix => { + out.push_str(self.op.as_sql()); + out.push_char(' '); + self.expr.write_operand(dialect, out, prec, true) + } + UnarySpacing::GluedPrefix => { + out.push_str(self.op.as_sql()); + self.expr.write_operand(dialect, out, prec, true) + } + } + } +} + +/// Infix operation, including arithmetic, comparison, and boolean operators. +/// +/// `!=` is stored as `<>`. An unknown spelling is kept as a custom operator. +/// The dialect may rewrite `%`, `^`, `||`, and shifts into function calls. +/// +/// ``` +/// use rapidquery::expr::enum_::Binary; +/// use rapidquery::expr::enum_::BinaryOp; +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use std::str::FromStr; +/// +/// let expr = Binary { +/// left: Box::new(ExprInner::Ident(Ident::from_str("age").unwrap())), +/// op: BinaryOp::Gt, +/// right: Box::new(ExprInner::Ident(Ident::from_str("n").unwrap())), +/// }; +/// assert_eq!(expr.op.as_sql(), ">"); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Binary { + pub left: Box, + pub op: BinaryOp, + pub right: Box, +} + +impl Binary { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("BinaryExpr"); + + fmt.positional(self.left.python_repr(py)?) + .positional(self.op.as_sql()) + .positional(self.right.python_repr(py)?) + .close(); + + Ok(fmt) + } +} + +#[inline] +fn is_arithmetic_binary_op(op: &BinaryOp) -> bool { + matches!( + op, + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod + ) +} + +/// `name(left, right)` with an unquoted function name. +#[inline] +fn write_binary_call( + dialect: &PyDialect, + out: &mut W, + name: &str, + left: &ExprInner, + right: &ExprInner, +) -> PrepareResult<()> { + out.push_str(name); + out.push_char('('); + left.write_sql(dialect, out)?; + out.push_str(", "); + right.write_sql(dialect, out)?; + out.push_char(')'); + Ok(()) +} + +/// Infix `left SPELLING right`. +/// +/// `paren_arithmetic` parenthesizes a `+ - * / %` side. Infix `||` (and MsSql +/// `+` used as concat) needs that because dialect precedence disagrees with +/// the tree: SQLite binds `||` tighter than arithmetic, and MsSql `+` is the +/// same token as addition. +#[inline] +fn write_infix( + bin: &Binary, + dialect: &PyDialect, + out: &mut W, + spelling: &str, + paren_arithmetic: bool, +) -> PrepareResult<()> { + let prec = bin.op.precedence(); + write_infix_side(dialect, out, &bin.left, prec, false, paren_arithmetic)?; + out.push_char(' '); + out.push_str(spelling); + out.push_char(' '); + write_infix_side(dialect, out, &bin.right, prec, true, paren_arithmetic) +} + +#[inline] +fn write_infix_side( + dialect: &PyDialect, + out: &mut W, + expr: &ExprInner, + prec: u8, + paren_if_equal: bool, + paren_arithmetic: bool, +) -> PrepareResult<()> { + if paren_arithmetic && matches!(expr, ExprInner::Binary(b) if is_arithmetic_binary_op(&b.op)) { + out.push_char('('); + expr.write_sql(dialect, out)?; + out.push_char(')'); + return Ok(()); + } + expr.write_operand(dialect, out, prec, paren_if_equal) +} + +impl WriteSql for Binary { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match &self.op { + BinaryOp::StringConcat => match dialect.0.string_concat() { + flags::StringConcat::ConcatFunction => { + write_binary_call(dialect, out, "CONCAT", &self.left, &self.right) + } + flags::StringConcat::Pipes => write_infix(self, dialect, out, "||", true), + flags::StringConcat::Plus => write_infix(self, dialect, out, "+", true), + }, + BinaryOp::BitXor => match dialect.0.bitxor() { + flags::BitXor::Unsupported => Err(unsupported("bitwise XOR is not supported by this dialect")), + flags::BitXor::Caret => write_infix(self, dialect, out, "^", false), + flags::BitXor::Hash => write_infix(self, dialect, out, "#", false), + flags::BitXor::XORFunction => write_binary_call(dialect, out, "xor", &self.left, &self.right), + flags::BitXor::BitXORFunction => write_binary_call(dialect, out, "BITXOR", &self.left, &self.right), + }, + BinaryOp::Mod => match dialect.0.modulo() { + flags::Modulo::Percent => write_infix(self, dialect, out, "%", false), + flags::Modulo::Function => write_binary_call(dialect, out, "MOD", &self.left, &self.right), + flags::Modulo::Infix => write_infix(self, dialect, out, "MOD", false), + }, + BinaryOp::BitAnd => match dialect.0.bitwise() { + flags::Bitwise::Infix | flags::Bitwise::InfixNoShift | flags::Bitwise::InfixAndShift => { + write_infix(self, dialect, out, "&", false) + } + flags::Bitwise::BitShiftFunctions | flags::Bitwise::ShiftFunctions | flags::Bitwise::BitAndOnly => { + write_binary_call(dialect, out, "BITAND", &self.left, &self.right) + } + }, + BinaryOp::BitOr => match dialect.0.bitwise() { + flags::Bitwise::Infix | flags::Bitwise::InfixNoShift => write_infix(self, dialect, out, "|", false), + flags::Bitwise::BitShiftFunctions | flags::Bitwise::ShiftFunctions => { + write_binary_call(dialect, out, "BITOR", &self.left, &self.right) + } + flags::Bitwise::BitAndOnly | flags::Bitwise::InfixAndShift => { + Err(unsupported("bitwise OR is not supported by this dialect")) + } + }, + BinaryOp::LShift => match dialect.0.bitwise() { + flags::Bitwise::Infix | flags::Bitwise::InfixAndShift => write_infix(self, dialect, out, "<<", false), + flags::Bitwise::BitShiftFunctions => { + write_binary_call(dialect, out, "BITSHIFTLEFT", &self.left, &self.right) + } + flags::Bitwise::ShiftFunctions => write_binary_call(dialect, out, "SHIFTLEFT", &self.left, &self.right), + flags::Bitwise::InfixNoShift | flags::Bitwise::BitAndOnly => { + Err(unsupported("bitwise shift is not supported by this dialect")) + } + }, + BinaryOp::RShift => match dialect.0.bitwise() { + flags::Bitwise::Infix | flags::Bitwise::InfixAndShift => write_infix(self, dialect, out, ">>", false), + flags::Bitwise::BitShiftFunctions => { + write_binary_call(dialect, out, "BITSHIFTRIGHT", &self.left, &self.right) + } + flags::Bitwise::ShiftFunctions => { + write_binary_call(dialect, out, "SHIFTRIGHT", &self.left, &self.right) + } + flags::Bitwise::InfixNoShift | flags::Bitwise::BitAndOnly => { + Err(unsupported("bitwise shift is not supported by this dialect")) + } + }, + _ => write_infix(self, dialect, out, self.op.as_sql(), false), + } + } +} + +/// `expr IS [NOT] NULL`, `TRUE`, `FALSE`, or `UNKNOWN`. +/// +/// `negated` selects `IS NOT`. The kind is a fixed word, not an expression. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::Is; +/// use rapidquery::expr::enum_::IsKind; +/// use std::str::FromStr; +/// +/// let expr = Is { +/// expr: Box::new(ExprInner::Ident(Ident::from_str("name").unwrap())), +/// negated: false, +/// kind: IsKind::Null, +/// }; +/// assert_eq!(expr.kind.as_sql(), "NULL"); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Is { + pub expr: Box, + pub negated: bool, + pub kind: IsKind, +} + +impl Is { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("IsExpr"); + + fmt.positional(self.expr.python_repr(py)?) + .positional(self.kind.as_sql()) + .keyword_true("negated", self.negated) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for Is { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + if self.negated { + out.push_str(" IS NOT "); + } else { + out.push_str(" IS "); + } + out.push_str(self.kind.as_sql()); + Ok(()) + } +} + +/// Null-safe comparison, `IS [NOT] DISTINCT FROM`. +/// +/// Either side may be `NULL` without making the predicate unknown. The dialect +/// writes the standard form, a null-safe operator, or rejects the node. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::IsDistinctFrom; +/// use std::str::FromStr; +/// +/// let expr = IsDistinctFrom { +/// left: Box::new(ExprInner::Ident(Ident::from_str("a").unwrap())), +/// right: Box::new(ExprInner::Ident(Ident::from_str("b").unwrap())), +/// negated: false, +/// }; +/// assert!(!expr.negated); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct IsDistinctFrom { + pub left: Box, + pub right: Box, + pub negated: bool, +} + +impl IsDistinctFrom { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("IsDistinctFromExpr"); + + fmt.positional(self.left.python_repr(py)?) + .positional(self.right.python_repr(py)?) + .keyword_true("negated", self.negated) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for IsDistinctFrom { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.0.is_distinct_from() { + flags::IsDistinctFrom::Unsupported => Err(unsupported("IS DISTINCT FROM is not supported by this dialect")), + flags::IsDistinctFrom::Standard => { + self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; + if self.negated { + out.push_str(" IS NOT DISTINCT FROM "); + } else { + out.push_str(" IS DISTINCT FROM "); + } + self.right.write_operand(dialect, out, Self::PRECEDENCE, true) + } + flags::IsDistinctFrom::NullSafeEqual => { + // `<=>` is null-safe equal, i.e. `IS NOT DISTINCT FROM`. + if !self.negated { + out.push_str("NOT ("); + } + self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; + out.push_str(" <=> "); + self.right.write_operand(dialect, out, Self::PRECEDENCE, true)?; + if !self.negated { + out.push_char(')'); + } + Ok(()) + } + } + } +} + +/// Membership test, `expr [NOT] IN (list…)`. +/// +/// An empty list renders as a boolean literal and does not write `expr`. +/// `NOT IN` of an empty list is the true literal. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::InList; +/// use std::str::FromStr; +/// +/// let expr = InList { +/// expr: Box::new(ExprInner::Ident(Ident::from_str("id").unwrap())), +/// list: Vec::new(), +/// negated: false, +/// }; +/// assert!(expr.list.is_empty()); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct InList { + pub expr: Box, + pub list: Vec, + pub negated: bool, +} + +impl InList { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("InListExpr"); + + fmt.positional(self.expr.python_repr(py)?); + + let mut tuple = fmt.positional_tuple(); + for item in &self.list { + tuple.positional(item.python_repr(py)?); + } + tuple.close(); + + fmt.keyword_true("negated", self.negated); + fmt.close(); + Ok(fmt) + } +} + +impl WriteSql for InList { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + if self.list.is_empty() { + // `x IN ()` is false and `x NOT IN ()` is true, including when `x` is NULL. + dialect.0.write_boolean(out, self.negated)?; + return Ok(()); + } + + if dialect.0.parenthesize_in_between() { + out.push_char('('); + } + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + if self.negated { + out.push_str(" NOT IN ("); + } else { + out.push_str(" IN ("); + } + write_separated(dialect, out, &self.list, ", ")?; + out.push_char(')'); + if dialect.0.parenthesize_in_between() { + out.push_char(')'); + } + Ok(()) + } +} + +/// Inclusive range, `expr [NOT] BETWEEN low AND high`. +/// +/// Both bounds are expressions. There is no `SYMMETRIC` clause. Some dialects +/// parenthesize the whole predicate. +/// +/// ``` +/// use rapidquery::expr::enum_::Between; +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use std::str::FromStr; +/// +/// let expr = Between { +/// expr: Box::new(ExprInner::Ident(Ident::from_str("n").unwrap())), +/// negated: false, +/// low: Box::new(ExprInner::Ident(Ident::from_str("lo").unwrap())), +/// high: Box::new(ExprInner::Ident(Ident::from_str("hi").unwrap())), +/// }; +/// assert!(!expr.negated); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Between { + pub expr: Box, + pub negated: bool, + pub low: Box, + pub high: Box, +} + +impl Between { + pub(crate) const PRECEDENCE: u8 = 5; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("BetweenExpr"); + fmt.positional(self.expr.python_repr(py)?) + .positional(self.low.python_repr(py)?) + .positional(self.high.python_repr(py)?) + .keyword_true("negated", self.negated) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for Between { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + if dialect.0.parenthesize_in_between() { + out.push_char('('); + } + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + if self.negated { + out.push_str(" NOT BETWEEN "); + } else { + out.push_str(" BETWEEN "); + } + self.low.write_operand(dialect, out, Self::PRECEDENCE, true)?; + out.push_str(" AND "); + self.high.write_operand(dialect, out, Self::PRECEDENCE, true)?; + if dialect.0.parenthesize_in_between() { + out.push_char(')'); + } + Ok(()) + } +} + +/// Cast to a SQL type name: `CAST`, `TRY_CAST`, or `::`. +/// +/// `kind` stays one of those three. The dialect rewrites `TRY_CAST` to its own +/// spelling, such as `SAFE_CAST`, or rejects it. +/// +/// ``` +/// use rapidquery::data_type::DataTypeName; +/// use rapidquery::expr::enum_::Cast; +/// use rapidquery::expr::enum_::CastKind; +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use std::str::FromStr; +/// +/// let expr = Cast { +/// kind: CastKind::Cast, +/// expr: Box::new(ExprInner::Ident(Ident::from_str("id").unwrap())), +/// data_type_name: DataTypeName::from("int"), +/// }; +/// assert_eq!(expr.data_type_name.as_str(), "INT"); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Cast { + pub kind: CastKind, + pub expr: Box, + pub data_type_name: crate::data_type::DataTypeName, +} + +impl Cast { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("CastExpr"); + + fmt.positional(self.expr.python_repr(py)?) + .positional(self.data_type_name.as_str()); + + if self.kind != CastKind::Cast { + fmt.keyword("kind", self.kind.as_sql()); + } + + fmt.close(); + Ok(fmt) + } + + fn write_call(&self, dialect: &PyDialect, out: &mut W, name: &str) -> PrepareResult<()> { + out.push_str(name); + out.push_char('('); + self.expr.write_sql(dialect, out)?; + out.push_str(" AS "); + self.data_type_name.write_sql(dialect, out)?; + out.push_char(')'); + Ok(()) + } +} + +impl WriteSql for Cast { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match self.kind { + CastKind::Cast => self.write_call(dialect, out, "CAST"), + CastKind::TryCast => match dialect.0.try_cast() { + flags::TryCast::Unsupported => Err(unsupported("try cast is not supported by this dialect")), + flags::TryCast::TryCast => self.write_call(dialect, out, "TRY_CAST"), + flags::TryCast::TryCastWord => self.write_call(dialect, out, "TRYCAST"), + flags::TryCast::SafeCast => self.write_call(dialect, out, "SAFE_CAST"), + }, + CastKind::DoubleColon => { + if !dialect.0.supports_double_colon() { + return Err(unsupported("double-colon cast is not supported by this dialect")); + } + out.push_char('('); + self.expr.write_sql(dialect, out)?; + out.push_str(")::"); + self.data_type_name.write_sql(dialect, out)?; + Ok(()) + } + } + } +} + +/// `TRIM` of an expression, from one side, of an optional character set. +/// +/// `where_` selects both ends, the leading end, or the trailing end. `characters` +/// is `None` when the call strips spaces. The dialect picks the function name. +/// +/// ``` +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::enum_::Trim; +/// use rapidquery::expr::enum_::TrimWhere; +/// use std::str::FromStr; +/// +/// let expr = Trim { +/// expr: Box::new(ExprInner::Ident(Ident::from_str("s").unwrap())), +/// where_: TrimWhere::Both, +/// characters: None, +/// }; +/// assert!(expr.characters.is_none()); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Trim { + pub expr: Box, + pub where_: TrimWhere, + pub characters: Option>, +} + +impl Trim { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let characters = self + .characters + .as_ref() + .map(|characters| characters.python_repr(py)) + .transpose()?; + + let mut fmt = repr::ReprFormatter::new_call("TrimExpr"); + + fmt.positional(self.expr.python_repr(py)?); + + if self.where_ != TrimWhere::Both { + fmt.keyword("where", self.where_.as_sql()); + } + + fmt.keyword("characters", characters).close(); + Ok(fmt) + } +} + +#[inline] +fn write_trim_function(trim: &Trim, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + let name = match trim.where_ { + TrimWhere::Both => "TRIM", + TrimWhere::Leading => "LTRIM", + TrimWhere::Trailing => "RTRIM", + }; + out.push_str(name); + out.push_char('('); + trim.expr.write_sql(dialect, out)?; + if let Some(characters) = &trim.characters { + out.push_str(", "); + characters.write_sql(dialect, out)?; + } + out.push_char(')'); + Ok(()) +} + +impl WriteSql for Trim { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.0.trim() { + flags::Trim::SqlFrom => { + out.push_str("TRIM("); + match (&self.where_, &self.characters) { + (TrimWhere::Both, None) => self.expr.write_sql(dialect, out)?, + (TrimWhere::Both, Some(characters)) => { + characters.write_sql(dialect, out)?; + out.push_str(" FROM "); + self.expr.write_sql(dialect, out)?; + } + (side, None) => { + out.push_str(side.as_sql()); + out.push_str(" FROM "); + self.expr.write_sql(dialect, out)?; + } + (side, Some(characters)) => { + out.push_str(side.as_sql()); + out.push_char(' '); + characters.write_sql(dialect, out)?; + out.push_str(" FROM "); + self.expr.write_sql(dialect, out)?; + } + } + out.push_char(')'); + Ok(()) + } + flags::Trim::Function => write_trim_function(self, dialect, out), + flags::Trim::SpacesOnly => { + if self.characters.is_some() { + return Err(unsupported("trim characters are not supported by this dialect")); + } + write_trim_function(self, dialect, out) + } + } + } +} + +/// Expression with a collation name attached. +/// +/// The collation is an identifier. Dialects that spell this as a function quote +/// that name as a string instead. Rendering fails when collation is unsupported. +/// +/// ``` +/// use rapidquery::expr::enum_::Collate; +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use std::str::FromStr; +/// +/// let expr = Collate { +/// expr: Box::new(ExprInner::Ident(Ident::from_str("s").unwrap())), +/// collation: Ident::from_str("en_US").unwrap(), +/// }; +/// assert_eq!(expr.collation.len(), 1); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Collate { + pub expr: Box, + pub collation: Ident, +} + +impl Collate { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("CollateExpr"); + fmt.positional(self.expr.python_repr(py)?) + .positional(self.collation.python_repr()) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for Collate { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.0.collate() { + flags::Collate::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), + flags::Collate::InfixIdent => { + self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; + out.push_str(" COLLATE "); + self.collation.write_sql(dialect, out) + } + flags::Collate::FunctionString => { + let body = match self.collation.parts() { + [super::IdentPart::Quote(body) | super::IdentPart::Unquote(body)] if !body.is_empty() => { + body.as_str() + } + _ => { + return Err(unsupported("collation must be a single name")); + } + }; + out.push_str("COLLATE("); + self.expr.write_sql(dialect, out)?; + out.push_str(", "); + dialect.0.write_string_literal(out, body)?; + out.push_char(')'); + Ok(()) + } + } + } +} + +/// `CASE` expression, searched or matched against an operand. +/// +/// `operand` is `None` for `CASE WHEN`. Each `when` entry is a condition or +/// comparison value paired with its result. +/// +/// ``` +/// use rapidquery::expr::enum_::Case; +/// +/// let expr = Case { +/// operand: None, +/// when: Vec::new(), +/// else_result: None, +/// }; +/// assert!(expr.operand.is_none()); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Case { + pub operand: Option>, + pub when: Vec<(ExprInner, ExprInner)>, + pub else_result: Option>, +} + +impl Case { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let operand = self + .operand + .as_ref() + .map(|operand| operand.python_repr(py)) + .transpose()?; + + let mut fmt = repr::ReprFormatter::new_call("CaseExpr"); + fmt.positional(operand); + + for (when, then) in self.when.iter() { + fmt.append_call("when") + .positional(when.python_repr(py)?) + .positional(then.python_repr(py)?); + } + + if let Some(else_result) = &self.else_result { + fmt.append_call("else_").positional(else_result.python_repr(py)?); + } + + fmt.close(); + Ok(fmt) + } +} + +impl WriteSql for Case { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + out.push_str("CASE"); + if let Some(operand) = &self.operand { + out.push_char(' '); + operand.write_sql(dialect, out)?; + } + for (when, then) in &self.when { + out.push_str(" WHEN "); + when.write_sql(dialect, out)?; + out.push_str(" THEN "); + then.write_sql(dialect, out)?; + } + if let Some(else_result) = &self.else_result { + out.push_str(" ELSE "); + else_result.write_sql(dialect, out)?; + } + out.push_str(" END"); + Ok(()) + } +} + +/// SQL template. Each `{}` slot is filled by one argument expression. +/// +/// `{{` and `}}` write a literal brace. The template itself is not escaped. +/// Rendering fails when the number of slots and arguments differ. +/// +/// ``` +/// use rapidquery::expr::enum_::Custom; +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use std::str::FromStr; +/// +/// let expr = Custom { +/// sql: arcstr::ArcStr::from("COALESCE({}, 0)"), +/// args: vec![ExprInner::Ident(Ident::from_str("n").unwrap())], +/// }; +/// assert_eq!(expr.args.len(), 1); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Custom { + pub sql: arcstr::ArcStr, + pub args: Vec, +} + +impl Custom { + pub(crate) const PRECEDENCE: u8 = 0; + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let mut fmt = repr::ReprFormatter::new_call("CustomExpr"); + + fmt.positional(self.sql.as_str()); + + for arg in &self.args { + fmt.positional(arg.python_repr(py)?); + } + + fmt.close(); + Ok(fmt) + } +} + +impl WriteSql for Custom { + #[cfg_attr(feature = "nightly", optimize(speed))] + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + let bytes = self.sql.as_bytes(); + let mut i = 0; + let mut start = 0; + let mut used = 0usize; + + while i < bytes.len() { + if bytes[i] == b'{' { + if i + 1 < bytes.len() && bytes[i + 1] == b'{' { + out.push_str(&self.sql[start..i]); + out.push_char('{'); + i += 2; + start = i; + continue; + } + if i + 1 < bytes.len() && bytes[i + 1] == b'}' { + out.push_str(&self.sql[start..i]); + if used >= self.args.len() { + return Err(crate::internal::sql::PrepareError::CustomPlaceholders { + expected: used + 1, + got: self.args.len(), + }); + } + self.args[used].write_sql(dialect, out)?; + used += 1; + i += 2; + start = i; + continue; + } + } else if bytes[i] == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}' { + out.push_str(&self.sql[start..i]); + out.push_char('}'); + i += 2; + start = i; + continue; + } + i += 1; + } + out.push_str(&self.sql[start..]); + + if used != self.args.len() { + return Err(crate::internal::sql::PrepareError::CustomPlaceholders { + expected: used, + got: self.args.len(), + }); + } + Ok(()) + } +} diff --git a/src/expr/enum_/operator.rs b/src/expr/enum_/operator.rs new file mode 100644 index 0000000..17fee95 --- /dev/null +++ b/src/expr/enum_/operator.rs @@ -0,0 +1,258 @@ +/// Unary operator. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UnaryOp { + Not, + Plus, + Minus, + BitNot, +} + +/// How a [`UnaryOp`] is written relative to its operand. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum UnarySpacing { + /// Operator, space, operand (`NOT x`, `# path`). + SpacedPrefix, + /// Operator glued to the operand (`-1`, `|/9`). + GluedPrefix, +} + +impl UnaryOp { + #[inline] + pub fn parse(s: &str) -> Option { + if s.eq_ignore_ascii_case("not") { + return Some(Self::Not); + } + + match s { + "+" => Some(Self::Plus), + "-" => Some(Self::Minus), + "~" => Some(Self::BitNot), + _ => None, + } + } + + #[inline] + pub fn as_sql(self) -> &'static str { + match self { + Self::Not => "NOT", + Self::Plus => "+", + Self::Minus => "-", + Self::BitNot => "~", + } + } + + /// How this operator is written relative to its operand. + #[inline] + pub(crate) fn spacing(self) -> UnarySpacing { + match self { + Self::Not => UnarySpacing::SpacedPrefix, + Self::Plus | Self::Minus | Self::BitNot => UnarySpacing::GluedPrefix, + } + } + + /// Lower is tighter binding. `NOT` sits between comparisons and `AND`. + #[inline] + pub(crate) fn precedence(self) -> u8 { + match self { + Self::Not => 6, + Self::Plus | Self::Minus | Self::BitNot => 1, + } + } +} + +/// Binary operator. Unknown spellings become [`BinaryOp::Custom`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum BinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + Eq, + NotEq, + Lt, + LtEq, + Gt, + GtEq, + And, + Or, + BitAnd, + BitOr, + BitXor, + LShift, + RShift, + StringConcat, + Custom(arcstr::ArcStr), +} + +impl BinaryOp { + #[inline] + pub fn parse(s: &str) -> Self { + if s.eq_ignore_ascii_case("and") { + return Self::And; + } + if s.eq_ignore_ascii_case("or") { + return Self::Or; + } + + match s { + "+" => Self::Add, + "-" => Self::Sub, + "*" => Self::Mul, + "/" => Self::Div, + "%" => Self::Mod, + "=" => Self::Eq, + "<>" | "!=" => Self::NotEq, + "<" => Self::Lt, + "<=" => Self::LtEq, + ">" => Self::Gt, + ">=" => Self::GtEq, + "&" => Self::BitAnd, + "|" => Self::BitOr, + "^" => Self::BitXor, + "<<" => Self::LShift, + ">>" => Self::RShift, + "||" => Self::StringConcat, + other => Self::Custom(arcstr::ArcStr::from(other)), + } + } + + #[inline] + pub fn as_sql(&self) -> &str { + match self { + Self::Add => "+", + Self::Sub => "-", + Self::Mul => "*", + Self::Div => "/", + Self::Mod => "%", + Self::Eq => "=", + Self::NotEq => "<>", + Self::Lt => "<", + Self::LtEq => "<=", + Self::Gt => ">", + Self::GtEq => ">=", + Self::And => "AND", + Self::Or => "OR", + Self::BitAnd => "&", + Self::BitOr => "|", + Self::BitXor => "^", + Self::LShift => "<<", + Self::RShift => ">>", + Self::StringConcat => "||", + Self::Custom(s) => s.as_str(), + } + } + + /// Lower is tighter binding. Used by [`super::Binary`] and parenthesization. + #[inline] + pub fn precedence(&self) -> u8 { + match self { + Self::Mul | Self::Div | Self::Mod => 2, + Self::Add | Self::Sub => 3, + Self::BitAnd | Self::BitOr | Self::BitXor | Self::LShift | Self::RShift | Self::StringConcat => 4, + Self::Eq | Self::NotEq | Self::Lt | Self::LtEq | Self::Gt | Self::GtEq | Self::Custom(_) => 5, + Self::And => 7, + Self::Or => 9, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IsKind { + Null, + True, + False, + Unknown, +} + +impl IsKind { + #[inline] + pub fn parse(s: &str) -> Option { + if s.eq_ignore_ascii_case("NULL") { + Some(Self::Null) + } else if s.eq_ignore_ascii_case("TRUE") { + Some(Self::True) + } else if s.eq_ignore_ascii_case("FALSE") { + Some(Self::False) + } else if s.eq_ignore_ascii_case("UNKNOWN") { + Some(Self::Unknown) + } else { + None + } + } + + #[inline] + pub fn as_sql(self) -> &'static str { + match self { + Self::Null => "NULL", + Self::True => "TRUE", + Self::False => "FALSE", + Self::Unknown => "UNKNOWN", + } + } +} + +/// `CAST` / `TRY_CAST` / `::`. Dialect spelling of `TRY_CAST` is chosen at render time. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CastKind { + Cast, + TryCast, + /// `expr::type`. Opt-in; a plain `CAST` is never rewritten to this. + DoubleColon, +} + +impl CastKind { + #[inline] + pub fn parse(s: &str) -> Option { + if s.eq_ignore_ascii_case("CAST") { + Some(Self::Cast) + } else if s.eq_ignore_ascii_case("TRY_CAST") { + Some(Self::TryCast) + } else if s == "::" { + Some(Self::DoubleColon) + } else { + None + } + } + + #[inline] + pub fn as_sql(self) -> &'static str { + match self { + Self::Cast => "CAST", + Self::TryCast => "TRY_CAST", + Self::DoubleColon => "::", + } + } +} + +/// `BOTH` / `LEADING` / `TRAILING` in [`super::Trim`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TrimWhere { + Both, + Leading, + Trailing, +} + +impl TrimWhere { + #[inline] + pub fn parse(s: &str) -> Option { + if s.eq_ignore_ascii_case("BOTH") { + Some(Self::Both) + } else if s.eq_ignore_ascii_case("LEADING") { + Some(Self::Leading) + } else if s.eq_ignore_ascii_case("TRAILING") { + Some(Self::Trailing) + } else { + None + } + } + + #[inline] + pub fn as_sql(self) -> &'static str { + match self { + Self::Both => "BOTH", + Self::Leading => "LEADING", + Self::Trailing => "TRAILING", + } + } +} diff --git a/src/expr/enum_/value.rs b/src/expr/enum_/value.rs new file mode 100644 index 0000000..a6b99f6 --- /dev/null +++ b/src/expr/enum_/value.rs @@ -0,0 +1,68 @@ +//! A Python object plus the [`Serializer`] used to emit it as SQL. + +use pyo3::types::PyAnyMethods; +use pyo3::types::PyStringMethods; + +use crate::dialect::PyDialect; +use crate::internal::alias; +use crate::internal::repr; +use crate::internal::sql::PrepareResult; +use crate::internal::sql::SqlWriter; +use crate::internal::sql::WriteSql; +use crate::serializers::SerializerType; +use crate::serializers::serializer_repr; + +/// Python object plus the serializer that turns it into a SQL literal. +/// +/// Parameterized SQL binds [`Self::object`] and ignores the serializer. +/// Raw SQL runs the serializer. Python `None` becomes `NULL`. +/// +/// ``` +/// pyo3::Python::attach(|py| { +/// let object = pyo3::types::PyString::new(py, "hi").into_any().unbind(); +/// let value = rapidquery::expr::enum_::Value::new(object, rapidquery::serializers::SerializerType::String); +/// assert!(matches!(value.serializer, rapidquery::serializers::SerializerType::String)); +/// }); +/// ``` +#[derive(Debug, Clone)] +pub struct Value { + pub object: alias::PyObject, + pub serializer: SerializerType, +} + +impl Value { + #[inline] + pub fn new(object: alias::PyObject, serializer: SerializerType) -> Self { + Self { object, serializer } + } + + #[inline] + pub fn infer_bound(bound: alias::BoundObject) -> pyo3::PyResult { + SerializerType::infer_bound(&bound).map(|x| Self::new(bound.unbind(), x)) + } + + pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + let obj_repr = self.object.bind(py).repr()?; + let text = obj_repr.to_cow()?; + + let mut fmt = repr::ReprFormatter::new_call("Value"); + + fmt.positional_raw(&text) + .positional(serializer_repr(&self.serializer)) + .close(); + + Ok(fmt) + } +} + +impl WriteSql for Value { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + out.push_value(dialect, &self.serializer, &self.object) + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.serializer == other.serializer && self.object.as_ptr() == other.object.as_ptr() + } +} diff --git a/src/expr/extractors.rs b/src/expr/extractors.rs new file mode 100644 index 0000000..696aed7 --- /dev/null +++ b/src/expr/extractors.rs @@ -0,0 +1,268 @@ +use std::str::FromStr; + +use pyo3::types::PyAnyMethods; +use pyo3::types::PyListMethods; +use pyo3::types::PyTupleMethods; + +use super::enum_::ExprInner; +use super::enum_::Ident; +use super::enum_::IdentPart; +use super::enum_::LikePattern; +use super::enum_::Tuple; +use crate::internal::utils; + +/// `true` when `obj` is `Asterisk` or `type[Asterisk]`. +#[inline] +unsafe fn is_asterisk(obj: *mut pyo3::ffi::PyObject) -> bool { + let type_ptr = if pyo3::ffi::PyType_CheckExact(obj) == 1 { + obj.cast::() + } else { + pyo3::ffi::Py_TYPE(obj) + }; + type_ptr == crate::typeref::ASTERISK_TYPE +} + +/// Expression extracted from Python. A bare `str` becomes a value, not a name. +/// +/// Use this for arguments where a string is data. Identifiers still pass through +/// as identifiers. The wrapped node is [`ExprInner`]. +/// +/// ``` +/// use std::str::FromStr; +/// +/// use rapidquery::expr::enum_::ExprInner; +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::extractors::LiteralExprExtractor; +/// +/// let extracted = LiteralExprExtractor(ExprInner::Ident(Ident::from_str("id").unwrap())); +/// assert!(matches!(extracted.0, ExprInner::Ident(_))); +/// ``` +pub struct LiteralExprExtractor(pub ExprInner); + +/// Native [`Ident`] extracted from a Python identifier argument. +/// +/// Accepts `Ident`, one quoted `str` (including `"*"`), `Unquote`, `Asterisk`, +/// and `type[Asterisk]`. Sequences are rejected. +/// +/// ``` +/// use std::str::FromStr; +/// +/// use rapidquery::expr::enum_::Ident; +/// use rapidquery::expr::extractors::IdentExtractor; +/// +/// let extracted = IdentExtractor(Ident::from_str("users.id").unwrap()); +/// assert_eq!(extracted.0.len(), 2); +/// ``` +pub struct IdentExtractor(pub Ident); + +fn extract_expr( + obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>, + str_as_ident: bool, +) -> pyo3::PyResult { + let ptr = obj.as_ptr(); + unsafe { + if crate::typeref::check(ptr, crate::typeref::EXPR_TYPE) { + let expr = obj.cast_unchecked::(); + return Ok(expr.get().0.clone()); + } + + if crate::typeref::check_exact(ptr, crate::typeref::UNQUOTE_TYPE) { + let unquote = obj.cast_unchecked::(); + let part = IdentPart::unquoted(unquote.get().0.as_str()) + .map_err(|err| rapidquery_macros::pyerr!(PyValueError, "{}", err))?; + + return Ok(ExprInner::Ident(Ident::from_one(part))); + } + + if is_asterisk(ptr) { + return Ok(ExprInner::Ident(Ident::from_one(IdentPart::Asterisk))); + } + + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + let s = utils::borrow_py_str_buffer(obj.py(), ptr)?; + + // a bare `str` becomes [`ExprInner::Ident`] if `str_as_ident` + if str_as_ident { + let inner = Ident::from_str(s).map_err(|err| rapidquery_macros::pyerr!(PyValueError, "{}", err))?; + + return Ok(ExprInner::Ident(inner)); + } + + return Ok(ExprInner::Value(super::enum_::Value::new( + obj.as_any().clone().unbind(), + crate::serializers::SerializerType::String, + ))); + } + + if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { + let tuple = obj.cast_unchecked::(); + let mut items = Vec::with_capacity(tuple.len()); + + for item in tuple.iter() { + items.push(extract_expr(item.as_borrowed(), str_as_ident)?); + } + + return Ok(Tuple { items }.into()); + } + } + + // __expr__ protocol + rapidquery_macros::cust_protocol! { + on: obj, + attr: c"__expr__", + ty: crate::typeref::EXPR_TYPE, + matches: { + let expr = protocol.cast_into_unchecked::(); + return Ok(expr.get().0.clone()); + }, + mismatches: { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "__expr__ protocol attribute returns non-expr type: {}", + utils::get_type_name(obj.py(), protocol.as_ptr()) + ) + } + } + + super::enum_::Value::infer_bound(obj.as_any().clone()).map(ExprInner::Value) +} + +/// Scalar `LIKE` pattern, or a list/tuple of patterns (`LIKE ANY`). +/// +/// A Python `str` is never a sequence. An already-built `TupleExpr` is a +/// single pattern; a raw `tuple`/`list` is ANY. +pub fn extract_like_pattern(obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>) -> pyo3::PyResult { + let ptr = obj.as_ptr(); + unsafe { + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + return Ok(LikePattern::One(Box::new(extract_expr(obj, false)?))); + } + if pyo3::ffi::PyList_CheckExact(ptr) == 1 { + let list = obj.cast_unchecked::(); + return extract_like_any(list.iter()); + } + if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { + let tuple = obj.cast_unchecked::(); + return extract_like_any(tuple.iter()); + } + } + Ok(LikePattern::One(Box::new(extract_expr(obj, false)?))) +} + +fn extract_like_any<'py>(iter: impl IntoIterator>) -> pyo3::PyResult { + let mut items = Vec::new(); + for item in iter { + items.push(item.extract::()?.0); + } + if items.is_empty() { + return rapidquery_macros::pyerr!(Err(PyValueError), "LIKE ANY pattern list is empty"); + } + Ok(LikePattern::Any(items)) +} + +pub(super) enum ExtractedIdentArgument { + Ident(Ident), + Part(IdentPart), +} + +/// Hot-path extract: `str` first, then Ident / Unquote / Asterisk. +#[inline] +pub(super) fn extract_ident_argument( + obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>, +) -> pyo3::PyResult { + let ptr = obj.as_ptr(); + unsafe { + // SAFETY: `ptr` is a live borrowed `PyObject`. Type pointers are + // interned in `initialize_typeref` before any extractor runs. + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + let s = utils::borrow_py_str_buffer(obj.py(), ptr)?; + let part = IdentPart::quoted(s).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + + return Ok(ExtractedIdentArgument::Part(part)); + } + + if crate::typeref::check_exact(ptr, crate::typeref::IDENT_TYPE) { + let expr = obj.cast_unchecked::(); + + match &expr.get().0 { + ExprInner::Ident(ident) => { + return Ok(ExtractedIdentArgument::Ident(ident.clone())); + } + _ => unreachable!("Ident instance always holds ExprInner::Ident"), + } + } + + if crate::typeref::check_exact(ptr, crate::typeref::UNQUOTE_TYPE) { + let unquote = obj.cast_unchecked::(); + let part = IdentPart::unquoted(unquote.get().0.as_str()) + .map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + + return Ok(ExtractedIdentArgument::Part(part)); + } + + if is_asterisk(ptr) { + return Ok(ExtractedIdentArgument::Part(IdentPart::Asterisk)); + } + } + + // __ident__ protocol + rapidquery_macros::cust_protocol! { + on: obj, + attr: c"__ident__", + ty: crate::typeref::IDENT_TYPE, + matches: { + let expr = protocol.cast_into_unchecked::(); + + match &expr.get().0 { + ExprInner::Ident(ident) => { + return Ok(ExtractedIdentArgument::Ident(ident.clone())); + } + _ => unreachable!("Ident instance always holds ExprInner::Ident"), + } + }, + mismatches: { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "__ident__ protocol attribute returns non-ident type: {}", + utils::get_type_name(obj.py(), protocol.as_ptr()) + ) + } + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected str, Ident, Unquote or Asterisk, got {}", + unsafe { crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) } + ) +} + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for LiteralExprExtractor { + type Error = pyo3::PyErr; + + #[inline] + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + extract_expr(obj, false).map(Self) + } +} + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for IdentExtractor { + type Error = pyo3::PyErr; + + #[inline] + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + unsafe { + // SAFETY: `ptr` is a live borrowed `PyObject`. + if pyo3::ffi::PyUnicode_CheckExact(obj.as_ptr()) == 1 { + let s = utils::borrow_py_str_buffer(obj.py(), obj.as_ptr())?; + let inner = Ident::from_str(s).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + + return Ok(Self(inner)); + } + } + + match extract_ident_argument(obj)? { + ExtractedIdentArgument::Ident(ident) => Ok(Self(ident)), + ExtractedIdentArgument::Part(part) => Ok(Self(Ident::from_one(part))), + } + } +} diff --git a/src/expr/func.rs b/src/expr/func.rs new file mode 100644 index 0000000..9e39b37 --- /dev/null +++ b/src/expr/func.rs @@ -0,0 +1,129 @@ +use pyo3::types::PyAnyMethods; +use pyo3::types::PyTupleMethods; + +use crate::internal::alias; +use crate::internal::utils; + +use super::base::PyExpr; +use super::enum_::NullTreatment; +use super::extractors::IdentExtractor; +use super::extractors::LiteralExprExtractor; + +/// SQL function call. The name is an identifier, and each method returns a new call. +#[rapidquery_macros::pyclass("rapidquery.expr.Function", skip_from_py_object, extends = PyExpr)] +#[derive(Debug, Default)] +pub struct PyFunction; + +impl PyFunction { + #[inline(always)] + fn inner<'a>(slf: &'a pyo3::PyRef<'a, PyFunction>) -> &'a super::enum_::Function { + debug_assert!( + matches!(&slf.as_super().0, super::enum_::ExprInner::Function(..)), + "PyFunction instance always holds ExprInner::Function" + ); + + match &slf.as_super().0 { + super::enum_::ExprInner::Function(v) => v, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } + + fn wrap(py: pyo3::Python<'_>, inner: super::enum_::Function) -> pyo3::PyResult { + pyo3::Py::new( + py, + pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self), + ) + .map(pyo3::Py::into_any) + } +} + +#[pyo3::pymethods] +impl PyFunction { + #[new] + #[pyo3(signature = (name, /, *args, distinct = false))] + fn __new__( + name: IdentExtractor, + args: alias::ArgsType<'_>, + distinct: bool, + ) -> pyo3::PyResult> { + let mut extracted = Vec::with_capacity(args.len()); + for item in args.iter() { + extracted.push(item.extract::()?.0); + } + + let inner = super::enum_::Function { + name: name.0, + args: extracted, + distinct, + filter: None, + null_treatment: None, + }; + Ok(pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self)) + } + + /// Return a copy whose filter predicate is `predicate`. + fn filter(slf: pyo3::PyRef<'_, Self>, predicate: LiteralExprExtractor) -> pyo3::PyResult { + let mut inner = Self::inner(&slf).clone(); + inner.filter = Some(Box::new(predicate.0)); + Self::wrap(slf.py(), inner) + } + + /// Return a copy that writes `IGNORE NULLS`. + fn ignore_nulls(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { + let mut inner = Self::inner(&slf).clone(); + inner.null_treatment = Some(NullTreatment::IgnoreNulls); + Self::wrap(slf.py(), inner) + } + + /// Return a copy that writes `RESPECT NULLS`. + fn respect_nulls(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { + let mut inner = Self::inner(&slf).clone(); + inner.null_treatment = Some(NullTreatment::RespectNulls); + Self::wrap(slf.py(), inner) + } + + /// The function name. + fn get_name(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + PyExpr(super::enum_::ExprInner::Ident(inner.name.clone())).into_subclasses(slf.py()) + } + + /// Whether the call writes `DISTINCT`. + fn get_distinct(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.distinct + } + + /// The argument expressions. + fn get_args(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + + unsafe { + utils::iterator_to_tuple( + slf.py(), + inner.args.len() as isize, + inner + .args + .iter() + .map(|x| PyExpr(x.clone()).into_subclasses(py).map(|x| x.into_ptr())), + ) + } + } + + /// The filter predicate, or `None`. + fn get_filter(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + + match &inner.filter { + Some(e) => super::base::into_subclasses(py, (**e).clone()), + None => Ok(utils::new_pynone(py)), + } + } + + /// `IGNORE NULLS`, `RESPECT NULLS`, or `None`. + fn get_null_treatment(slf: pyo3::PyRef<'_, Self>) -> Option<&'static str> { + Self::inner(&slf).null_treatment.map(NullTreatment::as_sql) + } +} diff --git a/src/expr/ident.rs b/src/expr/ident.rs new file mode 100644 index 0000000..8131cd6 --- /dev/null +++ b/src/expr/ident.rs @@ -0,0 +1,148 @@ +use pyo3::types::PyTupleMethods; + +use super::enum_::Ident; +use super::enum_::IdentPart; +use super::enum_::ParseIdentError; +use super::extractors::ExtractedIdentArgument; +use super::extractors::extract_ident_argument; +use crate::internal::alias; +use crate::internal::repr; + +/// Convert a native part into the matching Python object (`str`, `Unquote`, +/// or `Asterisk`). +#[inline] +fn ident_part_to_pyobject<'py>( + py: pyo3::Python<'py>, + ident_part: &IdentPart, +) -> pyo3::PyResult> { + match ident_part { + IdentPart::Quote(s) => Ok(pyo3::types::PyString::new(py, s.as_str()).into_any()), + IdentPart::Unquote(s) => Ok(pyo3::Bound::new(py, PyUnquote(arcstr::ArcStr::from(s.as_str())))?.into_any()), + IdentPart::Asterisk => Ok(pyo3::Bound::new(py, PyAsterisk)?.into_any()), + } +} + +/// Dotted SQL name. A string part is quoted; `Asterisk` is the wildcard. +#[rapidquery_macros::pyclass("rapidquery.expr.Ident", skip_from_py_object, extends = super::base::PyExpr)] +#[derive(Debug, Default)] +pub struct PyIdent; + +impl PyIdent { + #[inline(always)] + fn inner<'a>(slf: &'a pyo3::PyRef<'a, Self>) -> &'a Ident { + debug_assert!( + matches!(&slf.as_super().0, super::enum_::ExprInner::Ident(..)), + "PyIdent instance always holds ExprInner::Ident" + ); + + match &slf.as_super().0 { + super::enum_::ExprInner::Ident(ident) => ident, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +#[pyo3::pymethods] +impl PyIdent { + #[new] + #[pyo3(signature = (*parts))] + fn __new__(parts: alias::ArgsType) -> pyo3::PyResult> { + let ident = match parts.len() { + 0 => return rapidquery_macros::pyerr!(Err(PyValueError), "{}", ParseIdentError::Empty), + 1 => match extract_ident_argument(parts.get_item(0)?.as_borrowed())? { + ExtractedIdentArgument::Ident(ident) => ident, + ExtractedIdentArgument::Part(part) => Ident::from_one(part), + }, + _ => { + let mut acc = smallvec::SmallVec::<[IdentPart; 4]>::new(); + for item in parts.iter() { + match extract_ident_argument(item.as_borrowed())? { + ExtractedIdentArgument::Ident(ident) => acc.extend(ident.into_parts()), + ExtractedIdentArgument::Part(part) => acc.push(part), + } + } + + Ident::from_parts(acc).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))? + } + }; + + Ok( + pyo3::PyClassInitializer::from(super::base::PyExpr(super::enum_::ExprInner::Ident(ident))) + .add_subclass(PyIdent), + ) + } + + fn __len__(slf: pyo3::PyRef<'_, Self>) -> usize { + Self::inner(&slf).len() + } + + fn __getitem__<'py>(slf: pyo3::PyRef<'py, Self>, index: isize) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + + let x = if index < 0 { + (inner.len() as isize) + index + } else { + index + }; + + match inner.parts().get(x as usize) { + Some(part) => ident_part_to_pyobject(slf.py(), part), + None => rapidquery_macros::pyerr!(Err(PyIndexError), "index out of range"), + } + } + + /// Copy the parts into a list. + fn to_list(slf: pyo3::PyRef<'_, Self>, py: pyo3::Python<'_>) -> pyo3::PyResult> { + Self::inner(&slf) + .parts() + .iter() + .map(|part| ident_part_to_pyobject(py, part).map(|bound| bound.unbind())) + .collect() + } +} + +/// Identifier body written with no quote characters. +#[rapidquery_macros::pyclass("rapidquery.expr.Unquote", skip_from_py_object, eq, hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PyUnquote(pub arcstr::ArcStr); + +#[pyo3::pymethods] +impl PyUnquote { + #[new] + #[pyo3(signature = (name, /))] + fn __new__(name: &str) -> pyo3::PyResult { + // validate the `name` + IdentPart::unquoted(name).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + + Ok(Self(arcstr::ArcStr::from(name))) + } + + /// The identifier body. + #[getter] + fn get_name(&self) -> &str { + self.0.as_str() + } + + fn __repr__(&self) -> String { + let mut fmt = repr::ReprFormatter::new_call("Unquote"); + fmt.positional(self.0.as_str()); + fmt.into_string() + } +} + +/// SQL wildcard `*`, used as the last part of an identifier. +#[rapidquery_macros::pyclass("rapidquery.expr.Asterisk", skip_from_py_object, eq, hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PyAsterisk; + +#[pyo3::pymethods] +impl PyAsterisk { + #[new] + fn __new__() -> Self { + Self + } + + fn __repr__(&self) -> &'static str { + "Asterisk()" + } +} diff --git a/src/expr/mod.rs b/src/expr/mod.rs new file mode 100644 index 0000000..91c7e4a --- /dev/null +++ b/src/expr/mod.rs @@ -0,0 +1,57 @@ +pub mod base; +pub mod enum_; +pub mod extractors; +pub mod func; +pub mod ident; +pub mod nodes; +pub mod value; + +#[pyo3::pymodule(name = "expr", gil_used = false)] +pub mod expr_mod { + #[pymodule_export] + use super::base::PyExpr; + + #[pymodule_export] + use super::func::PyFunction; + + #[pymodule_export] + use super::ident::PyAsterisk; + #[pymodule_export] + use super::ident::PyIdent; + #[pymodule_export] + use super::ident::PyUnquote; + + #[pymodule_export] + use super::value::PyValue; + + #[pymodule_export] + use super::nodes::PyBetweenExpr; + #[pymodule_export] + use super::nodes::PyBinaryExpr; + #[pymodule_export] + use super::nodes::PyCaseExpr; + #[pymodule_export] + use super::nodes::PyCastExpr; + #[pymodule_export] + use super::nodes::PyCollateExpr; + #[pymodule_export] + use super::nodes::PyCustomExpr; + #[pymodule_export] + use super::nodes::PyInListExpr; + #[pymodule_export] + use super::nodes::PyIsDistinctFromExpr; + #[pymodule_export] + use super::nodes::PyIsExpr; + #[pymodule_export] + use super::nodes::PyLikeExpr; + #[pymodule_export] + use super::nodes::PyRLikeExpr; + #[pymodule_export] + use super::nodes::PySimilarToExpr; + #[pymodule_export] + use super::nodes::PyTrimExpr; + #[pymodule_export] + use super::nodes::PyTupleExpr; + #[pymodule_export] + use super::nodes::PyUnaryExpr; +} diff --git a/src/expr/nodes.rs b/src/expr/nodes.rs new file mode 100644 index 0000000..95b71db --- /dev/null +++ b/src/expr/nodes.rs @@ -0,0 +1,777 @@ +use pyo3::types::PyAnyMethods; +use pyo3::types::PyTupleMethods; + +use super::base::PyExpr; +use super::enum_::ExprInner; +use super::extractors::IdentExtractor; +use super::extractors::LiteralExprExtractor; +use super::extractors::extract_like_pattern; +use crate::internal::alias; +use crate::internal::utils; + +macro_rules! empty_node { + ( + $( + $(#[$meta:meta])* + $name:ident($variant:ident($inner:ty)) => $pyname:literal, + )+ + ) => { + $( + $(#[$meta])* + #[rapidquery_macros::pyclass("rapidquery.expr" + $pyname, skip_from_py_object, extends = PyExpr)] + #[derive(Debug, Default)] + pub struct $name; + + impl $name { + #[inline] + fn inner<'a>(slf: &'a pyo3::PyRef<'a, $name>) -> &'a $inner { + debug_assert!( + matches!(&slf.as_super().0, ExprInner::$variant(..)), + concat!(stringify!($name), " instance always holds ExprInner::", stringify!($variant)) + ); + + match &slf.as_super().0 { + ExprInner::$variant(v) => v, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } + } + )+ + }; +} + +empty_node! { + /// Parenthesized row / tuple expression `(a, b, …)`. + PyTupleExpr(Tuple(super::enum_::Tuple)) => "TupleExpr", + + /// Prefix expression: `NOT x`, `+x`, `-x`, or `~x`. + PyUnaryExpr(Unary(super::enum_::Unary)) => "UnaryExpr", + + /// Binary expression (`a + b`, `a AND b`, custom operators). + PyBinaryExpr(Binary(super::enum_::Binary)) => "BinaryExpr", + + /// `expr IS [NOT] {NULL|TRUE|FALSE|UNKNOWN}`. + PyIsExpr(Is(super::enum_::Is)) => "IsExpr", + + /// `a IS [NOT] DISTINCT FROM b`. + PyIsDistinctFromExpr(IsDistinctFrom(super::enum_::IsDistinctFrom)) => "IsDistinctFromExpr", + + /// `expr [NOT] IN (… )`. + PyInListExpr(InList(super::enum_::InList)) => "InListExpr", + + /// `expr [NOT] BETWEEN low AND high`. + PyBetweenExpr(Between(super::enum_::Between)) => "BetweenExpr", + + /// `expr [NOT] LIKE / ILIKE [ANY] pattern`. + PyLikeExpr(Like(super::enum_::Like)) => "LikeExpr", + + /// `expr [NOT] SIMILAR TO pattern`. + PySimilarToExpr(SimilarTo(super::enum_::SimilarTo)) => "SimilarToExpr", + + /// Regex match (`RLIKE`, `~`, `REGEXP_LIKE`, …). + PyRLikeExpr(RLike(super::enum_::RLike)) => "RLikeExpr", + + /// `CAST(expr AS type)`, `TRY_CAST(expr AS type)`, or `(expr)::type`. + PyCastExpr(Cast(super::enum_::Cast)) => "CastExpr", + + /// `TRIM` of `expr`, optionally from one side and optionally of `characters`. + PyTrimExpr(Trim(super::enum_::Trim)) => "TrimExpr", + + /// `expr COLLATE collation`. + PyCollateExpr(Collate(super::enum_::Collate)) => "CollateExpr", + + /// `CASE [operand] WHEN … THEN … [ELSE …] END`. + PyCaseExpr(Case(super::enum_::Case)) => "CaseExpr", + + /// SQL template. `{}` slots are filled by arguments. `{{` and `}}` are braces. + PyCustomExpr(Custom(super::enum_::Custom)) => "CustomExpr", +} + +#[pyo3::pymethods] +impl PyTupleExpr { + #[new] + #[pyo3(signature = (*items))] + fn __new__(items: alias::ArgsType<'_>) -> pyo3::PyResult> { + use pyo3::types::PyTupleMethods; + + let mut exprs = Vec::with_capacity(items.len()); + for item in items.iter() { + exprs.push(item.extract::()?.0); + } + + Ok(pyo3::PyClassInitializer::from(PyExpr(super::enum_::Tuple { items: exprs }.into())).add_subclass(Self)) + } + + fn get_items(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let items = &Self::inner(&slf).items; + + unsafe { + utils::iterator_to_tuple( + py, + items.len() as isize, + items + .iter() + .map(|x| super::base::into_subclasses(py, x.clone()).map(|x| x.into_ptr())), + ) + } + } +} + +impl PyUnaryExpr { + pub(super) fn new_base(op: super::enum_::UnaryOp, expr: ExprInner) -> PyExpr { + let inner = super::enum_::Unary { + op, + expr: Box::new(expr), + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyUnaryExpr { + #[new] + #[pyo3(signature = (operator, expr, /))] + fn __new__(operator: &str, expr: LiteralExprExtractor) -> pyo3::PyResult> { + let Some(op) = super::enum_::UnaryOp::parse(operator) else { + return rapidquery_macros::pyerr!(Err(PyValueError), "unknown unary operator {:?}", operator); + }; + + let base = Self::new_base(op, expr.0); + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_operator(slf: pyo3::PyRef<'_, Self>) -> &'static str { + let inner = Self::inner(&slf); + inner.op.as_sql() + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } +} + +impl PyBinaryExpr { + pub(super) fn new_base(left: ExprInner, op: super::enum_::BinaryOp, right: ExprInner) -> PyExpr { + let inner = super::enum_::Binary { + left: Box::new(left), + op, + right: Box::new(right), + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyBinaryExpr { + #[new] + #[pyo3(signature = (left, operator, right, /))] + fn __new__( + left: LiteralExprExtractor, + operator: &str, + right: LiteralExprExtractor, + ) -> pyo3::PyClassInitializer { + let base = Self::new_base(left.0, super::enum_::BinaryOp::parse(operator), right.0); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_left(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.left).clone()) + } + + fn get_operator(slf: pyo3::PyRef<'_, Self>) -> String { + let inner = Self::inner(&slf); + inner.op.as_sql().to_owned() + } + + fn get_right(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.right).clone()) + } +} + +impl PyIsExpr { + pub(super) fn new_base(expr: ExprInner, kind: &str, negated: bool) -> pyo3::PyResult { + let Some(kind) = super::enum_::IsKind::parse(kind) else { + return rapidquery_macros::pyerr!( + Err(PyValueError), + "unknown IS kind {:?}; expected NULL, TRUE, FALSE, or UNKNOWN", + kind + ); + }; + + let inner = super::enum_::Is { + expr: Box::new(expr), + negated, + kind, + }; + Ok(PyExpr(inner.into())) + } +} + +#[pyo3::pymethods] +impl PyIsExpr { + #[new] + #[pyo3(signature = (expr, kind, /, *, negated = false))] + fn __new__( + expr: LiteralExprExtractor, + kind: &str, + negated: bool, + ) -> pyo3::PyResult> { + let base = Self::new_base(expr.0, kind, negated)?; + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_kind(slf: pyo3::PyRef<'_, Self>) -> &'static str { + let inner = Self::inner(&slf); + inner.kind.as_sql() + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyIsDistinctFromExpr { + pub(super) fn new_base(left: ExprInner, right: ExprInner, negated: bool) -> PyExpr { + let inner = super::enum_::IsDistinctFrom { + left: Box::new(left), + right: Box::new(right), + negated, + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyIsDistinctFromExpr { + #[new] + #[pyo3(signature = (left, right, /, *, negated = false))] + fn __new__( + left: LiteralExprExtractor, + right: LiteralExprExtractor, + negated: bool, + ) -> pyo3::PyClassInitializer { + let base = Self::new_base(left.0, right.0, negated); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_left(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.left).clone()) + } + + fn get_right(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.right).clone()) + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyInListExpr { + pub(super) fn new_base(expr: ExprInner, list: alias::BoundObject<'_>, negated: bool) -> pyo3::PyResult { + let mut items = Vec::new(); + for item in list.try_iter()? { + items.push(item?.extract::()?.0); + } + + let inner = super::enum_::InList { + expr: Box::new(expr), + list: items, + negated, + }; + Ok(PyExpr(inner.into())) + } +} + +#[pyo3::pymethods] +impl PyInListExpr { + #[new] + #[pyo3(signature = (expr, list, /, *, negated = false))] + fn __new__( + expr: LiteralExprExtractor, + list: alias::BoundObject<'_>, + negated: bool, + ) -> pyo3::PyResult> { + let base = Self::new_base(expr.0, list, negated)?; + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_list(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + + unsafe { + utils::iterator_to_tuple( + slf.py(), + inner.list.len() as isize, + inner + .list + .iter() + .map(|x| PyExpr(x.clone()).into_subclasses(py).map(|x| x.into_ptr())), + ) + } + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyBetweenExpr { + pub(super) fn new_base(expr: ExprInner, low: ExprInner, high: ExprInner, negated: bool) -> PyExpr { + let inner = super::enum_::Between { + expr: Box::new(expr), + negated, + low: Box::new(low), + high: Box::new(high), + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyBetweenExpr { + #[new] + #[pyo3(signature = (expr, low, high, /, *, negated = false))] + fn __new__( + expr: LiteralExprExtractor, + low: LiteralExprExtractor, + high: LiteralExprExtractor, + negated: bool, + ) -> pyo3::PyClassInitializer { + let base = Self::new_base(expr.0, low.0, high.0, negated); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_low(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.low).clone()) + } + + fn get_high(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.high).clone()) + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyLikeExpr { + pub(super) fn new_base( + expr: ExprInner, + pattern: alias::BoundObject<'_>, + negated: bool, + case_sensitive: bool, + escape: Option, + ) -> pyo3::PyResult { + let inner = super::enum_::Like { + case_sensitive, + negated, + expr: Box::new(expr), + pattern: extract_like_pattern(pattern.as_borrowed())?, + escape, + }; + Ok(PyExpr(inner.into())) + } +} + +#[pyo3::pymethods] +impl PyLikeExpr { + #[new] + #[pyo3(signature = (expr, pattern, /, *, negated = false, case_sensitive = true, escape = None))] + fn __new__( + expr: LiteralExprExtractor, + pattern: alias::BoundObject<'_>, + negated: bool, + case_sensitive: bool, + escape: Option, + ) -> pyo3::PyResult> { + let base = Self::new_base(expr.0, pattern, negated, case_sensitive, escape)?; + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_pattern(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + match &inner.pattern { + super::enum_::LikePattern::One(p) => super::base::into_subclasses(py, (**p).clone()), + super::enum_::LikePattern::Any(ps) => unsafe { + utils::iterator_to_tuple( + slf.py(), + ps.len() as isize, + ps.iter() + .map(|x| PyExpr(x.clone()).into_subclasses(py).map(|x| x.into_ptr())), + ) + }, + } + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } + + fn get_case_sensitive(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.case_sensitive + } + + fn get_escape(slf: pyo3::PyRef<'_, Self>) -> Option { + let inner = Self::inner(&slf); + inner.escape + } +} + +impl PySimilarToExpr { + pub(super) fn new_base(expr: ExprInner, pattern: ExprInner, negated: bool) -> PyExpr { + let inner = super::enum_::SimilarTo { + negated, + expr: Box::new(expr), + pattern: Box::new(pattern), + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PySimilarToExpr { + #[new] + #[pyo3(signature = (expr, pattern, /, *, negated = false))] + fn __new__( + expr: LiteralExprExtractor, + pattern: LiteralExprExtractor, + negated: bool, + ) -> pyo3::PyClassInitializer { + let base = Self::new_base(expr.0, pattern.0, negated); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_pattern(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.pattern).clone()) + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyRLikeExpr { + pub(super) fn new_base(expr: ExprInner, pattern: ExprInner, negated: bool) -> PyExpr { + let inner = super::enum_::RLike { + negated, + expr: Box::new(expr), + pattern: Box::new(pattern), + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyRLikeExpr { + #[new] + #[pyo3(signature = (expr, pattern, /, *, negated = false))] + fn __new__( + expr: LiteralExprExtractor, + pattern: LiteralExprExtractor, + negated: bool, + ) -> pyo3::PyClassInitializer { + let base = Self::new_base(expr.0, pattern.0, negated); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_pattern(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.pattern).clone()) + } + + fn get_negated(slf: pyo3::PyRef<'_, Self>) -> bool { + let inner = Self::inner(&slf); + inner.negated + } +} + +impl PyCastExpr { + pub(super) fn new_base(expr: ExprInner, data_type_name: &str, kind: &str) -> pyo3::PyResult { + let Some(kind) = super::enum_::CastKind::parse(kind) else { + return rapidquery_macros::pyerr!(Err(PyValueError), "unknown cast kind {:?}", kind); + }; + + let inner = super::enum_::Cast { + kind, + expr: Box::new(expr), + data_type_name: data_type_name.into(), + }; + Ok(PyExpr(inner.into())) + } +} + +#[pyo3::pymethods] +impl PyCastExpr { + #[new] + #[pyo3(signature = (expr, data_type_name, /, *, kind = "CAST"))] + fn __new__( + expr: LiteralExprExtractor, + data_type_name: &str, + kind: &str, + ) -> pyo3::PyResult> { + let base = Self::new_base(expr.0, data_type_name, kind)?; + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_data_type_name(slf: pyo3::PyRef<'_, Self>) -> String { + let inner = Self::inner(&slf); + inner.data_type_name.to_string() + } + + fn get_kind(slf: pyo3::PyRef<'_, Self>) -> &'static str { + let inner = Self::inner(&slf); + inner.kind.as_sql() + } +} + +impl PyTrimExpr { + pub(super) fn new_base(expr: ExprInner, r#where: &str, characters: Option) -> pyo3::PyResult { + let Some(where_) = super::enum_::TrimWhere::parse(r#where) else { + return rapidquery_macros::pyerr!( + Err(PyValueError), + "unknown TRIM where {:?}; expected BOTH, LEADING, or TRAILING", + r#where + ); + }; + + let inner = super::enum_::Trim { + expr: Box::new(expr), + where_, + characters: characters.map(Box::new), + }; + Ok(PyExpr(inner.into())) + } +} + +#[pyo3::pymethods] +impl PyTrimExpr { + #[new] + #[pyo3(signature = (expr, /, *, r#where = "BOTH", characters = None))] + fn __new__( + expr: LiteralExprExtractor, + r#where: &str, + characters: Option, + ) -> pyo3::PyResult> { + let base = Self::new_base(expr.0, r#where, characters.map(|LiteralExprExtractor(e)| e))?; + Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_where(slf: pyo3::PyRef<'_, Self>) -> &'static str { + let inner = Self::inner(&slf); + inner.where_.as_sql() + } + + fn get_characters(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + match inner.characters.as_ref() { + Some(expr) => super::base::into_subclasses(slf.py(), (**expr).clone()), + None => Ok(utils::new_pynone(slf.py())), + } + } +} + +impl PyCollateExpr { + pub(super) fn new_base(expr: ExprInner, collation: super::enum_::Ident) -> PyExpr { + let inner = super::enum_::Collate { + expr: Box::new(expr), + collation, + }; + PyExpr(inner.into()) + } +} + +#[pyo3::pymethods] +impl PyCollateExpr { + #[new] + #[pyo3(signature = (expr, collation, /))] + fn __new__(expr: LiteralExprExtractor, collation: IdentExtractor) -> pyo3::PyClassInitializer { + let base = Self::new_base(expr.0, collation.0); + pyo3::PyClassInitializer::from(base).add_subclass(Self) + } + + fn get_expr(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), (*inner.expr).clone()) + } + + fn get_collation(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + super::base::into_subclasses(slf.py(), inner.collation.clone().into()) + } +} +#[pyo3::pymethods] +impl PyCaseExpr { + #[new] + #[pyo3(signature = (operand = None, /))] + fn __new__(operand: Option) -> pyo3::PyResult> { + let inner = super::enum_::Case { + operand: operand.map(|LiteralExprExtractor(e)| Box::new(e)), + when: vec![], + else_result: None, + }; + Ok(pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self)) + } + + fn when( + slf: pyo3::PyRef<'_, Self>, + when: LiteralExprExtractor, + then: LiteralExprExtractor, + ) -> pyo3::PyResult { + let mut inner = Self::inner(&slf).clone(); + inner.when.push((when.0, then.0)); + + pyo3::Py::new( + slf.py(), + pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self), + ) + .map(pyo3::Py::into_any) + } + + fn else_(slf: pyo3::PyRef<'_, Self>, result: LiteralExprExtractor) -> pyo3::PyResult { + let mut inner = Self::inner(&slf).clone(); + inner.else_result = Some(Box::new(result.0)); + + pyo3::Py::new( + slf.py(), + pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self), + ) + .map(pyo3::Py::into_any) + } + + fn get_operand(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + + match inner.operand.as_ref() { + Some(expr) => { + let bound = super::base::into_subclasses(slf.py(), (**expr).clone())?; + Ok(bound) + } + None => Ok(slf.py().None().into_any().into_bound(slf.py())), + } + } + + fn get_when_list(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + + let mut out = Vec::with_capacity(inner.when.len()); + for (when, then) in inner.when.iter() { + let pair = pyo3::types::PyTuple::new( + py, + [ + super::base::into_subclasses(py, when.clone())?, + super::base::into_subclasses(py, then.clone())?, + ], + )?; + out.push(pair); + } + Ok(pyo3::types::PyTuple::new(py, out)?.into_any()) + } + + fn get_else(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let inner = Self::inner(&slf); + match inner.else_result.as_ref() { + Some(expr) => super::base::into_subclasses(slf.py(), (**expr).clone()), + None => Ok(utils::new_pynone(slf.py())), + } + } +} + +#[pyo3::pymethods] +impl PyCustomExpr { + #[new] + #[pyo3(signature = (sql, *args))] + fn __new__(sql: String, args: alias::ArgsType<'_>) -> pyo3::PyResult> { + let mut extracted = Vec::with_capacity(args.len()); + + for item in args.iter() { + extracted.push(item.extract::()?.0); + } + + let inner = super::enum_::Custom { + sql: sql.into(), + args: extracted, + }; + Ok(pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self)) + } + + fn get_sql(slf: pyo3::PyRef<'_, Self>) -> String { + let inner = Self::inner(&slf); + inner.sql.to_string() + } + + fn get_args(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let py = slf.py(); + let inner = Self::inner(&slf); + + unsafe { + utils::iterator_to_tuple( + slf.py(), + inner.args.len() as isize, + inner + .args + .iter() + .map(|x| PyExpr(x.clone()).into_subclasses(py).map(|x| x.into_ptr())), + ) + } + } +} diff --git a/src/expr/value.rs b/src/expr/value.rs new file mode 100644 index 0000000..f2347c0 --- /dev/null +++ b/src/expr/value.rs @@ -0,0 +1,78 @@ +use crate::internal::alias; +use crate::serializers::PySerializer; +use crate::serializers::SerializerExtractor; + +use super::enum_::Value; + +/// Python object kept until SQL is rendered. `prepare` binds it; `to_sql` serializes it. +#[rapidquery_macros::pyclass("rapidquery.expr.Value", skip_from_py_object, extends = super::base::PyExpr, generic)] +#[derive(Debug, Default)] +pub struct PyValue; + +impl PyValue { + #[inline(always)] + fn inner<'a>(slf: &'a pyo3::PyRef<'a, PyValue>) -> &'a Value { + debug_assert!( + matches!(&slf.as_super().0, super::enum_::ExprInner::Value(..)), + "PyValue instance always holds ExprInner::Value" + ); + + match &slf.as_super().0 { + super::enum_::ExprInner::Value(v) => v, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +#[pyo3::pymethods] +impl PyValue { + /// Wrap `object`. Omit `serializer` to infer one from its type. + #[new] + #[pyo3(signature = (object, serializer = None, /))] + fn __new__( + object: alias::BoundObject, + serializer: Option, + ) -> pyo3::PyResult> { + // handle Value(Value(...), new_serializer) + if unsafe { crate::typeref::check_exact(object.as_ptr(), crate::typeref::VALUE_TYPE) } { + let expr = unsafe { object.cast_unchecked::() }; + let expr_inner = expr.get().0.clone(); + + debug_assert!( + matches!(expr_inner, super::enum_::ExprInner::Value(..)), + "PyValue instance always holds ExprInner::Value" + ); + + let inner = match expr_inner { + super::enum_::ExprInner::Value(v) => v, + _ => unsafe { std::hint::unreachable_unchecked() }, + }; + + let value = match serializer { + Some(SerializerExtractor(s)) => Value::new(inner.object, s), + None => inner, + }; + + let new_expr = super::base::PyExpr(super::enum_::ExprInner::Value(value)); + return Ok(pyo3::PyClassInitializer::from(new_expr).add_subclass(PyValue)); + } + + let value = match serializer { + Some(SerializerExtractor(s)) => Value::new(object.unbind(), s), + None => Value::infer_bound(object)?, + }; + + let expr = super::base::PyExpr(super::enum_::ExprInner::Value(value)); + Ok(pyo3::PyClassInitializer::from(expr).add_subclass(PyValue)) + } + + /// The wrapped Python object. + fn get_object(slf: pyo3::PyRef<'_, Self>) -> alias::PyObject { + Self::inner(&slf).object.clone_ref(slf.py()) + } + + /// Serializer used by `to_sql`. + fn get_serializer(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + PySerializer(Self::inner(&slf).serializer.clone()).into_subclasses(slf.py()) + } +} diff --git a/src/internal/alias.rs b/src/internal/alias.rs new file mode 100644 index 0000000..3694619 --- /dev/null +++ b/src/internal/alias.rs @@ -0,0 +1,15 @@ +//! There are type aliases that are used across the library + +/// Type alias for `pyo3::Py` +pub type PyObject = pyo3::Py; + +/// Type alias for `pyo3::Bound<'a, pyo3::PyAny>` +pub type BoundObject<'a> = pyo3::Bound<'a, pyo3::PyAny>; + +/// Type alias for `&'a pyo3::Bound<'a, pyo3::types::PyTuple>`. +/// Use it directly as `*args` argument type. +pub type ArgsType<'a> = &'a pyo3::Bound<'a, pyo3::types::PyTuple>; + +/// Type alias for `&'a pyo3::Bound<'a, pyo3::types::PyDict>`. +/// Use it directly as `**kwds` argument type. +pub type KwdsType<'a> = &'a pyo3::Bound<'a, pyo3::types::PyDict>; diff --git a/src/internal/macro_rules.rs b/src/internal/macro_rules.rs deleted file mode 100644 index b9457bc..0000000 --- a/src/internal/macro_rules.rs +++ /dev/null @@ -1,203 +0,0 @@ -/// Implement python classes using `#[pyo3::pyclass]` macro. -/// -/// Usage: -/// ```rust,no-run -/// implement_pyclass! { -/// [generic] PyMyClass as "MyClass" { field: String } -/// } -/// ``` -#[macro_export] -macro_rules! implement_pyclass { - ( - $(#[$outer:meta])* - [$($pyclass_args:tt)*] $struct_name:ident as $python_name:literal $($rest:tt)* - ) => { - #[ - pyo3::pyclass( - module = "rapidquery._lib", - name = $python_name, - frozen, - immutable_type, - skip_from_py_object, - $($pyclass_args)* - ) - ] - $(#[$outer])* - pub struct $struct_name $($rest)* - }; - - ( - $(#[$outer:meta])* - immutable [$($pyclass_args:tt)*] $struct_name:ident($state_name:ident) as $python_name:literal $($rest:tt)* - ) => { - $(#[$outer])* - pub struct $state_name $($rest)* - - $crate::implement_pyclass! { - $(#[$outer])* - [$($pyclass_args)*] $struct_name as $python_name (pub $crate::internal::uninitialized::ImmutableUninit<$state_name>); - } - - impl $struct_name { - #[inline] - #[must_use] - pub fn uninit() -> Self { - Self( - $crate::internal::uninitialized::ImmutableUninit::uninit() - ) - } - } - - impl From<$state_name> for $struct_name { - fn from(value: $state_name) -> Self { - Self( - $crate::internal::uninitialized::ImmutableUninit::new(value) - ) - } - } - - impl AsRef<$state_name> for $struct_name { - fn as_ref(&self) -> &$state_name { - self.0.as_ref() - } - } - }; - - ( - $(#[$outer:meta])* - mutable [$($pyclass_args:tt)*] $struct_name:ident($state_name:ident) as $python_name:literal $($rest:tt)* - ) => { - $(#[$outer])* - pub struct $state_name $($rest)* - - $crate::implement_pyclass! { - $(#[$outer])* - [$($pyclass_args)*] $struct_name as $python_name (pub $crate::internal::uninitialized::MutableUninit<$state_name>); - } - - impl $struct_name { - #[inline] - #[must_use] - pub fn uninit() -> Self { - Self( - $crate::internal::uninitialized::MutableUninit::uninit() - ) - } - } - - impl From<$state_name> for $struct_name { - fn from(value: $state_name) -> Self { - Self( - $crate::internal::uninitialized::MutableUninit::new(value) - ) - } - } - }; -} - -/// Creates new [`PyErr`] -/// -/// Usage: -/// ```rust,no-run -/// new_raw_error!(PyValueError, "Message") -/// new_raw_error!(PyValueError, "Message {}", arg1) -/// ``` -#[macro_export] -macro_rules! new_py_error { - ($name:ident, $message:expr) => { - ::pyo3::exceptions::$name::new_err($message) - }; - ($name:ident, $message:expr, $($args:tt)*) => { - ::pyo3::exceptions::$name::new_err( - format!($message, $($args)*) - ) - }; -} - -/// Creates new [`Err(PyErr)`] -/// -/// Usage: -/// ```rust,no-run -/// new_error!(PyValueError, "Message") -/// new_error!(PyValueError, "Message {}", arg1) -/// ``` -#[macro_export] -macro_rules! new_error { - ($name:ident, $message:expr) => { - Err($crate::new_py_error!($name, $message)) - }; - ($name:ident, $message:expr, $($args:tt)*) => { - Err($crate::new_py_error!($name, $message, $($args)*)) - }; -} - -#[macro_export] -macro_rules! build_schema_statement { - ($backend:expr, $stmt:expr) => {{ - let builder = $crate::internal::get_schema_builder($backend)?; - let assert_unwind = std::panic::AssertUnwindSafe(|| $stmt.build_any(&*builder)); - - std::panic::catch_unwind(assert_unwind) - .map_err(|_| pyo3::PyErr::new::("build failed")) - }}; -} - -#[macro_export] -macro_rules! build_query_statement { - ($backend:expr, $stmt:expr) => {{ - let builder = $crate::internal::get_query_builder($backend)?; - let mut sql = String::with_capacity(255); - - let assert_unwind = - std::panic::AssertUnwindSafe(|| $stmt.build_collect_any_into(&*builder, &mut sql)); - - std::panic::catch_unwind(assert_unwind) - .map_err(|_| pyo3::PyErr::new::("build failed"))?; - - Ok(sql) - }}; -} - -#[macro_export] -macro_rules! build_query_parts { - ($py:expr, $backend:expr, $stmt:expr) => {{ - let builder = $crate::internal::get_query_builder($backend)?; - - let (placeholder, numbered) = builder.placeholder(); - let mut sql = sea_query::SqlWriterValues::new(placeholder, numbered); - - let assert_unwind = - std::panic::AssertUnwindSafe(|| $stmt.build_collect_any_into(&*builder, &mut sql)); - - std::panic::catch_unwind(assert_unwind) - .map_err(|_| pyo3::PyErr::new::("build failed"))?; - - let (sql, values) = sql.into_parts(); - - let values = { - values - .into_iter() - .map(|x| $crate::common::value::ValueState::from_sea_query_value($py, x)) - .map($crate::common::value::PyValue::from) - }; - - unsafe { - let tuple_ptr = pyo3::ffi::PyTuple_New(values.len() as isize); - if tuple_ptr.is_null() { - return Err(pyo3::PyErr::fetch($py)); - } - - for (index, key) in values.enumerate() { - let key = pyo3::Py::new($py, key).unwrap().into_ptr(); - - if pyo3::ffi::PyTuple_SetItem(tuple_ptr, index as isize, key) == -1 { - pyo3::ffi::Py_XDECREF(tuple_ptr); - pyo3::ffi::Py_XDECREF(key); - return Err(pyo3::PyErr::fetch($py)); - } - } - - Ok((sql, pyo3::Bound::from_owned_ptr($py, tuple_ptr))) - } - }}; -} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 08a17f4..1eb9c2d 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -1,76 +1,5 @@ -pub mod macro_rules; -pub mod parameters; +pub mod alias; +pub mod modules; pub mod repr; -pub mod type_engine; -pub mod uninitialized; - -/// Returns the type name of a [`pyo3::ffi::PyObject`]. -/// -/// Returns `""` on failure. -#[inline] -pub fn get_type_name<'a>(py: pyo3::Python<'a>, obj: *mut pyo3::ffi::PyObject) -> String { - use pyo3::types::{PyStringMethods, PyTypeMethods}; - - unsafe { - let type_ = pyo3::ffi::Py_TYPE(obj); - - if type_.is_null() { - String::from("") - } else { - let obj = pyo3::types::PyType::from_borrowed_type_ptr(py, type_); - - obj.fully_qualified_name() - .map(|x| x.to_string_lossy().into_owned()) - .unwrap_or_else(|_| String::from("")) - } - } -} - -pub type PyObject = pyo3::Py; -pub type BoundArgs<'a> = &'a pyo3::Bound<'a, pyo3::types::PyTuple>; -pub type BoundKwargs<'a> = &'a pyo3::Bound<'a, pyo3::types::PyDict>; -pub type BoundObject<'a> = pyo3::Bound<'a, pyo3::PyAny>; -pub type RefBoundObject<'a> = &'a pyo3::Bound<'a, pyo3::PyAny>; - -pub trait ToSeaQuery { - /// Convert to sea_query structures. - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> Output; -} - -#[inline(always)] -pub fn get_schema_builder( - name: impl AsRef, -) -> pyo3::PyResult> { - let name = name.as_ref(); - - if name == "sqlite" { - Ok(Box::new(sea_query::SqliteQueryBuilder)) - } else if name == "mysql" { - Ok(Box::new(sea_query::MysqlQueryBuilder)) - } else if name == "postgresql" || name == "postgres" { - Ok(Box::new(sea_query::PostgresQueryBuilder)) - } else { - Err(pyo3::exceptions::PyValueError::new_err(format!( - "invalid backend value, got {name}" - ))) - } -} - -#[inline(always)] -pub fn get_query_builder( - name: impl AsRef, -) -> pyo3::PyResult> { - let name = name.as_ref(); - - if name == "sqlite" { - Ok(Box::new(sea_query::SqliteQueryBuilder)) - } else if name == "mysql" { - Ok(Box::new(sea_query::MysqlQueryBuilder)) - } else if name == "postgresql" || name == "postgres" { - Ok(Box::new(sea_query::PostgresQueryBuilder)) - } else { - Err(pyo3::exceptions::PyValueError::new_err(format!( - "invalid backend value, got {name}" - ))) - } -} +pub mod sql; +pub mod utils; diff --git a/src/internal/modules/json.rs b/src/internal/modules/json.rs new file mode 100644 index 0000000..ae5dcb3 --- /dev/null +++ b/src/internal/modules/json.rs @@ -0,0 +1,138 @@ +//! Process-wide JSON dumps: `orjson` if importable, else stdlib `json`. +//! +//! The result is borrowed and passed to a callback so the SQL writer can +//! escape it without an intermediate `String`. + +use once_cell::sync::OnceCell; +use pyo3::types::PyAnyMethods; +use pyo3::types::PyModule; +use pyo3::types::PyTuple; + +use crate::internal::utils::borrow_py_str_buffer; + +#[cfg(all(feature = "only-stdlib-json", feature = "only-orjson"))] +compile_error!("Testing features `only-stdlib-json` and `only-orjson` cannot be used at a same time."); + +enum JsonDumper { + Orjson { + dumps: pyo3::Py, + }, + Stdlib { + dumps: pyo3::Py, + /// `("ensure_ascii", "allow_nan", "separators")` for vectorcall. + kwnames: pyo3::Py, + separators: pyo3::Py, + }, +} + +static DUMPER: OnceCell = OnceCell::new(); + +fn init_orjson(module: &pyo3::Bound<'_, PyModule>) -> pyo3::PyResult { + let dumps = module.getattr("dumps")?.unbind(); + Ok(JsonDumper::Orjson { dumps }) +} + +fn init_stdlib(py: pyo3::Python<'_>) -> pyo3::PyResult { + let module = py.import("json")?; + let dumps = module.getattr("dumps")?.unbind(); + let kwnames = PyTuple::new(py, ["ensure_ascii", "allow_nan", "separators"])?.unbind(); + let separators = PyTuple::new(py, [",", ":"])?.unbind(); + + Ok(JsonDumper::Stdlib { + dumps, + kwnames, + separators, + }) +} + +#[cfg(feature = "only-orjson")] +#[cold] +fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { + let module = py.import("orjson")?; + init_orjson(&module) +} + +#[cfg(feature = "only-stdlib-json")] +#[cold] +fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { + init_stdlib(py) +} + +#[cfg(not(any(feature = "only-stdlib-json", feature = "only-orjson")))] +#[cold] +fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { + if let Ok(module) = py.import("orjson") + && let Ok(dumper) = init_orjson(&module) + { + return Ok(dumper); + } + init_stdlib(py) +} + +/// Borrow JSON text from a `bytes` (orjson) or `str` (stdlib) dumps result. +/// +/// # Safety +/// `result` is a new reference still owned by the caller. The GIL is held. +/// The `&str` is only valid until `result` is `DECREF`'d. +#[inline] +#[cfg_attr(feature = "nightly", optimize(speed))] +unsafe fn visit_json_text( + py: pyo3::Python<'_>, + result: *mut pyo3::ffi::PyObject, + f: impl FnOnce(&str) -> pyo3::PyResult<()>, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyBytes_CheckExact(result) == 1 { + let buf = pyo3::ffi::PyBytes_AsString(result) as *const u8; + let len = pyo3::ffi::PyBytes_Size(result) as usize; + debug_assert!(!buf.is_null()); + // SAFETY: `orjson.dumps` returns UTF-8 JSON. The buffer is the + // exact `bytes` payload and stays alive until `result` is DECREF'd. + let text = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(buf, len)) }; + return f(text); + } + + if pyo3::ffi::PyUnicode_CheckExact(result) == 1 { + return f(borrow_py_str_buffer(py, result)?); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "JSON dumps must return bytes or str, got {}", + crate::internal::utils::get_type_name(py, result) + ) +} + +/// JSON-encode `obj` and pass the text to `f` before dropping the dumps result. +/// +/// # Safety +/// `obj` must be a valid borrowed Python object. The GIL must be held. +#[inline] +#[cfg_attr(feature = "nightly", optimize(speed))] +pub unsafe fn dumps( + obj: pyo3::Bound<'_, pyo3::PyAny>, + f: impl FnOnce(&str) -> pyo3::PyResult<()>, +) -> pyo3::PyResult<()> { + let py = obj.py(); + let dumper = DUMPER.get_or_try_init(|| init_import(py))?; + + let result = match dumper { + JsonDumper::Orjson { dumps } => unsafe { pyo3::ffi::PyObject_CallOneArg(dumps.as_ptr(), obj.as_ptr()) }, + JsonDumper::Stdlib { + dumps, + kwnames, + separators, + } => { + let false_ = unsafe { pyo3::ffi::Py_False() }; + let mut args = [obj.as_ptr(), false_, false_, separators.as_ptr()]; + unsafe { pyo3::ffi::PyObject_Vectorcall(dumps.as_ptr(), args.as_mut_ptr(), 1, kwnames.as_ptr()) } + } + }; + + if result.is_null() { + return Err(pyo3::PyErr::fetch(py)); + } + + let out = unsafe { visit_json_text(py, result, f) }; + unsafe { pyo3::ffi::Py_DECREF(result) }; + out +} diff --git a/src/internal/modules/mod.rs b/src/internal/modules/mod.rs new file mode 100644 index 0000000..b25218c --- /dev/null +++ b/src/internal/modules/mod.rs @@ -0,0 +1,5 @@ +//! Cached Python module imports. + +mod json; + +pub use json::dumps as json_dumps; diff --git a/src/internal/parameters.rs b/src/internal/parameters.rs deleted file mode 100644 index 4d9bfc4..0000000 --- a/src/internal/parameters.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub enum OptionalParam<'a> { - Undefined, - Defined(pyo3::Bound<'a, pyo3::PyAny>), -} - -impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for OptionalParam<'py> { - type Error = pyo3::PyErr; - - fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> Result { - Ok(Self::Defined(obj.to_owned())) - } -} diff --git a/src/internal/repr.rs b/src/internal/repr.rs index 892d029..27c74ca 100644 --- a/src/internal/repr.rs +++ b/src/internal/repr.rs @@ -1,309 +1,413 @@ -/// A helper to generate __repr__ methods. -#[derive(Default, Clone, Debug)] -pub struct ReprFormatter { - buf: String, - started: bool, +use std::fmt::Write; +use std::fmt::{self}; + +/// A value rendered straight into a [`ReprFormatter`] buffer. +/// +/// Omitted values write nothing and do not emit a separator. [`Option`] is +/// omitted when empty; [`IfTrue`] / [`IfFalse`] are omitted on the other polarity. +pub trait WriteRepr { + fn omit(&self) -> bool { + false + } + + fn write_repr(self, buf: &mut String); } -#[derive(Debug)] -pub struct ReprVec { - key: &'static str, - optional: bool, - items: String, +impl WriteRepr for &str { + fn write_repr(self, buf: &mut String) { + write!(buf, "{self:?}").unwrap(); + } } -#[derive(Debug)] -pub struct ReprNestedVec { - index: usize, - items: String, +impl WriteRepr for String { + fn write_repr(self, buf: &mut String) { + write!(buf, "{self:?}").unwrap(); + } } -impl ReprFormatter { - #[inline] - pub fn new(name: impl AsRef) -> Self { - Self { - buf: String::from("<") + name.as_ref(), - started: false, - } +impl WriteRepr for char { + fn write_repr(self, buf: &mut String) { + write!(buf, "{self:?}").unwrap(); } +} - #[inline] - pub fn new_with_pyref(pyref: &pyo3::PyRef<'_, T>) -> Self { - Self::new(crate::internal::get_type_name(pyref.py(), pyref.as_ptr())) +impl WriteRepr for bool { + fn write_repr(self, buf: &mut String) { + buf.push_str(if self { "True" } else { "False" }); } +} - #[inline] - pub fn vec(&self, key: &'static str, optional: bool) -> ReprVec { - ReprVec { - key, - optional, - items: String::new(), - } +/// Text inserted with no quoting or escaping. +pub struct Raw<'a>(pub &'a str); + +impl WriteRepr for Raw<'_> { + fn write_repr(self, buf: &mut String) { + buf.push_str(self.0); } +} + +/// Python `True`, written only when the flag is set. +pub struct IfTrue(pub bool); - pub fn take(&mut self) -> Self { - std::mem::take(self) +impl WriteRepr for IfTrue { + fn omit(&self) -> bool { + !self.0 } - pub fn finish(&mut self) -> String { - self.take().buf + ">" + fn write_repr(self, buf: &mut String) { + buf.push_str("True"); } +} - #[inline] - pub fn write(&mut self, part: impl AsRef) -> &mut Self { - if self.started { - self.buf.push(' '); - self.buf.push_str(part.as_ref()); - } else { - self.buf.push(' '); - self.buf.push_str(part.as_ref()); - self.started = true; - } +/// Python `False`, written only when the flag is clear. +pub struct IfFalse(pub bool); - self +impl WriteRepr for IfFalse { + fn omit(&self) -> bool { + self.0 } - #[inline] - pub fn pair(&mut self, key: &'static str, value: impl AsRef) -> &mut Self { - if key.is_empty() { - self.write(value) - } else { - self.write(format!("{}={}", key, value.as_ref())) - } + fn write_repr(self, buf: &mut String) { + buf.push_str("False"); } +} - #[inline] - pub fn quote(&mut self, key: &'static str, value: impl AsRef) -> &mut Self { - if key.is_empty() { - self.write(format!("'{}'", value.as_ref())) - } else { - self.write(format!("{}='{}'", key, value.as_ref())) - } +impl WriteRepr for ReprFormatter { + fn write_repr(mut self, buf: &mut String) { + self.close(); + buf.push_str(&self.buf); } +} - #[inline] - pub fn iden(&mut self, key: &'static str, value: &sea_query::DynIden) -> &mut Self { - self.quote(key, value.to_string()) +impl WriteRepr for Option { + fn omit(&self) -> bool { + match self { + Some(value) => value.omit(), + None => true, + } } - #[inline] - pub fn debug(&mut self, key: &'static str, value: impl std::fmt::Debug) -> &mut Self { - self.pair(key, format!("{value:?}")) + fn write_repr(self, buf: &mut String) { + if let Some(value) = self { + value.write_repr(buf); + } } +} + +/// Buffer for a Python `__repr__` of the form `Name(...)` or `[...]`. +/// +/// Arguments are written in place. A nested formatter is copied into the parent +/// once, after it is closed. Bracketed groups are a [`ReprGroup`] borrow of this +/// buffer; dropping the group writes the closer. +/// +/// ``` +/// use rapidquery::internal::repr::ReprFormatter; +/// +/// let mut fmt = ReprFormatter::new_call("Ident"); +/// fmt.positional("id"); +/// fmt.close(); +/// assert_eq!(fmt.as_str(), "Ident(\"id\")"); +/// ``` +pub struct ReprFormatter { + buf: String, + close_token: char, + is_separated: bool, + is_closed: bool, +} +impl ReprFormatter { #[inline] - pub fn display(&mut self, key: &'static str, value: impl std::fmt::Display) -> &mut Self { - self.pair(key, value.to_string()) + fn separate(&mut self) { + debug_assert!(!self.is_closed); + + if self.is_separated { + self.buf.push_str(", "); + } else { + self.is_separated = true; + } } #[inline] - pub fn map(&mut self, key: &'static str, value: T, func: F) -> &mut Self - where - R: AsRef, - F: FnOnce(T) -> R, - { - self.pair(key, func(value)) + fn push_key(&mut self, key: &str) { + self.buf.push_str(key); + self.buf.push('='); } #[inline] - pub fn optional_pair( - &mut self, - key: &'static str, - value: Option>, - ) -> &mut Self { - match value { - Some(x) => self.pair(key, x), - None => self, + fn write_value(&mut self, value: impl WriteRepr) -> bool { + if value.omit() { + return false; } + + self.separate(); + value.write_repr(&mut self.buf); + true } - #[inline] - pub fn optional_quote( - &mut self, - key: &'static str, - value: Option>, - ) -> &mut Self { - match value { - Some(x) => self.quote(key, x), - None => self, + fn open_group(&mut self, open: char, close: char) -> ReprGroup<'_> { + self.separate(); + self.buf.push(open); + self.is_separated = false; + + ReprGroup { + fmt: self, + close_token: close, + finished: false, } } +} - #[inline] - pub fn optional_iden( - &mut self, - key: &'static str, - value: Option<&sea_query::DynIden>, - ) -> &mut Self { - match value { - Some(x) => self.iden(key, x), - None => self, +impl ReprFormatter { + pub fn new_call(name: &str) -> Self { + let mut buf = String::with_capacity(name.len() + 32); + buf.push_str(name); + buf.push('('); + + Self { + buf, + close_token: ')', + is_separated: false, + is_closed: false, } } #[inline] - pub fn optional_debug( - &mut self, - key: &'static str, - value: Option, - ) -> &mut Self { - match value { - Some(x) => self.debug(key, x), - None => self, + pub fn new_list() -> Self { + Self { + buf: String::from("["), + close_token: ']', + is_separated: false, + is_closed: false, } } - #[inline] - pub fn optional_display( - &mut self, - key: &'static str, - value: Option, - ) -> &mut Self { - match value { - Some(x) => self.display(key, x), - None => self, - } + pub fn positional(&mut self, value: impl WriteRepr) -> &mut Self { + self.write_value(value); + self } - #[inline] - pub fn optional_boolean(&mut self, key: &'static str, value: bool) -> &mut Self { - if value { - self.pair(key, "true") - } else { - self - } + pub fn positional_debug(&mut self, value: &impl fmt::Debug) -> &mut Self { + self.separate(); + write!(self.buf, "{value:?}").unwrap(); + self } - #[inline] - pub fn optional_map( - &mut self, - key: &'static str, - value: Option, - func: F, - ) -> &mut Self - where - R: AsRef, - F: FnOnce(T) -> R, - { - match value { - Some(x) => self.map(key, x, func), - None => self, - } + pub fn positional_raw(&mut self, value: &str) -> &mut Self { + self.positional(Raw(value)) } -} -impl ReprVec { - #[inline] - pub fn vec(&self, index: usize) -> ReprNestedVec { - ReprNestedVec { - index, - items: String::new(), - } + pub fn positional_list(&mut self) -> ReprGroup<'_> { + self.open_group('[', ']') } - #[inline] - pub fn push(&mut self, index: usize, item: impl AsRef) -> &mut Self { - if index > 0 { - self.items.push_str(", "); + pub fn positional_tuple(&mut self) -> ReprGroup<'_> { + self.open_group('(', ')') + } + + pub fn keyword(&mut self, key: &str, value: impl WriteRepr) -> &mut Self { + if !value.omit() { + self.separate(); + self.push_key(key); + value.write_repr(&mut self.buf); } - self.items.push_str(item.as_ref()); + self } - #[inline] - pub fn quote(&mut self, index: usize, item: impl AsRef) -> &mut Self { - if index > 0 { - self.items.push_str(", "); - } - self.items.push('\''); - self.items.push_str(item.as_ref()); - self.items.push('\''); + pub fn keyword_debug(&mut self, key: &str, value: &impl fmt::Debug) -> &mut Self { + self.separate(); + self.push_key(key); + write!(self.buf, "{value:?}").unwrap(); self } - #[inline] - pub fn quote_iter(&mut self, iterator: I) -> &mut Self - where - D: AsRef, - I: Iterator, - { - for (index, item) in iterator.enumerate() { - self.quote(index, item); + pub fn keyword_raw(&mut self, key: &str, value: &str) -> &mut Self { + self.keyword(key, Raw(value)) + } + + /// Write `key=True` only when `value` is set. + pub fn keyword_true(&mut self, key: &str, value: bool) -> &mut Self { + self.keyword(key, IfTrue(value)) + } + + /// Write `key=False` only when `value` is clear. + pub fn keyword_false(&mut self, key: &str, value: bool) -> &mut Self { + self.keyword(key, IfFalse(value)) + } + + /// Write the closing character. Further calls leave the text unchanged. + pub fn close(&mut self) -> &mut Self { + if !self.is_closed { + self.buf.push(self.close_token); + self.is_closed = true; } self } - #[inline] - pub fn display_iter(&mut self, iterator: I) -> &mut Self - where - D: std::fmt::Display, - I: Iterator, - { - for (index, item) in iterator.enumerate() { - self.push(index, item.to_string()); - } + /// Append `suffix` after the closer. Closes first when still open. + pub fn append_suffix(&mut self, suffix: &str) -> &mut Self { + self.close(); + self.buf.push_str(suffix); self } - pub fn finish(&mut self, fmt: &mut ReprFormatter) { - if self.optional && self.items.is_empty() { - return; - } + /// Append `.method(` after the closer and reopen for more arguments. + pub fn append_call(&mut self, method: &str) -> &mut Self { + self.close(); + + self.buf.push('.'); + self.buf.push_str(method); + self.buf.push('('); + + self.is_closed = false; + self.is_separated = false; + self + } + + pub fn as_str(&self) -> &str { + &self.buf + } - self.items.insert(0, '['); - self.items.push(']'); - fmt.pair(self.key, std::mem::take(&mut self.items)); + /// Move the buffer out. Closes first when still open. + pub fn into_string(mut self) -> String { + self.close(); + self.buf } } -impl ReprNestedVec { - #[inline] - pub fn push(&mut self, index: usize, item: impl AsRef) -> &mut Self { - if index > 0 { - self.items.push_str(", "); +/// A `[...]` or `(...)` opened inside a [`ReprFormatter`]. +/// +/// Writes into the parent buffer. Drop writes the closer and marks the parent +/// argument list as separated, so the next sibling gets a comma. +pub struct ReprGroup<'a> { + fmt: &'a mut ReprFormatter, + close_token: char, + finished: bool, +} + +impl ReprGroup<'_> { + fn finish(&mut self) { + if !self.finished { + self.fmt.buf.push(self.close_token); + self.fmt.is_separated = true; + self.finished = true; } - self.items.push_str(item.as_ref()); + } + + pub fn positional(&mut self, value: impl WriteRepr) -> &mut Self { + self.fmt.positional(value); self } - #[inline] - pub fn quote(&mut self, index: usize, item: impl AsRef) -> &mut Self { - if index > 0 { - self.items.push_str(", "); - } - self.items.push('\''); - self.items.push_str(item.as_ref()); - self.items.push('\''); + pub fn positional_debug(&mut self, value: &impl fmt::Debug) -> &mut Self { + self.fmt.positional_debug(value); self } - #[inline] - pub fn quote_iter(&mut self, iterator: I) -> &mut Self - where - D: AsRef, - I: Iterator, - { - for (index, item) in iterator.enumerate() { - self.quote(index, item); - } + pub fn positional_raw(&mut self, value: &str) -> &mut Self { + self.fmt.positional_raw(value); self } - #[inline] - pub fn display_iter(&mut self, iterator: I) -> &mut Self - where - D: std::fmt::Display, - I: Iterator, - { - for (index, item) in iterator.enumerate() { - self.push(index, item.to_string()); - } + pub fn positional_list(&mut self) -> ReprGroup<'_> { + self.fmt.positional_list() + } + + pub fn positional_tuple(&mut self) -> ReprGroup<'_> { + self.fmt.positional_tuple() + } + + pub fn keyword(&mut self, key: &str, value: impl WriteRepr) -> &mut Self { + self.fmt.keyword(key, value); + self + } + + pub fn keyword_debug(&mut self, key: &str, value: &impl fmt::Debug) -> &mut Self { + self.fmt.keyword_debug(key, value); + self + } + + pub fn keyword_raw(&mut self, key: &str, value: &str) -> &mut Self { + self.fmt.keyword_raw(key, value); self } - pub fn finish(&mut self, fmtvec: &mut ReprVec) { - self.items.insert(0, '['); - self.items.push(']'); - fmtvec.push(self.index, std::mem::take(&mut self.items)); + pub fn keyword_true(&mut self, key: &str, value: bool) -> &mut Self { + self.fmt.keyword_true(key, value); + self + } + + pub fn keyword_false(&mut self, key: &str, value: bool) -> &mut Self { + self.fmt.keyword_false(key, value); + self + } + + /// Write the closer now. A later drop is a no-op. + pub fn close(mut self) { + self.finish(); + } +} + +impl Drop for ReprGroup<'_> { + fn drop(&mut self) { + self.finish(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn call_debug_escape_and_bools() { + let mut fmt = ReprFormatter::new_call("LikeExpr"); + + fmt.positional("a\"b\\c") + .keyword_false("case_sensitive", false) + .keyword_true("negated", true) + .keyword_true("distinct", false) + .keyword("escape", None::<&str>) + .keyword("where", Some("LEADING")); + + assert_eq!( + fmt.into_string(), + "LikeExpr(\"a\\\"b\\\\c\", case_sensitive=False, negated=True, where=\"LEADING\")" + ); + } + + #[test] + fn list_embeds_formatter_and_tuple() { + let mut ident = ReprFormatter::new_call("Ident"); + ident.positional("id"); + + let mut fmt = ReprFormatter::new_list(); + fmt.positional(ident); + fmt.positional_tuple() + .positional("when") + .positional_raw("Value(1, IntegerSerializer())"); + + fmt.keyword("else_", None::<&str>); + + assert_eq!( + fmt.into_string(), + "[Ident(\"id\"), (\"when\", Value(1, IntegerSerializer()))]" + ); + } + + #[test] + fn append_call_after_close() { + let mut argument = ReprFormatter::new_call("Ident"); + argument.positional("x").close(); + + let mut fmt = ReprFormatter::new_call("Function"); + fmt.positional("lag") + .append_suffix(".ignore_nulls()") + .append_call("filter") + .positional(argument); + + assert_eq!( + fmt.into_string(), + "Function(\"lag\").ignore_nulls().filter(Ident(\"x\"))" + ); } } diff --git a/src/internal/sql.rs b/src/internal/sql.rs new file mode 100644 index 0000000..ab53ef7 --- /dev/null +++ b/src/internal/sql.rs @@ -0,0 +1,404 @@ +//! SQL text buffers. +//! +//! [`WriteSql`] walks a node into a [`SqlWriter`]. [`BufferedSql`] inlines values. +//! [`ParameterizedSql`] binds them as placeholders. + +use std::fmt::Write; + +use pyo3::types::PyAnyMethods; + +use crate::dialect::PyDialect; +use crate::dialect::errors::FormatError; +use crate::dialect::flags::Placeholder; +use crate::internal::alias; +use crate::serializers::SerializerType; + +/// Error produced while building SQL. +#[derive(Debug)] +pub enum PrepareError { + /// A [`std::fmt::Write`] implementation returned [`std::fmt::Error`]. + Write, + /// An error that originated in Python (e.g. from `.extract()`). + Python(pyo3::PyErr), + /// A `Custom` template had a different number of `{}` slots than args. + CustomPlaceholders { expected: usize, got: usize }, +} + +impl std::fmt::Display for PrepareError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Write => f.write_str("failed to write SQL"), + Self::Python(err) => write!(f, "{err}"), + Self::CustomPlaceholders { expected, got } => { + write!(f, "custom expression has {expected} placeholders but {got} arguments") + } + } + } +} + +impl std::error::Error for PrepareError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Write | Self::CustomPlaceholders { .. } => None, + Self::Python(err) => Some(err), + } + } +} + +impl From for PrepareError { + fn from(_: std::fmt::Error) -> Self { + Self::Write + } +} + +impl From for PrepareError { + fn from(err: FormatError) -> Self { + match err { + FormatError::Write => Self::Write, + other => Self::Python(other.into()), + } + } +} + +impl From for pyo3::PyErr { + fn from(err: FormatError) -> Self { + match err { + FormatError::Write => { + rapidquery_macros::pyerr!(PyRuntimeError, "failed to write SQL") + } + FormatError::NulInString => { + rapidquery_macros::pyerr!(PyValueError, "NUL is not allowed in a string literal") + } + FormatError::Unsupported(name) => { + rapidquery_macros::pyerr!(PyNotImplementedError, "this dialect does not support {}", name) + } + } + } +} + +impl From for PrepareError { + fn from(err: pyo3::PyErr) -> Self { + Self::Python(err) + } +} + +impl From for pyo3::PyErr { + fn from(err: PrepareError) -> Self { + match err { + PrepareError::Python(err) => err, + PrepareError::Write => { + rapidquery_macros::pyerr!(PyRuntimeError, "failed to write SQL") + } + PrepareError::CustomPlaceholders { expected, got } => { + rapidquery_macros::pyerr!( + PyValueError, + "custom expression has {} placeholders but {} arguments", + expected, + got + ) + } + } + } +} + +pub type PrepareResult = Result; +pub type PrepareParams = smallvec::SmallVec<[alias::PyObject; 1]>; + +/// Destination for [`WriteSql`]. +/// +/// [`BufferedSql`] inlines values. [`ParameterizedSql`] binds them. Other methods +/// only append text. +pub trait SqlWriter: std::fmt::Write { + fn push_str(&mut self, s: &str) -> &mut Self; + + #[inline] + fn push_str_if(&mut self, cond: bool, s: &str) -> &mut Self { + if cond { self.push_str(s) } else { self } + } + + fn push_char(&mut self, c: char) -> &mut Self; + + #[inline] + fn push_char_if(&mut self, cond: bool, c: char) -> &mut Self { + if cond { self.push_char(c) } else { self } + } + + /// Raw (`SqlBuf`): `None` → `NULL`, else `serializer.serialize` into the + /// SQL buffer. Prepared: bind `object` as a placeholder; `serializer` is + /// unused. + fn push_value( + &mut self, + dialect: &PyDialect, + serializer: &SerializerType, + object: &alias::PyObject, + ) -> PrepareResult<()>; +} + +/// SQL buffer that inlines values. It does not keep a parameter list. +/// +/// Build one with a Python token, append text through [`SqlWriter`], then take +/// the string with [`Self::into_string`]. +/// +/// ``` +/// pyo3::Python::attach(|py| { +/// let buf = rapidquery::internal::sql::BufferedSql::new(py); +/// assert_eq!(buf.as_str(), ""); +/// }); +/// ``` +pub struct BufferedSql<'py> { + py: pyo3::Python<'py>, + sql: String, +} + +impl<'py> BufferedSql<'py> { + #[inline] + pub fn new(py: pyo3::Python<'py>) -> Self { + Self { py, sql: String::new() } + } + + #[inline] + pub fn with_capacity(py: pyo3::Python<'py>, capacity: usize) -> Self { + Self { + py, + sql: String::with_capacity(capacity), + } + } + + #[inline] + pub fn py(&self) -> pyo3::Python<'py> { + self.py + } + + #[inline] + pub fn as_str(&self) -> &str { + &self.sql + } + + #[inline] + pub fn into_string(self) -> String { + self.sql + } +} + +impl<'py> SqlWriter for BufferedSql<'py> { + #[inline] + fn push_str(&mut self, s: &str) -> &mut Self { + self.sql.push_str(s); + self + } + + #[inline] + fn push_char(&mut self, c: char) -> &mut Self { + self.sql.push(c); + self + } + + fn push_value( + &mut self, + dialect: &PyDialect, + serializer: &SerializerType, + object: &alias::PyObject, + ) -> PrepareResult<()> { + let bound = object.bind(self.py); + if bound.is_none() { + self.sql.push_str("NULL"); + return Ok(()); + } + serializer.serialize(dialect, bound, &mut self.sql)?; + Ok(()) + } +} + +impl<'py> std::fmt::Write for BufferedSql<'py> { + #[inline] + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.sql.push_str(s); + Ok(()) + } + + #[inline] + fn write_char(&mut self, c: char) -> std::fmt::Result { + self.sql.push(c); + Ok(()) + } +} + +/// SQL text plus the Python objects bound for each placeholder. +/// +/// [`std::fmt::Write`] appends text. [`Self::push_param`] appends a placeholder +/// and stores the object. Numbering starts at 1. +/// +/// ``` +/// pyo3::Python::attach(|py| { +/// let sql = rapidquery::internal::sql::ParameterizedSql::new(py); +/// assert_eq!(sql.sql(), ""); +/// assert!(sql.params().is_empty()); +/// }); +/// ``` +pub struct ParameterizedSql<'py> { + py: pyo3::Python<'py>, + sql: String, + params: PrepareParams, + next_id: usize, +} + +impl<'py> ParameterizedSql<'py> { + #[inline] + pub fn new(py: pyo3::Python<'py>) -> Self { + Self { + py, + sql: String::new(), + params: PrepareParams::new(), + next_id: 1, + } + } + + #[inline] + pub fn with_capacity(py: pyo3::Python<'py>, capacity: usize) -> Self { + Self { + py, + sql: String::with_capacity(capacity), + params: PrepareParams::new(), + next_id: 1, + } + } + + #[inline] + pub fn py(&self) -> pyo3::Python<'py> { + self.py + } + + #[inline] + pub fn sql(&self) -> &str { + self.sql.as_str() + } + + #[inline] + pub fn params(&self) -> &PrepareParams { + &self.params + } + + #[inline] + pub fn into_parts(self) -> (String, PrepareParams) { + (self.sql, self.params) + } +} + +impl ParameterizedSql<'_> { + /// Register `param` and append its placeholder to the SQL in one step. + #[inline] + pub fn push_param(&mut self, dialect: &PyDialect, param: alias::PyObject) -> &mut Self { + // SAFETY: Dialect::placeholder_character guarateed to store valid `char`. + let character = unsafe { char::from_u32_unchecked(dialect.0.placeholder_character()) }; + + match dialect.0.placeholder() { + Placeholder::Simple => { + self.sql.push(character); + } + Placeholder::Numbered => { + self.sql.push(character); + let _ = write!(self.sql, "{}", self.next_id); + self.next_id += 1; + } + } + + self.params.push(param); + self + } +} + +impl SqlWriter for ParameterizedSql<'_> { + #[inline] + fn push_str(&mut self, s: &str) -> &mut Self { + self.sql.push_str(s); + self + } + + #[inline] + fn push_char(&mut self, c: char) -> &mut Self { + self.sql.push(c); + self + } + + #[inline] + fn push_value( + &mut self, + dialect: &PyDialect, + _serializer: &SerializerType, + object: &alias::PyObject, + ) -> PrepareResult<()> { + self.push_param(dialect, object.clone_ref(self.py)); + Ok(()) + } +} + +impl std::fmt::Write for ParameterizedSql<'_> { + #[inline] + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.sql.push_str(s); + Ok(()) + } + + #[inline] + fn write_char(&mut self, c: char) -> std::fmt::Result { + self.sql.push(c); + Ok(()) + } +} + +/// Render `self` into `out`. The writer chooses literal vs placeholder for values. +pub trait WriteSql { + fn write_sql( + &self, + // NOTE FOR ME: + // Why `PyDialect` instead of Rust `Dialect`? + // because we accept custom serializers and custom serializer functions + // need to get dialect objects. + dialect: &PyDialect, + out: &mut W, + ) -> PrepareResult<()>; +} + +/// Render `stmt` as raw SQL (values inlined as literals). +pub fn write_sql<'py, T: WriteSql>(py: pyo3::Python<'py>, stmt: &T, dialect: &PyDialect) -> PrepareResult { + let mut buf = BufferedSql::with_capacity(py, 256); + stmt.write_sql(dialect, &mut buf)?; + Ok(buf.into_string()) +} + +/// Render `stmt` as a parameterized statement. +pub fn prepare<'py, T: WriteSql>( + py: pyo3::Python<'py>, + stmt: &T, + dialect: &PyDialect, +) -> PrepareResult<(String, PrepareParams)> { + let mut context = ParameterizedSql::with_capacity(py, 256); + stmt.write_sql(dialect, &mut context)?; + Ok(context.into_parts()) +} + +/// Write each item in order, writing `separator` between them +/// (not before the first or after the last). +/// +/// Use `","` / `", "` for lists and `" AND "` / `" OR "` for predicates. +#[cfg_attr(feature = "nightly", optimize(speed))] +pub fn write_separated<'a, T, W>( + dialect: &PyDialect, + out: &mut W, + items: impl IntoIterator, + separator: &str, +) -> PrepareResult<()> +where + T: WriteSql + 'a, + W: SqlWriter, +{ + let mut first = true; + for item in items { + if !first { + out.push_str(separator); + } + first = false; + item.write_sql(dialect, out)?; + } + Ok(()) +} diff --git a/src/internal/type_engine.rs b/src/internal/type_engine.rs deleted file mode 100644 index 039eab2..0000000 --- a/src/internal/type_engine.rs +++ /dev/null @@ -1,436 +0,0 @@ -use crate::internal::RefBoundObject; -use crate::sqltypes::SQLTypeTrait; - -/// Type engine is an enum which can control validations, serializations, -/// and deserializations of column types. -/// -/// [`SQLTypeTrait`] is implemented for this type. -pub struct TypeEngine( - pub std::sync::Arc, - pub std::ptr::NonNull, -); - -// SAFETY: PyObjects are safe between threads. -unsafe impl Send for TypeEngine {} -unsafe impl Sync for TypeEngine {} - -impl Clone for TypeEngine { - fn clone(&self) -> Self { - unsafe { - pyo3::ffi::Py_INCREF(self.1.as_ptr()); - } - - Self(self.0.clone(), self.1) - } -} - -impl Drop for TypeEngine { - fn drop(&mut self) { - unsafe { - pyo3::ffi::Py_DECREF(self.1.as_ptr()); - } - } -} - -macro_rules! wrap_typeengine { - ($result:expr, $ptr:expr) => { - unsafe { - pyo3::ffi::Py_INCREF($ptr); - Self( - std::sync::Arc::new($result), - std::ptr::NonNull::new_unchecked($ptr), - ) - } - }; - - ($py:expr, $result:expr, $initializer:expr) => { - unsafe { - let object_ptr = pyo3::Py::new($py, ($initializer, crate::sqltypes::PySQLTypeAbstract)) - .unwrap() - .into_ptr(); - - Self( - std::sync::Arc::new($result), - std::ptr::NonNull::new_unchecked(object_ptr), - ) - } - }; -} - -macro_rules! check_native_column_types { - ( - $object:expr, - $type_ptr:expr, - $($type:ty => $constant:ident,)* - ) => { - $( - if $type_ptr == crate::typeref::$constant { - let val = $object.cast_unchecked::<$type>(); - return Ok(wrap_typeengine!(val.get().clone(), $object.as_ptr())); - } - )* - }; -} - -macro_rules! infer_rules { - ( - $py:expr, - $object_ptr:expr, - $type_ptr:expr, - $(($cond:expr) => $result:expr,)* - ) => { - $( - if $cond { - return Ok(wrap_typeengine!($py, $result, $result)); - } - )* - }; -} - -impl TypeEngine { - /// Creates a new [`TypeEngine`]. - pub fn new(object: RefBoundObject<'_>) -> pyo3::PyResult { - unsafe { - let typ = pyo3::ffi::Py_TYPE(object.as_ptr()); - - check_native_column_types!( - object, - typ, - crate::sqltypes::PyBlobType => BLOB_COLUMN_TYPE, - crate::sqltypes::PyBinaryType => BINARY_COLUMN_TYPE, - crate::sqltypes::PyVarBinaryType => VAR_BINARY_COLUMN_TYPE, - crate::sqltypes::PyBitType => BIT_COLUMN_TYPE, - crate::sqltypes::PyVarBitType => VAR_BIT_COLUMN_TYPE, - crate::sqltypes::PyDateTimeType => DATETIME_COLUMN_TYPE, - crate::sqltypes::PyTimestampType => TIMESTAMP_COLUMN_TYPE, - crate::sqltypes::PyTimeType => TIME_COLUMN_TYPE, - crate::sqltypes::PyDateType => DATE_COLUMN_TYPE, - crate::sqltypes::PyJSONType => JSON_COLUMN_TYPE, - crate::sqltypes::PyJSONBinaryType => JSON_BINARY_COLUMN_TYPE, - crate::sqltypes::PyDecimalType => DECIMAL_COLUMN_TYPE, - crate::sqltypes::PyUUIDType => UUID_COLUMN_TYPE, - crate::sqltypes::PyINETType => INET_COLUMN_TYPE, - crate::sqltypes::PyMacAddressType => MAC_ADDRESS_COLUMN_TYPE, - crate::sqltypes::PyBooleanType => BOOLEAN_COLUMN_TYPE, - crate::sqltypes::PyTinyIntegerType => TINY_INTEGER_COLUMN_TYPE, - crate::sqltypes::PySmallIntegerType => SMALL_INTEGER_COLUMN_TYPE, - crate::sqltypes::PyIntegerType => INTEGER_COLUMN_TYPE, - crate::sqltypes::PyBigIntegerType => BIG_INTEGER_COLUMN_TYPE, - crate::sqltypes::PyTinyUnsignedType => TINY_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PySmallUnsignedType => SMALL_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyUnsignedType => UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyBigUnsignedType => BIG_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyFloatType => FLOAT_COLUMN_TYPE, - crate::sqltypes::PyDoubleType => DOUBLE_COLUMN_TYPE, - crate::sqltypes::PyTextType => TEXT_COLUMN_TYPE, - crate::sqltypes::PyCharType => CHAR_COLUMN_TYPE, - crate::sqltypes::PyStringType => STRING_COLUMN_TYPE, - crate::sqltypes::PyVectorType => VECTOR_COLUMN_TYPE, - crate::sqltypes::PyArrayType => ARRAY_COLUMN_TYPE, - crate::sqltypes::PyEnumType => ENUM_COLUMN_TYPE, - ); - - crate::new_error!( - PyTypeError, - "expected SQLTypeAbstract, got {:?}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ) - } - } - - /// Tries to guess a native column type depends on type of the `object`. - #[cfg_attr(feature = "optimize", optimize(speed))] - pub fn infer_pyobject(object: RefBoundObject<'_>) -> pyo3::PyResult { - unsafe { - let object_ptr = object.as_ptr(); - let object_type_ptr = pyo3::ffi::Py_TYPE(object_ptr); - - if pyo3::ffi::PyLong_CheckExact(object_ptr) == 1 { - return Ok(wrap_typeengine!( - object.py(), - crate::sqltypes::PyBigIntegerType, - crate::sqltypes::PyBigIntegerType - )); - } - - infer_rules!( - object.py(), - object_ptr, - object_type_ptr, - - (pyo3::ffi::Py_IsNone(object_ptr) == 1) => crate::sqltypes::PyBooleanType, - (pyo3::ffi::PyBool_Check(object_ptr) == 1) => crate::sqltypes::PyBooleanType, - (pyo3::ffi::PyFloat_CheckExact(object_ptr) == 1) => crate::sqltypes::PyDoubleType, - (pyo3::ffi::PyUnicode_CheckExact(object_ptr) == 1) => crate::sqltypes::PyTextType, - (pyo3::ffi::PyBytes_CheckExact(object_ptr) == 1) => crate::sqltypes::PyBlobType, - - (object_type_ptr == crate::typeref::STD_DATETIME_TYPE) => crate::sqltypes::PyDateTimeType, - (object_type_ptr == crate::typeref::STD_DATE_TYPE) => crate::sqltypes::PyDateType, - (object_type_ptr == crate::typeref::STD_TIME_TYPE) => crate::sqltypes::PyTimeType, - (object_type_ptr == crate::typeref::STD_DECIMAL_TYPE) => crate::sqltypes::PyDecimalType(None), - (object_type_ptr == crate::typeref::STD_UUID_TYPE) => crate::sqltypes::PyUUIDType, - ); - - if (pyo3::ffi::PyDict_CheckExact(object_ptr) == 1 - || pyo3::ffi::PyList_CheckExact(object_ptr) == 1) - && crate::sqltypes::_validate_json_object(object.py(), object_ptr).is_ok() - { - return Ok(wrap_typeengine!( - object.py(), - crate::sqltypes::PyJSONType, - crate::sqltypes::PyJSONType - )); - } - - crate::new_error!( - PyTypeError, - "Could not infer column/sql type for {:?}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ) - } - } - - /// Tries to guess a native column type depends on type of the `object`. - #[cfg_attr(feature = "optimize", optimize(speed))] - pub fn infer_value(py: pyo3::Python<'_>, object: &sea_query::Value) -> Self { - match object { - sea_query::Value::Bool(_) => wrap_typeengine!( - py, - crate::sqltypes::PyBooleanType, - crate::sqltypes::PyBooleanType - ), - sea_query::Value::TinyInt(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyTinyIntegerType, - crate::sqltypes::PyTinyIntegerType - ) - } - sea_query::Value::SmallInt(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PySmallIntegerType, - crate::sqltypes::PySmallIntegerType - ) - } - sea_query::Value::Int(_) => wrap_typeengine!( - py, - crate::sqltypes::PyIntegerType, - crate::sqltypes::PyIntegerType - ), - sea_query::Value::BigInt(_) => wrap_typeengine!( - py, - crate::sqltypes::PyBigIntegerType, - crate::sqltypes::PyBigIntegerType - ), - sea_query::Value::TinyUnsigned(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyTinyUnsignedType, - crate::sqltypes::PyTinyUnsignedType - ) - } - sea_query::Value::SmallUnsigned(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PySmallUnsignedType, - crate::sqltypes::PySmallUnsignedType - ) - } - sea_query::Value::Unsigned(_) => wrap_typeengine!( - py, - crate::sqltypes::PyUnsignedType, - crate::sqltypes::PyUnsignedType - ), - sea_query::Value::BigUnsigned(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyBigUnsignedType, - crate::sqltypes::PyBigUnsignedType - ) - } - sea_query::Value::Float(_) => wrap_typeengine!( - py, - crate::sqltypes::PyFloatType, - crate::sqltypes::PyFloatType - ), - sea_query::Value::Double(_) => wrap_typeengine!( - py, - crate::sqltypes::PyDoubleType, - crate::sqltypes::PyDoubleType - ), - sea_query::Value::String(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyStringType(None), - crate::sqltypes::PyStringType(None) - ) - } - sea_query::Value::Char(_) => wrap_typeengine!( - py, - crate::sqltypes::PyCharType(None), - crate::sqltypes::PyCharType(None) - ), - sea_query::Value::Bytes(_) => { - wrap_typeengine!(py, crate::sqltypes::PyBlobType, crate::sqltypes::PyBlobType) - } - sea_query::Value::Json(_) => { - wrap_typeengine!(py, crate::sqltypes::PyJSONType, crate::sqltypes::PyJSONType) - } - sea_query::Value::ChronoDate(_) => { - wrap_typeengine!(py, crate::sqltypes::PyDateType, crate::sqltypes::PyDateType) - } - sea_query::Value::ChronoTime(_) => { - wrap_typeengine!(py, crate::sqltypes::PyTimeType, crate::sqltypes::PyTimeType) - } - sea_query::Value::ChronoDateTime(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyDateTimeType, - crate::sqltypes::PyDateTimeType - ) - } - sea_query::Value::ChronoDateTimeUtc(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyDateTimeType, - crate::sqltypes::PyDateTimeType - ) - } - sea_query::Value::ChronoDateTimeLocal(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyDateTimeType, - crate::sqltypes::PyDateTimeType - ) - } - sea_query::Value::ChronoDateTimeWithTimeZone(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyDateTimeType, - crate::sqltypes::PyDateTimeType - ) - } - sea_query::Value::Uuid(_) => { - wrap_typeengine!(py, crate::sqltypes::PyUUIDType, crate::sqltypes::PyUUIDType) - } - sea_query::Value::Decimal(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyDecimalType(None), - crate::sqltypes::PyDecimalType(None) - ) - } - sea_query::Value::Array(_, Some(nested)) => { - let nested_type_engine = { - if nested.is_empty() { - // Vec is empty so the nested type is not important - wrap_typeengine!( - py, - crate::sqltypes::PyBooleanType, - crate::sqltypes::PyBooleanType - ) - } else { - Self::infer_value(py, &nested[0]) - } - }; - - wrap_typeengine!( - py, - crate::sqltypes::PyArrayType(nested_type_engine), - crate::sqltypes::PyArrayType(nested_type_engine.clone()) - ) - } - sea_query::Value::Array(_, None) => { - // Vec is None so the nested type is not important - let nested_type_engine = wrap_typeengine!( - py, - crate::sqltypes::PyBooleanType, - crate::sqltypes::PyBooleanType - ); - wrap_typeengine!( - py, - crate::sqltypes::PyArrayType(nested_type_engine), - crate::sqltypes::PyArrayType(nested_type_engine.clone()) - ) - } - sea_query::Value::Vector(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyVectorType(None), - crate::sqltypes::PyVectorType(None) - ) - } - sea_query::Value::IpNetwork(_) => { - wrap_typeengine!(py, crate::sqltypes::PyINETType, crate::sqltypes::PyINETType) - } - sea_query::Value::MacAddress(_) => { - wrap_typeengine!( - py, - crate::sqltypes::PyMacAddressType, - crate::sqltypes::PyMacAddressType - ) - } - } - } - - pub fn as_pyobject<'py>(&self, py: pyo3::Python<'py>) -> pyo3::Bound<'py, pyo3::PyAny> { - unsafe { pyo3::Bound::from_borrowed_ptr(py, self.1.as_ptr()) } - } -} - -impl SQLTypeTrait for TypeEngine { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - self.0.to_sea_query_column_type() - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - self.0.validate(py, ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - self.0.deserialize(py, value) - } - - fn to_sql_type_name(&self) -> String { - self.0.to_sql_type_name() - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - self.0.serialize(py, ptr) - } -} - -impl std::fmt::Display for TypeEngine { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - unsafe { - pyo3::Python::attach_unchecked(|py| { - let object = self.as_pyobject(py); - std::fmt::Display::fmt(&object, f) - }) - } - } -} - -impl std::fmt::Debug for TypeEngine { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - unsafe { - pyo3::Python::attach_unchecked(|py| { - let object = self.as_pyobject(py); - std::fmt::Debug::fmt(&object, f) - }) - } - } -} diff --git a/src/internal/uninitialized.rs b/src/internal/uninitialized.rs deleted file mode 100644 index aa796c1..0000000 --- a/src/internal/uninitialized.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Helpers for creating __init__ methods for types. - -use ::std::cell::UnsafeCell; -use ::std::sync::Arc; - -/// An uninitialized value which can nominally be written to only once. -#[repr(transparent)] -pub struct ImmutableUninit(UnsafeCell>); - -/// An uninitialized Arc-Mutex. -#[repr(transparent)] -pub struct MutableUninit(Arc<::parking_lot::Mutex>>); - -impl ImmutableUninit { - /// Creates a new uninitialized instance. - #[inline] - #[must_use] - pub const fn uninit() -> Self { - Self(UnsafeCell::new(None)) - } - - /// Creates a new initialized instance. - #[inline] - #[must_use] - pub const fn new(val: T) -> Self { - Self(UnsafeCell::new(Some(val))) - } - - pub fn is_initialized(&self) -> bool { - unsafe { - let inner = &*self.0.get(); - inner.is_some() - } - } - - /// Initializes the self by `val`. - /// - /// # Safety - /// - It is Undefined Behavior to call this while any other reference(s) to the wrapped value are alive. - /// - Mutating the wrapped value through other means while the returned reference is alive is Undefined Behavior. - #[inline] - pub unsafe fn set(&self, val: T) { - let _ = (*self.0.get()).replace(val); - } - - /// Gets a reference to the wrapped value. - #[inline] - pub fn get_checked(&self) -> Option<&T> { - unsafe { - let inner = &*self.0.get(); - inner.as_ref() - } - } - - /// Unwraps the value, consuming the [`ImmutableUninit`]. - pub fn into_inner(self) -> Option { - self.0.into_inner() - } -} - -impl AsRef for ImmutableUninit { - /// Gets a reference to the wrapped value. - /// - /// # Panics - /// Panics if you forgot to call [`ImmutableUninit::set()`] - #[inline] - fn as_ref(&self) -> &T { - unsafe { - let inner = &*self.0.get(); - - inner.as_ref().expect( - "Object did not initialized yet. This happens when you forget to call __init__ \ - method. This is a critical issue.", - ) - } - } -} -impl Default for ImmutableUninit { - /// Creates a new uninitialized instance, like [`ImmutableUninit::uninit`] - #[inline] - fn default() -> Self { - Self::uninit() - } -} -impl Clone for ImmutableUninit { - fn clone(&self) -> Self { - let res = Self::uninit(); - - if let Some(x) = self.get_checked() { - unsafe { - res.set(x.clone()); - } - } - - res - } -} -impl ::std::fmt::Debug for ImmutableUninit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut tuple = f.debug_tuple("ImmutableUninit"); - - match self.get_checked() { - Some(x) => tuple.field(&x).finish(), - None => tuple.field(&"").finish(), - } - } -} -impl From for ImmutableUninit { - fn from(value: T) -> Self { - ImmutableUninit::new(value) - } -} -unsafe impl Send for ImmutableUninit {} -unsafe impl Sync for ImmutableUninit {} - -impl MutableUninit { - /// Creates a new uninitialized instance. - #[inline] - #[must_use] - pub fn uninit() -> Self { - Self(Arc::new(::parking_lot::Mutex::new(None))) - } - - /// Creates a new initialized instance. - #[inline] - #[must_use] - pub fn new(val: T) -> Self { - Self(Arc::new(::parking_lot::Mutex::new(Some(val)))) - } - - pub fn is_initialized(&self) -> bool { - self.0.lock().is_some() - } - - /// Initializes the self by `val`. - #[inline] - pub fn set(&self, val: T) { - let _ = (*self.0.lock()).replace(val); - } - - /// Gets a reference to the wrapped value. - /// - /// # Panics - /// Panics if you forgot to call [`ImmutableUninit::set()`] - pub fn lock(&self) -> parking_lot::MappedMutexGuard<'_, T> { - let inner = self.0.lock(); - assert!( - inner.as_ref().is_some(), - "Object did not initialized yet. This happens when you forget to call __init__ \ - method. This is a critical issue.", - ); - - unsafe { parking_lot::MutexGuard::map(inner, |x| x.as_mut().unwrap_unchecked()) } - } - - /// Unwraps the value, consuming the [`MutableInit`]. - pub fn into_inner(self) -> Arc<::parking_lot::Mutex>> { - self.0 - } -} - -impl Default for MutableUninit { - /// Creates a new uninitialized instance, like [`MutableUninit::uninit`] - #[inline] - fn default() -> Self { - Self::uninit() - } -} -impl Clone for MutableUninit { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} -impl ::std::fmt::Debug for MutableUninit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut tuple = f.debug_tuple("MutableUninit"); - - match &*self.0.lock() { - Some(x) => tuple.field(&x).finish(), - None => tuple.field(&"").finish(), - } - } -} -impl From for MutableUninit { - #[inline] - fn from(value: T) -> Self { - Self::new(value) - } -} diff --git a/src/internal/utils/mod.rs b/src/internal/utils/mod.rs new file mode 100644 index 0000000..4043190 --- /dev/null +++ b/src/internal/utils/mod.rs @@ -0,0 +1,88 @@ +mod strings; +mod submodule_ext; + +pub use strings::*; +pub use submodule_ext::*; + +/// Borrow the cached UTF-8 buffer of a Python `str`. +/// +/// # Safety +/// +/// `ptr` must be a live unicode object (`PyUnicode_Check` / exact). The +/// returned slice is valid until that object is mutated or the GIL is +/// released. The GIL must be held (`py`). +#[inline] +pub unsafe fn borrow_py_str_buffer<'a>(py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject) -> pyo3::PyResult<&'a str> { + let mut size: pyo3::ffi::Py_ssize_t = 0; + let data = pyo3::ffi::PyUnicode_AsUTF8AndSize(ptr, &mut size); + if data.is_null() || size < 0 { + Err(pyo3::PyErr::fetch(py)) + } else { + // SAFETY: `PyUnicode_AsUTF8AndSize` returns UTF-8; `size` is the byte + // length. The pointer is non-null on the success path. + Ok(std::str::from_utf8_unchecked(std::slice::from_raw_parts( + data as *const u8, + size as usize, + ))) + } +} + +/// Fully-qualified type name of `obj` (`int`, `rapidquery._core.dialect.PostgreSQL`, …). +/// +/// Returns `""` if `Py_TYPE` is null or the name lookup fails. +/// +/// # Safety +/// +/// `obj` must be a valid, non-null `PyObject*` (a borrowed `Bound` +/// pointer is fine). The GIL must be held (`py`). +#[inline] +pub unsafe fn get_type_name<'a>(py: pyo3::Python<'a>, obj: *mut pyo3::ffi::PyObject) -> String { + use pyo3::types::PyStringMethods; + use pyo3::types::PyTypeMethods; + + let type_ = pyo3::ffi::Py_TYPE(obj); + + if type_.is_null() { + String::from("") + } else { + let obj = pyo3::types::PyType::from_borrowed_type_ptr(py, type_); + + obj.fully_qualified_name() + .map(|x| x.to_string_lossy().into_owned()) + .unwrap_or_else(|_| String::from("")) + } +} + +/// Gets the Python builtin value `None` as [`pyo3::Bound<'_, pyo3::PyAny>`]. +#[inline] +pub fn new_pynone<'py>(py: pyo3::Python<'py>) -> pyo3::Bound<'py, pyo3::PyAny> { + unsafe { pyo3::Bound::from_borrowed_ptr(py, pyo3::ffi::Py_None()) } +} + +/// Converts smallvec into PyTuple using pyo3 FFI +/// +/// # Safety +/// The items returned by the iterator must be owned, not borrowed. +#[inline] +#[cfg_attr(feature = "nightly", optimize(speed))] +pub unsafe fn iterator_to_tuple<'py, I: Iterator>>( + py: pyo3::Python<'py>, + len: isize, + iter: I, +) -> pyo3::PyResult> { + let tuple = pyo3::ffi::PyTuple_New(len); + + for (ix, item) in iter.enumerate() { + match item { + Ok(ptr) => { + pyo3::ffi::PyTuple_SetItem(tuple, ix as isize, ptr); + } + Err(err) => { + pyo3::ffi::Py_DECREF(tuple); + return Err(err); + } + } + } + + Ok(pyo3::Bound::from_owned_ptr(py, tuple)) +} diff --git a/src/internal/utils/strings.rs b/src/internal/utils/strings.rs new file mode 100644 index 0000000..515e32f --- /dev/null +++ b/src/internal/utils/strings.rs @@ -0,0 +1,301 @@ +//! Allocation-conscious string replacement that writes directly to a [`fmt::Write`]. +// Assisted-by: Grok + +use std::fmt::Write; +use std::fmt::{self}; + +/// A search needle that can be located inside a UTF-8 haystack. +pub trait ReplacePattern: Copy { + /// First match of this pattern in `haystack`. + /// + /// Returns `(byte_offset, match_len_in_bytes)`. + fn find(self, haystack: &str) -> Option<(usize, usize)>; + + /// If this pattern is exactly one ASCII byte, return that byte. + /// + /// Used to prove that match boundaries are UTF-8 char boundaries without + /// inspecting the haystack. + fn as_ascii_byte(self) -> Option { + None + } +} + +impl ReplacePattern for char { + #[inline] + fn find(self, haystack: &str) -> Option<(usize, usize)> { + haystack.find(self).map(|i| (i, self.len_utf8())) + } + + #[inline] + fn as_ascii_byte(self) -> Option { + self.is_ascii().then_some(self as u8) + } +} + +impl ReplacePattern for &str { + #[inline] + fn find(self, haystack: &str) -> Option<(usize, usize)> { + // An empty needle matches at every byte offset and would loop forever + // (or insert between every character). Treat it as "no match". + if self.is_empty() { + return None; + } + haystack.find(self).map(|i| (i, self.len())) + } + + #[inline] + fn as_ascii_byte(self) -> Option { + let bytes = self.as_bytes(); + match bytes { + [b] if b.is_ascii() => Some(*b), + _ => None, + } + } +} + +/// Replace every occurrence of `from` with `to`, writing the result to `out`. +/// +/// Matches are non-overlapping and searched left to right, same as [`str::replace`]. +/// Empty `&str` needles are ignored (no infinite loop, input is copied as-is). +#[inline] +pub fn replace_to(haystack: &str, from: P, to: &str, out: &mut W) -> fmt::Result +where + P: ReplacePattern, + W: Write, +{ + // Single ASCII byte → single ASCII byte: still go through the generic loop. + // `str::find(char)` already uses memchr, which beats a hand-rolled byte scan + // when matches are sparse, and the generic loop is correct for every other case. + let _ = from.as_ascii_byte(); + + let mut last_end = 0; + let mut search_at = 0; + + while let Some((rel, len)) = from.find(&haystack[search_at..]) { + let start = search_at + rel; + out.write_str(&haystack[last_end..start])?; + out.write_str(to)?; + last_end = start + len; + search_at = last_end; + + // Defensive: a zero-width match would otherwise spin. + if len == 0 { + break; + } + } + + out.write_str(&haystack[last_end..]) +} + +/// Replace multiple single ASCII bytes in one pass over `haystack`. +/// +/// Equivalent to applying each pair as a simultaneous substitution against the +/// *original* input (not a chain of sequential [`str::replace`] calls). If the +/// same byte appears more than once, the last pair wins. +/// +/// # Constraints +/// - Each key must be an ASCII byte (`0..=127`). Non-ASCII keys are skipped so +/// that splits cannot land inside a multi-byte UTF-8 scalar. +/// - Replacement values may be any string, including empty (deletion). +/// +/// # Complexity +/// O(n + k) where n is the input length and k is the number of pairs. +pub fn replace_ascii_to(haystack: &str, replacements: &[(u8, &str)], out: &mut W) -> fmt::Result { + if replacements.is_empty() { + return out.write_str(haystack); + } + + // table[b] == 0 → no replacement + // table[b] == i + 1 → replacements[i] + // 512 bytes on the stack, no heap, last write wins. + let mut table = [0u16; 256]; + for (i, &(byte, _)) in replacements.iter().enumerate() { + if !byte.is_ascii() { + continue; + } + table[byte as usize] = i as u16 + 1; + } + + let bytes = haystack.as_bytes(); + let mut last_end = 0; + + for (i, &b) in bytes.iter().enumerate() { + let slot = table[b as usize]; + if slot == 0 { + continue; + } + + write_utf8_prefix(out, bytes, last_end, i)?; + out.write_str(replacements[slot as usize - 1].1)?; + last_end = i + 1; + } + + write_utf8_prefix(out, bytes, last_end, bytes.len()) +} + +/// Write `bytes[from..to]` as a string. +/// +/// # Safety invariant +/// `bytes` is the raw view of a valid `&str`, and `from..to` only starts/ends on +/// ASCII bytes or the slice ends — hence a char boundary. +#[inline] +fn write_utf8_prefix(out: &mut W, bytes: &[u8], from: usize, to: usize) -> fmt::Result { + if from == to { + return Ok(()); + } + // SAFETY: see invariant above. Callers only advance `from` past ASCII matches + // or to `bytes.len()`. + let chunk = unsafe { bytes.get_unchecked(from..to) }; + out.write_str(unsafe { std::str::from_utf8_unchecked(chunk) }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn replace(haystack: &str, from: impl ReplacePattern, to: &str) -> String { + let mut out = String::with_capacity(haystack.len()); + replace_to(haystack, from, to, &mut out).unwrap(); + out + } + + fn replace_ascii(s: &str, replacements: &[(u8, &str)]) -> String { + let mut out = String::with_capacity(s.len()); + replace_ascii_to(s, replacements, &mut out).unwrap(); + out + } + + #[test] + fn str_pattern() { + assert_eq!(replace("hello world", "o", "0"), "hell0 w0rld"); + } + + #[test] + fn char_pattern() { + assert_eq!(replace("banana", 'a', "AA"), "bAAnAAnAA"); + } + + #[test] + fn ascii_char_to_ascii_str() { + assert_eq!(replace("banana", 'a', "_"), "b_n_n_"); + } + + #[test] + fn no_match() { + assert_eq!(replace("hello", "z", "x"), "hello"); + } + + #[test] + fn unicode_pattern() { + assert_eq!(replace("café café", "é", "e"), "cafe cafe"); + } + + #[test] + fn multi_char_string_pattern() { + assert_eq!(replace("aaaa", "aa", "b"), "bb"); + } + + #[test] + fn overlapping_is_non_overlapping_left_to_right() { + assert_eq!(replace("aaa", "aa", "b"), "ba"); + } + + #[test] + fn empty_haystack() { + assert_eq!(replace("", "a", "b"), ""); + } + + #[test] + fn empty_needle_is_ignored() { + assert_eq!(replace("keep", "", "x"), "keep"); + } + + #[test] + fn delete_via_empty_replacement() { + assert_eq!(replace("a_b_c", "_", ""), "abc"); + } + + #[test] + fn replacement_longer_than_match() { + assert_eq!(replace("ab", "b", "BBB"), "aBBB"); + } + + #[test] + fn quote_and_nul() { + let input = "O'Brien\0's \0dog's\0 name"; + let expected = input.replace('\'', "''").replace('\0', ""); + let got = replace_ascii(input, &[(b'\'', "''"), (b'\0', "")]); + assert_eq!(got, expected); + } + + #[test] + fn single_replacement_matches_naive() { + let input = "hello world"; + assert_eq!(replace_ascii(input, &[(b'o', "0")]), input.replace('o', "0")); + } + + #[test] + fn no_matches_ascii() { + assert_eq!(replace_ascii("hello world", &[(b'z', "Z"), (b'x', "X")]), "hello world"); + } + + #[test] + fn empty_input_ascii() { + assert_eq!(replace_ascii("", &[(b'a', "b")]), ""); + } + + #[test] + fn empty_replacements_list() { + assert_eq!(replace_ascii("unchanged string", &[]), "unchanged string"); + } + + #[test] + fn many_pairs_at_once() { + let input = "a-b_c.d,e;f:g"; + let expected = input + .replace('-', "_DASH_") + .replace('.', "_DOT_") + .replace(',', "_COMMA_") + .replace(';', "_SEMI_") + .replace(':', "_COLON_"); + + let got = replace_ascii( + input, + &[ + (b'-', "_DASH_"), + (b'.', "_DOT_"), + (b',', "_COMMA_"), + (b';', "_SEMI_"), + (b':', "_COLON_"), + ], + ); + assert_eq!(got, expected); + } + + #[test] + fn unicode_content_preserved() { + let input = "café\0naïve'test"; + let expected = input.replace('\'', "''").replace('\0', ""); + assert_eq!(replace_ascii(input, &[(b'\'', "''"), (b'\0', "")]), expected); + } + + #[test] + fn last_duplicate_wins() { + assert_eq!(replace_ascii("abc", &[(b'a', "X"), (b'a', "Y")]), "Ybc"); + } + + #[test] + fn non_ascii_keys_are_skipped() { + // 0xC3 is a UTF-8 lead byte in "é". Treating it as a split point would be UB. + assert_eq!(replace_ascii("café", &[(0xC3, "X")]), "café"); + } + + #[test] + fn matches_std_replace_for_char() { + let samples = ["", "a", "aaa", "café", "hello\0world", "🇺🇸"]; + for s in samples { + assert_eq!(replace(s, 'a', "AA"), s.replace('a', "AA")); + assert_eq!(replace(s, "l", "_"), s.replace("l", "_")); + } + } +} diff --git a/src/internal/utils/submodule_ext.rs b/src/internal/utils/submodule_ext.rs new file mode 100644 index 0000000..742cdf1 --- /dev/null +++ b/src/internal/utils/submodule_ext.rs @@ -0,0 +1,48 @@ +//! Register nested `#[pymodule]` exports in `sys.modules`. +//! +//! PyO3 nested modules are attributes of the parent, but `import +//! parent.child` looks up `sys.modules["parent.child"]`. Without this walk, +//! `import rapidquery._core.dialect` fails even though `_core.dialect` works. +//! +//! Workaround: +//! +//! After registration, the child's `__name__` / `__package__` are set to +//! the dotted path (`rapidquery._core.dialect`) so `repr` and pickling +//! match the import path. + +use pyo3::types::PyAnyMethods; +use pyo3::types::PyModule; +use pyo3::types::PyModuleMethods; + +fn _inner_init(module: &pyo3::Bound<'_, PyModule>, path: &str) -> pyo3::PyResult<()> { + let py = module.py(); + let sys_modules = py.import("sys")?.getattr("modules")?; + for submod_name in module.index()? { + let submod_name = submod_name.extract::()?; + let submod = module.getattr(&submod_name)?; + let modpath = format!("{path}.{submod_name}"); + + if let Ok(submod) = submod.cast::() { + submod.setattr("__name__", &modpath)?; + if let Some((package, _)) = modpath.rsplit_once('.') { + submod.setattr("__package__", package)?; + } + + _inner_init(submod, &modpath)?; + sys_modules.set_item(&modpath, submod)?; + } + } + Ok(()) +} + +pub trait PyModuleSubmoduleExt { + fn init_submodules(&self) -> pyo3::PyResult<()>; +} + +impl PyModuleSubmoduleExt for pyo3::Bound<'_, pyo3::types::PyModule> { + fn init_submodules(&self) -> pyo3::PyResult<()> { + let mod_path = self.name()?.extract::()?; + _inner_init(self, &mod_path)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 84f0465..d9d852d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,46 +1,65 @@ -#![allow(unused_unsafe)] -#![allow(clippy::macro_metavars_in_unsafe)] -#![allow(clippy::too_many_arguments)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] -#![warn(clippy::print_stdout)] -#![warn(clippy::print_stderr)] -#![warn(clippy::dbg_macro)] -#![feature(optimize_attribute)] -#![feature(once_cell_try)] +#![allow(unsafe_op_in_unsafe_fn)] +#![cfg_attr(feature = "nightly", feature(optimize_attribute))] +pub mod data_type; +pub mod dialect; +pub mod expr; pub mod internal; -mod typeref; +pub mod serializers; +pub mod typeref; -mod common; -mod mysql; -mod postgres; -mod query; -mod schema; -mod sqlite; -mod sqltypes; +pub mod dcl; +pub mod ddl; +pub mod dml; +pub mod tcl; -/// RapidQuery core which is written in Rust. +/// RapidQuery native extension. #[pyo3::pymodule(gil_used = false)] -mod _lib { +mod _core { #[pymodule_export] - use super::common::common_module; + use super::dialect::dialect_mod; + #[pymodule_export] - use super::mysql::mysql_module; + use super::serializers::serializers_mod; + #[pymodule_export] - use super::postgres::postgres_module; + use super::expr::expr_mod; + #[pymodule_export] - use super::query::query_module; + use super::ddl::ddl_mod; + #[pymodule_export] - use super::schema::schema_module; + use super::dcl::dcl_mod; + #[pymodule_export] - use super::sqlite::sqlite_module; + use super::dml::dml_mod; + #[pymodule_export] - use super::sqltypes::sqltypes_module; + use super::tcl::tcl_mod; #[pymodule_init] #[cold] fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + use crate::internal::utils::PyModuleSubmoduleExt; + use pyo3::types::PyModuleMethods; + crate::typeref::initialize_typeref(m.py()); - Ok(()) + + m.add( + "_feature_only_stdlib_json", + cfg_select! { + feature = "only-stdlib-json" => true, + not(feature = "only-stdlib-json") => false, + }, + )?; + m.add( + "_feature_only_orjson", + cfg_select! { + feature = "only-orjson" => true, + not(feature = "only-orjson") => false, + }, + )?; + + m.init_submodules() } } diff --git a/src/mysql.rs b/src/mysql.rs deleted file mode 100644 index 2a2e0fc..0000000 --- a/src/mysql.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// MySQL-Only functions -/// -/// **Comming Soon ...** -#[pyo3::pymodule(name = "mysql")] -pub mod mysql_module {} diff --git a/src/postgres.rs b/src/postgres.rs deleted file mode 100644 index 16e8dea..0000000 --- a/src/postgres.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Postgres-Only functions -/// -/// **Comming Soon ...** -#[pyo3::pymodule(name = "postgres")] -pub mod postgres_module {} diff --git a/src/query/base.rs b/src/query/base.rs deleted file mode 100644 index 4c8d2fa..0000000 --- a/src/query/base.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::internal::{BoundArgs, BoundKwargs, BoundObject}; - -crate::implement_pyclass! { - /// Subclass of query statements. - #[derive(Debug, Clone, Copy)] - [subclass] PyQueryStatement as "QueryStatement"; -} - -#[pyo3::pymethods] -impl PyQueryStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self - } - - fn __init__(&self) {} - - /// Build a SQL string representation. - /// - /// **This method is unsafe and can cause SQL injection.** use `.build()` method instead. - #[pyo3(signature = (backend, /))] - #[allow(unused_variables)] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - Err(pyo3::exceptions::PyNotImplementedError::new_err(())) - } - - /// Build the SQL statement with parameter values. - /// - /// @signature (self, backend: _BackendName, /) -> tuple[str, tuple[Value, ...]] - #[pyo3(signature = (backend, /))] - #[allow(unused_variables)] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - Err(pyo3::exceptions::PyNotImplementedError::new_err(())) - } -} diff --git a/src/query/case.rs b/src/query/case.rs deleted file mode 100644 index 106b38d..0000000 --- a/src/query/case.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, RefBoundObject, ToSeaQuery}; - -crate::implement_pyclass! { - mutable [subclass] PyCaseStatement(CaseStatementState) as "CaseStatement" { - pub when: Vec<(PyExpr, PyExpr)>, - pub r#else: Option, - } -} - -impl Clone for CaseStatementState { - fn clone(&self) -> Self { - Self { - when: self.when.clone(), - r#else: self.r#else.clone(), - } - } -} - -impl ToSeaQuery for CaseStatementState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::CaseStatement { - let mut stmt = sea_query::CaseStatement::new(); - - for (x, y) in self.when.iter() { - stmt = stmt.case(x.0.clone(), y.0.clone()); - } - - if let Some(x) = &self.r#else { - stmt = stmt.finally(x.0.clone()); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyCaseStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - pub fn __init__(&self) -> pyo3::PyResult<()> { - self.0.set(CaseStatementState { - when: vec![], - r#else: None, - }); - Ok(()) - } - - fn when<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - result: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr as condition, got {}", - crate::internal::get_type_name(condition.py(), condition.as_ptr()) - ); - } - - let condition = condition.cast_unchecked::().get().clone(); - let result = PyExpr::try_from(result)?; - - slf.0.lock().when.push((condition, result)); - } - - Ok(slf) - } - - fn else_<'a>( - slf: pyo3::PyRef<'a, Self>, - result: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let result = PyExpr::try_from(result)?; - slf.0.lock().r#else = Some(result); - - Ok(slf) - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self, py: pyo3::Python) -> PyExpr { - let stmt = self.0.lock().to_sea_query(py); - PyExpr(sea_query::SimpleExpr::Case(Box::new(stmt))) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - py: pyo3::Python, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = self.to_expr(py); - - let state = crate::query::select::SelectLabelState { - expr, - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - fn __copy__(&self) -> Self { - let lock = self.0.lock(); - lock.clone().into() - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - let mut fmt = ReprFormatter::new_with_pyref(&slf); - - fmt.vec("when", false) - .display_iter( - lock.when - .iter() - .map(|x| format!("{} => {}", x.0.__repr__(), x.1.__repr__())), - ) - .finish(&mut fmt); - - fmt.optional_map("else", lock.r#else.as_ref(), |x| x.__repr__()) - .finish() - } -} diff --git a/src/query/delete.rs b/src/query/delete.rs deleted file mode 100644 index 5663c7a..0000000 --- a/src/query/delete.rs +++ /dev/null @@ -1,220 +0,0 @@ -use super::base::PyQueryStatement; -use super::ordering::PyOrdering; -use super::returning::PyReturning; -use crate::common::expression::PyExpr; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, RefBoundObject, ToSeaQuery}; - -crate::implement_pyclass! { - /// Builds DELETE SQL statements with a fluent interface. - /// - /// Provides a chainable API for constructing DELETE queries with support for: - /// - WHERE conditions for filtering - /// - LIMIT for restricting deletion count - /// - ORDER BY for determining deletion order - /// - RETURNING clauses for getting deleted data - #[derive(Clone)] - mutable [subclass, extends=PyQueryStatement] PyDeleteStatement(DeleteStatementState) as "DeleteStatement" { - pub table: PyTableName, - pub r#where: Option, - pub limit: Option, - pub returning_clause: Option, - pub orders: Vec, - } -} - -impl ToSeaQuery for DeleteStatementState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::DeleteStatement { - let mut stmt = sea_query::DeleteStatement::new(); - stmt.from_table(self.table.clone()); - - if let Some(x) = &self.r#where { - stmt.and_where(x.0.clone()); - } - if let Some(x) = self.limit { - stmt.limit(x); - } - - if let Some(x) = &self.returning_clause { - stmt.returning(x.0.to_sea_query(py)); - } - - for order in self.orders.iter() { - if let Some(x) = order.null_order { - stmt.order_by_expr_with_nulls(order.target.0.clone(), order.order.clone(), x); - } else { - stmt.order_by_expr(order.target.0.clone(), order.order.clone()); - } - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyDeleteStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PyQueryStatement) { - (Self::uninit(), PyQueryStatement) - } - - pub fn __init__(&self, table: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let table = PyTableName::try_from(table)?; - - let state = DeleteStatementState { - table, - r#where: None, - limit: None, - returning_clause: None, - orders: vec![], - }; - self.0.set(state); - Ok(()) - } - - /// Specify the table to delete from. - #[allow(clippy::wrong_self_convention)] - fn from_table<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let table = PyTableName::try_from(table)?; - - { - let mut lock = slf.0.lock(); - lock.table = table; - } - Ok(slf) - } - - /// Limit the number of rows to delete. - fn limit(slf: pyo3::PyRef<'_, Self>, n: u64) -> pyo3::PyRef<'_, Self> { - { - let mut lock = slf.0.lock(); - lock.limit = Some(n); - } - - slf - } - - /// Specify columns to return from the inserted rows. - #[pyo3(signature=(clause))] - fn returning<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyReturning>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.returning_clause = Some(clause.get().clone()); - } - Ok(slf) - } - - /// Add a WHERE condition to filter rows to delete. - fn r#where<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(condition.py(), condition.as_ptr()) - ); - } - - let condition = condition.cast_unchecked::().get().clone(); - let mut lock = slf.0.lock(); - - match std::mem::take(&mut lock.r#where) { - None => { - lock.r#where = Some(condition); - } - Some(x) => { - lock.r#where = Some(PyExpr(x.0.and(condition.0))); - } - } - } - - Ok(slf) - } - - /// Remove where conditions from statement. - fn clear_where(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().r#where = None; - slf - } - - /// Specify the order in which to delete rows. Typically used with - /// `.limit` method to delete specific rows. - #[pyo3(signature=(clause))] - fn order_by<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyOrdering>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.orders.push(clause.get().clone()); - } - - Ok(slf) - } - - /// Remove orders from statement. - fn clear_order_by(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().orders.clear(); - slf - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_statement!(backend, stmt) - } - - #[pyo3(signature = (backend, /))] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_parts!(py, backend, stmt) - } - - fn __copy__<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Bound::new(py, (lock.clone().into(), PyQueryStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .map("table", &lock.table, |x| x.__repr__()) - .optional_map("where", lock.r#where.as_ref(), |x| x.__repr__()) - .optional_display("limit", lock.limit.as_ref()) - .take(); - - fmt.vec("orders", true) - .display_iter(lock.orders.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.optional_map("returning", lock.returning_clause.as_ref(), |x| { - x.__repr__() - }) - .finish() - } -} diff --git a/src/query/insert.rs b/src/query/insert.rs deleted file mode 100644 index 8f7bb82..0000000 --- a/src/query/insert.rs +++ /dev/null @@ -1,463 +0,0 @@ -use super::base::PyQueryStatement; -use super::on_conflict::PyOnConflict; -use super::returning::PyReturning; -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::PyExpr; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; - -#[derive(Debug, Default)] -pub enum InsertValueSource { - #[default] - None, - Single(Vec), - Many(Vec>), - Select( - /// Always is `PySelectStatement` - PyObject, - ), -} - -crate::implement_pyclass! { - /// Builds INSERT SQL statements with a fluent interface. - /// - /// Provides a chainable API for constructing INSERT queries with support for: - /// - Single or multiple row insertion - /// - Conflict resolution (UPSERT) - /// - RETURNING clauses - /// - REPLACE functionality - /// - Default values - mutable [subclass, extends=PyQueryStatement] PyInsertStatement(InsertStatementState) as "InsertStatement" { - pub replace: bool, - pub table: PyTableName, - pub columns: Vec, - pub source: InsertValueSource, - - /// Always is `Option`. - pub on_conflict: Option, - pub returning_clause: Option, - pub default_values: Option, - } -} - -impl InsertValueSource { - fn clone_ref(&self, py: pyo3::Python) -> Self { - match self { - Self::Single(x) => Self::Single(x.clone()), - Self::Many(x) => Self::Many(x.clone()), - Self::Select(x) => Self::Select(x.clone_ref(py)), - Self::None => Self::None, - } - } -} - -impl InsertStatementState { - fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - replace: self.replace, - table: self.table.clone(), - columns: self.columns.clone(), - source: self.source.clone_ref(py), - on_conflict: self.on_conflict.as_ref().map(|x| x.clone_ref(py)), - returning_clause: self.returning_clause.clone(), - default_values: self.default_values, - } - } -} - -impl ToSeaQuery for InsertStatementState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::InsertStatement { - let mut stmt = sea_query::InsertStatement::new(); - stmt.into_table(self.table.clone()); - - if self.replace { - stmt.replace(); - } - stmt.columns(self.columns.iter().map(sea_query::Alias::new)); - - match &self.source { - InsertValueSource::None => (), - InsertValueSource::Single(x) => { - stmt.values(x.iter().map(|x| x.0.clone())).unwrap(); - } - InsertValueSource::Many(x) => { - for y in x.iter() { - stmt.values(y.iter().map(|x| x.0.clone())).unwrap(); - } - } - InsertValueSource::Select(subquery) => unsafe { - let subquery = - subquery.cast_bound_unchecked::(py); - - stmt.select_from(subquery.get().0.lock().to_sea_query(py)) - .unwrap(); - }, - } - - if let Some(x) = &self.on_conflict { - let py_oc = unsafe { x.cast_bound_unchecked::(py) }; - stmt.on_conflict(py_oc.get().0.lock().to_sea_query(py)); - } - if let Some(rows) = self.default_values { - stmt.or_default_values_many(rows); - } - if let Some(x) = &self.returning_clause { - stmt.returning(x.0.to_sea_query(py)); - } - - stmt - } -} - -impl InsertStatementState { - #[inline] - fn values_from_dictionary( - &mut self, - kwds: pyo3::Bound<'_, pyo3::types::PyDict>, - ) -> pyo3::PyResult<()> { - use pyo3::types::{PyAnyMethods, PyDictMethods}; - - if !self.columns.is_empty() && self.columns.len() != kwds.len() { - return Err(pyo3::PyErr::new::( - "values length isn't equal to columns length - this occurres when you're calling \ - `.values()` method multiple times with different columns.", - )); - } - - let mut cols = Vec::with_capacity(kwds.len()); - let mut vals = Vec::with_capacity(kwds.len()); - - unsafe { - for (key, value) in kwds.into_iter() { - let key = key.extract::().unwrap_unchecked(); - - cols.push(key); - vals.push(PyExpr::try_from(&value)?); - } - } - - match std::mem::take(&mut self.source) { - InsertValueSource::None | InsertValueSource::Select(_) => { - self.source = InsertValueSource::Single(vals); - } - InsertValueSource::Single(oldvals) => { - self.source = InsertValueSource::Many(vec![oldvals, vals]); - } - InsertValueSource::Many(mut arr_of_vals) => { - arr_of_vals.push(vals); - self.source = InsertValueSource::Many(arr_of_vals); - } - } - self.columns = cols; - - Ok(()) - } - - #[inline] - fn values_from_tuple(&mut self, args: BoundArgs<'_>) -> pyo3::PyResult<()> { - use pyo3::types::PyTupleMethods; - - if !self.columns.is_empty() && self.columns.len() != args.len() { - return Err(pyo3::PyErr::new::( - "values length isn't equal to columns length", - )); - } - - let mut vals = Vec::with_capacity(args.len()); - for value in args.iter() { - vals.push(PyExpr::try_from(&value)?); - } - - match std::mem::take(&mut self.source) { - InsertValueSource::None | InsertValueSource::Select(_) => { - self.source = InsertValueSource::Single(vals); - } - InsertValueSource::Single(oldvals) => { - self.source = InsertValueSource::Many(vec![oldvals, vals]); - } - InsertValueSource::Many(mut arr_of_vals) => { - arr_of_vals.push(vals); - self.source = InsertValueSource::Many(arr_of_vals); - } - } - - Ok(()) - } -} - -#[pyo3::pymethods] -impl PyInsertStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PyQueryStatement) { - (Self::uninit(), PyQueryStatement) - } - - pub fn __init__(&self, table: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let table = PyTableName::try_from(table)?; - - let state = InsertStatementState { - replace: false, - table, - columns: vec![], - source: InsertValueSource::None, - on_conflict: None, - returning_clause: None, - default_values: None, - }; - self.0.set(state); - Ok(()) - } - - /// Convert this INSERT to a REPLACE statement. - /// - /// REPLACE will delete existing rows that conflict with the new row - /// before inserting. - fn replace(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - { - let mut lock = slf.0.lock(); - lock.replace = true; - } - - slf - } - - /// Specify the target table for insertion. - fn into<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let table = PyTableName::try_from(table)?; - - { - let mut lock = slf.0.lock(); - lock.table = table; - } - Ok(slf) - } - - /// Specify the columns for insertion. - /// - /// There's no need to use this method when you're specifying column - /// names in `.values` method. - #[pyo3(signature=(*args))] - fn columns<'a>( - slf: pyo3::PyRef<'a, Self>, - args: BoundArgs<'a>, - ) -> pyo3::PyResult> { - use pyo3::types::PyTupleMethods; - - let mut columns = Vec::with_capacity(args.len()); - - for col in args.iter() { - let column_ref = PyColumnRef::try_from(&col)?; - - match column_ref.name { - Some(x) => columns.push(x.to_string()), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "Insert.columns cannot accept asterisk '*'", - )) - } - } - } - - { - let mut lock = slf.0.lock(); - lock.columns = columns; - } - Ok(slf) - } - - /// Specify values to insert. Also you can specify columns using keyword arguments. - #[pyo3(signature=(*args, **kwds))] - fn values<'a>( - slf: pyo3::PyRef<'a, Self>, - args: BoundArgs<'a>, - kwds: Option>, - ) -> pyo3::PyResult> { - use pyo3::types::PyTupleMethods; - - if !PyTupleMethods::is_empty(args) && kwds.is_some() { - return crate::new_error!( - PyTypeError, - "cannot use both args and kwargs at the same time" - ); - } - - if let Some(kwds) = kwds { - let mut lock = slf.0.lock(); - lock.values_from_dictionary(kwds.clone())?; - } else { - let mut lock = slf.0.lock(); - lock.values_from_tuple(args)?; - } - - Ok(slf) - } - - /// Specify a select query whose values to be inserted. - fn select_from<'a>( - slf: pyo3::PyRef<'a, Self>, - statement: BoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) == 0 - { - return crate::new_error!( - PyTypeError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - - // We have to return error if columns mismatch, like what SeaQuery does - let cast = statement.cast_unchecked::(); - - let val_len = cast.get().0.lock().exprs.len(); - let col_len = slf.0.lock().columns.len(); - - if col_len != val_len { - return crate::new_error!( - PyValueError, - "columns and values length mismatch: {} != {}", - col_len, - val_len, - ); - } - } - - { - let mut lock = slf.0.lock(); - lock.source = InsertValueSource::Select(statement.unbind()); - } - Ok(slf) - } - - /// Use DEFAULT VALUES if no values were specified. The `rows` - /// Specifies number of rows to insert with default values. - #[pyo3(signature=(rows=1))] - fn or_default_values(slf: pyo3::PyRef<'_, Self>, rows: u32) -> pyo3::PyRef<'_, Self> { - { - let mut lock = slf.0.lock(); - lock.default_values = Some(rows); - } - - slf - } - - /// Specify conflict resolution behavior (UPSERT). - fn on_conflict<'a>( - slf: pyo3::PyRef<'a, Self>, - action: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(action.as_ptr(), crate::typeref::ON_CONFLICT_TYPE) == 0 - { - return crate::new_error!( - PyTypeError, - "expected OnConflict, got {}", - crate::internal::get_type_name(action.py(), action.as_ptr()) - ); - } - - let mut lock = slf.0.lock(); - lock.on_conflict = Some(action.clone().unbind()); - } - - Ok(slf) - } - - /// Specify columns to return from the inserted rows. - #[pyo3(signature=(clause))] - fn returning<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyReturning>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.returning_clause = Some(clause.get().clone()); - } - Ok(slf) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_statement!(backend, stmt) - } - - #[pyo3(signature = (backend, /))] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_parts!(py, backend, stmt) - } - - fn __copy__<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Bound::new(py, (lock.clone_ref(py).into(), PyQueryStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .optional_boolean("replace", lock.replace) - .map("into", &lock.table, |x| x.__repr__()) - .take(); - - fmt.vec("columns", true) - .quote_iter(lock.columns.iter()) - .finish(&mut fmt); - - fmt.optional_display("on_conflict", lock.on_conflict.as_ref()); - - match &lock.source { - InsertValueSource::None => { - fmt.optional_display("default_rows", lock.default_values); - } - InsertValueSource::Select(x) => { - fmt.display("select_from", x); - } - InsertValueSource::Single(x) => { - fmt.vec("values", true) - .display_iter(x.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - } - InsertValueSource::Many(x) => { - let mut fmtvec = fmt.vec("values", true); - - for (index, nested) in x.iter().enumerate() { - fmtvec - .vec(index) - .display_iter(nested.iter().map(|x| x.__repr__())) - .finish(&mut fmtvec); - } - - fmtvec.finish(&mut fmt); - } - } - - fmt.optional_map("returning", lock.returning_clause.as_ref(), |x| { - x.__repr__() - }) - .finish() - } -} diff --git a/src/query/mod.rs b/src/query/mod.rs deleted file mode 100644 index deccbbf..0000000 --- a/src/query/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -pub mod base; -pub mod case; -pub mod delete; -pub mod insert; -pub mod on_conflict; -pub mod ordering; -pub mod returning; -pub mod select; -pub mod update; -pub mod window; -pub mod with; - -#[pyo3::pymodule(name = "query")] -pub mod query_module { - #[pymodule_export] - use super::base::PyQueryStatement; - - #[pymodule_export] - use super::on_conflict::PyOnConflict; - - #[pymodule_export] - use super::returning::PyReturning; - - #[pymodule_export] - use super::insert::PyInsertStatement; - - #[pymodule_export] - use super::ordering::PyOrdering; - - #[pymodule_export] - use super::delete::PyDeleteStatement; - - #[pymodule_export] - use super::update::PyUpdateStatement; - - #[pymodule_export] - use super::window::PyFrame; - - #[pymodule_export] - use super::window::PyWindowStatement; - - #[pymodule_export] - use super::select::PySelectLabel; - - #[pymodule_export] - use super::select::PySelectStatement; - - #[pymodule_export] - use super::case::PyCaseStatement; - - #[pymodule_export] - use super::with::PyWithClause; - - #[pymodule_export] - use super::with::PyWithQuery; -} diff --git a/src/query/on_conflict.rs b/src/query/on_conflict.rs deleted file mode 100644 index 4354599..0000000 --- a/src/query/on_conflict.rs +++ /dev/null @@ -1,326 +0,0 @@ -use pyo3::types::{PyAnyMethods, PyDictMethods, PyTupleMethods}; -use sea_query::IntoIden; - -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, RefBoundObject, ToSeaQuery}; - -#[derive(Debug, Clone)] -pub enum OnConflictUpdate { - Column(sea_query::DynIden), - Expr(sea_query::DynIden, PyExpr), -} - -#[derive(Debug, Clone, Default)] -pub enum OnConflictAction { - #[default] - None, - DoNothing(Vec), - DoUpdate(Vec), -} - -crate::implement_pyclass! { - /// Specifies conflict resolution behavior for INSERT statements. - /// - /// Handles situations where an INSERT would violate a unique constraint - /// or primary key. - /// - /// This corresponds to INSERT ... ON CONFLICT in PostgreSQL and - /// INSERT ... ON DUPLICATE KEY UPDATE in MySQL. - #[derive(Debug, Clone)] - mutable [subclass] PyOnConflict(OnConflictState) as "OnConflict" { - targets: Vec, - action: OnConflictAction, - target_where: Option, - action_where: Option, - } -} - -impl ToSeaQuery for OnConflictState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::OnConflict { - let mut stmt = sea_query::OnConflict::columns(self.targets.clone()); - - match &self.action { - OnConflictAction::None => (), - OnConflictAction::DoNothing(x) => { - if x.is_empty() { - stmt.do_nothing(); - } else { - stmt.do_nothing_on(x.iter().cloned()); - } - } - OnConflictAction::DoUpdate(x) => { - let mut columns = Vec::new(); - let mut exprs = Vec::new(); - - for val in x.iter() { - match val { - OnConflictUpdate::Column(name) => columns.push(name.clone()), - OnConflictUpdate::Expr(name, expr) => { - exprs.push((name.clone(), expr.0.clone())); - } - } - } - - stmt.update_columns(columns); - stmt.values(exprs); - } - } - - if let Some(x) = &self.action_where { - stmt.action_and_where(x.0.clone()); - } - if let Some(x) = &self.target_where { - stmt.target_and_where(x.0.clone()); - } - - stmt - } -} - -impl OnConflictState { - #[inline] - fn update_from_dictionary( - &mut self, - kwds: pyo3::Bound<'_, pyo3::types::PyDict>, - ) -> pyo3::PyResult<()> { - let mut actions = Vec::with_capacity(kwds.len()); - - for (key, val) in kwds.iter() { - unsafe { - let val = PyExpr::try_from(&val)?; - - let action = OnConflictUpdate::Expr( - sea_query::Alias::new(key.extract::().unwrap_unchecked()).into_iden(), - val, - ); - actions.push(action); - } - } - - match &mut self.action { - OnConflictAction::DoNothing(_) | OnConflictAction::None => { - self.action = OnConflictAction::DoUpdate(actions); - } - OnConflictAction::DoUpdate(x) => { - x.append(&mut actions); - } - } - - Ok(()) - } - - #[inline] - fn update_from_tuple(&mut self, args: BoundArgs<'_>) -> pyo3::PyResult<()> { - let mut actions = Vec::with_capacity(args.len()); - - for key in args.iter() { - let column_ref = PyColumnRef::try_from(&key)?; - match column_ref.name { - Some(x) => actions.push(OnConflictUpdate::Column(x)), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "OnConflict cannot accept asterisk '*' as column", - )) - } - } - } - - match &mut self.action { - OnConflictAction::DoNothing(_) | OnConflictAction::None => { - self.action = OnConflictAction::DoUpdate(actions); - } - OnConflictAction::DoUpdate(x) => { - x.append(&mut actions); - } - } - - Ok(()) - } -} - -#[pyo3::pymethods] -impl PyOnConflict { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[pyo3(signature=(*targets))] - fn __init__(&self, targets: BoundArgs<'_>) -> pyo3::PyResult<()> { - if targets.is_empty() { - let state = OnConflictState { - targets: vec![], - action: OnConflictAction::None, - target_where: None, - action_where: None, - }; - - self.0.set(state); - return Ok(()); - } - - let mut normalized_targets = Vec::with_capacity(targets.len()); - for item in targets.iter() { - let column_ref = PyColumnRef::try_from(&item)?; - match column_ref.name { - Some(x) => normalized_targets.push(x), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "OnConflict cannot accept asterisk '*' as target", - )) - } - } - } - - let state = OnConflictState { - targets: normalized_targets, - action: OnConflictAction::None, - target_where: None, - action_where: None, - }; - self.0.set(state); - Ok(()) - } - - /// Specify DO NOTHING action for conflicts. - /// - /// When a conflict occurs, the conflicting row will be skipped. - /// - /// `keys` parameter provides primary keys if you are using MySQL, for MySQL specific polyfill. - #[pyo3(signature=(*keys))] - fn do_nothing<'a>( - slf: pyo3::PyRef<'a, Self>, - keys: &pyo3::Bound<'a, pyo3::types::PyTuple>, - ) -> pyo3::PyResult> { - if keys.is_empty() { - let mut lock = slf.0.lock(); - lock.action = OnConflictAction::DoNothing(vec![]); - drop(lock); - - return Ok(slf); - } - - let mut normalized_keys: Vec = Vec::with_capacity(keys.len()); - for item in keys.iter() { - let column_ref = PyColumnRef::try_from(&item)?; - match column_ref.name { - Some(x) => normalized_keys.push(x), - None => { - return Err(pyo3::exceptions::PyValueError::new_err( - "OnConflict cannot accept asterisk '*' as column", - )) - } - } - } - - { - let mut lock = slf.0.lock(); - - match &mut lock.action { - OnConflictAction::DoUpdate(_) | OnConflictAction::None => { - lock.action = OnConflictAction::DoNothing(normalized_keys); - } - OnConflictAction::DoNothing(x) => { - x.append(&mut normalized_keys); - } - } - } - - Ok(slf) - } - - /// Specify DO UPDATE action for conflicts using column names, or with explicit values. - #[pyo3(signature=(*args, **kwds))] - fn do_update<'a>( - slf: pyo3::PyRef<'a, Self>, - args: BoundArgs<'a>, - kwds: Option>, - ) -> pyo3::PyResult> { - if !PyTupleMethods::is_empty(args) { - let mut lock = slf.0.lock(); - lock.update_from_tuple(args)?; - } - if let Some(kwds) = kwds { - let mut lock = slf.0.lock(); - lock.update_from_dictionary(kwds.clone())?; - } - - Ok(slf) - } - - /// Add a WHERE clause to the conflict target (partial unique index). - fn target_where<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(slf.py(), condition.as_ptr()) - ); - } - - let mut lock = slf.0.lock(); - lock.target_where = Some(condition.cast_unchecked::().get().clone()); - } - - Ok(slf) - } - - /// Add a WHERE clause to the conflict action (conditional update). - fn action_where<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(slf.py(), condition.as_ptr()) - ); - } - - let mut lock = slf.0.lock(); - lock.action_where = Some(condition.cast_unchecked::().get().clone()); - } - - Ok(slf) - } - - fn __copy__(&self) -> Self { - let lock = self.0.lock(); - lock.clone().into() - } - - pub fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf); - - fmt.vec("targets", true) - .quote_iter(lock.targets.iter().map(|x| x.to_string())) - .finish(&mut fmt); - - match &lock.action { - OnConflictAction::DoNothing(_) => { - fmt.quote("action", "DO NOTHING"); - } - OnConflictAction::DoUpdate(_) => { - fmt.quote("action", "DO UPDATE"); - } - OnConflictAction::None => (), - } - - fmt.optional_map("target_where", lock.target_where.as_ref(), |x| x.__repr__()) - .optional_map("action_where", lock.action_where.as_ref(), |x| x.__repr__()) - .finish() - } -} diff --git a/src/query/ordering.rs b/src/query/ordering.rs deleted file mode 100644 index e15d891..0000000 --- a/src/query/ordering.rs +++ /dev/null @@ -1,114 +0,0 @@ -use sea_query::IntoColumnRef; - -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::BoundObject; - -#[inline] -fn map_order_to_str(order: &sea_query::Order) -> String { - match order { - sea_query::Order::Asc => String::from("ASC"), - sea_query::Order::Desc => String::from("DESC"), - _ => unsafe { std::hint::unreachable_unchecked() }, - } -} - -#[inline] -fn map_null_ordering_to_str(order: Option) -> Option { - match order { - Some(sea_query::NullOrdering::First) => Some(String::from("FIRST")), - Some(sea_query::NullOrdering::Last) => Some(String::from("LAST")), - None => None, - } -} - -crate::implement_pyclass! { - // NOTE: It's a very simple clause, so I think it's OK to be a final type. - - /// Specifies ordering behavior for UPDATE, DELETE, and SELECT statements. - #[derive(Debug, Clone)] - [] PyOrdering as "Ordering" { - pub target: PyExpr, - pub order: sea_query::Order, - pub null_order: Option, - } -} - -#[pyo3::pymethods] -impl PyOrdering { - #[new] - #[pyo3(signature=(target, order = String::from("ASC"), null_order=None))] - fn __new__( - target: BoundObject<'_>, - order: String, - null_order: Option, - ) -> pyo3::PyResult { - let target = unsafe { - if pyo3::ffi::Py_TYPE(target.as_ptr()) == crate::typeref::EXPR_TYPE { - target.cast_unchecked::().get().clone() - } else { - let column_ref = PyColumnRef::try_from(&target)?; - - PyExpr(sea_query::SimpleExpr::Column(column_ref.into_column_ref())) - } - }; - - let order = match order.to_ascii_lowercase().as_str() { - "asc" => sea_query::Order::Asc, - "desc" => sea_query::Order::Desc, - _ => { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "unknown order: {order}" - ))) - } - }; - - let null_order = match null_order { - None => None, - Some(x) => match x.to_ascii_lowercase().as_str() { - "first" => Some(sea_query::NullOrdering::First), - "last" => Some(sea_query::NullOrdering::Last), - _ => { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "unknown null_order: {x}" - ))) - } - }, - }; - - Ok(Self { - target, - order, - null_order, - }) - } - - /// Target expression. - #[getter] - fn target(&self) -> PyExpr { - self.target.clone() - } - - #[getter] - fn order(&self) -> String { - map_order_to_str(&self.order) - } - - #[getter] - fn null_order(&self) -> Option { - map_null_ordering_to_str(self.null_order) - } - - fn __copy__(&self) -> Self { - self.clone() - } - - pub fn __repr__(&self) -> String { - ReprFormatter::new("Ordering") - .map("target", &self.target, |x| x.__repr__()) - .map("order", &self.order, map_order_to_str) - .optional_quote("null_ordering", map_null_ordering_to_str(self.null_order)) - .finish() - } -} diff --git a/src/query/returning.rs b/src/query/returning.rs deleted file mode 100644 index 5871af5..0000000 --- a/src/query/returning.rs +++ /dev/null @@ -1,92 +0,0 @@ -use pyo3::types::PyTupleMethods; -use sea_query::IntoColumnRef; - -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, ToSeaQuery}; - -#[derive(Debug, Clone)] -pub enum ReturningState { - All, - Exprs(Vec), -} - -crate::implement_pyclass! { - // NOTE: It's a very simple clause, so I think it's OK to be a final type. - - /// RETURNING clause. - /// - /// Works on PostgreSQL and SQLite>=3.35.0. - #[derive(Debug, Clone)] - [] PyReturning as "Returning" (pub ReturningState); -} - -impl ToSeaQuery for ReturningState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::ReturningClause { - match self { - Self::All => sea_query::ReturningClause::All, - Self::Exprs(x) => { - sea_query::ReturningClause::Exprs(x.iter().map(|x| x.0.clone()).collect()) - } - } - } -} - -#[pyo3::pymethods] -impl PyReturning { - /// Specify columns you need to return - #[new] - #[pyo3(signature=(*args))] - pub fn __new__(args: BoundArgs<'_>) -> pyo3::PyResult { - let mut columns = Vec::with_capacity(args.len()); - - for col in args.iter() { - unsafe { - if pyo3::ffi::Py_TYPE(col.as_ptr()) == crate::typeref::EXPR_TYPE { - let value = col.cast_into_unchecked::(); - - columns.push(value.get().clone()); - continue; - } - } - - let column_ref = PyColumnRef::try_from(&col)?; - if column_ref.name.is_none() { - return Ok(Self(ReturningState::All)); - } - - columns.push(PyExpr(column_ref.into_column_ref().into())); - } - - Ok(Self(ReturningState::Exprs(columns))) - } - - /// Return all columns. Same as `self.columns("*")`. - #[classmethod] - fn all(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(ReturningState::All) - } - - fn __copy__(&self) -> Self { - self.clone() - } - - pub fn __repr__(&self) -> String { - let mut fmt = ReprFormatter::new("Returning"); - - match &self.0 { - ReturningState::All => { - fmt.pair("", "*"); - } - ReturningState::Exprs(x) => { - fmt.vec("", false) - .display_iter(x.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - } - } - - fmt.finish() - } -} diff --git a/src/query/select.rs b/src/query/select.rs deleted file mode 100644 index f2dc887..0000000 --- a/src/query/select.rs +++ /dev/null @@ -1,1241 +0,0 @@ -use super::base::PyQueryStatement; -use super::ordering::PyOrdering; -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::{PyExpr, PyFunc}; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; -use crate::query::window::PyWindowStatement; - -use pyo3::types::{PyAnyMethods, PyTupleMethods}; -use sea_query::{IntoColumnRef, IntoIden}; - -/// Window type in [`PySelectLabel`] -pub enum SelectLabelWindow { - Name(sea_query::DynIden), - Query( - /// Always is `PyWindowStatement` - PyObject, - ), -} - -/// Select references in [`PySelectStatement`] -/// -/// It exactly does what [`sea_query::TableRef`] does -pub enum SelectReference { - SubQuery( - /// Always is `PySelectStatement` - PyObject, - sea_query::DynIden, - ), - Func(PyFunc, sea_query::DynIden), - TableName(PyTableName), -} - -/// Distinct mode in [`PySelectStatement`] -#[derive(Debug, Clone, Default)] -pub enum DistinctMode { - #[default] - None, - Distinct, - DistinctOn(Vec), -} - -/// Lock mode and options in [`PySelectStatement`] -#[derive(Debug, Clone)] -pub struct LockMode { - pub r#type: sea_query::LockType, - pub behavior: Option, - pub tables: Vec, -} - -/// Join mode and options in [`PySelectStatement`] -pub struct JoinMode { - pub r#type: sea_query::JoinType, - pub reference: SelectReference, - pub on: Option, - pub lateral: bool, -} - -crate::implement_pyclass! { - /// Represents a column expression with an optional alias in a SELECT statement. - /// - /// Used to specify both the expression to select and an optional alias name - /// for the result column. - immutable [subclass] PySelectLabel(SelectLabelState) as "SelectLabel" { - pub expr: PyExpr, - pub alias: Option, - pub window: Option, - } -} -crate::implement_pyclass! { - /// Builds SELECT SQL statements with a fluent interface. - /// - /// Provides a chainable API for constructing SELECT queries with support for: - /// - Column selection with expressions and aliases - /// - Table and subquery sources - /// - Filtering with WHERE and HAVING - /// - Joins (inner, left, right, full, cross, lateral) - /// - Grouping and aggregation - /// - Ordering and pagination - /// - Set operations (UNION, EXCEPT, INTERSECT) - /// - Row locking for transactions - /// - DISTINCT queries - mutable [subclass, extends=PyQueryStatement] PySelectStatement(SelectStatementState) as "SelectStatement" { - pub references: Vec, - - /// Always is `Vec` - pub exprs: Vec, - - pub r#where: Option, - pub groups: Vec, - - pub having: Option, - pub orders: Vec, - pub distinct: DistinctMode, - pub joins: Vec, - pub lock: Option, - pub limit: Option, - pub offset: Option, - - /// Always is `Option<(_, PyWindowStatement)>` - pub window: Option<(sea_query::DynIden, PyObject)>, - - /// Always is `Option<(_, PySelectStatement)>` - pub unions: Vec<(sea_query::UnionType, PyObject)>, - - // TODO - // pub table_sample: Option, - // pub index_hint: Option, - } -} - -#[allow(clippy::derivable_impls)] -impl Default for SelectStatementState { - fn default() -> Self { - Self { - references: Default::default(), - exprs: Default::default(), - r#where: Default::default(), - groups: Default::default(), - unions: Default::default(), - having: Default::default(), - orders: Default::default(), - distinct: Default::default(), - joins: Default::default(), - lock: Default::default(), - limit: Default::default(), - offset: Default::default(), - window: Default::default(), - } - } -} - -impl SelectLabelState { - fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - expr: self.expr.clone(), - alias: self.alias.clone(), - window: self.window.as_ref().map(|x| match x { - SelectLabelWindow::Name(x) => SelectLabelWindow::Name(x.clone()), - SelectLabelWindow::Query(x) => SelectLabelWindow::Query(x.clone_ref(py)), - }), - } - } -} - -impl SelectReference { - fn clone_ref(&self, py: pyo3::Python) -> Self { - match self { - Self::Func(x, y) => Self::Func(x.clone(), y.clone()), - Self::TableName(x) => Self::TableName(x.clone()), - Self::SubQuery(x, y) => Self::SubQuery(x.clone_ref(py), y.clone()), - } - } -} - -impl JoinMode { - fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - r#type: self.r#type, - reference: self.reference.clone_ref(py), - on: self.on.clone(), - lateral: self.lateral, - } - } -} - -impl SelectStatementState { - fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - references: self.references.iter().map(|x| x.clone_ref(py)).collect(), - exprs: self.exprs.iter().map(|x| x.clone_ref(py)).collect(), - r#where: self.r#where.clone(), - groups: self.groups.clone(), - having: self.having.clone(), - orders: self.orders.clone(), - distinct: self.distinct.clone(), - joins: self.joins.iter().map(|x| x.clone_ref(py)).collect(), - lock: self.lock.clone(), - limit: self.limit, - offset: self.offset, - window: self - .window - .as_ref() - .map(|(x, y)| (x.clone(), y.clone_ref(py))), - unions: self - .unions - .iter() - .map(|(x, y)| (*x, y.clone_ref(py))) - .collect(), - } - } -} - -impl TryFrom> for SelectLabelWindow { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject<'_>) -> Result { - unsafe { - // Window statement - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::WINDOW_STATEMENT_TYPE) - == 1 - { - Ok(Self::Query(value.clone().unbind())) - } - // String - else if pyo3::ffi::PyUnicode_CheckExact(value.as_ptr()) == 1 { - let window_name = value.extract::().unwrap_unchecked(); - Ok(Self::Name(sea_query::Alias::new(window_name).into_iden())) - } - // Other types - else { - crate::new_error!( - PyTypeError, - "expected WindowStatement or str for SelectLabel window, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ) - } - } - } -} - -#[inline] -pub fn cast_into_select_label<'a>(value: BoundObject<'a>) -> pyo3::PyResult> { - unsafe { - // SelectLabel itself - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::SELECT_LABEL_TYPE) == 1 { - return Ok(value); - } - - let state = SelectLabelState { - expr: PyExpr::try_from(&value)?, - alias: None, - window: None, - }; - let result: PySelectLabel = state.into(); - pyo3::Bound::new(value.py(), result).map(|x| x.into_any()) - } -} - -#[inline] -fn map_str_to_join_type(value: Option) -> pyo3::PyResult { - match value.map(|x| x.to_ascii_uppercase()) { - Some(x) => match x.as_str() { - "CROSS" => Ok(sea_query::JoinType::CrossJoin), - "FULL" => Ok(sea_query::JoinType::FullOuterJoin), - "INNER" => Ok(sea_query::JoinType::InnerJoin), - "LEFT" => Ok(sea_query::JoinType::LeftJoin), - "RIGHT" => Ok(sea_query::JoinType::RightJoin), - _ => { - crate::new_error!( - PyValueError, - "acceptable join types are 'CROSS', 'FULL', 'INNER', 'RIGHT', and 'LEFT'. got \ - {}", - x - ) - } - }, - None => Ok(sea_query::JoinType::Join), - } -} - -impl SelectReference { - fn from_table(value: RefBoundObject<'_>) -> pyo3::PyResult { - let table = PyTableName::try_from(value)?; - Ok(Self::TableName(table)) - } - - fn from_subquery(value: RefBoundObject<'_>, alias: String) -> pyo3::PyResult { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::SELECT_STATEMENT_TYPE) - == 0 - { - return crate::new_error!( - PyValueError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ); - } - } - let alias = sea_query::Alias::new(alias).into_iden(); - - Ok(Self::SubQuery(value.clone().unbind(), alias)) - } - - fn from_function(value: RefBoundObject<'_>, alias: String) -> pyo3::PyResult { - let function = unsafe { - let type_ptr = pyo3::ffi::Py_TYPE(value.as_ptr()); - - // Func type - if type_ptr == crate::typeref::FUNC_TYPE { - value.cast_unchecked::().get().clone() - } - // Expr type - else if type_ptr == crate::typeref::EXPR_TYPE { - let casted_expr = value.cast_unchecked::(); - let get_casted_expr = casted_expr.get(); - - if let sea_query::SimpleExpr::FunctionCall(x) = &get_casted_expr.0 { - PyFunc(x.clone()) - } else { - return crate::new_error!(PyValueError, "given Expr is not a function call"); - } - } - // Other types - else { - return crate::new_error!( - PyValueError, - "expected Func or Expr, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ); - } - }; - let alias = sea_query::Alias::new(alias).into_iden(); - - Ok(Self::Func(function, alias)) - } -} - -impl std::fmt::Display for SelectReference { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::TableName(x) => write!(f, "{}", x.__repr__()), - Self::Func(x, alias) => write!(f, "({}, {})", x.__repr__(), alias.to_string()), - Self::SubQuery(x, alias) => write!(f, "({}, {})", x, alias.to_string()), - } - } -} - -impl std::fmt::Display for JoinMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("(")?; - - match self.r#type { - sea_query::JoinType::Join => (), - sea_query::JoinType::CrossJoin => write!(f, "CROSS, ")?, - sea_query::JoinType::FullOuterJoin => write!(f, "FULL, ")?, - sea_query::JoinType::InnerJoin => write!(f, "INNER, ")?, - sea_query::JoinType::LeftJoin => write!(f, "LEFT, ")?, - sea_query::JoinType::RightJoin => write!(f, "RIGHT, ")?, - } - - if let Some(on) = &self.on { - write!(f, "{}, {}", self.reference, on.__repr__())?; - } else { - write!(f, "{}", self.reference)?; - } - - if self.lateral { - write!(f, ", LATERAL")?; - } - - f.write_str(")") - } -} - -impl std::fmt::Display for LockMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "(")?; - match self.r#type { - sea_query::LockType::KeyShare => write!(f, "KEY SHARE")?, - sea_query::LockType::NoKeyUpdate => write!(f, "NO KEY UPDATE")?, - sea_query::LockType::Share => write!(f, "SHARE")?, - sea_query::LockType::Update => write!(f, "UPDATE")?, - } - - if let Some(x) = self.behavior { - match x { - sea_query::LockBehavior::Nowait => write!(f, ", NOWAIT")?, - sea_query::LockBehavior::SkipLocked => write!(f, ", SKIP")?, - } - } - - if self.tables.is_empty() { - return write!(f, ")"); - } - - write!(f, ", [")?; - for (index, item) in self.tables.iter().enumerate() { - if index == 0 { - write!(f, "{}", item.__repr__())?; - } else { - write!(f, ", {}", item.__repr__())?; - } - } - write!(f, "])") - } -} - -impl ToSeaQuery for SelectLabelWindow { - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::WindowSelectType { - match self { - Self::Name(x) => sea_query::WindowSelectType::Name(x.clone()), - SelectLabelWindow::Query(x) => { - let statement = unsafe { x.cast_bound_unchecked::(py) }; - let lock = statement.get().0.lock(); - - sea_query::WindowSelectType::Query(lock.to_sea_query(py)) - } - } - } -} - -impl ToSeaQuery for SelectLabelState { - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::SelectExpr { - if self.alias.is_none() { - // If expr is column, setting alias can optimize the SQL query. - let default_alias = { - match &self.expr.0 { - sea_query::SimpleExpr::Column(sea_query::ColumnRef::Column(name)) => { - Some(name.clone()) - } - sea_query::SimpleExpr::Column(sea_query::ColumnRef::TableColumn(_, name)) => { - Some(name.clone()) - } - sea_query::SimpleExpr::Column(sea_query::ColumnRef::SchemaTableColumn( - _, - _, - name, - )) => Some(name.clone()), - _ => None, - } - }; - - sea_query::SelectExpr { - expr: self.expr.0.clone(), - alias: default_alias, - window: self.window.as_ref().map(|x| x.to_sea_query(py)), - } - } else { - sea_query::SelectExpr { - expr: self.expr.0.clone(), - alias: self - .alias - .as_ref() - .map(|x| sea_query::Alias::new(x).into_iden()), - - window: self.window.as_ref().map(|x| x.to_sea_query(py)), - } - } - } -} - -impl ToSeaQuery for SelectStatementState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::SelectStatement { - let mut stmt = sea_query::SelectStatement::new(); - - // Distinct mode - match &self.distinct { - DistinctMode::None => (), - DistinctMode::Distinct => { - stmt.distinct(); - } - DistinctMode::DistinctOn(cols) => { - stmt.distinct_on(cols.clone()); - } - } - - // References - for table in self.references.iter() { - match table { - SelectReference::TableName(x) => unsafe { - stmt.from(x.clone()); - }, - SelectReference::Func(x, alias) => unsafe { - stmt.from_function(x.0.clone(), alias.clone()); - }, - SelectReference::SubQuery(x, alias) => unsafe { - let x = unsafe { x.cast_bound_unchecked::(py) }; - let lock = x.get().0.lock(); - - stmt.from_subquery(lock.to_sea_query(py), alias.clone()); - }, - } - } - - // Columns - stmt.exprs(self.exprs.iter().map(|x| unsafe { - let expr = x.cast_bound_unchecked::(py); - expr.get().0.as_ref().to_sea_query(py) - })); - - // Groups - stmt.add_group_by(self.groups.iter().map(|x| x.0.clone())); - - // Condition - stmt.and_where_option(self.r#where.as_ref().map(|x| x.0.clone())); - - // Having - if let Some(x) = &self.having { - stmt.and_having(x.0.clone()); - } - - // Limit & Offset - if let Some(n) = self.limit { - stmt.limit(n); - } - if let Some(n) = self.offset { - stmt.offset(n); - } - - // Orders - for order in self.orders.iter() { - if let Some(x) = order.null_order { - stmt.order_by_expr_with_nulls(order.target.0.clone(), order.order.clone(), x); - } else { - stmt.order_by_expr(order.target.0.clone(), order.order.clone()); - } - } - - // Lock mode - if let Some(lock) = &self.lock { - match (lock.behavior, lock.tables.is_empty()) { - (Some(behavior), false) => { - stmt.lock_with_tables_behavior( - lock.r#type, - lock.tables.iter().cloned(), - behavior, - ); - } - (Some(behavior), true) => { - stmt.lock_with_behavior(lock.r#type, behavior); - } - (None, false) => { - stmt.lock_with_tables(lock.r#type, lock.tables.iter().cloned()); - } - (None, true) => { - stmt.lock(lock.r#type); - } - } - } - - // Unions - stmt.unions(self.unions.iter().map(|(union_type, union_stmt)| { - let union_stmt = unsafe { union_stmt.cast_bound_unchecked::(py) }; - let union_lock = union_stmt.get().0.lock(); - - (*union_type, union_lock.to_sea_query(py)) - })); - - // Joins - for join in self.joins.iter() { - let condition = - sea_query::Condition::all().add_option(join.on.as_ref().map(|x| x.0.clone())); - - match (&join.reference, join.lateral) { - (SelectReference::TableName(x), _) => { - stmt.join(join.r#type, x.clone(), condition); - } - (SelectReference::Func(func, alias), _) => { - stmt.join( - join.r#type, - sea_query::TableRef::FunctionCall(func.0.clone(), alias.clone()), - condition, - ); - } - (SelectReference::SubQuery(subquery, alias), true) => { - let subquery_stmt = - unsafe { subquery.cast_bound_unchecked::(py) }; - let subquery_lock = subquery_stmt.get().0.lock(); - - stmt.join_lateral( - join.r#type, - subquery_lock.to_sea_query(py), - alias.clone(), - condition, - ); - } - (SelectReference::SubQuery(subquery, alias), false) => { - let subquery_stmt = - unsafe { subquery.cast_bound_unchecked::(py) }; - let subquery_lock = subquery_stmt.get().0.lock(); - - stmt.join_subquery( - join.r#type, - subquery_lock.to_sea_query(py), - alias.clone(), - condition, - ); - } - } - } - - // Window - if let Some((window_name, window)) = &self.window { - let window = unsafe { window.cast_bound_unchecked::(py) }; - let lock = window.get().0.lock(); - - stmt.window(window_name.clone(), lock.to_sea_query(py)); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PySelectLabel { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[pyo3(signature=(expr, alias=None, window=None))] - fn __init__( - &self, - expr: RefBoundObject<'_>, - alias: Option, - window: Option>, - ) -> pyo3::PyResult<()> { - let window = match window { - Some(x) => Some(SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = PyExpr::try_from(expr)?; - - let state = SelectLabelState { - expr, - alias, - window, - }; - unsafe { - self.0.set(state); - } - Ok(()) - } - - #[getter] - fn expr(&self) -> PyExpr { - self.0.as_ref().expr.clone() - } - - #[getter] - fn alias(&self) -> Option { - self.0.as_ref().alias.clone() - } - - #[getter] - fn window<'a>(&self, py: pyo3::Python<'a>) -> Option> { - use pyo3::IntoPyObjectExt; - - let inner = self.0.as_ref(); - - match &inner.window { - Some(ref select_window) => match select_window { - SelectLabelWindow::Name(name) => { - Some(name.to_string().into_bound_py_any(py).unwrap()) - } - SelectLabelWindow::Query(w) => Some(w.bind(py).clone()), - }, - None => None, - } - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> Self { - let lock = self.0.as_ref(); - lock.clone_ref(py).into() - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .map("expr", &inner.expr, |x| x.__repr__()) - .optional_quote("alias", inner.alias.as_ref()) - .take(); - - match &inner.window { - Some(SelectLabelWindow::Name(x)) => { - fmt.iden("window", x); - } - Some(SelectLabelWindow::Query(x)) => { - fmt.display("window", x); - } - None => {} - } - - fmt.finish() - } -} - -#[pyo3::pymethods] -impl PySelectStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PyQueryStatement) { - (Self::uninit(), PyQueryStatement) - } - - #[pyo3(signature=(*exprs))] - pub fn __init__(&self, exprs: BoundArgs<'_>) -> pyo3::PyResult<()> { - let mut casted = Vec::new(); - for item in exprs.iter() { - casted.push(cast_into_select_label(item)?.unbind()); - } - - let state = SelectStatementState { - exprs: casted, - ..Default::default() - }; - self.0.set(state); - Ok(()) - } - - #[pyo3(signature=(*on))] - fn distinct<'a>( - slf: pyo3::PyRef<'a, Self>, - on: BoundArgs<'a>, - ) -> pyo3::PyResult> { - if on.is_empty() { - // Distinct mode without specific column - slf.0.lock().distinct = DistinctMode::Distinct; - } else { - let mut cols = Vec::new(); - - for item in on.iter() { - cols.push(PyColumnRef::try_from(&item)?); - } - - slf.0.lock().distinct = DistinctMode::DistinctOn(cols); - } - - Ok(slf) - } - - #[pyo3(signature=(*args))] - fn columns<'a>( - slf: pyo3::PyRef<'a, Self>, - args: BoundArgs<'a>, - ) -> pyo3::PyResult> { - let mut casted = Vec::new(); - for item in args.iter() { - let column_ref = PyColumnRef::try_from(&item)?; - - let state = SelectLabelState { - expr: PyExpr(sea_query::SimpleExpr::Column(column_ref.into_column_ref())), - alias: None, - window: None, - }; - let result: PySelectLabel = state.into(); - - casted.push(pyo3::Py::new(slf.py(), result)?.into_any()); - } - - slf.0.lock().exprs.append(&mut casted); - Ok(slf) - } - - #[pyo3(signature=(*args))] - fn exprs<'a>( - slf: pyo3::PyRef<'a, Self>, - args: BoundArgs<'a>, - ) -> pyo3::PyResult> { - let mut casted = Vec::new(); - for item in args.iter() { - casted.push(cast_into_select_label(item)?.unbind()); - } - - slf.0.lock().exprs.append(&mut casted); - Ok(slf) - } - - #[allow(clippy::wrong_self_convention)] - fn from_table<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let reference = SelectReference::from_table(table)?; - - slf.0.lock().references.push(reference); - Ok(slf) - } - - #[allow(clippy::wrong_self_convention)] - fn from_subquery<'a>( - slf: pyo3::PyRef<'a, Self>, - subquery: RefBoundObject<'a>, - alias: String, - ) -> pyo3::PyResult> { - if subquery.as_ptr() == slf.as_ptr() { - return crate::new_error!(PyValueError, "select statement cannot select from itself!"); - } - - let reference = SelectReference::from_subquery(subquery, alias)?; - - slf.0.lock().references.push(reference); - Ok(slf) - } - - #[allow(clippy::wrong_self_convention)] - fn from_function<'a>( - slf: pyo3::PyRef<'a, Self>, - function: RefBoundObject<'a>, - alias: String, - ) -> pyo3::PyResult> { - let reference = SelectReference::from_function(function, alias)?; - - slf.0.lock().references.push(reference); - Ok(slf) - } - - fn limit(slf: pyo3::PyRef<'_, Self>, n: u64) -> pyo3::PyRef<'_, Self> { - slf.0.lock().limit = Some(n); - slf - } - - fn offset(slf: pyo3::PyRef<'_, Self>, n: u64) -> pyo3::PyRef<'_, Self> { - slf.0.lock().offset = Some(n); - slf - } - - fn r#where<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(condition.py(), condition.as_ptr()) - ); - } - - let condition = condition.cast_unchecked::().get().clone(); - let mut lock = slf.0.lock(); - - match std::mem::take(&mut lock.r#where) { - None => { - lock.r#where = Some(condition); - } - Some(x) => { - lock.r#where = Some(PyExpr(x.0.and(condition.0))); - } - } - } - - Ok(slf) - } - - /// Remove where conditions from statement. - fn clear_where(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().r#where = None; - slf - } - - /// Specify the order in which to delete rows. - #[pyo3(signature=(clause))] - fn order_by<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyOrdering>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.orders.push(clause.get().clone()); - } - - Ok(slf) - } - - /// Remove orders from statement. - fn clear_order_by(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().orders.clear(); - slf - } - - fn having<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(condition.py(), condition.as_ptr()) - ); - } - - let condition = condition.cast_unchecked::().get().clone(); - let mut lock = slf.0.lock(); - - match std::mem::take(&mut lock.having) { - None => { - lock.having = Some(condition); - } - Some(x) => { - lock.having = Some(PyExpr(x.0.and(condition.0))); - } - } - } - - Ok(slf) - } - - #[pyo3(signature=(r#type=String::from("UPDATE"), behavior=None, tables=Vec::new()))] - fn lock( - slf: pyo3::PyRef<'_, Self>, - r#type: String, - behavior: Option, - tables: Vec, - ) -> pyo3::PyResult> { - let lock_type = match r#type.to_ascii_uppercase().as_str() { - "UPDATE" => sea_query::LockType::Update, - "NO KEY UPDATE" => sea_query::LockType::NoKeyUpdate, - "SHARE" => sea_query::LockType::Share, - "KEY SHARE" => sea_query::LockType::KeyShare, - _ => { - return crate::new_error!( - PyValueError, - "acceptable lock types are 'UPDATE', 'NO KEY UPDATE', 'SHARE', and 'KEY \ - SHARE'. got {}", - r#type - ) - } - }; - let lock_behavior = match behavior.map(|x| x.to_ascii_uppercase()) { - Some(x) => match x.as_str() { - "NOWAIT" => Some(sea_query::LockBehavior::Nowait), - "SKIP" => Some(sea_query::LockBehavior::SkipLocked), - _ => { - return crate::new_error!( - PyValueError, - "acceptable lock behaviors are 'NOWAIT', and 'SKIP'. got {}", - x - ) - } - }, - None => None, - }; - - let mut tbs = Vec::with_capacity(tables.len()); - - for table in tables.into_iter() { - tbs.push(PyTableName::try_from(table.bind(slf.py()))?); - } - - slf.0.lock().lock = Some(LockMode { - r#type: lock_type, - behavior: lock_behavior, - tables: tbs, - }); - Ok(slf) - } - - #[pyo3(signature=(*groups))] - fn group_by<'a>( - slf: pyo3::PyRef<'a, Self>, - groups: BoundArgs<'a>, - ) -> pyo3::PyResult> { - let mut exprs = Vec::with_capacity(groups.len()); - - for expr in groups.iter() { - let target = unsafe { - if pyo3::ffi::Py_TYPE(expr.as_ptr()) == crate::typeref::EXPR_TYPE { - expr.cast_unchecked::().get().clone() - } else { - let column_ref = PyColumnRef::try_from(&expr)?; - - PyExpr(sea_query::SimpleExpr::Column(column_ref.into_column_ref())) - } - }; - - exprs.push(target); - } - - slf.0.lock().groups.append(&mut exprs); - Ok(slf) - } - - #[pyo3(signature=(statement, r#type=String::from("DISTINCT")))] - fn union<'a>( - slf: pyo3::PyRef<'a, Self>, - statement: BoundObject<'a>, - r#type: String, - ) -> pyo3::PyResult> { - unsafe { - if statement.as_ptr() == slf.as_ptr() { - return crate::new_error!( - PyValueError, - "select statement cannot select from itself!" - ); - } - - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::SELECT_STATEMENT_TYPE, - ) == 0 - { - return crate::new_error!( - PyValueError, - "expected SelectStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - } - - let union_type = match r#type.to_ascii_uppercase().as_str() { - "ALL" => sea_query::UnionType::All, - "INTERSECT" => sea_query::UnionType::Intersect, - "DISTINCT" => sea_query::UnionType::Distinct, - "EXCEPT" => sea_query::UnionType::Except, - _ => { - return crate::new_error!( - PyValueError, - "acceptable union types are 'ALL', 'INTERSECT', 'DISTINCT', and 'EXCEPT'. got \ - {}", - r#type - ) - } - }; - - slf.0.lock().unions.push((union_type, statement.unbind())); - Ok(slf) - } - - #[pyo3(signature=(table, on=None, r#type=None))] - fn join<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - on: Option>, - r#type: Option, - ) -> pyo3::PyResult> { - let reference = SelectReference::from_table(table)?; - let join_mode = JoinMode { - r#type: map_str_to_join_type(r#type)?, - reference, - on: match on { - Some(x) => Some(PyExpr::try_from(x)?), - None => None, - }, - lateral: false, - }; - - slf.0.lock().joins.push(join_mode); - Ok(slf) - } - - #[pyo3(signature=(function, alias, on=None, r#type=None))] - fn join_function<'a>( - slf: pyo3::PyRef<'a, Self>, - function: RefBoundObject<'a>, - alias: String, - on: Option>, - r#type: Option, - ) -> pyo3::PyResult> { - let reference = SelectReference::from_function(function, alias)?; - - let join_mode = JoinMode { - r#type: map_str_to_join_type(r#type)?, - reference, - on: match on { - Some(x) => Some(PyExpr::try_from(x)?), - None => None, - }, - lateral: false, - }; - - slf.0.lock().joins.push(join_mode); - Ok(slf) - } - - #[pyo3(signature=(subquery, alias, on=None, r#type=None, lateral=false))] - fn join_subquery<'a>( - slf: pyo3::PyRef<'a, Self>, - subquery: RefBoundObject<'a>, - alias: String, - on: Option>, - r#type: Option, - lateral: bool, - ) -> pyo3::PyResult> { - if subquery.as_ptr() == slf.as_ptr() { - return crate::new_error!(PyValueError, "select statement cannot select from itself!"); - } - - let reference = SelectReference::from_subquery(subquery, alias)?; - - let join_mode = JoinMode { - r#type: map_str_to_join_type(r#type)?, - reference, - on: match on { - Some(x) => Some(PyExpr::try_from(x)?), - None => None, - }, - lateral, - }; - slf.0.lock().joins.push(join_mode); - Ok(slf) - } - - #[pyo3(signature=(name, statement))] - fn window<'a>( - slf: pyo3::PyRef<'a, Self>, - name: String, - statement: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - statement.as_ptr(), - crate::typeref::WINDOW_STATEMENT_TYPE, - ) == 0 - { - return crate::new_error!( - PyTypeError, - "expected WindowStatement, got {}", - crate::internal::get_type_name(statement.py(), statement.as_ptr()) - ); - } - } - - slf.0.lock().window = Some(( - sea_query::Alias::new(name).into_iden(), - statement.clone().unbind(), - )); - Ok(slf) - } - - /// Shorthand for `Expr(self)` - fn to_expr(&self, py: pyo3::Python) -> PyExpr { - let stmt = self.0.lock().to_sea_query(py); - let subquery = sea_query::SubQueryStatement::SelectStatement(stmt); - PyExpr(sea_query::SimpleExpr::SubQuery(None, Box::new(subquery))) - } - - /// Shorthand for `SelectLabel(self, alias, window)` - #[pyo3(signature=(alias, window=None))] - fn label( - &self, - py: pyo3::Python, - alias: String, - window: Option>, - ) -> pyo3::PyResult { - let window = match window { - Some(x) => Some(crate::query::select::SelectLabelWindow::try_from(x)?), - None => None, - }; - let expr = self.to_expr(py); - - let state = crate::query::select::SelectLabelState { - expr, - alias: Some(alias), - window, - }; - Ok(state.into()) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_statement!(backend, stmt) - } - - #[pyo3(signature = (backend, /))] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_parts!(py, backend, stmt) - } - - fn __copy__<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Bound::new(py, (lock.clone_ref(py).into(), PyQueryStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf); - - match &lock.distinct { - DistinctMode::None => (), - DistinctMode::Distinct => { - fmt.pair("distinct", "true"); - } - DistinctMode::DistinctOn(x) => { - fmt.vec("distinct", false) - .display_iter(x.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - } - } - - fmt.vec("references", false) - .display_iter(lock.references.iter()) - .finish(&mut fmt); - - fmt.vec("exprs", false) - .display_iter(lock.exprs.iter()) - .finish(&mut fmt); - - fmt.optional_map("where", lock.r#where.as_ref(), |x| x.__repr__()); - - fmt.vec("groups", true) - .display_iter(lock.groups.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.optional_map("having", lock.having.as_ref(), |x| x.__repr__()); - - fmt.vec("orders", true) - .display_iter(lock.orders.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.vec("joins", true) - .display_iter(lock.joins.iter()) - .finish(&mut fmt); - - fmt.optional_display("lock", lock.lock.as_ref()) - .optional_display("offset", lock.offset) - .optional_display("limit", lock.limit) - .optional_map("window", lock.window.as_ref(), |(name, stmt)| { - format!("('{}', {})", name.to_string(), stmt) - }); - - fmt.vec("unions", true) - .display_iter( - lock.unions - .iter() - .map(|(union_type, stmt)| match union_type { - sea_query::UnionType::All => format!("(ALL, {})", stmt), - sea_query::UnionType::Distinct => format!("(DISTINCT, {})", stmt), - sea_query::UnionType::Except => format!("(EXCEPT, {})", stmt), - sea_query::UnionType::Intersect => format!("(INTERSECT, {})", stmt), - }), - ) - .finish(&mut fmt); - - fmt.finish() - } -} diff --git a/src/query/update.rs b/src/query/update.rs deleted file mode 100644 index cf46b9f..0000000 --- a/src/query/update.rs +++ /dev/null @@ -1,289 +0,0 @@ -use sea_query::IntoIden; - -use super::base::PyQueryStatement; -use super::ordering::PyOrdering; -use super::returning::PyReturning; -use crate::common::expression::PyExpr; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, RefBoundObject, ToSeaQuery}; - -crate::implement_pyclass! { - /// Builds UPDATE SQL statements with a fluent interface. - /// - /// Provides a chainable API for constructing UPDATE queries with support for: - /// - Setting column values - /// - WHERE conditions for filtering - /// - LIMIT for restricting update count - /// - ORDER BY for determining update order - /// - RETURNING clauses for getting updated data - #[derive(Clone)] - mutable [subclass, extends=PyQueryStatement] PyUpdateStatement(UpdateStatementState) as "UpdateStatement" { - pub table: PyTableName, - pub from_table: Option, - pub values: Vec<(sea_query::DynIden, PyExpr)>, - pub r#where: Option, - pub limit: Option, - pub returning_clause: Option, - pub orders: Vec, - } -} - -impl ToSeaQuery for UpdateStatementState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::UpdateStatement { - let mut stmt = sea_query::UpdateStatement::new(); - stmt.table(self.table.clone()); - - if let Some(x) = &self.from_table { - stmt.from(x.clone()); - } - stmt.values(self.values.iter().map(|(x, y)| (x.clone(), y.0.clone()))); - - if let Some(x) = &self.r#where { - stmt.and_where(x.0.clone()); - } - if let Some(x) = self.limit { - stmt.limit(x); - } - if let Some(x) = &self.returning_clause { - stmt.returning(x.0.to_sea_query(py)); - } - - for order in self.orders.iter() { - if let Some(x) = order.null_order { - stmt.order_by_expr_with_nulls(order.target.0.clone(), order.order.clone(), x); - } else { - stmt.order_by_expr(order.target.0.clone(), order.order.clone()); - } - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyUpdateStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PyQueryStatement) { - (Self::uninit(), PyQueryStatement) - } - - pub fn __init__(&self, table: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let table = PyTableName::try_from(table)?; - - let state = UpdateStatementState { - table, - from_table: None, - values: vec![], - r#where: None, - limit: None, - returning_clause: None, - orders: vec![], - }; - self.0.set(state); - Ok(()) - } - - /// Specify the table to update. - fn table<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let table = PyTableName::try_from(table)?; - - { - let mut lock = slf.0.lock(); - lock.table = table; - } - Ok(slf) - } - - /// Update using data from another table (`UPDATE .. FROM ..`). - /// - /// MySQL doesn't support the UPDATE FROM syntax. And the current implementation attempt to - /// tranform it to the UPDATE JOIN syntax, which only works for one join target. - #[allow(clippy::wrong_self_convention)] - fn from_table<'a>( - slf: pyo3::PyRef<'a, Self>, - table: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let table = PyTableName::try_from(table)?; - - { - let mut lock = slf.0.lock(); - lock.from_table = Some(table); - } - Ok(slf) - } - - /// Limit the number of rows to update. - fn limit(slf: pyo3::PyRef<'_, Self>, n: u64) -> pyo3::PyRef<'_, Self> { - { - let mut lock = slf.0.lock(); - lock.limit = Some(n); - } - - slf - } - - /// Specify columns to return from the inserted rows. - #[pyo3(signature=(clause))] - fn returning<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyReturning>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.returning_clause = Some(clause.get().clone()); - } - Ok(slf) - } - - /// Add a WHERE condition to filter rows to update. - fn r#where<'a>( - slf: pyo3::PyRef<'a, Self>, - condition: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::Py_TYPE(condition.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr, got {}", - crate::internal::get_type_name(condition.py(), condition.as_ptr()) - ); - } - - let condition = condition.cast_unchecked::().get().clone(); - let mut lock = slf.0.lock(); - - match std::mem::take(&mut lock.r#where) { - None => { - lock.r#where = Some(condition); - } - Some(x) => { - lock.r#where = Some(PyExpr(x.0.and(condition.0))); - } - } - } - - Ok(slf) - } - - /// Remove where conditions from statement. - fn clear_where(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().r#where = None; - slf - } - - /// Specify the order in which to delete rows. Typically used with - /// `.limit` method to delete specific rows. - #[pyo3(signature=(clause))] - fn order_by<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyOrdering>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.orders.push(clause.get().clone()); - } - - Ok(slf) - } - - /// Remove orders from statement. - fn clear_order_by(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> { - slf.0.lock().orders.clear(); - slf - } - - /// Specify columns and their new values. - #[pyo3(signature=(**kwds))] - fn values<'a>( - slf: pyo3::PyRef<'a, Self>, - kwds: Option>, - ) -> pyo3::PyResult> { - use pyo3::types::{PyAnyMethods, PyDictMethods}; - - if kwds.is_none() { - return Ok(slf); - } - - let kwds = unsafe { kwds.unwrap_unchecked() }; - let mut values = Vec::with_capacity(kwds.len()); - - unsafe { - for (key, value) in kwds.iter() { - let key = key.extract::().unwrap_unchecked(); - values.push(( - (sea_query::Alias::new(key).into_iden()), - PyExpr::try_from(&value)?, - )); - } - } - - { - let mut lock = slf.0.lock(); - lock.values.append(&mut values); - } - Ok(slf) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_statement!(backend, stmt) - } - - #[pyo3(signature = (backend, /))] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_parts!(py, backend, stmt) - } - - fn __copy__<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Bound::new(py, (lock.clone().into(), PyQueryStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .map("table", &lock.table, |x| x.__repr__()) - .optional_map("from_table", lock.from_table.as_ref(), |x| x.__repr__()) - .take(); - - fmt.vec("values", false) - .display_iter( - lock.values - .iter() - .map(|x| format!("('{}', {})", x.0.to_string(), x.1.__repr__())), - ) - .finish(&mut fmt); - - fmt.vec("orders", true) - .display_iter(lock.orders.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.optional_display("limit", lock.limit) - .optional_map("where", lock.r#where.as_ref(), |x| x.__repr__()) - .optional_map("returning", lock.returning_clause.as_ref(), |x| { - x.__repr__() - }) - .finish() - } -} diff --git a/src/query/window.rs b/src/query/window.rs deleted file mode 100644 index 8dd94b6..0000000 --- a/src/query/window.rs +++ /dev/null @@ -1,218 +0,0 @@ -use pyo3::types::PyTupleMethods; -use sea_query::{IntoColumnRef, OverStatement}; - -use super::ordering::PyOrdering; -use crate::common::column_ref::PyColumnRef; -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, RefBoundObject, ToSeaQuery}; - -/// Frame clause -#[derive(Debug, Clone, PartialEq)] -pub struct FrameClause { - pub r#type: sea_query::FrameType, - pub start: sea_query::Frame, - pub end: Option, -} - -crate::implement_pyclass! { - /// Window frame start and frame end clause. Use its classmethods. - #[derive(Debug, Clone)] - [] PyFrame as "Frame" (pub sea_query::Frame); -} -crate::implement_pyclass! { - /// Window expression. - /// - /// # References: - /// - /// 1. - /// 2. - /// 3. - #[derive(Clone)] - mutable [subclass] PyWindowStatement(WindowStatementState) as "WindowStatement" { - pub partition_by: Vec, - pub orders: Vec, - pub frame: Option - } -} - -#[pyo3::pymethods] -impl PyFrame { - #[classmethod] - fn unbounded_preceding(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Frame::UnboundedPreceding) - } - - #[classmethod] - fn current_row(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Frame::CurrentRow) - } - - #[classmethod] - fn unbounded_following(_cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self(sea_query::Frame::UnboundedFollowing) - } - - #[classmethod] - fn following(_cls: &pyo3::Bound<'_, pyo3::types::PyType>, val: u32) -> Self { - Self(sea_query::Frame::Following(val)) - } - - #[classmethod] - fn preceding(_cls: &pyo3::Bound<'_, pyo3::types::PyType>, val: u32) -> Self { - Self(sea_query::Frame::Preceding(val)) - } -} - -impl ToSeaQuery for WindowStatementState { - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::WindowStatement { - let mut stmt = sea_query::WindowStatement::new(); - - for partition in &self.partition_by { - stmt.add_partition_by(partition.0.clone()); - } - - if let Some(x) = &self.frame { - stmt.frame(x.r#type.clone(), x.start.clone(), x.end.clone()); - } - - for order in self.orders.iter() { - if let Some(x) = order.null_order { - stmt.order_by_expr_with_nulls(order.target.0.clone(), order.order.clone(), x); - } else { - stmt.order_by_expr(order.target.0.clone(), order.order.clone()); - } - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyWindowStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - #[pyo3(signature=(*partition_by))] - pub fn __init__(&self, partition_by: BoundArgs<'_>) -> pyo3::PyResult<()> { - let mut partitions = Vec::with_capacity(partition_by.len()); - - for partition in partition_by.iter() { - let partition = unsafe { - if pyo3::ffi::Py_TYPE(partition.as_ptr()) == crate::typeref::EXPR_TYPE { - partition.cast_unchecked::().get().clone() - } else { - let column_ref = PyColumnRef::try_from(&partition)?; - - PyExpr(sea_query::SimpleExpr::Column(column_ref.into_column_ref())) - } - }; - - partitions.push(partition); - } - - let state = WindowStatementState { - partition_by: partitions, - orders: Vec::new(), - frame: None, - }; - self.0.set(state); - Ok(()) - } - - /// Partition by column or custom expression. - fn partition<'a>( - slf: pyo3::PyRef<'a, Self>, - partition_by: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let partition_by = unsafe { - if pyo3::ffi::Py_TYPE(partition_by.as_ptr()) == crate::typeref::EXPR_TYPE { - partition_by.cast_unchecked::().get().clone() - } else { - let column_ref = PyColumnRef::try_from(partition_by)?; - - PyExpr(sea_query::SimpleExpr::Column(column_ref.into_column_ref())) - } - }; - - { - let mut lock = slf.0.lock(); - lock.partition_by.push(partition_by); - } - Ok(slf) - } - - #[pyo3(signature=(clause))] - fn order_by<'a>( - slf: pyo3::PyRef<'a, Self>, - clause: pyo3::Bound<'_, PyOrdering>, - ) -> pyo3::PyResult> { - { - let mut lock = slf.0.lock(); - lock.orders.push(clause.get().clone()); - } - - Ok(slf) - } - - #[pyo3(signature=(frame_type, frame_start, frame_end=None))] - fn frame<'a>( - slf: pyo3::PyRef<'a, Self>, - frame_type: String, - frame_start: pyo3::Bound<'_, PyFrame>, - frame_end: Option>, - ) -> pyo3::PyResult> { - let frame_type = match frame_type.to_ascii_lowercase().as_str() { - "rows" => sea_query::FrameType::Rows, - "range" => sea_query::FrameType::Range, - _ => { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "invalid frame type, expected 'rows' or 'range'; got {:?}", - frame_type - ))); - } - }; - - { - let mut lock = slf.0.lock(); - lock.frame = Some(FrameClause { - r#type: frame_type, - start: frame_start.get().0.clone(), - end: frame_end.map(|x| x.get().0.clone()), - }) - } - Ok(slf) - } - - fn __copy__(&self) -> Self { - let lock = self.0.lock(); - lock.clone().into() - } - - pub fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf); - - fmt.vec("partition_by", true) - .display_iter(lock.partition_by.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.vec("orders", true) - .display_iter(lock.orders.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.optional_map("frame_type", lock.frame.as_ref(), |x| { - format!("{:?}", x.r#type) - }) - .optional_map("frame_start", lock.frame.as_ref(), |x| { - format!("{:?}", x.start) - }) - .optional_map("frame_end", lock.frame.as_ref(), |x| format!("{:?}", x.end)) - .finish() - } -} diff --git a/src/query/with.rs b/src/query/with.rs deleted file mode 100644 index 4a8c5b1..0000000 --- a/src/query/with.rs +++ /dev/null @@ -1,522 +0,0 @@ -use sea_query::IntoIden; - -use crate::common::expression::PyExpr; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; -use crate::new_error; -use crate::query::base::PyQueryStatement; - -pub enum CommonTableExpressionQuery { - Select( - /// Always is `SelectStatement` - PyObject, - ), - Delete( - /// Always is `DeleteStatement` - PyObject, - ), - Update( - /// Always is `UpdateStatement` - PyObject, - ), - Insert( - /// Always is `InsertStatement` - PyObject, - ), -} - -pub struct CommonTableExpression { - pub name: String, - pub query: CommonTableExpressionQuery, - pub columns: Vec, - pub materialized: Option, -} - -crate::implement_pyclass! { - /// A WITH clause can contain one or multiple common table expressions (CTEs). - /// - /// These named queries can act as a "query local table" that are materialized during execution and - /// then can be used by the query prefixed with the WITH clause. - /// - /// A CTE is a name, column names and a query returning data for those columns. - /// - /// Some databases (like sqlite) restrict the acceptable kinds of queries inside of the WITH clause - /// CTEs. These databases only allow `SelectStatement`s to form a CTE. - /// - /// Other databases like postgres allow modification queries (UPDATE, DELETE) inside of the WITH - /// clause but they have to return a table. (They must have a RETURNING clause). - /// - /// RapidQuery doesn't check this or restrict the kind of CTE that you can create - /// in rust. This means that you can put an UPDATE or DELETE queries into WITH clause and RapidQuery - /// will succeed in generating that kind of sql query but the execution inside the database will - /// fail because they are invalid. - /// - /// It is your responsibility to ensure that the kind of WITH clause that you put together makes - /// sense and valid for that database that you are using. - /// - /// NOTE that for recursive WITH queries (in sql: "WITH RECURSIVE") you can only have a - /// single CTE inside of the WITH clause. That query must match certain - /// requirements: - /// * It is a query of UNION or UNION ALL of two queries. - /// * The first part of the query (the left side of the UNION) must be executable first in itself. - /// It must be non-recursive. (Cannot contain self reference) - /// * The self reference must appear in the right hand side of the UNION. - /// * The query can only have a single self-reference. - /// * Recursive data-modifying statements are not supported, but you can use the results of a - /// recursive SELECT query in a data-modifying statement. (like so: WITH RECURSIVE - /// cte_name(a,b,c,d) AS (SELECT ... UNION SELECT ... FROM ... JOIN cte_name ON ... WHERE ...) - /// DELETE FROM table WHERE table.a = cte_name.a) - /// - /// Recursive with query generation will raise `RuntimeError` if you specify more than one CTE. - mutable [subclass] PyWithClause(WithClauseState) as "WithClause" { - pub recursive: bool, - pub cte_expressions: Vec, - pub cycle: Option, - pub search: Option, - } -} -crate::implement_pyclass! { - /// A WITH query. A simple SQL query that has a WITH clause (`WithClause`). - /// - /// For full description, see `WithClause`'s documentation. - mutable [subclass, extends=PyQueryStatement] PyWithQuery(WithQueryState) as "WithQuery" { - /// Always is `PyWithClause` - pub with_clause: PyObject, - pub query: CommonTableExpressionQuery, - } -} - -impl TryFrom> for CommonTableExpressionQuery { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject) -> Result { - unsafe { - let value_ptr = value.as_ptr(); - - // SelectStatement - if pyo3::ffi::PyObject_TypeCheck(value_ptr, crate::typeref::SELECT_STATEMENT_TYPE) == 1 - { - Ok(Self::Select(value.clone().unbind())) - } - // DeleteStatement - else if pyo3::ffi::PyObject_TypeCheck( - value_ptr, - crate::typeref::DELETE_STATEMENT_TYPE, - ) == 1 - { - Ok(Self::Delete(value.clone().unbind())) - } - // UpdateStatement - else if pyo3::ffi::PyObject_TypeCheck( - value_ptr, - crate::typeref::UPDATE_STATEMENT_TYPE, - ) == 1 - { - Ok(Self::Update(value.clone().unbind())) - } - // InsertStatement - else if pyo3::ffi::PyObject_TypeCheck( - value_ptr, - crate::typeref::INSERT_STATEMENT_TYPE, - ) == 1 - { - Ok(Self::Insert(value.clone().unbind())) - } - // Other types - else { - crate::new_error!( - PyTypeError, - "expected SelectStatement, DeleteStatement, UpdateStatement, or \ - InsertStatement, got {}", - crate::internal::get_type_name(value.py(), value_ptr) - ) - } - } - } -} - -#[inline(always)] -fn cast_into_with_clause<'a, 'b>(py: pyo3::Python<'a>, value: &'b PyObject) -> &'b PyWithClause -where - 'a: 'b, -{ - let bound = value.bind(py); - - unsafe { - let casted = bound.cast_unchecked::(); - casted.get() - } -} - -impl std::fmt::Display for CommonTableExpressionQuery { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Select(x) => write!(f, "{x}"), - Self::Update(x) => write!(f, "{x}"), - Self::Delete(x) => write!(f, "{x}"), - Self::Insert(x) => write!(f, "{x}"), - } - } -} - -impl std::fmt::Display for CommonTableExpression { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "('{}', {}", self.name, self.query)?; - - if !self.columns.is_empty() { - write!(f, ", {:?}", self.columns)?; - } - match self.materialized { - Some(true) => write!(f, ", materialized")?, - Some(false) => write!(f, ", not materialized")?, - None => {} - } - - write!(f, ")") - } -} - -impl ToSeaQuery for CommonTableExpression { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::CommonTableExpression { - let (mut stmt, select_used) = { - match &self.query { - CommonTableExpressionQuery::Select(x) if self.columns.is_empty() => { - let select = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = select.get().0.lock().to_sea_query(py); - - (sea_query::CommonTableExpression::from_select(result), true) - } - _ => (sea_query::CommonTableExpression::new(), false), - } - }; - stmt.table_name(sea_query::Alias::new(&self.name).into_iden()); - - match &self.query { - CommonTableExpressionQuery::Select(x) => { - if !select_used { - let delete = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = delete.get().0.lock().to_sea_query(py); - stmt.query(result); - } - } - CommonTableExpressionQuery::Delete(x) => { - let delete = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = delete.get().0.lock().to_sea_query(py); - stmt.query(result); - } - CommonTableExpressionQuery::Update(x) => { - let update = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = update.get().0.lock().to_sea_query(py); - stmt.query(result); - } - CommonTableExpressionQuery::Insert(x) => { - let insert = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = insert.get().0.lock().to_sea_query(py); - stmt.query(result); - } - } - - if !self.columns.is_empty() { - stmt.columns(self.columns.iter().map(sea_query::Alias::new)); - } - if let Some(x) = self.materialized { - stmt.materialized(x); - } - - stmt - } -} - -impl ToSeaQuery for WithClauseState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::WithClause { - let mut stmt = sea_query::WithClause::new(); - - if self.recursive { - stmt.recursive(true); - } - for cte in self.cte_expressions.iter() { - stmt.cte(cte.to_sea_query(py)); - } - if let Some(x) = &self.cycle { - stmt.cycle(x.clone()); - } - if let Some(x) = &self.search { - stmt.search(x.clone()); - } - - stmt - } -} - -impl ToSeaQuery for WithQueryState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::WithQuery { - let clause = unsafe { - let x = cast_into_with_clause(py, &self.with_clause); - x.0.lock().to_sea_query(py) - }; - - match &self.query { - CommonTableExpressionQuery::Select(x) => { - let select = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = select.get().0.lock().to_sea_query(py); - clause.query(result) - } - CommonTableExpressionQuery::Delete(x) => { - let delete = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = delete.get().0.lock().to_sea_query(py); - clause.query(result) - } - CommonTableExpressionQuery::Update(x) => { - let update = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = update.get().0.lock().to_sea_query(py); - clause.query(result) - } - CommonTableExpressionQuery::Insert(x) => { - let insert = - unsafe { x.cast_bound_unchecked::(py) }; - - let result = insert.get().0.lock().to_sea_query(py); - clause.query(result) - } - } - } -} - -#[pyo3::pymethods] -impl PyWithClause { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self::uninit() - } - - fn __init__(&self) -> pyo3::PyResult<()> { - let state = WithClauseState { - recursive: false, - cte_expressions: vec![], - cycle: None, - search: None, - }; - self.0.set(state); - Ok(()) - } - - /// Sets whether this clause is a recursive with clause of not. - /// If set to true it will generate a 'WITH RECURSIVE' query. - /// - /// You can only specify a single CTE containing a union query - /// if this is set to true. - fn recursive<'a>(slf: pyo3::PyRef<'a, Self>) -> pyo3::PyRef<'a, Self> { - slf.0.lock().recursive = true; - slf - } - - /// Add a CTE to this with clause. - #[pyo3(signature=(name, query, columns=Vec::new(), materialized=None))] - fn cte<'a>( - slf: pyo3::PyRef<'a, Self>, - name: String, - query: RefBoundObject<'a>, - columns: Vec, - materialized: Option, - ) -> pyo3::PyResult> { - let query = CommonTableExpressionQuery::try_from(query)?; - - slf.0.lock().cte_expressions.push(CommonTableExpression { - name, - query, - columns, - materialized, - }); - Ok(slf) - } - - /// For recursive `WithQuery` `WithClause`s the CYCLE sql clause can be specified to avoid creating - /// an infinite traversals that loops on graph cycles indefinitely. You specify an expression that - /// identifies a node in the graph and that will be used to determine during the iteration of - /// the execution of the query when appending of new values whether the new values are distinct new - /// nodes or are already visited and therefore they should be added again into the result. - /// - /// A query can have both SEARCH and CYCLE clauses. - /// - /// This setting is not meaningful if the query is not recursive. - /// Some databases don’t support this clause. In that case this option will be silently ignored. - fn cycle<'a>( - slf: pyo3::PyRef<'a, Self>, - expr: Option>, - set_as: Option, - using: Option, - ) -> pyo3::PyResult> { - let mut cycle = sea_query::Cycle::new(); - - if let Some(x) = expr { - let val = PyExpr::try_from(x)?; - cycle.expr(val.0); - } - if let Some(x) = set_as { - cycle.set(sea_query::Alias::new(x)); - } - if let Some(x) = using { - cycle.using(sea_query::Alias::new(x)); - } - - slf.0.lock().cycle = Some(cycle); - Ok(slf) - } - - /// For recursive `WithQuery` `WithClause`s the traversing order can be specified in some databases - /// that support this functionality. - /// - /// A query can have both SEARCH and CYCLE clauses. - /// - /// This setting is not meaningful if the query is not recursive. - /// Some databases don’t support this clause. In that case this option will be silently ignored. - /// - /// The `expr` used must specify an alias which will be the name that you can use to order - /// the result of the CTE. - fn search<'a>( - slf: pyo3::PyRef<'a, Self>, - expr: Option>, - order: Option, - ) -> pyo3::PyResult> { - let mut search = sea_query::Search::new(); - - if let Some(x) = expr { - let val = x.cast_into::()?; - - search.expr(val.get().as_ref().to_sea_query(slf.py())); - } - - if let Some(x) = order { - match x.to_ascii_lowercase().as_str() { - "breadth" => { - search.order(sea_query::SearchOrder::BREADTH); - } - "depth" => { - search.order(sea_query::SearchOrder::DEPTH); - } - _ => return new_error!(PyValueError, "unknown order: {}", x), - } - } - slf.0.lock().search = Some(search); - Ok(slf) - } - - /// You can turn this into a `WithQuery` using this function. - /// The resulting WITH query will execute the argument query with this WITH clause. - fn query<'a>( - slf: pyo3::PyRef<'a, Self>, - val: RefBoundObject<'a>, - ) -> pyo3::PyResult> { - let query = CommonTableExpressionQuery::try_from(val)?; - - let state = WithQueryState { - with_clause: unsafe { pyo3::Bound::from_borrowed_ptr(slf.py(), slf.as_ptr()).unbind() }, - query, - }; - let result: PyWithQuery = state.into(); - - pyo3::Bound::new(slf.py(), (result, PyQueryStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .optional_boolean("recursive", lock.recursive) - .take(); - - fmt.vec("cte", false) - .display_iter(lock.cte_expressions.iter()) - .finish(&mut fmt); - - fmt.finish() - } -} - -#[pyo3::pymethods] -impl PyWithQuery { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PyQueryStatement) { - (Self::uninit(), PyQueryStatement) - } - - fn __init__(&self, clause: BoundObject, query: RefBoundObject) -> pyo3::PyResult<()> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(clause.as_ptr(), crate::typeref::WITH_CLAUSE_TYPE) == 0 - { - return crate::new_error!( - PyTypeError, - "expected WithClause, got {}", - crate::internal::get_type_name(clause.py(), clause.as_ptr()) - ); - } - } - let query = CommonTableExpressionQuery::try_from(query)?; - - let state = WithQueryState { - with_clause: clause.unbind(), - query, - }; - self.0.set(state); - Ok(()) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - use sea_query::QueryStatementBuilder; - - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_statement!(backend, stmt) - } - - #[pyo3(signature = (backend, /))] - fn build<'a>( - &self, - py: pyo3::Python<'a>, - backend: String, - ) -> pyo3::PyResult<(String, BoundObject<'a>)> { - use sea_query::QueryStatementBuilder; - - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_query_parts!(py, backend, stmt) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .display("clause", &lock.with_clause) - .display("query", &lock.query) - .finish() - } -} diff --git a/src/schema/alter_table.rs b/src/schema/alter_table.rs deleted file mode 100644 index 20121c1..0000000 --- a/src/schema/alter_table.rs +++ /dev/null @@ -1,620 +0,0 @@ -use super::base::PySchemaStatement; -use crate::common::column::PyColumn; -use crate::common::column_ref::PyColumnRef; -use crate::common::foreign_key::PyForeignKey; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; - -use pyo3::PyTypeInfo; -use sea_query::IntoIden; - -crate::implement_pyclass! { - // Base class for all ALTER TABLE operation types. - /// - /// This abstract base class represents the different types of modifications - /// that can be made to an existing table structure, such as adding/dropping - /// columns, modifying column definitions, or managing foreign keys. - #[derive(Debug, Clone, Copy)] - [subclass] PyAlterTableBaseOption as "AlterTableBaseOption"; -} -crate::implement_pyclass! { - /// ALTER TABLE operation to add a new column. - /// - /// Adds a column to an existing table with optional IF NOT EXISTS clause - /// to prevent errors if the column already exists. - #[derive(Debug)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableAddColumnOption(AlterTableAddColumnOptionState) - as "AlterTableAddColumnOption" { - /// Always is `PyColumn` - column: PyObject, - if_not_exists: bool, - } -} -crate::implement_pyclass! { - /// ALTER TABLE operation to add a foreign key constraint. - /// - /// Adds referential integrity between tables by creating a foreign key - /// relationship on an existing table. - #[derive(Debug)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableAddForeignKeyOption(AlterTableAddForeignKeyOptionState) - as "AlterTableAddForeignKeyOption" { - /// Always is `PyForeignKey` - foreign_key: PyObject, - } -} -crate::implement_pyclass! { - /// ALTER TABLE operation to drop an existing column. - /// - /// Removes a column from the table. This operation may fail if the column - /// is referenced by other database objects. - #[derive(Debug, Clone)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableDropColumnOption(AlterTableDropColumnOptionState) - as "AlterTableDropColumnOption" { name: sea_query::DynIden } -} -crate::implement_pyclass! { - /// ALTER TABLE operation to drop a foreign key constraint. - /// - /// Removes a foreign key relationship by its constraint name. - #[derive(Debug, Clone)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableDropForeignKeyOption(AlterTableDropForeignKeyOptionState) - as "AlterTableDropForeignKeyOption" { name: sea_query::DynIden } -} -crate::implement_pyclass! { - /// ALTER TABLE operation to modify a column definition. - /// - /// Changes properties of an existing column such as type, nullability, - /// default value, or other constraints. - #[derive(Debug)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableModifyColumnOption(AlterTableModifyColumnOptionState) - as "AlterTableModifyColumnOption" { - /// Always is `PyColumn` - column: PyObject, - } -} -crate::implement_pyclass! { - /// ALTER TABLE operation to rename a column. - /// - /// Changes the name of an existing column without modifying its type - /// or constraints. - #[derive(Debug, Clone)] - immutable [subclass, extends=PyAlterTableBaseOption] PyAlterTableRenameColumnOption(AlterTableRenameColumnOptionState) - as "AlterTableRenameColumnOption" { - from_name: sea_query::DynIden, - to_name: sea_query::DynIden, - } -} -crate::implement_pyclass! { - /// Represents an ALTER TABLE SQL statement. - /// - /// Provides a flexible way to modify existing table structures by applying - /// one or more alteration operations such as adding/dropping columns, - /// modifying column definitions, or managing constraints. - /// - /// Multiple operations can be batched together in a single ALTER TABLE - /// statement for efficiency. - #[derive(Debug)] - mutable [subclass, extends=PySchemaStatement] PyAlterTable(AlterTableState) as "AlterTable" { - name: PyTableName, - options: Vec, - } -} - -#[pyo3::pymethods] -impl PyAlterTableAddColumnOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature = (column, if_not_exists=false))] - fn __init__(&self, column: RefBoundObject<'_>, if_not_exists: bool) -> pyo3::PyResult<()> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(column.as_ptr(), crate::typeref::COLUMN_TYPE) == 0 { - return crate::new_error!( - PyTypeError, - "expected Column, got {}", - crate::internal::get_type_name(column.py(), column.as_ptr()) - ); - } - } - - let result = AlterTableAddColumnOptionState { - column: column.clone().unbind(), - if_not_exists, - }; - unsafe { - self.0.set(result); - } - Ok(()) - } - - #[getter] - fn column(&self, py: pyo3::Python) -> PyObject { - self.0.as_ref().column.clone_ref(py) - } - - #[getter] - fn if_not_exists(&self) -> bool { - self.0.as_ref().if_not_exists - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .display("column", &inner.column) - .optional_boolean("if_not_exists", inner.if_not_exists) - .finish() - } -} - -#[pyo3::pymethods] -impl PyAlterTableAddForeignKeyOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature = (foreign_key))] - fn __init__(&self, foreign_key: RefBoundObject<'_>) -> pyo3::PyResult<()> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(foreign_key.as_ptr(), crate::typeref::FOREIGN_KEY_TYPE) - == 0 - { - return crate::new_error!( - PyTypeError, - "expected ForeignKey, got {}", - crate::internal::get_type_name(foreign_key.py(), foreign_key.as_ptr()) - ); - } - } - - let result = AlterTableAddForeignKeyOptionState { - foreign_key: foreign_key.clone().unbind(), - }; - unsafe { - self.0.set(result); - } - Ok(()) - } - - #[getter] - fn foreign_key(&self, py: pyo3::Python) -> PyObject { - self.0.as_ref().foreign_key.clone_ref(py) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .display("foreign_key", &inner.foreign_key) - .finish() - } -} - -#[pyo3::pymethods] -impl PyAlterTableDropColumnOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature=(name))] - fn __init__(&self, name: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let column_ref = PyColumnRef::try_from(name)?; - - match column_ref.name { - Some(x) => { - let result = AlterTableDropColumnOptionState { name: x }; - unsafe { - self.0.set(result); - } - Ok(()) - } - None => crate::new_error!( - PyValueError, - "AlterTableDropColumnOption cannot accept asterisk '*' as name" - ), - } - } - - #[getter] - fn name(&self) -> String { - self.0.as_ref().name.to_string() - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .iden("name", &inner.name) - .finish() - } -} - -#[pyo3::pymethods] -impl PyAlterTableDropForeignKeyOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature=(name))] - fn __init__(&self, name: String) -> pyo3::PyResult<()> { - let result = AlterTableDropForeignKeyOptionState { - name: sea_query::Alias::new(name).into_iden(), - }; - unsafe { - self.0.set(result); - } - Ok(()) - } - - #[getter] - fn name(&self) -> String { - self.0.as_ref().name.to_string() - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .iden("name", &inner.name) - .finish() - } -} - -#[pyo3::pymethods] -impl PyAlterTableModifyColumnOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature = (column))] - fn __init__(&self, column: RefBoundObject<'_>) -> pyo3::PyResult<()> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(column.as_ptr(), crate::typeref::COLUMN_TYPE) == 0 { - return crate::new_error!( - PyTypeError, - "expected Column, got {}", - crate::internal::get_type_name(column.py(), column.as_ptr()) - ); - } - } - - let result = AlterTableModifyColumnOptionState { - column: column.clone().unbind(), - }; - unsafe { - self.0.set(result); - } - Ok(()) - } - - #[getter] - fn column(&self, py: pyo3::Python) -> PyObject { - self.0.as_ref().column.clone_ref(py) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .display("column", &inner.column) - .finish() - } -} - -#[pyo3::pymethods] -impl PyAlterTableRenameColumnOption { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__( - args: BoundArgs<'_>, - kwds: Option>, - ) -> (Self, PyAlterTableBaseOption) { - (Self::uninit(), PyAlterTableBaseOption) - } - - #[pyo3(signature = (from_name, to_name))] - fn __init__( - &self, - from_name: RefBoundObject<'_>, - to_name: RefBoundObject<'_>, - ) -> pyo3::PyResult<()> { - let from_column_ref = PyColumnRef::try_from(from_name)?; - let to_column_ref = PyColumnRef::try_from(to_name)?; - - if from_column_ref.name.is_none() { - return Err(pyo3::exceptions::PyValueError::new_err( - "AlterTableRenameColumnOption cannot accept asterisk '*' as from_name", - )); - } - if to_column_ref.name.is_none() { - return Err(pyo3::exceptions::PyValueError::new_err( - "AlterTableRenameColumnOption cannot accept asterisk '*' as to_name", - )); - } - - unsafe { - let result = AlterTableRenameColumnOptionState { - from_name: from_column_ref.name.unwrap_unchecked(), - to_name: to_column_ref.name.unwrap_unchecked(), - }; - self.0.set(result); - Ok(()) - } - } - - #[getter] - #[allow(clippy::wrong_self_convention)] - fn from_name(&self) -> String { - self.0.as_ref().from_name.clone().to_string() - } - - #[getter] - fn to_name(&self) -> String { - self.0.as_ref().to_name.clone().to_string() - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.as_ref(); - - ReprFormatter::new_with_pyref(&slf) - .iden("from_name", &inner.from_name) - .iden("to_name", &inner.to_name) - .finish() - } -} - -impl ToSeaQuery for AlterTableState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::TableAlterStatement { - let mut stmt = sea_query::TableAlterStatement::new(); - stmt.table(self.name.clone()); - - for op in self.options.iter() { - unsafe { - let op_type = pyo3::ffi::Py_TYPE(op.as_ptr()); - - if op_type == PyAlterTableAddColumnOption::type_object_raw(py) { - let add_column_opt = op.cast_bound_unchecked::(py); - let get_add_column_opt = add_column_opt.get().as_ref(); - let py_column = get_add_column_opt - .column - .cast_bound_unchecked::(py) - .get() - .clone(); - - let column_def: sea_query::ColumnDef = py_column.0.lock().to_sea_query(py); - - if get_add_column_opt.if_not_exists { - stmt.add_column_if_not_exists(column_def); - } else { - stmt.add_column(column_def); - } - } else if op_type == PyAlterTableAddForeignKeyOption::type_object_raw(py) { - let add_fk = op.cast_bound_unchecked::(py); - let get_add_fk = add_fk.get().as_ref(); - - let py_fk = get_add_fk - .foreign_key - .cast_bound_unchecked::(py) - .get() - .clone(); - - let table_fk: sea_query::TableForeignKey = py_fk.0.lock().to_sea_query(py); - stmt.add_foreign_key(&table_fk); - } else if op_type == PyAlterTableDropColumnOption::type_object_raw(py) { - let bound = op.cast_bound_unchecked::(py); - let x = bound.get(); - - stmt.drop_column(x.as_ref().name.clone()); - } else if op_type == PyAlterTableDropForeignKeyOption::type_object_raw(py) { - let bound = op.cast_bound_unchecked::(py); - let x = bound.get(); - - stmt.drop_foreign_key(x.0.as_ref().name.clone()); - } else if op_type == PyAlterTableModifyColumnOption::type_object_raw(py) { - let bound = op.cast_bound_unchecked::(py); - let x = bound.get().as_ref(); - - let py_column = x.column.cast_bound_unchecked::(py).get().clone(); - - let column_def: sea_query::ColumnDef = py_column.0.lock().to_sea_query(py); - stmt.modify_column(column_def); - } else if op_type == PyAlterTableRenameColumnOption::type_object_raw(py) { - let bound = op.cast_bound_unchecked::(py); - let x = bound.get().as_ref(); - - stmt.rename_column(x.from_name.clone(), x.to_name.clone()); - } - } - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyAlterTable { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[pyo3(signature = (name, options=Vec::new()))] - fn __init__( - &self, - name: RefBoundObject<'_>, - options: Vec>, - ) -> pyo3::PyResult<()> { - let name = PyTableName::try_from(name)?; - - unsafe { - for opt in options.iter() { - if pyo3::ffi::PyObject_TypeCheck( - opt.as_ptr(), - crate::typeref::ALTER_TABLE_BASE_OPTION_TYPE, - ) == 0 - { - return crate::new_error!( - PyTypeError, - "expected AlterTableBaseOption, got {}", - crate::internal::get_type_name(opt.py(), opt.as_ptr()) - ); - } - } - } - - let state = AlterTableState { - name, - options: options.into_iter().map(|x| x.unbind()).collect(), - }; - self.0.set(state); - Ok(()) - } - - /// The name of the table to alter. - #[getter] - fn name(&self) -> PyTableName { - self.0.lock().name.clone() - } - - #[setter] - fn set_name(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.name = val; - Ok(()) - } - - /// The list of alteration operations to apply. - #[getter] - fn options(&self, py: pyo3::Python) -> Vec { - self.0 - .lock() - .options - .iter() - .map(|x| x.clone_ref(py)) - .collect() - } - - #[setter] - fn set_options(&self, val: Vec>) -> pyo3::PyResult<()> { - unsafe { - for opt in val.iter() { - if pyo3::ffi::PyObject_TypeCheck( - opt.as_ptr(), - crate::typeref::ALTER_TABLE_BASE_OPTION_TYPE, - ) == 0 - { - return crate::new_error!( - PyTypeError, - "expected AlterTableBaseOption, got {}", - crate::internal::get_type_name(opt.py(), opt.as_ptr()) - ); - } - } - } - - let mut lock = self.0.lock(); - lock.options = val.into_iter().map(|x| x.unbind()).collect(); - Ok(()) - } - - /// Add an alteration operation to this ALTER TABLE statement. - fn add_option<'a>( - slf: pyo3::PyRef<'a, Self>, - opt: BoundObject<'a>, - ) -> pyo3::PyResult> { - unsafe { - if pyo3::ffi::PyObject_TypeCheck( - opt.as_ptr(), - crate::typeref::ALTER_TABLE_BASE_OPTION_TYPE, - ) == 0 - { - return crate::new_error!( - PyTypeError, - "expected AlterTableBaseOption, got {}", - crate::internal::get_type_name(opt.py(), opt.as_ptr()) - ); - } - } - - { - let mut lock = slf.0.lock(); - lock.options.push(opt.unbind()); - } - Ok(slf) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - - // Prevent "No alter option found" panic - if lock.options.is_empty() { - return crate::new_error!( - PyRuntimeError, - "No alter option found. You have to specify at least one alter option." - ); - } - - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - - let new_state = AlterTableState { - name: lock.name.clone(), - options: lock.options.iter().map(|x| x.clone_ref(py)).collect(), - }; - - pyo3::Py::new(py, (new_state.into(), PySchemaStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let inner = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .map("name", &inner.name, |x| x.__repr__()) - .finish() - } -} diff --git a/src/schema/base.rs b/src/schema/base.rs deleted file mode 100644 index a50af53..0000000 --- a/src/schema/base.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::internal::{BoundArgs, BoundKwargs}; - -crate::implement_pyclass! { - /// Subclass of schema statements. - #[derive(Debug, Clone, Copy)] - [subclass] PySchemaStatement as "SchemaStatement"; -} - -#[pyo3::pymethods] -impl PySchemaStatement { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> Self { - Self - } - - fn __init__(&self) {} - - /// Build a SQL string representation. - #[pyo3(signature = (backend, /))] - #[allow(unused_variables)] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, backend: String) -> pyo3::PyResult { - Err(pyo3::exceptions::PyNotImplementedError::new_err(())) - } -} diff --git a/src/schema/index.rs b/src/schema/index.rs deleted file mode 100644 index 2ff6169..0000000 --- a/src/schema/index.rs +++ /dev/null @@ -1,684 +0,0 @@ -use sea_query::IntoIden; - -use super::base::PySchemaStatement; -use crate::common::expression::PyExpr; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, RefBoundObject, ToSeaQuery}; - -#[inline] -fn map_str_to_index_order(value: String) -> pyo3::PyResult { - match value.to_ascii_uppercase().as_str() { - "ASC" => Ok(sea_query::IndexOrder::Asc), - "DESC" => Ok(sea_query::IndexOrder::Desc), - _ => Err(pyo3::exceptions::PyValueError::new_err(format!( - "unknown order: {}", - value - ))), - } -} - -#[inline] -fn map_index_order_to_str(value: &sea_query::IndexOrder) -> String { - match value { - sea_query::IndexOrder::Asc => String::from("ASC"), - sea_query::IndexOrder::Desc => String::from("DESC"), - } -} - -pub const OPT_PRIMARY: u8 = 1 << 0; -pub const OPT_UNIQUE: u8 = 1 << 1; -pub const OPT_IF_NOT_EXISTS: u8 = 1 << 2; -pub const OPT_NULLS_NOT_DISTINCT: u8 = 1 << 3; - -crate::implement_pyclass! { - /// Defines a column within an index specification. - /// - /// Represents a single column's participation in an index, including: - /// - The column name - /// - Optional prefix length (for partial indexing) - /// - Sort order (ascending or descending) - /// - /// Used within `Index` to specify which columns are indexed - /// and how they should be ordered. - /// - /// NOTE: this class is immutable and frozen. - #[derive(Debug, Clone)] - [] PyIndexColumn as "IndexColumn" { - pub name: sea_query::DynIden, - pub order: Option, - pub prefix: Option, - } -} -crate::implement_pyclass! { - /// Represents a database index specification. - /// - /// This class defines the structure and properties of a database index, - /// including column definitions, uniqueness constraints, index type, - /// and partial indexing conditions. - /// - /// You can use it to generate `CREATE INDEX` SQL expressions. - #[derive(Debug)] - mutable [subclass, extends=PySchemaStatement] PyIndex(IndexState) as "Index" { - pub name: Option, - pub columns: Vec, - pub table: Option, - pub options: u8, - pub index_type: Option, - pub r#where: Option, - pub include: Vec, - } -} -crate::implement_pyclass! { - /// Represents a DROP INDEX SQL statement. - /// - /// Builds index deletion statements with support for: - /// - Conditional deletion (IF EXISTS) - /// - Table-specific index dropping - #[derive(Debug)] - mutable [subclass, extends=PySchemaStatement] PyDropIndex(DropIndexState) as "DropIndex" { - pub name: String, - pub table: PyTableName, - pub if_exists: bool, - } -} - -impl sea_query::IntoIndexColumn for PyIndexColumn { - fn into_index_column(self) -> sea_query::IndexColumn { - match (self.prefix, self.order) { - (Some(p), Some(o)) => (self.name, p, o).into_index_column(), - (Some(p), None) => (self.name, p).into_index_column(), - (None, Some(o)) => (self.name, o).into_index_column(), - (None, None) => self.name.into_index_column(), - } - } -} - -impl TryFrom> for PyIndexColumn { - type Error = pyo3::PyErr; - - fn try_from(value: RefBoundObject<'_>) -> Result { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(value.as_ptr(), crate::typeref::INDEX_COLUMN_TYPE) == 1 - { - let casted_value = value.cast_unchecked::(); - return Ok(casted_value.get().clone()); - } - - let column_ref = - crate::common::column_ref::PyColumnRef::try_from(value).map_err(|_| { - crate::new_py_error!( - PyTypeError, - "expected IndexColumn, ColumnRef, Column, str, or object.to_column_ref \ - property, got {}", - crate::internal::get_type_name(value.py(), value.as_ptr()) - ) - })?; - - match column_ref.name { - Some(x) => { - let inner = Self { - name: x, - order: None, - prefix: None, - }; - Ok(inner) - } - None => Err(pyo3::exceptions::PyValueError::new_err( - "IndexColumn cannot accept asterisk '*' as column", - )), - } - } - } -} - -#[pyo3::pymethods] -impl PyIndexColumn { - #[new] - #[pyo3(signature=(name, order=None, prefix=None))] - fn __new__(name: String, order: Option, prefix: Option) -> pyo3::PyResult { - let state = Self { - name: sea_query::Alias::new(name).into_iden(), - order: match order { - None => None, - Some(x) => Some(map_str_to_index_order(x)?), - }, - prefix, - }; - Ok(state) - } - - /// The name of the column to include in the index. - #[getter] - fn name(&self) -> String { - self.name.to_string() - } - - /// Number of characters to index for string columns (prefix indexing). - #[getter] - fn prefix(&self) -> Option { - self.prefix - } - - /// Sort order for this column. - #[getter] - fn order(&self) -> Option { - self.order.as_ref().map(map_index_order_to_str) - } - - fn __copy__(&self) -> Self { - self.clone() - } - - fn __repr__(&self) -> String { - ReprFormatter::new("IndexColumn") - .iden("name", &self.name) - .optional_display("prefix", self.prefix) - .optional_map("order", self.order.as_ref(), map_index_order_to_str) - .finish() - } -} - -impl Clone for IndexState { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - columns: self.columns.clone(), - table: self.table.clone(), - options: self.options, - index_type: self.index_type.clone(), - r#where: self.r#where.clone(), - include: self.include.clone(), - } - } -} - -impl ToSeaQuery for IndexState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::IndexCreateStatement { - let mut stmt = sea_query::IndexCreateStatement::new(); - - if let Some(x) = &self.name { - stmt.name(x); - } - if let Some(x) = &self.table { - stmt.table(x.clone()); - } - - for col in self.columns.iter() { - stmt.col(col.clone()); - } - - if let Some(x) = &self.index_type { - stmt.index_type(sea_query::IndexType::Custom( - sea_query::Alias::new(x.clone()).into_iden(), - )); - } - - for include in self.include.iter() { - stmt.include(sea_query::Alias::new(include)); - } - - if self.options & OPT_PRIMARY > 0 { - stmt.primary(); - } - if self.options & OPT_IF_NOT_EXISTS > 0 { - stmt.if_not_exists(); - } - if self.options & OPT_NULLS_NOT_DISTINCT > 0 { - stmt.nulls_not_distinct(); - } - if self.options & OPT_UNIQUE > 0 { - stmt.unique(); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyIndex { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[ - pyo3( - signature = ( - name, - columns, - table=None, - *, - primary=false, - if_not_exists=false, - nulls_not_distinct=false, - unique=false, - index_type=None, - r#where=None, - include=Vec::new() - ) - ) - ] - fn __init__( - &self, - name: Option, - columns: Vec>, - table: Option>, - primary: bool, - if_not_exists: bool, - nulls_not_distinct: bool, - unique: bool, - index_type: Option, - r#where: Option>, - include: Vec, - ) -> pyo3::PyResult<()> { - let table = match table { - Some(x) => Some(PyTableName::try_from(x)?), - None => None, - }; - - let r#where = match r#where { - None => None, - Some(x) => unsafe { - if pyo3::ffi::Py_TYPE(x.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected Expr or None, got {}", - crate::internal::get_type_name(x.py(), x.as_ptr()) - ); - } - - Some(x.cast_unchecked::().get().clone()) - }, - }; - - if columns.is_empty() { - return Err(pyo3::exceptions::PyValueError::new_err( - "columns cannot be empty", - )); - } - - let mut cols = Vec::with_capacity(columns.len()); - for c in columns.into_iter() { - cols.push(PyIndexColumn::try_from(&c)?); - } - - let mut options = 0u8; - if primary { - options |= OPT_PRIMARY; - } - if unique { - options |= OPT_UNIQUE; - } - if nulls_not_distinct { - options |= OPT_NULLS_NOT_DISTINCT; - } - if if_not_exists { - options |= OPT_IF_NOT_EXISTS; - } - - let state = IndexState { - name, - columns: cols, - table, - options, - index_type, - r#where, - include, - }; - self.0.set(state); - - Ok(()) - } - - /// Index name. - #[getter] - fn name(&self) -> Option { - let lock = self.0.lock(); - lock.name.clone() - } - - #[setter] - fn set_name(&self, val: Option) { - let mut lock = self.0.lock(); - lock.name = val; - } - - /// The table on which to create the index. - #[getter] - fn table(&self) -> Option { - let lock = self.0.lock(); - lock.table.clone() - } - - #[setter] - fn set_table(&self, val: Option>) -> pyo3::PyResult<()> { - let val = match val { - Some(x) => Some(PyTableName::try_from(x)?), - None => None, - }; - - let mut lock = self.0.lock(); - lock.table = val; - Ok(()) - } - - /// Whether this is a primary key constraint. - #[getter] - fn primary(&self) -> bool { - self.0.lock().options & OPT_PRIMARY > 0 - } - - #[setter] - fn set_primary(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_PRIMARY; - } else { - lock.options &= !OPT_PRIMARY; - } - } - - /// Whether this is a unique constraint. - #[getter] - fn unique(&self) -> bool { - self.0.lock().options & OPT_UNIQUE > 0 - } - - #[setter] - fn set_unique(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_UNIQUE; - } else { - lock.options &= !OPT_UNIQUE; - } - } - - /// Whether NULL values should be considered equal for uniqueness. - #[getter] - fn nulls_not_distinct(&self) -> bool { - self.0.lock().options & OPT_NULLS_NOT_DISTINCT > 0 - } - - #[setter] - fn set_nulls_not_distinct(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_NULLS_NOT_DISTINCT; - } else { - lock.options &= !OPT_NULLS_NOT_DISTINCT; - } - } - - /// Whether to use IF NOT EXISTS clause. - #[getter] - fn if_not_exists(&self) -> bool { - self.0.lock().options & OPT_IF_NOT_EXISTS > 0 - } - - #[setter] - fn set_if_not_exists(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= OPT_IF_NOT_EXISTS; - } else { - lock.options &= !OPT_IF_NOT_EXISTS; - } - } - - /// The columns that make up this index. - #[getter] - fn columns(&self) -> Vec { - let lock = self.0.lock(); - lock.columns.clone() - } - - #[setter] - fn set_columns(&self, val: Vec>) -> pyo3::PyResult<()> { - if val.is_empty() { - return Err(pyo3::exceptions::PyValueError::new_err( - "columns cannot be empty", - )); - } - - let mut cols = Vec::with_capacity(val.len()); - for c in val.into_iter() { - cols.push(PyIndexColumn::try_from(&c)?); - } - - let mut lock = self.0.lock(); - lock.columns = cols; - Ok(()) - } - - /// The type/algorithm for this index. - #[getter] - fn index_type(&self) -> Option { - let lock = self.0.lock(); - lock.index_type.clone() - } - - #[setter] - fn set_index_type(&self, val: Option) -> pyo3::PyResult<()> { - let mut lock = self.0.lock(); - lock.index_type = val; - Ok(()) - } - - /// Condition for partial indexing. - #[getter] - fn r#where(&self) -> Option { - let lock = self.0.lock(); - lock.r#where.clone() - } - - #[setter] - fn set_where(&self, val: Option>) -> pyo3::PyResult<()> { - let val = match val { - None => None, - Some(x) => Some(PyExpr::try_from(x)?), - }; - - let mut lock = self.0.lock(); - lock.r#where = val; - Ok(()) - } - - /// Additional columns to include in the index for covering queries - #[getter] - fn include(&self) -> Vec { - let lock = self.0.lock(); - lock.include.clone() - } - - #[setter] - fn set_include(&self, val: Vec) { - let mut lock = self.0.lock(); - lock.include = val; - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - - pyo3::Py::new(py, (lock.clone().into(), PySchemaStatement)) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - - if lock.table.is_none() { - return crate::new_error!( - PyRuntimeError, - "To generate a CREATE INDEX statement, both `table` and `name` parameters are \ - necessary." - ); - } - - if lock.name.is_none() { - return crate::new_error!( - PyRuntimeError, - "To generate a CREATE INDEX statement, both `table` and `name` parameters are \ - necessary." - ); - } - - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - pub fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .optional_quote("name", lock.name.as_ref()) - .optional_map("table", lock.table.as_ref(), |x| x.__repr__()) - .take(); - - fmt.vec("columns", true) - .display_iter(lock.columns.iter().map(|x| x.__repr__())) - .finish(&mut fmt); - - fmt.optional_boolean("if_not_exists", lock.options & OPT_IF_NOT_EXISTS > 0) - .optional_boolean("primary", lock.options & OPT_PRIMARY > 0) - .optional_boolean("unique", lock.options & OPT_UNIQUE > 0) - .optional_boolean( - "nulls_not_distinct", - lock.options & OPT_NULLS_NOT_DISTINCT > 0, - ) - .optional_quote("index_type", lock.index_type.as_ref()) - .optional_map("where", lock.r#where.as_ref(), |x| x.__repr__()); - - fmt.vec("include", true) - .display_iter(lock.include.iter()) - .finish(&mut fmt); - - fmt.finish() - } -} - -impl Clone for DropIndexState { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - table: self.table.clone(), - if_exists: self.if_exists, - } - } -} - -impl ToSeaQuery for DropIndexState { - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::IndexDropStatement { - let mut stmt = sea_query::IndexDropStatement::new(); - - stmt.name(&self.name); - stmt.table(self.table.clone()); - - if self.if_exists { - stmt.if_exists(); - } - stmt - } -} - -#[pyo3::pymethods] -impl PyDropIndex { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[pyo3(signature = (name, table, if_exists=false))] - fn __init__( - &self, - name: String, - table: RefBoundObject<'_>, - if_exists: bool, - ) -> pyo3::PyResult<()> { - let table = PyTableName::try_from(table)?; - - let state = DropIndexState { - name, - table, - if_exists, - }; - self.0.set(state); - Ok(()) - } - - /// The name of the index to drop. - #[getter] - fn name(&self) -> String { - let lock = self.0.lock(); - lock.name.clone() - } - - #[setter] - fn set_name(&self, val: String) { - let mut lock = self.0.lock(); - lock.name = val; - } - - /// The table from which to drop the index. - #[getter] - fn table(&self) -> PyTableName { - let lock = self.0.lock(); - lock.table.clone() - } - - #[setter] - fn set_table(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.table = val; - Ok(()) - } - - /// Whether to use IF EXISTS clause to avoid errors. - #[getter] - fn if_exists(slf: pyo3::PyRef<'_, Self>) -> bool { - slf.0.lock().if_exists - } - - #[setter] - fn set_if_exists(slf: pyo3::PyRef<'_, Self>, val: bool) { - let mut lock = slf.0.lock(); - lock.if_exists = val; - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - - pyo3::Py::new(py, (lock.clone().into(), PySchemaStatement)) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .quote("name", &lock.name) - .map("table", &lock.table, |x| x.__repr__()) - .optional_boolean("if_exists", lock.if_exists) - .finish() - } -} diff --git a/src/schema/mod.rs b/src/schema/mod.rs deleted file mode 100644 index 08fae68..0000000 --- a/src/schema/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub mod alter_table; -pub mod base; -pub mod index; -pub mod table; -pub mod table_operations; - -#[pyo3::pymodule(name = "schema")] -pub mod schema_module { - - #[pymodule_export] - use super::base::PySchemaStatement; - - #[pymodule_export] - use super::alter_table::PyAlterTable; - #[pymodule_export] - use super::alter_table::PyAlterTableAddColumnOption; - #[pymodule_export] - use super::alter_table::PyAlterTableAddForeignKeyOption; - #[pymodule_export] - use super::alter_table::PyAlterTableBaseOption; - #[pymodule_export] - use super::alter_table::PyAlterTableDropColumnOption; - #[pymodule_export] - use super::alter_table::PyAlterTableDropForeignKeyOption; - #[pymodule_export] - use super::alter_table::PyAlterTableModifyColumnOption; - #[pymodule_export] - use super::alter_table::PyAlterTableRenameColumnOption; - - #[pymodule_export] - use super::index::PyDropIndex; - #[pymodule_export] - use super::index::PyIndex; - #[pymodule_export] - use super::index::PyIndexColumn; - - #[pymodule_export] - use super::table::PyTable; - - #[pymodule_export] - use super::table_operations::PyDropTable; - #[pymodule_export] - use super::table_operations::PyRenameTable; - #[pymodule_export] - use super::table_operations::PyTruncateTable; -} diff --git a/src/schema/table.rs b/src/schema/table.rs deleted file mode 100644 index 021b22b..0000000 --- a/src/schema/table.rs +++ /dev/null @@ -1,510 +0,0 @@ -use super::base::PySchemaStatement; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, BoundObject, PyObject, RefBoundObject, ToSeaQuery}; - -use pyo3::types::PyTupleMethods; - -pub const OPT_IF_NOT_EXISTS: u8 = 1 << 0; -pub const OPT_TEMPORARY: u8 = 1 << 1; - -crate::implement_pyclass! { - /// Represents a complete database table definition. - /// - /// This class encapsulates all aspects of a table structure including: - /// - Column definitions with their types and constraints - /// - Indexes for query optimization - /// - Foreign key relationships for referential integrity - /// - Check constraints for data validation - /// - Table-level options like engine, collation, and character set - /// - /// Used to generate CREATE TABLE SQL statements with full schema specifications. - mutable [subclass, extends=PySchemaStatement] PyTable(TableState) as "Table" { - pub name: PyTableName, - - /// Always is `Vec` - pub columns: Vec, - - /// Always is `Vec` - pub indexes: Vec, - - /// Always is `Vec` - pub foreign_keys: Vec, - - /// Always is `Vec` - pub checks: Vec, - - pub options: u8, - pub comment: Option, - pub engine: Option, - pub collate: Option, - pub character_set: Option, - pub extra: Option, - } -} - -impl TableState { - fn clone_ref(&self, py: pyo3::Python) -> Self { - Self { - name: self.name.clone(), - columns: self.columns.iter().map(|x| x.clone_ref(py)).collect(), - indexes: self.indexes.iter().map(|x| x.clone_ref(py)).collect(), - foreign_keys: self.foreign_keys.iter().map(|x| x.clone_ref(py)).collect(), - checks: self.checks.iter().map(|x| x.clone_ref(py)).collect(), - options: self.options, - comment: self.comment.clone(), - engine: self.engine.clone(), - collate: self.collate.clone(), - character_set: self.character_set.clone(), - extra: self.extra.clone(), - } - } -} - -impl ToSeaQuery for TableState { - #[cfg_attr(feature = "optimize", optimize(speed))] - fn to_sea_query<'a>(&self, py: pyo3::Python<'a>) -> sea_query::TableCreateStatement { - let mut stmt = sea_query::TableCreateStatement::new(); - stmt.table(self.name.clone()); - - // Add columns - for column in self.columns.iter() { - let col = unsafe { column.cast_bound_unchecked::(py) }; - - let column_def: sea_query::ColumnDef = col.get().0.lock().to_sea_query(py); - stmt.col(column_def); - } - - // Add primary and unique indexes - for index in self.indexes.iter() { - let ix = unsafe { index.cast_bound_unchecked::(py) }; - - let lock = ix.get().0.lock(); - if lock.options & super::index::OPT_PRIMARY > 0 - || lock.options & super::index::OPT_UNIQUE > 0 - { - let mut index_stmt = lock.to_sea_query(py); - stmt.index(&mut index_stmt); - } - } - - // Add foreign keys - for foreign_key in self.foreign_keys.iter() { - let fk = unsafe { - foreign_key.cast_bound_unchecked::(py) - }; - - let mut fk_stmt = fk.get().0.lock().to_sea_query(py); - stmt.foreign_key(&mut fk_stmt); - } - - // Add check constraints - for check in self.checks.iter() { - let simple_expr = - unsafe { check.cast_bound_unchecked::(py) }; - - stmt.check(simple_expr.get().0.clone()); - } - - if self.options & OPT_IF_NOT_EXISTS > 0 { - stmt.if_not_exists(); - } - if self.options & OPT_TEMPORARY > 0 { - stmt.temporary(); - } - - if let Some(x) = &self.comment { - stmt.comment(x); - } - if let Some(x) = &self.engine { - stmt.engine(x); - } - if let Some(x) = &self.collate { - stmt.collate(x); - } - if let Some(x) = &self.character_set { - stmt.character_set(x); - } - if let Some(x) = &self.extra { - stmt.extra(x); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyTable { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[ - pyo3( - signature=( - name, - *args, - if_not_exists=false, - temporary=false, - comment=None, - engine=None, - collate=None, - character_set=None, - extra=None, - ) - ) - ] - fn __init__( - &self, - name: RefBoundObject<'_>, - args: BoundArgs<'_>, - if_not_exists: bool, - temporary: bool, - comment: Option, - engine: Option, - collate: Option, - character_set: Option, - extra: Option, - ) -> pyo3::PyResult<()> { - let name = PyTableName::try_from(name)?; - - let mut columns = Vec::new(); - let mut indexes = Vec::new(); - let mut foreign_keys = Vec::new(); - let mut checks = Vec::new(); - - for object in args.iter() { - unsafe { - // columns - if pyo3::ffi::PyObject_TypeCheck(object.as_ptr(), crate::typeref::COLUMN_TYPE) == 1 - { - columns.push(object.unbind()); - } - // indexes - else if pyo3::ffi::PyObject_TypeCheck(object.as_ptr(), crate::typeref::INDEX_TYPE) - == 1 - { - indexes.push(object.unbind()); - } - // foreign keys - else if pyo3::ffi::PyObject_TypeCheck( - object.as_ptr(), - crate::typeref::FOREIGN_KEY_TYPE, - ) == 1 - { - foreign_keys.push(object.unbind()); - } - // expressions (check constraints) - else if pyo3::ffi::Py_TYPE(object.as_ptr()) == crate::typeref::EXPR_TYPE { - checks.push(object.unbind()); - } else { - return crate::new_error!( - PyTypeError, - "expected Column, Index, ForeignKey, or Expr, got {}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ); - } - } - } - - let mut options = 0u8; - if if_not_exists { - options |= OPT_IF_NOT_EXISTS; - } - if temporary { - options |= OPT_TEMPORARY; - } - - let state = TableState { - name, - columns, - indexes, - foreign_keys, - checks, - options, - comment, - engine, - collate, - character_set, - extra, - }; - self.0.set(state); - Ok(()) - } - - /// The name of this table. - #[getter] - fn name(&self) -> PyTableName { - let lock = self.0.lock(); - lock.name.clone() - } - - /// Table columns. - #[getter] - fn columns(&self, py: pyo3::Python) -> Vec { - let lock = self.0.lock(); - lock.columns.iter().map(|x| x.clone_ref(py)).collect() - } - - #[setter] - fn set_columns(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut columns = Vec::new(); - - for object in val.into_iter() { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(object.as_ptr(), crate::typeref::COLUMN_TYPE) == 0 - { - return crate::new_error!( - PyTypeError, - "expected iterable of Column, found {}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ); - } - } - - columns.push(object.unbind()); - } - - self.0.lock().columns = columns; - Ok(()) - } - - /// Table indexes. - #[getter] - fn indexes(&self, py: pyo3::Python) -> Vec { - let lock = self.0.lock(); - lock.indexes.iter().map(|x| x.clone_ref(py)).collect() - } - - #[setter] - fn set_indexes(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut indexes = Vec::new(); - - for object in val.into_iter() { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(object.as_ptr(), crate::typeref::INDEX_TYPE) == 0 { - return crate::new_error!( - PyTypeError, - "expected iterable of Index, found {}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ); - } - } - - indexes.push(object.unbind()); - } - - self.0.lock().indexes = indexes; - Ok(()) - } - - /// Table foreign keys. - #[getter] - fn foreign_keys(&self, py: pyo3::Python) -> Vec { - let lock = self.0.lock(); - lock.foreign_keys.iter().map(|x| x.clone_ref(py)).collect() - } - - #[setter] - fn set_foreign_keys(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut foreign_keys = Vec::new(); - - for object in val.into_iter() { - unsafe { - if pyo3::ffi::PyObject_TypeCheck(object.as_ptr(), crate::typeref::FOREIGN_KEY_TYPE) - == 0 - { - return crate::new_error!( - PyTypeError, - "expected iterable of ForeignKey, found {}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ); - } - } - - foreign_keys.push(object.unbind()); - } - - self.0.lock().foreign_keys = foreign_keys; - Ok(()) - } - - /// Table check constraints. - #[getter] - fn checks(&self, py: pyo3::Python) -> Vec { - let lock = self.0.lock(); - lock.checks.iter().map(|x| x.clone_ref(py)).collect() - } - - #[setter] - fn set_checks(&self, val: Vec>) -> pyo3::PyResult<()> { - let mut checks = Vec::new(); - - for object in val.into_iter() { - unsafe { - if pyo3::ffi::Py_TYPE(object.as_ptr()) != crate::typeref::EXPR_TYPE { - return crate::new_error!( - PyTypeError, - "expected iterable of Expr, found {}", - crate::internal::get_type_name(object.py(), object.as_ptr()) - ); - } - } - - checks.push(object.unbind()); - } - - self.0.lock().checks = checks; - Ok(()) - } - - /// Whether to use IF NOT EXISTS clause to avoid errors if table exists. - #[getter] - fn if_not_exists(&self) -> bool { - self.0.lock().options & OPT_IF_NOT_EXISTS > 0 - } - - /// Whether this is a temporary table that exists only for the session. - #[getter] - fn temporary(&self) -> bool { - self.0.lock().options & OPT_TEMPORARY > 0 - } - - /// Comment describing the purpose of this table. - #[getter] - fn comment(&self) -> Option { - self.0.lock().comment.clone() - } - - #[setter] - fn set_comment(&self, val: Option) { - self.0.lock().comment = val; - } - - /// Storage engine for the table (e.g., InnoDB, MyISAM for MySQL). - #[getter] - fn engine(&self) -> Option { - self.0.lock().engine.clone() - } - - #[setter] - fn set_engine(&self, val: Option) { - self.0.lock().engine = val; - } - - /// Collation for string comparisons and sorting in this table. - #[getter] - fn collate(&self) -> Option { - self.0.lock().collate.clone() - } - - #[setter] - fn set_collate(&self, val: Option) { - self.0.lock().collate = val; - } - - /// Character set encoding for text data in this table. - #[getter] - fn character_set(&self) -> Option { - self.0.lock().character_set.clone() - } - - #[setter] - fn set_character_set(&self, val: Option) { - self.0.lock().character_set = val; - } - - /// Additional table-specific options for the CREATE TABLE statement. - #[getter] - fn extra(&self) -> Option { - self.0.lock().extra.clone() - } - - #[setter] - fn set_extra(&self, val: Option) { - self.0.lock().extra = val; - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - - let mut sqls = vec![crate::build_schema_statement!(&backend, stmt)?]; - - for index in lock.indexes.iter() { - let ix = unsafe { index.cast_bound_unchecked::(py) }; - let ix_lock = ix.get().0.lock(); - - if ix_lock.options & super::index::OPT_PRIMARY > 0 - || ix_lock.options & super::index::OPT_UNIQUE > 0 - { - continue; - } - - // Index name and table is necessary here - if ix_lock.name.is_none() { - return Err(pyo3::exceptions::PyValueError::new_err( - "You should always set name for indexes that aren't primary or unique", - )); - } - - let mut index_stmt = ix_lock.to_sea_query(py); - index_stmt.table(lock.name.clone()); - - if lock.options & OPT_IF_NOT_EXISTS > 0 { - index_stmt.if_not_exists(); - } - - sqls.push(crate::build_schema_statement!(&backend, index_stmt)?); - } - - Ok(sqls.join(";\n")) - } - - #[getter] - fn __table_name__(&self) -> PyTableName { - self.0.lock().name.clone() - } - - fn __copy__<'a>(&self, py: pyo3::Python<'a>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Bound::new(py, (lock.clone_ref(py).into(), PySchemaStatement)) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - let mut fmt = ReprFormatter::new_with_pyref(&slf) - .map("name", &lock.name, |x| x.__repr__()) - .take(); - - fmt.vec("columns", true) - .display_iter(lock.columns.iter()) - .finish(&mut fmt); - - fmt.vec("indexes", true) - .display_iter(lock.indexes.iter()) - .finish(&mut fmt); - - fmt.vec("foreign_keys", true) - .display_iter(lock.foreign_keys.iter()) - .finish(&mut fmt); - - fmt.vec("checks", true) - .display_iter(lock.checks.iter()) - .finish(&mut fmt); - - fmt.optional_boolean("if_not_exists", lock.options & OPT_IF_NOT_EXISTS > 0) - .optional_boolean("temporary", lock.options & OPT_TEMPORARY > 0) - .optional_quote("comment", lock.comment.as_ref()) - .optional_quote("engine", lock.engine.as_ref()) - .optional_quote("collate", lock.collate.as_ref()) - .optional_quote("character_set", lock.character_set.as_ref()) - .finish() - } -} diff --git a/src/schema/table_operations.rs b/src/schema/table_operations.rs deleted file mode 100644 index 19ac707..0000000 --- a/src/schema/table_operations.rs +++ /dev/null @@ -1,364 +0,0 @@ -use super::base::PySchemaStatement; -use crate::common::table_ref::PyTableName; -use crate::internal::repr::ReprFormatter; -use crate::internal::{BoundArgs, BoundKwargs, RefBoundObject, ToSeaQuery}; - -pub const DROP_OPT_IF_EXISTS: u8 = 1 << 0; -pub const DROP_OPT_CASCADE: u8 = 1 << 1; -pub const DROP_OPT_RESTRICT: u8 = 1 << 2; - -crate::implement_pyclass! { - /// Represents a DROP TABLE SQL statement. - /// - /// Builds table deletion statements with support for: - /// - Conditional deletion (IF EXISTS) to avoid errors - /// - CASCADE to drop dependent objects - /// - RESTRICT to prevent deletion if dependencies exist - mutable [subclass, extends=PySchemaStatement] PyDropTable(DropTableState) as "DropTable" { - name: PyTableName, - options: u8, - } -} -crate::implement_pyclass! { - /// Represents a RENAME TABLE SQL statement. - /// - /// Changes the name of an existing table to a new name. Both names can be - /// schema-qualified if needed. - mutable [subclass, extends=PySchemaStatement] PyRenameTable(RenameTableState) as "RenameTable" { - from_name: PyTableName, - to_name: PyTableName, - } -} -crate::implement_pyclass! { - /// Represents a TRUNCATE TABLE SQL statement. - /// - /// Quickly removes all rows from a table, typically faster than DELETE - /// and with different transaction and trigger behavior depending on the - /// database system. - mutable [subclass, extends=PySchemaStatement] PyTruncateTable(TruncateTableState) as "TruncateTable" { - name: PyTableName, - } -} - -impl Clone for DropTableState { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - options: self.options, - } - } -} - -impl ToSeaQuery for DropTableState { - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::TableDropStatement { - let mut stmt = sea_query::TableDropStatement::new(); - stmt.table(self.name.clone()); - - if self.options & DROP_OPT_IF_EXISTS > 0 { - stmt.if_exists(); - } - if self.options & DROP_OPT_RESTRICT > 0 { - stmt.restrict(); - } - if self.options & DROP_OPT_CASCADE > 0 { - stmt.cascade(); - } - - stmt - } -} - -#[pyo3::pymethods] -impl PyDropTable { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[pyo3(signature = (name, *, if_exists=false, cascade=false, restrict=false))] - fn __init__( - &self, - name: RefBoundObject<'_>, - if_exists: bool, - cascade: bool, - restrict: bool, - ) -> pyo3::PyResult<()> { - let name = PyTableName::try_from(name)?; - - let mut options = 0u8; - if if_exists { - options |= DROP_OPT_IF_EXISTS; - } - if cascade { - options |= DROP_OPT_CASCADE; - } - if restrict { - options |= DROP_OPT_RESTRICT; - } - - let state = DropTableState { name, options }; - self.0.set(state); - Ok(()) - } - - /// The table name to drop. - #[getter] - fn name(&self) -> PyTableName { - let lock = self.0.lock(); - lock.name.clone() - } - - #[setter] - fn set_name(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.name = val; - Ok(()) - } - - #[getter] - fn if_exists(&self) -> bool { - self.0.lock().options & DROP_OPT_IF_EXISTS > 0 - } - - #[setter] - fn set_if_exists(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= DROP_OPT_IF_EXISTS; - } else { - lock.options &= !DROP_OPT_IF_EXISTS; - } - } - - #[getter] - fn cascade(&self) -> bool { - self.0.lock().options & DROP_OPT_CASCADE > 0 - } - - #[setter] - fn set_cascade(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= DROP_OPT_CASCADE; - } else { - lock.options &= !DROP_OPT_CASCADE; - } - } - - #[getter] - fn restrict(&self) -> bool { - self.0.lock().options & DROP_OPT_RESTRICT > 0 - } - - #[setter] - fn set_restrict(&self, value: bool) { - let mut lock = self.0.lock(); - if value { - lock.options |= DROP_OPT_RESTRICT; - } else { - lock.options &= !DROP_OPT_RESTRICT; - } - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Py::new(py, (lock.clone().into(), PySchemaStatement)) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .map("name", &lock.name, |x| x.__repr__()) - .optional_boolean("if_exists", lock.options & DROP_OPT_IF_EXISTS > 0) - .optional_boolean("cascade", lock.options & DROP_OPT_CASCADE > 0) - .optional_boolean("restrict", lock.options & DROP_OPT_RESTRICT > 0) - .finish() - } -} - -impl Clone for RenameTableState { - fn clone(&self) -> Self { - Self { - from_name: self.from_name.clone(), - to_name: self.to_name.clone(), - } - } -} - -impl ToSeaQuery for RenameTableState { - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::TableRenameStatement { - let mut stmt = sea_query::TableRenameStatement::new(); - stmt.table(self.from_name.clone(), self.to_name.clone()); - stmt - } -} - -#[pyo3::pymethods] -impl PyRenameTable { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[pyo3(signature = (from_name, to_name))] - fn __init__( - &self, - from_name: RefBoundObject<'_>, - to_name: RefBoundObject<'_>, - ) -> pyo3::PyResult<()> { - let from_name = PyTableName::try_from(from_name)?; - let to_name = PyTableName::try_from(to_name)?; - - let state = RenameTableState { from_name, to_name }; - self.0.set(state); - Ok(()) - } - - /// The current name of the table. - #[getter] - #[allow(clippy::wrong_self_convention)] - fn from_name(&self) -> PyTableName { - let lock = self.0.lock(); - lock.from_name.clone() - } - - #[setter] - fn set_from_name(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.from_name = val; - Ok(()) - } - - /// The new name for the table. - #[getter] - fn to_name(&self) -> PyTableName { - let lock = self.0.lock(); - lock.to_name.clone() - } - - #[setter] - fn set_to_name(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.to_name = val; - Ok(()) - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Py::new(py, (lock.clone().into(), PySchemaStatement)) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .map("from_name", &lock.from_name, |x| x.__repr__()) - .map("to_name", &lock.to_name, |x| x.__repr__()) - .finish() - } -} - -impl Clone for TruncateTableState { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - } - } -} - -impl ToSeaQuery for TruncateTableState { - fn to_sea_query<'a>(&self, _py: pyo3::Python<'a>) -> sea_query::TableTruncateStatement { - let mut stmt = sea_query::TableTruncateStatement::new(); - stmt.table(self.name.clone()); - stmt - } -} - -#[pyo3::pymethods] -impl PyTruncateTable { - #[new] - #[allow(unused_variables)] - #[pyo3(signature=(*args, **kwds))] - fn __new__(args: BoundArgs<'_>, kwds: Option>) -> (Self, PySchemaStatement) { - (Self::uninit(), PySchemaStatement) - } - - #[pyo3(signature = (name))] - fn __init__(&self, name: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let name = PyTableName::try_from(name)?; - - let state = TruncateTableState { name }; - self.0.set(state); - Ok(()) - } - - /// The name of the table to truncate. - #[getter] - fn name(&self) -> PyTableName { - let lock = self.0.lock(); - lock.name.clone() - } - - #[setter] - fn set_name(&self, val: RefBoundObject<'_>) -> pyo3::PyResult<()> { - let val = PyTableName::try_from(val)?; - - let mut lock = self.0.lock(); - lock.name = val; - Ok(()) - } - - fn __copy__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { - let lock = self.0.lock(); - pyo3::Py::new(py, (lock.clone().into(), PySchemaStatement)) - } - - #[pyo3(signature = (backend, /))] - #[allow(clippy::wrong_self_convention)] - fn to_sql(&self, py: pyo3::Python<'_>, backend: String) -> pyo3::PyResult { - let lock = self.0.lock(); - let stmt = lock.to_sea_query(py); - drop(lock); - - crate::build_schema_statement!(backend, stmt) - } - - fn __repr__(slf: pyo3::PyRef<'_, Self>) -> String { - let lock = slf.0.lock(); - - ReprFormatter::new_with_pyref(&slf) - .map("name", &lock.name, |x| x.__repr__()) - .finish() - } -} diff --git a/src/serializers/functions.rs b/src/serializers/functions.rs new file mode 100644 index 0000000..bba7383 --- /dev/null +++ b/src/serializers/functions.rs @@ -0,0 +1,521 @@ +use pyo3::types::PyAnyMethods; +use pyo3::types::PyTzInfoAccess; + +use crate::dialect::PyDialect; +use crate::dialect::errors::FormatError; +use crate::dialect::flags; +use crate::internal::alias; +use crate::internal::utils; +use std::fmt::Write; +use std::str::FromStr; + +/// `str(obj)` copied into a Rust `String`. +/// +/// # Safety +/// `ptr` must be *borrowed*, non-null, and a valid Python object. +#[inline] +pub(super) unsafe fn pyobject_string(py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject) -> pyo3::PyResult { + let s = pyo3::ffi::PyObject_Str(ptr); + if s.is_null() { + return Err(pyo3::PyErr::fetch(py)); + } + let out = utils::borrow_py_str_buffer(py, s).map(str::to_owned); + pyo3::ffi::Py_DECREF(s); + out +} + +#[inline(always)] +fn write_quoted(dialect: &flags::DialectFlags, out: &mut String, raw: &str) -> pyo3::PyResult<()> { + dialect.write_string_literal(out, raw).map_err(Into::into) +} + +#[inline] +fn write_with_keyword( + dialect: &flags::DialectFlags, + out: &mut String, + keyword: Option<&str>, + raw: &str, +) -> pyo3::PyResult<()> { + if let Some(keyword) = keyword { + out.push_str(keyword); + out.push(' '); + } + write_quoted(dialect, out, raw) +} + +#[inline(always)] +fn timestamp_keyword(format: flags::TimestampFormat, aware: bool) -> Option<&'static str> { + match format { + flags::TimestampFormat::Quoted => None, + flags::TimestampFormat::Timestamptz => Some(if aware { "TIMESTAMPTZ" } else { "TIMESTAMP" }), + flags::TimestampFormat::WithTimezone => Some(if aware { "TIMESTAMP WITH TIMEZONE" } else { "TIMESTAMP" }), + } +} + +#[inline] +pub(super) unsafe fn write_integer( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyLong_Check(ptr) != 1 { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected int for integer serialization, got {}", + utils::get_type_name(py, ptr) + ); + } + + let mut overflow = 0i32; + let n = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut overflow); + + if overflow != 0 { + // Fallback to `str(obj)` for very long integers. + let text = pyobject_string(py, ptr)?; + out.push_str(&text); + return Ok(()); + } + + if n == -1 && !pyo3::ffi::PyErr_Occurred().is_null() { + return Err(pyo3::PyErr::fetch(py)); + } + + let _ = write!(out, "{n}"); + Ok(()) +} + +#[inline] +pub(super) unsafe fn write_float( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { + let v = pyo3::ffi::PyFloat_AsDouble(ptr); + if !v.is_finite() { + return rapidquery_macros::pyerr!( + Err(PyValueError), + "cannot serialize non-finite float {} as a numeric literal", + v + ); + } + + // `{:?}` so `1.0` stays distinct from `1` + let _ = write!(out, "{v:?}"); + return Ok(()); + } + + if pyo3::ffi::PyLong_CheckExact(ptr) == 1 || crate::typeref::check_exact(ptr, crate::typeref::STD_DECIMAL_TYPE) { + // Fallback to `str(obj)` for int/decimal.Decimal + let text = pyobject_string(py, ptr)?; + out.push_str(&text); + return Ok(()); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected float/int/Decimal for numeric serialization, got {}", + utils::get_type_name(py, ptr) + ) +} + +#[inline] +pub(super) unsafe fn write_bool( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyBool_Check(ptr) == 1 { + let value = pyo3::ffi::Py_True() == ptr; + dialect.write_boolean(out, value)?; + Ok(()) + } else { + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected bool for boolean serialization, got {}", + utils::get_type_name(py, ptr) + ) + } +} + +#[inline] +pub(super) unsafe fn write_str( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyUnicode_Check(ptr) == 1 { + return dialect + .write_string_literal(out, utils::borrow_py_str_buffer(py, ptr)?) + .map_err(Into::into); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected str for string serialization, got {}", + utils::get_type_name(py, ptr) + ) +} + +#[inline] +pub(super) unsafe fn write_uuid( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + let text = if crate::typeref::check_exact(ptr, crate::typeref::STD_UUID_TYPE) { + pyobject_string(py, ptr)? + } else if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + let borrowed = utils::borrow_py_str_buffer(py, ptr)?; + uuid::Uuid::try_parse(borrowed) + .map_err(|e| rapidquery_macros::pyerr!(PyValueError, "invalid uuid str: {}", e))? + .hyphenated() + .to_string() + } else { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected uuid.UUID/str for UUID serialization, got {}", + utils::get_type_name(py, ptr) + ); + }; + + match dialect.uuid_format() { + flags::UuidFormat::Quoted => write_quoted(dialect, out, &text), + flags::UuidFormat::UuidKeyword => { + out.push_str("UUID "); + write_quoted(dialect, out, &text) + } + flags::UuidFormat::CastUuid => { + write_quoted(dialect, out, &text)?; + out.push_str("::uuid"); + Ok(()) + } + } +} + +#[inline] +pub(super) unsafe fn write_bytes( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyBytes_CheckExact(ptr) == 1 { + let buf = pyo3::ffi::PyBytes_AsString(ptr) as *const u8; + let len = pyo3::ffi::PyBytes_Size(ptr) as usize; + debug_assert!(!buf.is_null()); + + dialect + .write_bytes(out, std::slice::from_raw_parts(buf, len)) + .map_err(Into::into) + } else { + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected bytes for binary serialization, got {}", + utils::get_type_name(py, ptr) + ) + } +} + +pub(super) unsafe fn write_bit( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { + let raw = utils::borrow_py_str_buffer(py, ptr)?; + + if raw.is_empty() || !raw.bytes().all(|b| b == b'0' || b == b'1') { + return rapidquery_macros::pyerr!( + Err(PyValueError), + "expected a string of '0'/'1' for bit serialization, got {:?}", + raw + ); + } + + dialect.write_bits(out, raw).map_err(Into::into) + } else { + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected str of bits for bit serialization, got {}", + utils::get_type_name(py, ptr) + ) + } +} + +#[inline] +pub(super) unsafe fn write_json( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + crate::internal::modules::json_dumps(unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) }, |text| match dialect + .json_format() + { + flags::JsonFormat::Quoted => write_quoted(dialect, out, text), + flags::JsonFormat::JsonKeyword => { + out.push_str("JSON "); + write_quoted(dialect, out, text) + } + flags::JsonFormat::ParseJson => { + out.push_str("PARSE_JSON("); + write_quoted(dialect, out, text)?; + out.push(')'); + Ok(()) + } + }) +} + +#[inline] +pub(super) unsafe fn write_date( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if crate::typeref::check_exact(ptr, crate::typeref::STD_DATE_TYPE) { + let val: pyo3::Bound<'_, pyo3::types::PyDate> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; + + let date: chrono::NaiveDate = val.extract()?; + let mut body = String::new(); + let _ = date.format("%Y-%m-%d").write_to(&mut body); + let keyword = match dialect.date_format() { + flags::DateFormat::Quoted => None, + flags::DateFormat::Keyword => Some("DATE"), + }; + write_with_keyword(dialect, out, keyword, &body) + } else { + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected datetime.date for date serialization, got {}", + utils::get_type_name(py, ptr) + ) + } +} + +pub(super) unsafe fn write_time( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + if crate::typeref::check_exact(ptr, crate::typeref::STD_TIME_TYPE) { + let val: pyo3::Bound<'_, pyo3::types::PyTime> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; + + let time: chrono::NaiveTime = val.extract()?; + let mut body = String::new(); + let _ = time.format("%H:%M:%S%.f").write_to(&mut body); + let keyword = match dialect.time_format() { + flags::TimeFormat::Quoted => None, + flags::TimeFormat::Keyword => Some("TIME"), + }; + write_with_keyword(dialect, out, keyword, &body) + } else { + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected datetime.time for time serialization, got {}", + utils::get_type_name(py, ptr) + ) + } +} + +#[inline] +pub(super) unsafe fn write_datetime( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + use chrono::TimeZone; + + if crate::typeref::check_exact(ptr, crate::typeref::STD_DATETIME_TYPE) { + let val: pyo3::Bound<'_, pyo3::types::PyDateTime> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; + + if val.get_tzinfo().is_none() { + let datetime: chrono::NaiveDateTime = val.extract()?; + let mut body = String::new(); + let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f").write_to(&mut body); + return write_with_keyword( + dialect, + out, + timestamp_keyword(dialect.timestamp_format(), false), + &body, + ); + } + + let datetime: chrono::DateTime = val.extract()?; + let mut body = String::new(); + let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); + return write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format(), true), &body); + } + + if pyo3::ffi::PyLong_CheckExact(ptr) == 1 { + let mut has_overflow: i32 = 0; + let num = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut has_overflow); + if has_overflow != 0 { + return rapidquery_macros::pyerr!(Err(PyOverflowError), "out of range"); + } + + return match chrono::Utc.timestamp_opt(num, 0) { + chrono::offset::LocalResult::Single(datetime) => { + let mut body = String::new(); + let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); + write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format(), true), &body) + } + _ => rapidquery_macros::pyerr!( + Err(PyOverflowError), + "timestamp is invalid for datetime serialization: {}", + num + ), + }; + } + + if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { + let num = pyo3::ffi::PyFloat_AsDouble(ptr); + return match chrono::Utc.timestamp_opt(num.trunc() as i64, (num.fract() * 1_000_000_000.0) as u32) { + chrono::offset::LocalResult::Single(datetime) => { + let mut body = String::new(); + let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); + write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format(), true), &body) + } + _ => rapidquery_macros::pyerr!( + Err(PyOverflowError), + "timestamp is invalid for datetime serialization: {}", + num + ), + }; + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected datetime.datetime/int/float for datetime serialization, got {}", + utils::get_type_name(py, ptr) + ) +} + +pub(super) unsafe fn write_inet( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + let text = pyobject_string(py, ptr)?; + let net = + ipnetwork::IpNetwork::from_str(&text).map_err(|e| rapidquery_macros::pyerr!(PyValueError, e.to_string()))?; + let body = net.to_string(); + + match dialect.inet_format() { + flags::InetFormat::Unsupported => Err(FormatError::Unsupported("InetSerializer").into()), + flags::InetFormat::Quoted => write_quoted(dialect, out, &body), + flags::InetFormat::Cast => { + write_quoted(dialect, out, &body)?; + out.push_str("::inet"); + Ok(()) + } + } +} + +pub(super) unsafe fn write_macaddr( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &flags::DialectFlags, + out: &mut String, +) -> pyo3::PyResult<()> { + let text = pyobject_string(py, ptr)?; + let addr = + mac_address::MacAddress::from_str(&text).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + let body = addr.to_string(); + + match dialect.macaddr_format() { + flags::MacAddrFormat::Unsupported => Err(FormatError::Unsupported("MacAddressSerializer").into()), + flags::MacAddrFormat::Quoted => write_quoted(dialect, out, &body), + flags::MacAddrFormat::Cast => { + write_quoted(dialect, out, &body)?; + out.push_str("::macaddr"); + Ok(()) + } + } +} + +pub(super) unsafe fn write_array( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &PyDialect, + element: &super::inner::SerializerType, + out: &mut String, +) -> pyo3::PyResult<()> { + let (len, getter): ( + isize, + unsafe fn(*mut pyo3::ffi::PyObject, isize) -> *mut pyo3::ffi::PyObject, + ) = if pyo3::ffi::PyList_CheckExact(ptr) == 1 { + (pyo3::ffi::PyList_Size(ptr), |p, i| pyo3::ffi::PyList_GetItem(p, i)) + } else if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { + (pyo3::ffi::PyTuple_Size(ptr), |p, i| pyo3::ffi::PyTuple_GetItem(p, i)) + } else { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected list/tuple for array serialization, got {}", + utils::get_type_name(py, ptr) + ); + }; + + let close = match dialect.0.array_format() { + flags::ArrayFormat::Unsupported => { + return Err(FormatError::Unsupported("ArraySerializer").into()); + } + flags::ArrayFormat::Constructor => { + out.push_str("ARRAY["); + ']' + } + flags::ArrayFormat::List => { + out.push('['); + ']' + } + flags::ArrayFormat::Function => { + out.push_str("array("); + ')' + } + }; + + for i in 0..len { + if i > 0 { + out.push_str(", "); + } + let item = getter(ptr, i); + if item.is_null() { + return Err(pyo3::PyErr::fetch(py)); + } + let borrowed = alias::BoundObject::from_borrowed_ptr(py, item); + element.serialize(dialect, &borrowed, out)?; + } + out.push(close); + Ok(()) +} + +pub(super) unsafe fn write_custom( + py: pyo3::Python<'_>, + ptr: *mut pyo3::ffi::PyObject, + dialect: &PyDialect, + function: &pyo3::Py, + object: &alias::BoundObject<'_>, + out: &mut String, +) -> pyo3::PyResult<()> { + let _ = ptr; + + let output = function.call1(py, (dialect.clone(), object))?; + if pyo3::ffi::PyUnicode_CheckExact(output.as_ptr()) != 1 { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "custom serializer must return str, got {}", + utils::get_type_name(py, output.as_ptr()) + ); + } + + out.push_str(utils::borrow_py_str_buffer(py, output.as_ptr())?); + Ok(()) +} diff --git a/src/serializers/inner.rs b/src/serializers/inner.rs new file mode 100644 index 0000000..85cd397 --- /dev/null +++ b/src/serializers/inner.rs @@ -0,0 +1,194 @@ +use crate::dialect::PyDialect; +use crate::internal::alias; +use crate::internal::modules; +use crate::internal::utils; + +/// A thin enum which specifies the serializer type. +/// +/// You can use it to serialize different Python objects. +#[derive(Debug, Clone)] +pub enum SerializerType { + Integer, + Float, + Boolean, + String, + Uuid, + Bytes, + Bit, + Json, + Date, + Time, + Datetime, + Inet, + MacAddress, + Unspecified, + Array(Box), + Custom(alias::PyObject), +} + +impl PartialEq for SerializerType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer, Self::Integer) + | (Self::Float, Self::Float) + | (Self::Boolean, Self::Boolean) + | (Self::String, Self::String) + | (Self::Uuid, Self::Uuid) + | (Self::Bytes, Self::Bytes) + | (Self::Bit, Self::Bit) + | (Self::Json, Self::Json) + | (Self::Date, Self::Date) + | (Self::Time, Self::Time) + | (Self::Datetime, Self::Datetime) + | (Self::Inet, Self::Inet) + | (Self::MacAddress, Self::MacAddress) + | (Self::Unspecified, Self::Unspecified) => true, + (Self::Array(a), Self::Array(b)) => a == b, + (Self::Custom(a), Self::Custom(b)) => { + // We prefer to do not use Python level equal operator + // to avoid GIL problems. This guarantees safety. + std::ptr::eq(a.as_ptr(), b.as_ptr()) + } + _ => false, + } + } +} +impl Eq for SerializerType {} + +impl std::hash::Hash for SerializerType { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::Array(element) => element.hash(state), + Self::Custom(function) => { + // Same note like [`SerializerType::eq`] + (function.as_ptr() as usize).hash(state) + } + _ => {} + } + } +} + +impl SerializerType { + pub fn infer_bound(bound: &alias::BoundObject) -> pyo3::PyResult { + unsafe { + let object_ptr = bound.as_ptr(); + let object_type_ptr = pyo3::ffi::Py_TYPE(object_ptr); + + // None / bool -> Boolean + // + // Why `None`? + // Actually we don't use [`Serializer`] on `None` values. + // So it's not important what [`Serializer`] is. + // `None` values are handled by [`crate::sql::SqlWriter`] + if pyo3::ffi::Py_IsNone(object_ptr) == 1 || pyo3::ffi::PyBool_Check(object_ptr) == 1 { + return Ok(Self::Boolean); + } + + // float / decimal -> Float + if pyo3::ffi::PyFloat_CheckExact(object_ptr) == 1 || object_type_ptr == crate::typeref::STD_DECIMAL_TYPE { + return Ok(Self::Float); + } + + // int/enum.IntEnum -> Integer + if pyo3::ffi::PyLong_Check(object_ptr) == 1 { + return Ok(Self::Integer); + } + + // str/enum.StrEnum -> String + if pyo3::ffi::PyUnicode_Check(object_ptr) == 1 { + return Ok(Self::String); + } + + // bytes -> Bytes + if pyo3::ffi::PyBytes_CheckExact(object_ptr) == 1 { + return Ok(Self::Bytes); + } + + // date -> Date + if object_type_ptr == crate::typeref::STD_DATE_TYPE { + return Ok(Self::Date); + } + + // time -> Time + if object_type_ptr == crate::typeref::STD_TIME_TYPE { + return Ok(Self::Time); + } + + // datetime -> Datetime + if object_type_ptr == crate::typeref::STD_DATETIME_TYPE { + return Ok(Self::Datetime); + } + + // UUID -> Uuid + if object_type_ptr == crate::typeref::STD_UUID_TYPE { + return Ok(Self::Uuid); + } + + // list/dict -> Json + if (pyo3::ffi::PyList_CheckExact(object_ptr) == 1 || pyo3::ffi::PyDict_CheckExact(object_ptr) == 1) + && modules::json_dumps(bound.clone(), |_| Ok(())).is_ok() + { + return Ok(Self::Json); + } + } + + // __serializer__ protocol + rapidquery_macros::cust_protocol! { + on: bound, + attr: c"__serializer__", + ty: crate::typeref::SERIALIZER_TYPE, + matches: { + let serializer = protocol.cast_into_unchecked::(); + return Ok(serializer.get().0.clone()); + }, + mismatches: { + return rapidquery_macros::pyerr!( + Err(PyTypeError), + "__serializer__ protocol attribute returns non-serializer type: {}", + utils::get_type_name(bound.py(), protocol.as_ptr()) + ) + } + } + + // * -> Unspecified + Ok(Self::Unspecified) + } +} + +impl SerializerType { + pub fn serialize( + &self, + dialect: &PyDialect, + object: &alias::BoundObject<'_>, + out: &mut String, + ) -> pyo3::PyResult<()> { + let py = object.py(); + let ptr = object.as_ptr(); + + unsafe { + match self { + Self::Integer => super::functions::write_integer(py, ptr, out), + Self::Float => super::functions::write_float(py, ptr, out), + Self::Boolean => super::functions::write_bool(py, ptr, &dialect.0, out), + Self::String => super::functions::write_str(py, ptr, &dialect.0, out), + Self::Uuid => super::functions::write_uuid(py, ptr, &dialect.0, out), + Self::Bytes => super::functions::write_bytes(py, ptr, &dialect.0, out), + Self::Bit => super::functions::write_bit(py, ptr, &dialect.0, out), + Self::Json => super::functions::write_json(py, ptr, &dialect.0, out), + Self::Date => super::functions::write_date(py, ptr, &dialect.0, out), + Self::Time => super::functions::write_time(py, ptr, &dialect.0, out), + Self::Datetime => super::functions::write_datetime(py, ptr, &dialect.0, out), + Self::Inet => super::functions::write_inet(py, ptr, &dialect.0, out), + Self::MacAddress => super::functions::write_macaddr(py, ptr, &dialect.0, out), + Self::Array(element) => super::functions::write_array(py, ptr, dialect, element, out), + Self::Unspecified => { + let text = super::functions::pyobject_string(py, ptr)?; + out.push_str(&text); + Ok(()) + } + Self::Custom(s) => super::functions::write_custom(py, ptr, dialect, s, object, out), + } + } + } +} diff --git a/src/serializers/mod.rs b/src/serializers/mod.rs new file mode 100644 index 0000000..709e4d8 --- /dev/null +++ b/src/serializers/mod.rs @@ -0,0 +1,301 @@ +//! Serializers + +mod functions; +mod inner; + +pub use inner::SerializerType; + +use crate::internal::alias; +use crate::internal::repr; + +pub fn serializer_repr(serializer: &SerializerType) -> repr::ReprFormatter { + let mut fmt = repr::ReprFormatter::new_call(serializer_type_to_pyname(serializer)); + + if let SerializerType::Array(element) = serializer { + fmt.positional(serializer_repr(element)); + } + + fmt.close(); + fmt +} + +/// Turns a Python object into a SQL literal for one dialect. +#[rapidquery_macros::pyclass("rapidquery.serializers.Serializer", subclass, from_py_object, generic)] +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct PySerializer(pub SerializerType); + +#[pyo3::pymethods] +impl PySerializer { + /// Renders `object` as a SQL literal for `dialect`. + fn serialize(&self, dialect: crate::dialect::PyDialect, object: alias::BoundObject<'_>) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.serialize(&dialect, &object, &mut buf)?; + Ok(buf) + } + + fn __repr__(&self) -> String { + serializer_repr(&self.0).into_string() + } + + // Bound so only instances match. + fn __eq__(&self, other: &pyo3::Bound<'_, Self>) -> bool { + self.0 == other.get().0 + } + + fn __hash__(&self) -> u64 { + use std::hash::Hash; + use std::hash::Hasher; + + let mut hasher = std::hash::DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } +} + +macro_rules! impl_unit_serializers { + ( + $( + $(#[$outer:meta])* + $variant:ident => $name:ident => $pyname:literal, + )+ + ) => { + $( + $(#[$outer])* + #[rapidquery_macros::pyclass("rapidquery.serializers" + $pyname, extends = PySerializer)] + #[derive(Debug, Default)] + pub struct $name; + + #[pyo3::pymethods] + impl $name { + #[new] + fn __new__() -> pyo3::PyClassInitializer { + pyo3::PyClassInitializer::from(PySerializer(SerializerType::$variant)) + .add_subclass(Self) + } + } + )+ + + pub(super) fn serializer_type_to_pyname(serializer_type: &SerializerType) -> &'static str { + match serializer_type { + $( + SerializerType::$variant {..} => $pyname, + )+ + + SerializerType::Array(_) => "ArraySerializer", + SerializerType::Custom(_) => "CustomSerializer", + } + } + + impl PySerializer { + /// Wrap `self` as the matching Python subclass. + pub fn into_subclasses<'py>( + self, + py: pyo3::Python<'py>, + ) -> pyo3::PyResult> { + match &self.0 { + $( + SerializerType::$variant {..} => { + pyo3::Bound::new( + py, + pyo3::PyClassInitializer::from(self).add_subclass($name), + ) + .map(|x| x.into_any()) + } + )+ + + SerializerType::Array(_) => { + pyo3::Bound::new( + py, + pyo3::PyClassInitializer::from(self).add_subclass(PyArraySerializer), + ) + .map(|x| x.into_any()) + }, + SerializerType::Custom(_) => { + pyo3::Bound::new( + py, + pyo3::PyClassInitializer::from(self).add_subclass(PyCustomSerializer), + ) + .map(|x| x.into_any()) + } + } + } + } + }; +} + +impl_unit_serializers! { + /// Bare SQL integer. Accepts `int`, not `bool`. + Integer => PyIntegerSerializer => "IntegerSerializer", + + /// Bare SQL number for `float`, `int`, or `Decimal`. + Float => PyFloatSerializer => "FloatSerializer", + + /// Dialect boolean literal for a Python `bool`. + Boolean => PyBooleanSerializer => "BooleanSerializer", + + /// Quoted SQL string for `str` or a string enum member. + String => PyStringSerializer => "StringSerializer", + + /// UUID literal for `uuid.UUID` or `str`. + Uuid => PyUuidSerializer => "UuidSerializer", + + /// Blob or bytea literal for exact `bytes`. + Bytes => PyBytesSerializer => "BytesSerializer", + + /// Bit-string literal for a non-empty `0`/`1` string. + Bit => PyBitSerializer => "BitSerializer", + + /// JSON literal for an object the JSON encoder can dump. + Json => PyJSONSerializer => "JSONSerializer", + + /// Date literal for `datetime.date`, not `datetime.datetime`. + Date => PyDateSerializer => "DateSerializer", + + /// Time literal for `datetime.time`. + Time => PyTimeSerializer => "TimeSerializer", + + /// Timestamp literal for `datetime` or a UTC epoch number. + Datetime => PyDatetimeSerializer => "DatetimeSerializer", + + /// Inet literal for an IP address, network, or address string. + Inet => PyInetSerializer => "InetSerializer", + + /// MAC address literal for a `:` or `-` separated string. + MacAddress => PyMacAddressSerializer => "MacAddressSerializer", + + /// Inserts `str(object)` with no quoting and no type check. + Unspecified => PyUnspecifiedSerializer => "UnspecifiedSerializer", +} + +/// `ARRAY[…]` serializer. Each item is rendered by the element serializer. +#[rapidquery_macros::pyclass("rapidquery.serializers.ArraySerializer", extends = PySerializer)] +#[derive(Debug, Default)] +pub struct PyArraySerializer; + +#[pyo3::pymethods] +impl PyArraySerializer { + /// Build an array serializer from the serializer used for each item. + #[new] + #[inline] + fn __new__(element: PySerializer) -> pyo3::PyClassInitializer { + let inner = SerializerType::Array(Box::new(element.0)); + pyo3::PyClassInitializer::from(PySerializer(inner)).add_subclass(Self) + } + + /// Returns the serializer used for each array element. + /// + /// Returns: + /// The `Serializer` passed to the constructor. + #[getter] + fn get_element(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + let base = slf.as_super(); + match &base.0 { + SerializerType::Array(element) => PySerializer((**element).clone()).into_subclasses(slf.py()), + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +/// Serializer that inserts the `str` returned by a callable. The text is not escaped. +#[rapidquery_macros::pyclass("rapidquery.serializers.CustomSerializer", extends = PySerializer)] +#[derive(Debug, Default)] +pub struct PyCustomSerializer; + +#[pyo3::pymethods] +impl PyCustomSerializer { + /// Build a serializer. `function(dialect, object)` must return `str`. + #[new] + fn __new__(function: alias::PyObject) -> pyo3::PyResult> { + unsafe { + if pyo3::ffi::PyCallable_Check(function.as_ptr()) != 1 { + return rapidquery_macros::pyerr!(Err(PyTypeError), "the 'function' parameter must be callable"); + } + } + let inner = SerializerType::Custom(function); + Ok(pyo3::PyClassInitializer::from(PySerializer(inner)).add_subclass(Self)) + } + + /// The callable passed to the constructor. + #[getter] + fn get_function(slf: pyo3::PyRef<'_, Self>) -> alias::PyObject { + let base = slf.as_super(); + match &base.0 { + SerializerType::Custom(function) => function.clone(), + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +/// Python `Serializer` extracted into a [`SerializerType`]. +/// +/// Use this as a `FromPyObject` argument when a method accepts only serializer +/// instances. Other objects fail extraction. +/// +/// ``` +/// use rapidquery::serializers::SerializerExtractor; +/// use rapidquery::serializers::SerializerType; +/// +/// let extracted = SerializerExtractor(SerializerType::Integer); +/// assert!(matches!(extracted.0, SerializerType::Integer)); +/// ``` +pub struct SerializerExtractor(pub SerializerType); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for SerializerExtractor { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + let ptr = obj.as_ptr(); + + unsafe { + // SAFETY: `ptr` is a live borrowed `PyObject`. Type pointers are + // interned in `initialize_typeref` before any extractor runs. + if crate::typeref::check(ptr, crate::typeref::SERIALIZER_TYPE) { + let serializer = obj.cast_unchecked::(); + return Ok(Self(serializer.get().0.clone())); + } + } + + rapidquery_macros::pyerr!(Err(PyTypeError), "expected Serializer, got {}", unsafe { + crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) + }) + } +} + +#[pyo3::pymodule(name = "serializers", gil_used = false)] +pub mod serializers_mod { + #[pymodule_export] + use super::PyArraySerializer; + #[pymodule_export] + use super::PyBitSerializer; + #[pymodule_export] + use super::PyBooleanSerializer; + #[pymodule_export] + use super::PyBytesSerializer; + #[pymodule_export] + use super::PyCustomSerializer; + #[pymodule_export] + use super::PyDateSerializer; + #[pymodule_export] + use super::PyDatetimeSerializer; + #[pymodule_export] + use super::PyFloatSerializer; + #[pymodule_export] + use super::PyInetSerializer; + #[pymodule_export] + use super::PyIntegerSerializer; + #[pymodule_export] + use super::PyJSONSerializer; + #[pymodule_export] + use super::PyMacAddressSerializer; + #[pymodule_export] + use super::PySerializer; + #[pymodule_export] + use super::PyStringSerializer; + #[pymodule_export] + use super::PyTimeSerializer; + #[pymodule_export] + use super::PyUnspecifiedSerializer; + #[pymodule_export] + use super::PyUuidSerializer; +} diff --git a/src/sqlite.rs b/src/sqlite.rs deleted file mode 100644 index b5783a7..0000000 --- a/src/sqlite.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// SQLite-Only functions -/// -/// **Comming Soon ...** -#[pyo3::pymodule(name = "sqlite")] -pub mod sqlite_module {} diff --git a/src/sqltypes/abstracts.rs b/src/sqltypes/abstracts.rs deleted file mode 100644 index 1b9ba0a..0000000 --- a/src/sqltypes/abstracts.rs +++ /dev/null @@ -1,138 +0,0 @@ -/// All of native SQL types should implement this trait. -/// -/// This is useful for generic typing. -pub trait SQLTypeTrait { - /// Returns the related sea_query column type - fn to_sea_query_column_type(&self) -> ::sea_query::ColumnType; - - /// Returns SQL type name in string (uses PostgreSQL builder) - fn to_sql_type_name(&self) -> String { - let mut name = String::with_capacity(10); - let builder = sea_query::PostgresQueryBuilder; - sea_query::TableBuilder::prepare_column_type( - &builder, - &self.to_sea_query_column_type(), - &mut name, - ); - name - } - - /// Validates the `ptr` and makes sure that we can process it by using - /// `serialize` method. - /// - /// ### Safety - /// - The `ptr` should be borrowed. - unsafe fn validate( - &self, - py: ::pyo3::Python, - ptr: *mut ::pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()>; - - /// Converts PyObject into sea_query::Value - /// - /// ### Safety - /// - The `ptr` should be borrowed. - /// - The `ptr` should be validated by `validate` method. - unsafe fn serialize( - &self, - py: ::pyo3::Python, - ptr: *mut ::pyo3::ffi::PyObject, - ) -> pyo3::PyResult<::sea_query::Value>; - - /// Converts sea_query::Value into PyObject - /// - /// ### Safety - /// - The returned object is owned and you should call Py_DECREF if you do - /// not need it. - unsafe fn deserialize( - &self, - py: ::pyo3::Python, - value: &::sea_query::Value, - ) -> pyo3::PyResult<*mut ::pyo3::ffi::PyObject>; -} - -crate::implement_pyclass! { - /// Base class for all SQL column data types. - /// - /// This abstract base class represents SQL data types that can be used in - /// column definitions. Each subclass implements a specific SQL data type - /// with its particular characteristics, constraints, and backend-specific - /// representations. - #[derive(Debug, Clone, Copy)] - [subclass, generic] PySQLTypeAbstract as "SQLTypeAbstract"; -} - -#[pyo3::pymethods] -impl PySQLTypeAbstract { - /// Type name. e.g. `'INTEGER'`, `'STRING'` - /// - /// It also may be a property. This function must NOT raise any error. - #[getter] - fn __type_name__(&self) -> pyo3::PyResult<()> { - Err(pyo3::exceptions::PyNotImplementedError::new_err(())) - } -} - -#[macro_export] -macro_rules! implement_sqltype_pymethods { - ($name:ident) => { - #[pyo3::pymethods] - impl $name { - #[new] - fn __new__() -> (Self, PySQLTypeAbstract) { - (Self, PySQLTypeAbstract) - } - - /// Type name. e.g. `'INTEGER'`, `'STRING'` - /// - /// It also may be a property. This function must NOT raise any error. - #[getter] - fn __type_name__(&self) -> String { - self.to_sql_type_name() - } - - fn __repr__(&self) -> String { - let result = String::from(stringify!($name)); - result[2..].to_string() + "()" - } - } - }; - - ( - $name:ident, - init(|$param:ident: $param_type:ty| $init:expr), - $return_type:literal - $(, signature($($signature:tt)*))? - ) => { - #[pyo3::pymethods] - impl $name { - #[new] - $(#[pyo3(signature=($($signature)*))])? - fn __new__($param: $param_type) -> (Self, PySQLTypeAbstract) { - ($init, PySQLTypeAbstract) - } - - /// Type name. e.g. `'INTEGER'`, `'STRING'` - /// - /// It also may be a property. This function must NOT raise any error. - #[getter] - fn __type_name__(&self) -> String { - self.to_sql_type_name() - } - - #[getter] - fn $param(&self) -> $param_type { - self.0 - } - - fn __repr__(&self) -> String { - let result = format!( - concat!(stringify!($name), "(", stringify!($param), "={:?})"), - self.0 - ); - result[2..].to_string() - } - } - }; -} -pub(super) use implement_sqltype_pymethods; diff --git a/src/sqltypes/binary.rs b/src/sqltypes/binary.rs deleted file mode 100644 index 6c6e077..0000000 --- a/src/sqltypes/binary.rs +++ /dev/null @@ -1,352 +0,0 @@ -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -crate::implement_pyclass! { - /// Binary large object column type (BLOB). - /// - /// Stores large binary data such as images, documents, audio files, or - /// any binary content. Size limits vary by database system. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBlobType as "Blob"; -} -crate::implement_pyclass! { - /// Fixed-length binary data column type (BINARY). - /// - /// Stores binary data of a fixed length. Values shorter than the specified - /// length are padded. Useful for storing hashes, keys, or other binary - /// data with consistent length. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBinaryType as "Binary" (pub u32); -} -crate::implement_pyclass! { - /// Variable-length binary data column type (VARBINARY). - /// - /// Stores binary data of variable length up to a specified maximum. - /// More storage-efficient than BINARY for binary data of varying lengths. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyVarBinaryType as "VarBinary" (pub Option); -} -crate::implement_pyclass! { - /// Fixed-length bit string column type (BIT). - /// - /// Stores a fixed number of bits. Useful for storing boolean flags efficiently - /// or binary data where individual bits have meaning. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBitType as "Bit" (pub Option); -} -crate::implement_pyclass! { - /// Variable-length bit string column type (VARBIT). - /// - /// Stores a variable number of bits up to a specified maximum. More flexible - /// than fixed BIT type for bit strings of varying lengths. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyVarBitType as "VarBit" (pub u32); - -} - -#[inline] -unsafe fn _serialize_function( - object: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult { - let buffer = pyo3::ffi::PyBytes_AsString(object) as *const u8; - let size = pyo3::ffi::PyBytes_Size(object) as usize; - - debug_assert!(!buffer.is_null()); - - let val = std::slice::from_raw_parts(buffer, size); - - Ok(sea_query::Value::Bytes(Some(Box::new(val.to_vec())))) -} - -#[inline] -unsafe fn _deserialize_function( - py: pyo3::Python, - value: &[u8], -) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - let pyptr = pyo3::ffi::PyBytes_FromStringAndSize(std::ptr::null(), value.len() as isize); - - if pyptr.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - let buffer = pyo3::ffi::PyBytes_AsString(pyptr) as *mut u8; - debug_assert!(!buffer.is_null()); - - let mutable = std::slice::from_raw_parts_mut(buffer, value.len()); - mutable.copy_from_slice(value); - - Ok(pyptr) -} - -impl SQLTypeTrait for PyBlobType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Blob - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBytes_CheckExact(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bytes for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bytes(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Bytes(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bytes for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyBinaryType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Binary(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBytes_CheckExact(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bytes for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bytes(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Bytes(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bytes for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyVarBinaryType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::VarBinary( - self.0 - .map_or(sea_query::StringLen::None, sea_query::StringLen::N), - ) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBytes_CheckExact(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bytes for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bytes(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Bytes(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bytes for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyBitType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Bit(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBytes_CheckExact(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bytes for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bytes(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Bytes(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bytes for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyVarBitType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::VarBit(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBytes_CheckExact(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bytes for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bytes(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Bytes(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bytes for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!(PyBlobType); -super::abstracts::implement_sqltype_pymethods!( - PyBinaryType, - init(|length: u32| Self(length)), - "int", - signature(length = 255) -); -super::abstracts::implement_sqltype_pymethods!( - PyVarBinaryType, - init(|length: Option| Self(length)), - "int", - signature(length = None) -); -super::abstracts::implement_sqltype_pymethods!( - PyBitType, - init(|length: Option| Self(length)), - "int", - signature(length = None) -); -super::abstracts::implement_sqltype_pymethods!( - PyVarBitType, - init(|length: u32| Self(length)), - "int", - signature(length = 255) -); diff --git a/src/sqltypes/datetimes.rs b/src/sqltypes/datetimes.rs deleted file mode 100644 index 596c4f9..0000000 --- a/src/sqltypes/datetimes.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -use chrono::TimeZone; -use pyo3::types::{PyAnyMethods, PyTzInfoAccess}; -use pyo3::IntoPyObject; - -crate::implement_pyclass! { - /// Date and time column type (DATETIME). - /// - /// Stores both date and time information without timezone awareness. - /// Suitable for recording timestamps, event times, or scheduling information - /// when timezone handling is managed at the application level. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyDateTimeType as "DateTime"; -} -crate::implement_pyclass! { - /// Timestamp column type (TIMESTAMP). - /// - /// Stores timestamp values, often with automatic update capabilities. - /// Behavior varies by database system. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyTimestampType as "Timestamp" (pub bool); -} -crate::implement_pyclass! { - /// Time-only column type (TIME). - /// - /// Stores time information without date component. Useful for storing - /// daily schedules, opening hours, or any time-based data that repeats - /// daily regardless of the specific date. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyTimeType as "Time"; -} -crate::implement_pyclass! { - /// Date-only column type (DATE). - /// - /// Stores date information without time component. Ideal for birth dates, - /// deadlines, or any date-based data where time precision is not needed. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyDateType as "Date"; - -} - -#[inline(always)] -unsafe fn _serialize_datetime( - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult { - let val: pyo3::Bound<'_, pyo3::types::PyDateTime> = - pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; - - let tzinfo = val.get_tzinfo(); - - if tzinfo.is_none() { - let result = Box::new(val.extract()?); - Ok(sea_query::Value::ChronoDateTime(Some(result))) - } else { - let result = Box::new(val.extract()?); - Ok(sea_query::Value::ChronoDateTimeWithTimeZone(Some(result))) - } -} - -impl SQLTypeTrait for PyDateTimeType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::DateTime - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::Py_TYPE(ptr) != crate::typeref::STD_DATETIME_TYPE { - crate::new_error!( - PyTypeError, - "expected datetime.datetime for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_datetime(py, ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::ChronoDateTime(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTimeWithTimeZone(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTimeUtc(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTimeLocal(Some(x)) => { - let pyobject = x.to_utc().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTime(None) - | sea_query::Value::ChronoDateTimeWithTimeZone(None) - | sea_query::Value::ChronoDateTimeUtc(None) - | sea_query::Value::ChronoDateTimeLocal(None) => Ok(pyo3::ffi::Py_None()), - - _ => crate::new_error!( - PyTypeError, - "expected datetime for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyTimestampType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - if self.0 { - sea_query::ColumnType::TimestampWithTimeZone - } else { - sea_query::ColumnType::Timestamp - } - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::Py_TYPE(ptr) != crate::typeref::STD_DATETIME_TYPE - && pyo3::ffi::PyLong_CheckExact(ptr) != 1 - && pyo3::ffi::PyFloat_CheckExact(ptr) != 1 - { - crate::new_error!( - PyTypeError, - "expected datetime.datetime/int/float for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - if pyo3::ffi::PyLong_CheckExact(ptr) == 1 { - let mut has_overflow: i32 = 0; - let num = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut has_overflow); - - if has_overflow == 1 { - return Err(pyo3::exceptions::PyOverflowError::new_err("out of range")); - } - - match chrono::Utc.timestamp_opt(num, 0) { - chrono::offset::LocalResult::Single(result) => { - return Ok(sea_query::Value::ChronoDateTimeUtc(Some(Box::new(result)))); - } - _ => { - return Err(pyo3::exceptions::PyOverflowError::new_err( - "timestamp is invalid", - )); - } - } - } - - if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { - let num = pyo3::ffi::PyFloat_AsDouble(ptr); - - match chrono::Utc - .timestamp_opt(num.trunc() as i64, (num.fract() * 1_000_000_000.0) as u32) - { - chrono::offset::LocalResult::Single(result) => { - return Ok(sea_query::Value::ChronoDateTimeUtc(Some(Box::new(result)))); - } - _ => { - return Err(pyo3::exceptions::PyOverflowError::new_err( - "timestamp is invalid", - )); - } - } - } - - _serialize_datetime(py, ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::ChronoDateTime(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTimeWithTimeZone(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTimeUtc(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDateTime(None) - | sea_query::Value::ChronoDateTimeWithTimeZone(None) - | sea_query::Value::ChronoDateTimeUtc(None) => Ok(pyo3::ffi::Py_None()), - - _ => crate::new_error!( - PyTypeError, - "expected datetime for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyDateType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Date - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::Py_TYPE(ptr) != crate::typeref::STD_DATE_TYPE { - crate::new_error!( - PyTypeError, - "expected datetime.date for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let val: pyo3::Bound<'_, pyo3::types::PyDate> = - pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; - - let result = Box::new(val.extract()?); - Ok(sea_query::Value::ChronoDate(Some(result))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::ChronoDate(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoDate(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected date for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyTimeType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Time - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::Py_TYPE(ptr) != crate::typeref::STD_TIME_TYPE { - crate::new_error!( - PyTypeError, - "expected datetime.time for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let val: pyo3::Bound<'_, pyo3::types::PyTime> = - pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; - - let result = Box::new(val.extract()?); - Ok(sea_query::Value::ChronoTime(Some(result))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::ChronoTime(Some(x)) => { - let pyobject = x.clone().into_pyobject(py)?; - Ok(pyobject.into_ptr()) - } - sea_query::Value::ChronoTime(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected time for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!(PyDateTimeType); -super::abstracts::implement_sqltype_pymethods!(PyDateType); -super::abstracts::implement_sqltype_pymethods!(PyTimeType); -super::abstracts::implement_sqltype_pymethods!( - PyTimestampType, - init(|timezone: bool| Self(timezone)), - "bool", - signature(timezone = false) -); diff --git a/src/sqltypes/json.rs b/src/sqltypes/json.rs deleted file mode 100644 index 981503b..0000000 --- a/src/sqltypes/json.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -use pyo3::types::PyAnyMethods; - -crate::implement_pyclass! { - /// JSON data column type (JSON). - /// - /// Stores JSON documents with validation and indexing capabilities. - /// Allows for flexible schema design and complex nested data structures - /// while maintaining some query capabilities. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyJSONType as "JSON"; -} -crate::implement_pyclass! { - /// Binary JSON column type (JSONB). - /// - /// Stores JSON documents in a binary format for improved performance. - /// Provides faster query and manipulation operations compared to text-based - /// JSON storage, with additional indexing capabilities. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyJSONBinaryType as "JSONBinary"; -} - -/// Import json module only once -#[inline(always)] -pub fn import_json_module( - py: pyo3::Python<'_>, -) -> pyo3::PyResult<&pyo3::Bound<'_, pyo3::types::PyModule>> { - static JSON_CLS: std::sync::OnceLock> = - std::sync::OnceLock::new(); - - let json = JSON_CLS.get_or_try_init(|| py.import("json").map(|x| x.unbind())); - json.map(|x| x.bind(py)) -} - -/// Serialize pyobject with Python `json` module -/// -/// Note: `ptr` should be borrowed -#[inline(always)] -pub fn _serialize_object_with_pyjson( - py: pyo3::Python<'_>, - ptr: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - let json = import_json_module(py)?; - let dumps_func = json.getattr("dumps")?; - - unsafe { - let arg1 = pyo3::Bound::from_borrowed_ptr(py, ptr); - dumps_func.call1((arg1,)).map(|x| x.into_ptr()) - } -} - -/// Deserialize pyobject with Python `json` module -/// -/// Note: `ptr` should be borrowed -#[inline(always)] -pub fn _deserialize_object_with_pyjson( - py: pyo3::Python<'_>, - ptr: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - let json = import_json_module(py)?; - let loads_func = json.getattr("loads")?; - - unsafe { - let arg1 = pyo3::Bound::from_borrowed_ptr(py, ptr); - loads_func.call1((arg1,)).map(|x| x.into_ptr()) - } -} - -/// Try to serialize pyobject to validate pyobject is JSON-serializable -#[inline] -#[cfg_attr(feature = "optimize", optimize(speed))] -pub fn _validate_json_object( - py: pyo3::Python<'_>, - ptr: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult<()> { - unsafe { - // Fast path - if (pyo3::ffi::PyLong_CheckExact(ptr) == 1) - || (pyo3::ffi::PyUnicode_CheckExact(ptr) == 1) - || (pyo3::ffi::PyFloat_CheckExact(ptr) == 1) - || (pyo3::ffi::Py_IsNone(ptr) == 1) - { - return Ok(()); - } - } - - _serialize_object_with_pyjson(py, ptr)?; - Ok(()) -} - -#[inline] -#[cfg_attr(feature = "optimize", optimize(speed))] -unsafe fn _serialize_function( - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult { - let serialized = _serialize_object_with_pyjson(py, ptr)?; - - let mut size: pyo3::ffi::Py_ssize_t = 0; - let c_str = pyo3::ffi::PyUnicode_AsUTF8AndSize(serialized, &mut size); - - if c_str.is_null() || size < 0 { - pyo3::ffi::Py_DECREF(serialized); - Err(pyo3::PyErr::fetch(py)) - } else { - let val = std::ffi::CStr::from_ptr(c_str); - let val = serde_json::from_slice::(val.to_bytes()); - - pyo3::ffi::Py_DECREF(serialized); - - let val = - val.map_err(|x| pyo3::PyErr::new::(x.to_string()))?; - - Ok(sea_query::Value::Json(Some(Box::new(val)))) - } -} - -#[inline] -#[cfg_attr(feature = "optimize", optimize(speed))] -unsafe fn _deserialize_function( - py: pyo3::Python, - value: &serde_json::Value, -) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - let encoded = serde_json::to_vec(value) - .map_err(|x| pyo3::PyErr::new::(x.to_string()))?; - - let val = pyo3::types::PyString::intern(py, std::str::from_utf8_unchecked(&encoded)); - - let val = _deserialize_object_with_pyjson(py, val.as_ptr())?; - Ok(val) -} - -impl SQLTypeTrait for PyJSONType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Json - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - _validate_json_object(py, ptr) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(py, ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Json(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Json(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected json for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyJSONBinaryType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::JsonBinary - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - _validate_json_object(py, ptr) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_function(py, ptr) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Json(Some(x)) => _deserialize_function(py, x), - sea_query::Value::Json(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected json for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!(PyJSONType); -super::abstracts::implement_sqltype_pymethods!(PyJSONBinaryType); diff --git a/src/sqltypes/mod.rs b/src/sqltypes/mod.rs deleted file mode 100644 index 5cbd5dc..0000000 --- a/src/sqltypes/mod.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! All SQL types with their serialization and deserialization methods. -//! -//! With support for custom types for Python. - -mod abstracts; -mod binary; -mod datetimes; -mod json; -mod others; -mod primitives; -mod vector; - -pub use abstracts::*; -pub use binary::*; -pub use datetimes::*; -pub use json::*; -pub use others::*; -pub use primitives::*; -pub use vector::*; - -#[pyo3::pymodule(name = "sqltypes")] -pub mod sqltypes_module { - // NOTE: SQLTypes, PyExpr, PyFunc, PyTableName & PyColumnRef could never mark as subclass. - // these should be immutable and final types. - - // sqltypes::abstracts - #[pymodule_export] - use super::PySQLTypeAbstract; - - // sqltypes::binary - #[pymodule_export] - use super::PyBinaryType; - #[pymodule_export] - use super::PyBitType; - #[pymodule_export] - use super::PyBlobType; - #[pymodule_export] - use super::PyVarBinaryType; - #[pymodule_export] - use super::PyVarBitType; - - // sqltypes::datetimes - #[pymodule_export] - use super::PyDateTimeType; - #[pymodule_export] - use super::PyDateType; - #[pymodule_export] - use super::PyTimeType; - #[pymodule_export] - use super::PyTimestampType; - - // sqltypes::json - #[pymodule_export] - use super::PyJSONBinaryType; - #[pymodule_export] - use super::PyJSONType; - - // sqltypes::vector - #[pymodule_export] - use super::PyVectorType; - - // sqltypes::others - #[pymodule_export] - use super::PyArrayType; - #[pymodule_export] - use super::PyDecimalType; - #[pymodule_export] - use super::PyEnumType; - #[pymodule_export] - use super::PyINETType; - #[pymodule_export] - use super::PyMacAddressType; - #[pymodule_export] - use super::PyUUIDType; - - // sqltypes::primitives - #[pymodule_export] - use super::PyBigIntegerType; - #[pymodule_export] - use super::PyBigUnsignedType; - #[pymodule_export] - use super::PyBooleanType; - #[pymodule_export] - use super::PyCharType; - #[pymodule_export] - use super::PyDoubleType; - #[pymodule_export] - use super::PyFloatType; - #[pymodule_export] - use super::PyIntegerType; - #[pymodule_export] - use super::PySmallIntegerType; - #[pymodule_export] - use super::PySmallUnsignedType; - #[pymodule_export] - use super::PyStringType; - #[pymodule_export] - use super::PyTextType; - #[pymodule_export] - use super::PyTinyIntegerType; - #[pymodule_export] - use super::PyTinyUnsignedType; - #[pymodule_export] - use super::PyUnsignedType; -} diff --git a/src/sqltypes/others.rs b/src/sqltypes/others.rs deleted file mode 100644 index 684433f..0000000 --- a/src/sqltypes/others.rs +++ /dev/null @@ -1,491 +0,0 @@ -use pyo3::types::PyAnyMethods; -use pyo3::IntoPyObject; -use sea_query::IntoIden; -use std::str::FromStr; - -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -crate::implement_pyclass! { - /// Exact numeric decimal column type (DECIMAL/NUMERIC). - /// - /// Stores exact numeric values with fixed precision and scale. Essential for - /// financial calculations, currency values, or any situation where exact - /// decimal representation is required without floating-point approximation. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyDecimalType as "Decimal" (pub Option<(u32, u32)>); -} -crate::implement_pyclass! { - /// UUID column type (UUID). - /// - /// Stores universally unique identifiers. Ideal for distributed systems, - /// primary keys, or any situation where globally unique identifiers are - /// needed without central coordination. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyUUIDType as "UUID"; -} -crate::implement_pyclass! { - /// Internet address column type (INET). - /// - /// Stores IPv4 or IPv6 addresses, with or without subnet specification. - /// More flexible than CIDR type, allowing both host addresses and network ranges. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyINETType as "INET"; -} -crate::implement_pyclass! { - /// MAC address column type (MACADDR). - /// - /// Stores MAC (Media Access Control) addresses for network devices. - /// Provides validation and formatting for 6-byte MAC addresses. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyMacAddressType as "MacAddress"; -} -crate::implement_pyclass! { - /// Enumeration column type (ENUM). - /// - /// Stores one value from a predefined set of allowed string values. - /// Provides type safety and storage efficiency for categorical data - /// with a fixed set of possible values. - #[derive(Debug, Clone)] - [extends=PySQLTypeAbstract] PyEnumType as "Enum" { - pub name: sea_query::DynIden, - pub variants: Vec, - } -} - -impl SQLTypeTrait for PyDecimalType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Decimal(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyLong_CheckExact(ptr) == 1 { - let mut has_overflow: i32 = 0; - let _ = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut has_overflow); - - if has_overflow == 1 { - return Err(pyo3::exceptions::PyOverflowError::new_err("out of range")); - } - - return Ok(()); - } - - if pyo3::ffi::Py_TYPE(ptr) == crate::typeref::STD_DECIMAL_TYPE - || pyo3::ffi::PyFloat_CheckExact(ptr) == 1 - { - return Ok(()); - } - - if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { - let val = super::primitives::_serialize_string(py, ptr).map_err(|x| { - pyo3::PyErr::new::(x.to_string()) - })?; - - rust_decimal::Decimal::from_str_exact(&val).or_else(|_| { - rust_decimal::Decimal::from_scientific(&val).map_err(|x| { - pyo3::PyErr::new::(x.to_string()) - }) - })?; - return Ok(()); - } - - crate::new_error!( - PyTypeError, - "expected decimal.Decimal/int/float/str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - if pyo3::ffi::PyLong_CheckExact(ptr) == 1 { - let mut has_overflow: i32 = 0; - let num = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut has_overflow); - - if has_overflow == 1 { - return Err(pyo3::exceptions::PyOverflowError::new_err("out of range")); - } - - return Ok(sea_query::Value::Decimal(Some(Box::new( - rust_decimal::Decimal::new(num, 0), - )))); - } - - if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { - use rust_decimal::prelude::FromPrimitive; - let num = pyo3::ffi::PyFloat_AsDouble(ptr); - - return rust_decimal::Decimal::from_f64(num) - .ok_or_else(|| { - pyo3::PyErr::new::( - "This value cannot be represented as a decimal", - ) - }) - .map(|x| sea_query::Value::Decimal(Some(Box::new(x)))); - } - - if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { - let val = super::primitives::_serialize_string(py, ptr).map_err(|x| { - pyo3::PyErr::new::(x.to_string()) - })?; - - return rust_decimal::Decimal::from_str_exact(&val) - .or_else(|_| { - rust_decimal::Decimal::from_scientific(&val).map_err(|x| { - pyo3::PyErr::new::(x.to_string()) - }) - }) - .map(|x| sea_query::Value::Decimal(Some(Box::new(x)))); - } - - if pyo3::ffi::Py_TYPE(ptr) == crate::typeref::STD_DECIMAL_TYPE { - let borrowed = pyo3::Borrowed::from_ptr(py, ptr); - let val = borrowed.to_string(); - - return rust_decimal::Decimal::from_str_radix(&val, 10) - .or_else(|_| { - rust_decimal::Decimal::from_scientific(&val).map_err(|x| { - pyo3::PyErr::new::(x.to_string()) - }) - }) - .map(|x| sea_query::Value::Decimal(Some(Box::new(x)))); - } - - crate::new_error!( - PyTypeError, - "expected decimal.Decimal/int/float/str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - use pyo3::IntoPyObject; - - match value { - sea_query::Value::Decimal(Some(x)) => x.into_pyobject(py).map(|x| x.into_ptr()), - sea_query::Value::Decimal(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected decimal for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyUUIDType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Uuid - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::Py_TYPE(ptr) != crate::typeref::STD_UUID_TYPE { - crate::new_error!( - PyTypeError, - "expected uuid.UUID for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let result = pyo3::Bound::from_borrowed_ptr(py, ptr) - .clone() - .extract::<::uuid::Uuid>()?; - - Ok(sea_query::Value::Uuid(Some(Box::new(result)))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Uuid(Some(x)) => { - let result = x.clone().into_pyobject(py)?; - Ok(result.into_ptr()) - } - sea_query::Value::Uuid(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected uuid for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyINETType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Inet - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - let string = super::primitives::_serialize_string(py, ptr)?; - - ipnetwork::Ipv4Network::from_str(&string) - .map_err(|x| pyo3::exceptions::PyValueError::new_err(x.to_string()))?; - - Ok(()) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let string = super::primitives::_serialize_string(py, ptr)?; - - ipnetwork::Ipv4Network::from_str(&string) - .map(|x| sea_query::Value::IpNetwork(Some(Box::new(ipnetwork::IpNetwork::V4(x))))) - .map_err(|x| pyo3::exceptions::PyValueError::new_err(x.to_string())) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::IpNetwork(Some(x)) => { - super::primitives::_deserialize_string(py, &x.to_string()) - } - sea_query::Value::IpNetwork(None) => Ok(pyo3::ffi::Py_None()), - sea_query::Value::String(Some(x)) => super::primitives::_deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected ipnetwork/str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyMacAddressType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::MacAddr - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - let string = super::primitives::_serialize_string(py, ptr)?; - - mac_address::MacAddress::from_str(&string) - .map_err(|x| pyo3::exceptions::PyValueError::new_err(x.to_string()))?; - - Ok(()) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let string = super::primitives::_serialize_string(py, ptr)?; - - mac_address::MacAddress::from_str(&string) - .map(|x| sea_query::Value::MacAddress(Some(Box::new(x)))) - .map_err(|x| pyo3::exceptions::PyValueError::new_err(x.to_string())) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::MacAddress(Some(x)) => { - super::primitives::_deserialize_string(py, &x.to_string()) - } - sea_query::Value::MacAddress(None) => Ok(pyo3::ffi::Py_None()), - sea_query::Value::String(Some(x)) => super::primitives::_deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected mac_address/str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -const ENUM_TYPE_VALUE: &std::ffi::CStr = c"value"; - -impl SQLTypeTrait for PyEnumType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Enum { - name: self.name.clone(), - variants: self.variants.clone(), - } - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - // https://github.com/ijl/orjson/blob/master/src/util.rs#L55 - if (*pyo3::ffi::Py_TYPE(ptr)).ob_base.ob_base.ob_type == crate::typeref::STD_ENUM_TYPE { - let attribute = pyo3::ffi::PyObject_GetAttrString(ptr, ENUM_TYPE_VALUE.as_ptr()); - - if attribute.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - if pyo3::ffi::PyUnicode_CheckExact(attribute) == 0 { - let type_error = crate::new_error!( - PyTypeError, - "Enum value wasn't str for {} serialization, was {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, attribute) - ); - - pyo3::ffi::Py_DECREF(attribute); - return type_error; - } - } else if pyo3::ffi::PyUnicode_CheckExact(ptr) == 0 { - return crate::new_error!( - PyTypeError, - "expected Enum/str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ); - } - - Ok(()) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - let string = { - if (*pyo3::ffi::Py_TYPE(ptr)).ob_base.ob_base.ob_type == crate::typeref::STD_ENUM_TYPE { - let attribute = pyo3::ffi::PyObject_GetAttrString(ptr, ENUM_TYPE_VALUE.as_ptr()); - - if attribute.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - // SAFETY: Type of the attribute was checked in `validate` method - attribute - } else { - pyo3::ffi::Py_INCREF(ptr); - - // SAFETY: Type of the attribute was checked in `validate` method - ptr - } - }; - - let serialized = super::primitives::_serialize_string(py, string); - pyo3::ffi::Py_DECREF(string); - - serialized.map(|x| sea_query::Value::String(Some(Box::new(x)))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::String(Some(x)) => super::primitives::_deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!(PyUUIDType); -super::abstracts::implement_sqltype_pymethods!(PyINETType); -super::abstracts::implement_sqltype_pymethods!(PyMacAddressType); -super::abstracts::implement_sqltype_pymethods!( - PyDecimalType, - init(|context: Option<(u32, u32)>| Self(context)), - "tuple[int, int] | None", - signature(context = None) -); - -#[pyo3::pymethods] -impl PyEnumType { - #[new] - fn __new__(name: String, variants: Vec) -> (Self, PySQLTypeAbstract) { - let slf = Self { - name: sea_query::Alias::new(name).into_iden(), - variants: variants - .into_iter() - .map(|x| sea_query::Alias::new(x).into_iden()) - .collect(), - }; - - (slf, PySQLTypeAbstract) - } - - /// Type name. e.g. `'INTEGER'`, `'STRING'` - /// - /// It also may be a property. This function must NOT raise any error. - #[getter] - fn __type_name__(&self) -> String { - self.to_sql_type_name() - } - - #[getter] - fn name(&self) -> String { - self.name.to_string() - } - - #[getter] - fn variants(&self) -> Vec { - self.variants.iter().map(|x| x.to_string()).collect() - } - - fn __repr__(&self) -> String { - format!( - "EnumType(name={:?}, variants={:?})", - self.name.to_string(), - self.variants.iter().map(|x| x.to_string()) - ) - } -} diff --git a/src/sqltypes/primitives.rs b/src/sqltypes/primitives.rs deleted file mode 100644 index a4cb8ff..0000000 --- a/src/sqltypes/primitives.rs +++ /dev/null @@ -1,556 +0,0 @@ -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -crate::implement_pyclass! { - /// Boolean column type (BOOLEAN). - /// - /// Stores true/false values. The standard way to store boolean data, - /// though implementation varies by database (some use TINYINT(1) or - /// similar representations). - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBooleanType as "Boolean"; -} -crate::implement_pyclass! { - /// Very small integer column type (TINYINT). - /// - /// Typically stores integers in the range -128 to 127 (signed) or 0 to 255 - /// (unsigned). Useful for flags, small counters, or enumerated values. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyTinyIntegerType as "TinyInteger"; -} -crate::implement_pyclass! { - /// Small integer column type (SMALLINT). - /// - /// Typically stores integers in the range -32,768 to 32,767 (signed) or - /// 0 to 65,535 (unsigned). Good for moderate-sized counters or numeric codes. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PySmallIntegerType as "SmallInteger"; -} -crate::implement_pyclass! { - /// Standard integer column type (INTEGER/INT). - /// - /// The most common integer type, typically storing 32-bit integers in the - /// range -2,147,483,648 to 2,147,483,647 (signed). Suitable for most - /// numeric data including IDs, quantities, and counters. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyIntegerType as "Integer"; -} -crate::implement_pyclass! { - /// Large integer column type (BIGINT). - /// - /// Stores 64-bit integers for very large numeric values. Essential for - /// high-volume systems, timestamps, large counters, or when integer - /// overflow is a concern. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBigIntegerType as "BigInteger"; -} -crate::implement_pyclass! { - /// Unsigned tiny integer column type. - /// - /// Stores small positive integers only, typically 0 to 255. Useful for - /// small counters, percentages, or enumerated values that are always positive. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyTinyUnsignedType as "TinyUnsigned"; -} -crate::implement_pyclass! { - /// Unsigned small integer column type. - /// - /// Stores moderate positive integers only, typically 0 to 65,535. Good for - /// larger counters or numeric codes that are always positive. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PySmallUnsignedType as "SmallUnsigned"; -} -crate::implement_pyclass! { - /// Unsigned integer column type. - /// - /// Stores positive integers only, typically 0 to 4,294,967,295. Doubles the - /// positive range compared to signed integers, useful for IDs and counters - /// that will never be negative. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyUnsignedType as "Unsigned"; -} -crate::implement_pyclass! { - /// Unsigned big integer column type. - /// - /// Stores very large positive integers only. Provides the maximum positive - /// integer range for high-volume systems or when very large positive - /// values are required. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyBigUnsignedType as "BigUnsigned"; -} -crate::implement_pyclass! { - /// Single-precision floating point column type (FLOAT). - /// - /// Stores approximate numeric values with single precision. Suitable for - /// scientific calculations, measurements, or any numeric data where some - /// precision loss is acceptable in exchange for storage efficiency. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyFloatType as "Float"; -} -crate::implement_pyclass! { - /// Double-precision floating point column type (DOUBLE). - /// - /// Stores approximate numeric values with double precision. Provides higher - /// precision than FLOAT for scientific calculations or when more accuracy - /// is required in floating-point operations. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyDoubleType as "Double"; -} -crate::implement_pyclass! { - /// Large text column type (TEXT). - /// - /// Represents a large text field capable of storing long strings without - /// a predefined length limit. Suitable for storing articles, comments, - /// descriptions, or any text content that may be very long. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyTextType as "Text"; -} -crate::implement_pyclass! { - /// Fixed-length character string column type (CHAR). - /// - /// Represents a fixed-length character string. Values shorter than the - /// specified length are padded with spaces. Suitable for storing data - /// with consistent, known lengths like country codes or status flags. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyCharType as "Char" (pub Option); -} -crate::implement_pyclass! { - /// Variable-length character string column type (VARCHAR). - /// - /// Represents a variable-length character string with a maximum length limit. - /// This is the most common string type for storing text data of varying lengths - /// like names, descriptions, or user input. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyStringType as "String" (pub Option); -} - -impl SQLTypeTrait for PyBooleanType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Boolean - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyBool_Check(ptr) != 1 { - crate::new_error!( - PyTypeError, - "expected bool for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - _py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - if pyo3::ffi::Py_True() == ptr { - Ok(sea_query::Value::Bool(Some(true))) - } else { - Ok(sea_query::Value::Bool(Some(false))) - } - } - - unsafe fn deserialize( - &self, - _py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Bool(Some(x)) if *x => Ok(pyo3::ffi::Py_True()), - sea_query::Value::Bool(Some(x)) if !*x => Ok(pyo3::ffi::Py_False()), - sea_query::Value::Bool(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected bool for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -macro_rules! implement_numeric_SQLTypeTrait { - ( - $name:ident, - $type:ty, - $python_type_name:literal, - $column_type:ident, - $value_type:ident, - $convertfunction:ident, - $($checkfunction:ident,)+ - ) => { - impl SQLTypeTrait for $name { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::$column_type - } - - unsafe fn validate(&self, py: pyo3::Python, ptr: *mut pyo3::ffi::PyObject) -> pyo3::PyResult<()> { - self.serialize(py, ptr)?; - Ok(()) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - $( - if pyo3::ffi::$checkfunction(ptr) == 1 { - let val = pyo3::ffi::$convertfunction(ptr); - - if !pyo3::ffi::PyErr_Occurred().is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - return TryInto::<$type>::try_into(val) - .map_err(|_| pyo3::exceptions::PyOverflowError::new_err("out of range")) - .map(|x| sea_query::Value::$value_type(Some(x))); - } - )+ - - crate::new_error!( - PyTypeError, - concat!("expected ", $python_type_name, " for {} serialization, got {}"), - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - use pyo3::IntoPyObject; - - match value { - sea_query::Value::BigInt(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::Int(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::SmallInt(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::TinyInt(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::BigUnsigned(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::Unsigned(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::SmallUnsigned(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::TinyUnsigned(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::Double(Some(x)) => Ok((*x).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::Float(Some(x)) => Ok((*x as f64).into_pyobject(py).unwrap().into_ptr()), - sea_query::Value::BigInt(None) - | sea_query::Value::Int(None) - | sea_query::Value::SmallInt(None) - | sea_query::Value::TinyInt(None) - | sea_query::Value::BigUnsigned(None) - | sea_query::Value::Unsigned(None) - | sea_query::Value::SmallUnsigned(None) - | sea_query::Value::TinyUnsigned(None) => Ok(py.None().into_ptr()), - _ => crate::new_error!( - PyTypeError, - "expected int for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } - } - }; -} - -implement_numeric_SQLTypeTrait!( - PyBigIntegerType, - i64, - "int", - BigInteger, - BigInt, - PyLong_AsLongLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyIntegerType, - i32, - "int", - Integer, - Int, - PyLong_AsLongLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PySmallIntegerType, - i16, - "int", - SmallInteger, - SmallInt, - PyLong_AsLongLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyTinyIntegerType, - i8, - "int", - TinyInteger, - TinyInt, - PyLong_AsLongLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyBigUnsignedType, - u64, - "int", - BigUnsigned, - BigUnsigned, - PyLong_AsUnsignedLongLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyUnsignedType, - u32, - "int", - Unsigned, - Unsigned, - PyLong_AsUnsignedLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PySmallUnsignedType, - u16, - "int", - SmallUnsigned, - SmallUnsigned, - PyLong_AsUnsignedLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyTinyUnsignedType, - u8, - "int", - TinyUnsigned, - TinyUnsigned, - PyLong_AsUnsignedLong, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyFloatType, - // We're using f64 instead of f32 'cause we don't have f32::try_from(f64) - f64, - "float", - Float, - Double, - PyFloat_AsDouble, - PyFloat_CheckExact, - PyLong_CheckExact, -); -implement_numeric_SQLTypeTrait!( - PyDoubleType, - f64, - "float", - Float, - Double, - PyFloat_AsDouble, - PyFloat_CheckExact, - PyLong_CheckExact, -); - -#[inline] -pub(super) unsafe fn _serialize_string( - py: pyo3::Python, - object: *mut pyo3::ffi::PyObject, -) -> pyo3::PyResult { - let mut size: pyo3::ffi::Py_ssize_t = 0; - let c_str = pyo3::ffi::PyUnicode_AsUTF8AndSize(object, &mut size); - - if c_str.is_null() || size < 0 { - Err(pyo3::PyErr::fetch(py)) - } else { - let val = std::ffi::CStr::from_ptr(c_str); - Ok(std::str::from_utf8_unchecked(val.to_bytes()).into()) - } -} - -#[inline] -pub(super) unsafe fn _deserialize_string( - py: pyo3::Python, - value: &str, -) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - let val = pyo3::types::PyString::intern(py, std::str::from_utf8_unchecked(value.as_bytes())); - Ok(val.into_ptr()) -} - -impl SQLTypeTrait for PyTextType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Text - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyUnicode_CheckExact(ptr) == 0 { - crate::new_error!( - PyTypeError, - "expected str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - object: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_string(py, object).map(|x| sea_query::Value::String(Some(Box::new(x)))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::String(Some(x)) => _deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyCharType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Char(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyUnicode_CheckExact(ptr) == 0 { - crate::new_error!( - PyTypeError, - "expected str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - object: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_string(py, object).map(|x| sea_query::Value::String(Some(Box::new(x)))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::String(Some(x)) => _deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyStringType { - #[inline(always)] - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::String( - self.0 - .map_or(sea_query::StringLen::None, sea_query::StringLen::N), - ) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - if pyo3::ffi::PyUnicode_CheckExact(ptr) == 0 { - crate::new_error!( - PyTypeError, - "expected str for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } else { - Ok(()) - } - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - object: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - _serialize_string(py, object).map(|x| sea_query::Value::String(Some(Box::new(x)))) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::String(Some(x)) => _deserialize_string(py, x), - sea_query::Value::String(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected str for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!(PyBooleanType); -super::abstracts::implement_sqltype_pymethods!(PyBigIntegerType); -super::abstracts::implement_sqltype_pymethods!(PyIntegerType); -super::abstracts::implement_sqltype_pymethods!(PySmallIntegerType); -super::abstracts::implement_sqltype_pymethods!(PyTinyIntegerType); -super::abstracts::implement_sqltype_pymethods!(PyBigUnsignedType); -super::abstracts::implement_sqltype_pymethods!(PyUnsignedType); -super::abstracts::implement_sqltype_pymethods!(PySmallUnsignedType); -super::abstracts::implement_sqltype_pymethods!(PyTinyUnsignedType); -super::abstracts::implement_sqltype_pymethods!(PyFloatType); -super::abstracts::implement_sqltype_pymethods!(PyDoubleType); -super::abstracts::implement_sqltype_pymethods!(PyTextType); -super::abstracts::implement_sqltype_pymethods!( - PyCharType, - init(|length: Option| Self(length)), - "int | None", - signature(length = None) -); -super::abstracts::implement_sqltype_pymethods!( - PyStringType, - init(|length: Option| Self(length)), - "int | None", - signature(length = None) -); diff --git a/src/sqltypes/vector.rs b/src/sqltypes/vector.rs deleted file mode 100644 index 7a1081c..0000000 --- a/src/sqltypes/vector.rs +++ /dev/null @@ -1,337 +0,0 @@ -use pyo3::types::{PyListMethods, PyTupleMethods}; -use pyo3::IntoPyObject; - -use crate::internal::type_engine::TypeEngine; -use crate::internal::RefBoundObject; -use crate::sqltypes::abstracts::{PySQLTypeAbstract, SQLTypeTrait}; - -crate::implement_pyclass! { - /// Vector column type for storing mathematical vectors. - /// - /// Specialized type for storing vector data, often used in machine learning, - /// similarity search, or mathematical applications. - #[derive(Debug, Clone, Copy)] - [extends=PySQLTypeAbstract] PyVectorType as "Vector" (pub Option); -} -crate::implement_pyclass! { - /// Array column type for storing arrays of elements. - /// - /// Represents a column that stores arrays of a specified element type. - /// Useful in databases that support native array types (like PostgreSQL) - /// for storing lists of values in a single column. - #[derive(Clone)] - [extends=PySQLTypeAbstract] PyArrayType as "Array" (pub TypeEngine); -} - -#[inline] -#[cfg_attr(feature = "optimize", optimize(speed))] -unsafe fn _validate_iterable_ptr( - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - sql_type_name: &str, - mut condition: F, -) -> pyo3::PyResult<()> -where - F: FnMut(*mut pyo3::ffi::PyObject) -> bool, -{ - // We won't check all of elements, we only check some of them to improve performance `O(log(n))`. - - if pyo3::ffi::PyList_CheckExact(ptr) == 1 { - let mut size = pyo3::ffi::PyList_Size(ptr); - if size == 0 { - return Ok(()); - } - - while size > 0 { - // item is a owned pointer - let item = pyo3::ffi::PyList_GetItem(ptr, size - 1); - if item.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - if !condition(item) { - return Err(pyo3::exceptions::PyTypeError::new_err( - "invalid type found in the list", - )); - } - - size /= 2; - } - - return Ok(()); - } - - if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { - let mut size = pyo3::ffi::PyTuple_Size(ptr); - if size == 0 { - return Ok(()); - } - - while size > 0 { - // item is a borrowed pointer - let item = pyo3::ffi::PyTuple_GetItem(ptr, size - 1); - if item.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - if !condition(item) { - return Err(pyo3::exceptions::PyTypeError::new_err( - "invalid type found in the tuple", - )); - } - - size /= 2; - } - - return Ok(()); - } - - crate::new_error!( - PyTypeError, - "expected list/tuple for {} serialization, got {}", - sql_type_name, - crate::internal::get_type_name(py, ptr) - ) -} - -impl SQLTypeTrait for PyVectorType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Vector(self.0) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - _validate_iterable_ptr(py, ptr, &self.to_sql_type_name(), |item| { - pyo3::ffi::PyFloat_CheckExact(item) == 1 || pyo3::ffi::PyLong_CheckExact(item) == 1 - }) - } - - #[cfg_attr(feature = "optimize", optimize(speed))] - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - if pyo3::ffi::PyList_CheckExact(ptr) == 1 { - let bound = pyo3::Bound::from_borrowed_ptr(py, ptr); - let list = bound.cast_unchecked::(); - - let mut values: Vec = Vec::with_capacity(list.len()); - - for item in list.iter() { - if pyo3::ffi::PyFloat_CheckExact(item.as_ptr()) == 0 - && pyo3::ffi::PyLong_CheckExact(item.as_ptr()) == 0 - { - return crate::new_error!( - PyTypeError, - "expected list of floats for {} serialization, found {} in the list", - self.to_sql_type_name(), - crate::internal::get_type_name(py, item.as_ptr()) - ); - } - - values.push(pyo3::ffi::PyFloat_AsDouble(item.as_ptr()) as f32); - } - - return Ok(sea_query::Value::Vector(Some(Box::new(values.into())))); - } - - if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { - let bound = pyo3::Bound::from_borrowed_ptr(py, ptr); - let list = bound.cast_unchecked::(); - - let mut values: Vec = Vec::with_capacity(list.len()); - - for item in list.iter() { - if pyo3::ffi::PyFloat_CheckExact(item.as_ptr()) == 0 - && pyo3::ffi::PyLong_CheckExact(item.as_ptr()) == 0 - { - return crate::new_error!( - PyTypeError, - "expected tuple of floats for {} serialization, found {} in the tuple", - self.to_sql_type_name(), - crate::internal::get_type_name(py, item.as_ptr()) - ); - } - - values.push(pyo3::ffi::PyFloat_AsDouble(item.as_ptr()) as f32); - } - - return Ok(sea_query::Value::Vector(Some(Box::new(values.into())))); - } - - crate::new_error!( - PyTypeError, - "expected list[float]/tuple[float] for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } - - #[cfg_attr(feature = "optimize", optimize(speed))] - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Vector(Some(x)) => { - let list = x.as_slice().into_pyobject(py)?; - Ok(list.into_ptr()) - } - sea_query::Value::Vector(None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected vector for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -impl SQLTypeTrait for PyArrayType { - fn to_sea_query_column_type(&self) -> sea_query::ColumnType { - sea_query::ColumnType::Array(std::sync::Arc::new(self.0.to_sea_query_column_type())) - } - - unsafe fn validate( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult<()> { - _validate_iterable_ptr(py, ptr, &self.to_sql_type_name(), |item| { - self.0.validate(py, item).is_ok() - }) - } - - unsafe fn serialize( - &self, - py: pyo3::Python, - ptr: *mut pyo3::ffi::PyObject, - ) -> pyo3::PyResult { - if pyo3::ffi::PyList_CheckExact(ptr) == 1 { - let bound = pyo3::Bound::from_borrowed_ptr(py, ptr); - let list = bound.cast_unchecked::(); - - let mut values: Vec = Vec::with_capacity(list.len()); - - for item in list.iter() { - let val = self.0.serialize(py, item.as_ptr())?; - values.push(val); - } - - return Ok(sea_query::Value::Array( - // array type is not important - sea_query::ArrayType::BigInt, - Some(Box::new(values)), - )); - } - - if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { - let bound = pyo3::Bound::from_borrowed_ptr(py, ptr); - let list = bound.cast_unchecked::(); - - let mut values: Vec = Vec::with_capacity(list.len()); - - for item in list.iter() { - let val = self.0.serialize(py, item.as_ptr())?; - values.push(val); - } - - return Ok(sea_query::Value::Array( - // array type is not important - sea_query::ArrayType::BigInt, - Some(Box::new(values)), - )); - } - - crate::new_error!( - PyTypeError, - "expected list/tuple for {} serialization, got {}", - self.to_sql_type_name(), - crate::internal::get_type_name(py, ptr) - ) - } - - unsafe fn deserialize( - &self, - py: pyo3::Python, - value: &sea_query::Value, - ) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { - match value { - sea_query::Value::Array(_, Some(array)) => { - let pylist = pyo3::ffi::PyList_New(array.len() as isize); - - if pylist.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - - for (index, item) in array.iter().enumerate() { - let result = self.0.deserialize(py, item); - - match result { - Ok(x) => { - if pyo3::ffi::PyList_SetItem(pylist, index as isize, x) == 0 { - pyo3::ffi::Py_DECREF(x); - pyo3::ffi::Py_DECREF(pylist); - return Err(pyo3::PyErr::fetch(py)); - } - } - Err(e) => { - pyo3::ffi::Py_DECREF(pylist); - return Err(e); - } - } - } - - Ok(pylist) - } - sea_query::Value::Array(_, None) => Ok(pyo3::ffi::Py_None()), - _ => crate::new_error!( - PyTypeError, - "expected array for {} deserialization, got {:?}", - self.to_sql_type_name(), - value - ), - } - } -} - -super::abstracts::implement_sqltype_pymethods!( - PyVectorType, - init(|length: Option| Self(length)), - "int | None", - signature(length = None) -); - -#[pyo3::pymethods] -impl PyArrayType { - #[new] - fn __new__(element: RefBoundObject<'_>) -> pyo3::PyResult<(Self, PySQLTypeAbstract)> { - let type_engine = TypeEngine::new(element)?; - - Ok((Self(type_engine), PySQLTypeAbstract)) - } - - /// Type name. e.g. `'INTEGER'`, `'STRING'` - /// - /// It also may be a property. This function must NOT raise any error. - #[getter] - fn __type_name__(&self) -> String { - self.to_sql_type_name() - } - - #[getter] - fn element<'py>(&self, py: pyo3::Python<'py>) -> pyo3::Bound<'py, pyo3::PyAny> { - let ptr = self.0 .1.as_ptr(); - unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) } - } - - fn __repr__(&self) -> String { - format!("ArrayType(element={})", self.0) - } -} diff --git a/src/tcl/mod.rs b/src/tcl/mod.rs new file mode 100644 index 0000000..eb87779 --- /dev/null +++ b/src/tcl/mod.rs @@ -0,0 +1,12 @@ +//! Transaction Control Language + +/// Base of every TCL statement. Construct a subclass, not this class. +#[rapidquery_macros::pyclass("rapidquery.tcl.TransactionControlLanguage", subclass, skip_from_py_object)] +#[derive(Debug, Clone, Copy)] +pub struct PyTransactionControlLanguage; + +#[pyo3::pymodule(name = "tcl", gil_used = false)] +pub mod tcl_mod { + #[pymodule_export] + use super::PyTransactionControlLanguage; +} diff --git a/src/typeref.rs b/src/typeref.rs index 218b5ab..c45a99f 100644 --- a/src/typeref.rs +++ b/src/typeref.rs @@ -1,84 +1,49 @@ -// Column types -pub(crate) static mut BLOB_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut BINARY_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut VAR_BINARY_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut BIT_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut VAR_BIT_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut DATETIME_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TIMESTAMP_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TIME_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut DATE_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut JSON_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut JSON_BINARY_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut DECIMAL_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut UUID_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut INET_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut MAC_ADDRESS_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut BOOLEAN_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TINY_INTEGER_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut SMALL_INTEGER_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = - std::ptr::null_mut(); -pub(crate) static mut INTEGER_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut BIG_INTEGER_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TINY_UNSIGNED_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = - std::ptr::null_mut(); -pub(crate) static mut SMALL_UNSIGNED_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = - std::ptr::null_mut(); -pub(crate) static mut UNSIGNED_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut BIG_UNSIGNED_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut FLOAT_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut DOUBLE_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TEXT_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut CHAR_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut STRING_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut VECTOR_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut ARRAY_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut ENUM_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); +//! Interned `PyTypeObject*` cache. +// Dialect types +pub(crate) static mut DIALECT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); + +// Serializer types +pub(crate) static mut SERIALIZER_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); + +// Ident types +pub(crate) static mut IDENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); +pub(crate) static mut UNQUOTE_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); +pub(crate) static mut ASTERISK_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); + +// Value type pub(crate) static mut VALUE_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut COLUMN_REF_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TABLE_NAME_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); + +// Expr types pub(crate) static mut EXPR_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut FUNC_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut INDEX_COLUMN_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut INDEX_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut FOREIGN_KEY_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut TABLE_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut ON_CONFLICT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut WINDOW_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut SELECT_LABEL_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut SELECT_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut DELETE_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut UPDATE_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut INSERT_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut CASE_STATEMENT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut WITH_CLAUSE_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut ALTER_TABLE_BASE_OPTION_TYPE: *mut pyo3::ffi::PyTypeObject = - std::ptr::null_mut(); - -// Python standard libraries types + +// Python standard-library types (serializers) pub(crate) static mut STD_DECIMAL_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); pub(crate) static mut STD_UUID_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); pub(crate) static mut STD_DATETIME_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); pub(crate) static mut STD_DATE_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); pub(crate) static mut STD_TIME_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -pub(crate) static mut STD_ENUM_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); -unsafe fn get_type_object_for( - py: pyo3::Python, -) -> *mut pyo3::ffi::PyTypeObject { +unsafe fn get_type_object_for(py: pyo3::Python) -> *mut pyo3::ffi::PyTypeObject { + // Borrowed type object; lives as long as the `#[pyclass]` is registered + // on `rapidquery._core`, which is the process lifetime of this extension. T::type_object_raw(py) } +/// Look up `module_name.member_name` and return its type object. +/// +/// # Safety +/// +/// `PyImport_ImportModule` / dict lookup are not null-checked. A missing +/// stdlib module currently yields a null pointer (and a later crash if +/// dereferenced). Call only during module init, when the stdlib is present. unsafe fn look_up_type_object( module_name: &std::ffi::CStr, member_name: &std::ffi::CStr, ) -> *mut pyo3::ffi::PyTypeObject { let module = pyo3::ffi::PyImport_ImportModule(module_name.as_ptr()); let module_dict = pyo3::ffi::PyObject_GenericGetDict(module, std::ptr::null_mut()); - let ptr = pyo3::ffi::PyMapping_GetItemString(module_dict, member_name.as_ptr()) - .cast::(); + let ptr = pyo3::ffi::PyMapping_GetItemString(module_dict, member_name.as_ptr()).cast::(); pyo3::ffi::Py_DECREF(module_dict); pyo3::ffi::Py_DECREF(module); @@ -92,72 +57,32 @@ macro_rules! multiple_get_type_object_for { } #[cold] -#[cfg_attr(feature = "optimize", optimize(size))] fn _initialize_typeref(py: pyo3::Python) { unsafe { + // SAFETY: called once from [`initialize_typeref`] while the GIL is + // held (module init). The `static mut` cells are still null; we + // write each pointer exactly once. After this function returns they + // are only read. multiple_get_type_object_for!( py, - crate::sqltypes::PyBlobType => BLOB_COLUMN_TYPE, - crate::sqltypes::PyBinaryType => BINARY_COLUMN_TYPE, - crate::sqltypes::PyVarBinaryType => VAR_BINARY_COLUMN_TYPE, - crate::sqltypes::PyBitType => BIT_COLUMN_TYPE, - crate::sqltypes::PyVarBitType => VAR_BIT_COLUMN_TYPE, - crate::sqltypes::PyDateTimeType => DATETIME_COLUMN_TYPE, - crate::sqltypes::PyTimestampType => TIMESTAMP_COLUMN_TYPE, - crate::sqltypes::PyTimeType => TIME_COLUMN_TYPE, - crate::sqltypes::PyDateType => DATE_COLUMN_TYPE, - crate::sqltypes::PyJSONType => JSON_COLUMN_TYPE, - crate::sqltypes::PyJSONBinaryType => JSON_BINARY_COLUMN_TYPE, - crate::sqltypes::PyDecimalType => DECIMAL_COLUMN_TYPE, - crate::sqltypes::PyUUIDType => UUID_COLUMN_TYPE, - crate::sqltypes::PyINETType => INET_COLUMN_TYPE, - crate::sqltypes::PyMacAddressType => MAC_ADDRESS_COLUMN_TYPE, - crate::sqltypes::PyBooleanType => BOOLEAN_COLUMN_TYPE, - crate::sqltypes::PyTinyIntegerType => TINY_INTEGER_COLUMN_TYPE, - crate::sqltypes::PySmallIntegerType => SMALL_INTEGER_COLUMN_TYPE, - crate::sqltypes::PyIntegerType => INTEGER_COLUMN_TYPE, - crate::sqltypes::PyBigIntegerType => BIG_INTEGER_COLUMN_TYPE, - crate::sqltypes::PyTinyUnsignedType => TINY_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PySmallUnsignedType => SMALL_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyUnsignedType => UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyBigUnsignedType => BIG_UNSIGNED_COLUMN_TYPE, - crate::sqltypes::PyFloatType => FLOAT_COLUMN_TYPE, - crate::sqltypes::PyDoubleType => DOUBLE_COLUMN_TYPE, - crate::sqltypes::PyTextType => TEXT_COLUMN_TYPE, - crate::sqltypes::PyCharType => CHAR_COLUMN_TYPE, - crate::sqltypes::PyStringType => STRING_COLUMN_TYPE, - crate::sqltypes::PyVectorType => VECTOR_COLUMN_TYPE, - crate::sqltypes::PyArrayType => ARRAY_COLUMN_TYPE, - crate::sqltypes::PyEnumType => ENUM_COLUMN_TYPE, - crate::common::value::PyValue => VALUE_TYPE, - crate::common::table_ref::PyTableName => TABLE_NAME_TYPE, - crate::common::column_ref::PyColumnRef => COLUMN_REF_TYPE, - crate::common::expression::PyExpr => EXPR_TYPE, - crate::common::expression::PyFunc => FUNC_TYPE, - crate::common::column::PyColumn => COLUMN_TYPE, - crate::common::foreign_key::PyForeignKey => FOREIGN_KEY_TYPE, - crate::schema::index::PyIndexColumn => INDEX_COLUMN_TYPE, - crate::schema::index::PyIndex => INDEX_TYPE, - crate::schema::alter_table::PyAlterTableBaseOption => ALTER_TABLE_BASE_OPTION_TYPE, - crate::schema::table::PyTable => TABLE_TYPE, - crate::query::on_conflict::PyOnConflict => ON_CONFLICT_TYPE, - crate::query::window::PyWindowStatement => WINDOW_STATEMENT_TYPE, - crate::query::select::PySelectLabel => SELECT_LABEL_TYPE, - crate::query::select::PySelectStatement => SELECT_STATEMENT_TYPE, - crate::query::delete::PyDeleteStatement => DELETE_STATEMENT_TYPE, - crate::query::update::PyUpdateStatement => UPDATE_STATEMENT_TYPE, - crate::query::insert::PyInsertStatement => INSERT_STATEMENT_TYPE, - crate::query::case::PyCaseStatement => CASE_STATEMENT_TYPE, - crate::query::with::PyWithClause => WITH_CLAUSE_TYPE, + // dialects + crate::dialect::PyDialect => DIALECT_TYPE, + // serializers + crate::serializers::PySerializer => SERIALIZER_TYPE, + // exprs + crate::expr::base::PyExpr => EXPR_TYPE, + crate::expr::ident::PyIdent => IDENT_TYPE, + crate::expr::ident::PyUnquote => UNQUOTE_TYPE, + crate::expr::ident::PyAsterisk => ASTERISK_TYPE, + crate::expr::value::PyValue => VALUE_TYPE, ); STD_DECIMAL_TYPE = look_up_type_object(c"decimal", c"Decimal"); STD_UUID_TYPE = look_up_type_object(c"uuid", c"UUID"); - STD_ENUM_TYPE = look_up_type_object(c"enum", c"EnumMeta"); pyo3::ffi::PyDateTime_IMPORT(); - let datetime_capsule = pyo3::ffi::PyCapsule_Import(c"datetime.datetime_CAPI".as_ptr(), 1) - .cast::(); + let datetime_capsule = + pyo3::ffi::PyCapsule_Import(c"datetime.datetime_CAPI".as_ptr(), 1).cast::(); STD_DATETIME_TYPE = (*datetime_capsule).DateTimeType; STD_DATE_TYPE = (*datetime_capsule).DateType; @@ -165,8 +90,35 @@ fn _initialize_typeref(py: pyo3::Python) { } } +/// Fill the type-pointer cache. Safe to call more than once; only the first +/// call does work. Must run from `#[pymodule_init]` before any extractor +/// that reads the `static mut` cells. pub fn initialize_typeref(py: pyo3::Python) { static INIT: std::sync::Once = std::sync::Once::new(); INIT.call_once(|| _initialize_typeref(py)); } + +/// `type(obj) is T` — no subclass match. +/// +/// # Safety +/// +/// `ptr` must be a valid, non-null `PyObject*`. `r#type` must be a valid +/// `PyTypeObject*` (interned pointers from [`initialize_typeref`] are fine). +/// The GIL must be held. +#[inline(always)] +pub unsafe fn check_exact(ptr: *mut pyo3::ffi::PyObject, r#type: *mut pyo3::ffi::PyTypeObject) -> bool { + pyo3::ffi::Py_TYPE(ptr) == r#type +} + +/// `isinstance(obj, T)`. +/// +/// # Safety +/// +/// `ptr` must be a valid, non-null `PyObject*`. `r#type` must be a valid +/// `PyTypeObject*` (interned pointers from [`initialize_typeref`] are fine). +/// The GIL must be held. +#[inline(always)] +pub unsafe fn check(ptr: *mut pyo3::ffi::PyObject, r#type: *mut pyo3::ffi::PyTypeObject) -> bool { + pyo3::ffi::PyObject_TypeCheck(ptr, r#type) == 1 +} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..13b4163 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,46 @@ +import importlib.util + +import pytest +from rapidquery._core import _feature_only_stdlib_json + +from rapidquery.dialect import ( + BigQuery, + Databricks, + Dialect, + DuckDb, + Hive, + MsSql, + MySql, + Oracle, + PostgreSql, + Redshift, + Snowflake, + Spark, + Sqlite, + Teradata, +) + +USING_ORJSON = importlib.util.find_spec("orjson") is not None and not _feature_only_stdlib_json + + +ALL_DIALECTS: list[Dialect] = [ + BigQuery, + Databricks, + DuckDb, + Hive, + MsSql, + MySql, + Oracle, + PostgreSql, + Redshift, + Snowflake, + Spark, + Sqlite, + Teradata, +] + + +@pytest.fixture(params=ALL_DIALECTS) +def dialect(request: pytest.FixtureRequest) -> Dialect: + """Every builtin Dialects.""" + return request.param diff --git a/tests/mixin.py b/tests/mixin.py deleted file mode 100644 index 6351bdc..0000000 --- a/tests/mixin.py +++ /dev/null @@ -1,70 +0,0 @@ -import typing - -import rapidquery as rq - - -class TableNameInitMixin: - table_name_instance: typing.Callable - - def test_table_name_init(self): - class Prop: - @property - def __table_name__(self): - return "world.public.users" - - class Var: - __table_name__ = "world.public.users" - - self.table_name_instance(rq.TableName("users", "public", "world")) - self.table_name_instance("world.public.users") - self.table_name_instance(Prop()) - self.table_name_instance(Var) - - -class ReturningMixin: - def get_statement(self): - raise NotImplementedError - - def test_returning(self): - stmt = self.get_statement().returning(rq.Returning("id")) - assert 'RETURNING "id"' in stmt.to_sql("postgres") - - stmt = self.get_statement().returning(rq.Returning.all()) - assert "RETURNING *" in stmt.to_sql("postgres") - - -class OrderByMixin: - def get_statement(self): - raise NotImplementedError - - def test_order_by(self): - stmt = self.get_statement().order_by(rq.Ordering("id", "DESC")) - assert 'ORDER BY "id" DESC' in stmt.to_sql("postgres") - - def test_clear_order_by(self): - stmt = self.get_statement().order_by(rq.Ordering("id")) - assert "ORDER BY" in stmt.to_sql("mysql") - - stmt = stmt.clear_order_by() - assert "ORDER BY" not in stmt.to_sql("mysql") - - -class WhereMixin: - def get_statement(self): - raise NotImplementedError - - def test_clear_where(self): - stmt = self.get_statement().where(rq.Expr.val(True)) - assert "WHERE" in stmt.to_sql("mysql") - - stmt = stmt.clear_where() - assert "WHERE" not in stmt.to_sql("mysql") - - def test_where(self): - stmt = ( - self.get_statement() - .where(rq.Expr.col("id") > 10) - .where(rq.Expr.col("name") < 20) - .to_sql("sqlite") - ) - assert 'WHERE "id" > 10 AND "name" < 20' in stmt diff --git a/tests/test_common.py b/tests/test_common.py deleted file mode 100644 index c1dce08..0000000 --- a/tests/test_common.py +++ /dev/null @@ -1,343 +0,0 @@ -import pytest - -import rapidquery as rq - - -class TestColumn: - def test_init(self): - rq.Column("id", rq.sqltypes.Integer()) - - with pytest.raises(TypeError): - rq.Column("id", rq.sqltypes.Integer) # type: ignore - - with pytest.raises(TypeError): - rq.Column("id", "INTEGER") # type: ignore - - rq.Column("id", rq.sqltypes.String(), default="") - rq.Column("id", rq.sqltypes.String(), default=None) - rq.Column("id", rq.sqltypes.String(), default=rq.Expr.val(232)) - rq.Column("id", rq.sqltypes.String(), default=None, generated=232) - - with pytest.raises(TypeError): - rq.Column("id", rq.sqltypes.String(), default=3243) - - col = rq.Column( - "id", - rq.sqltypes.Float(), - primary_key=True, - auto_increment=True, - nullable=False, - extra="HELLO", - comment="WOW", - default=4.3, - generated=98, - stored_generated=True, - ) - assert col.name == "id" - assert type(col.type) is rq.sqltypes.Float - assert col.primary_key - assert col.auto_increment - assert col.nullable is False - assert col.extra == "HELLO" - assert col.comment == "WOW" - assert type(col.default) is rq.Expr - assert type(col.generated) is rq.Expr - assert col.stored_generated - - def test_adapt(self): - col = rq.Column("name", rq.sqltypes.String()) - col.adapt("Ali") - col.adapt(None) - - with pytest.raises(TypeError): - col.adapt(4) # type: ignore - - def test_column_ref_property(self): - col = rq.Column("name", rq.sqltypes.String()) - rq.AlterTableDropColumnOption(col) - - def test_label(self): - col = rq.Column("name", rq.sqltypes.String()) - label = col.label("a") - assert type(label) is rq.SelectLabel - assert label.alias == "a" - assert label.window is None - - def test_to_expr(self): - col = rq.Column("name", rq.sqltypes.String()) - assert type(col.to_expr()) is rq.Expr - - -class TestColumnRef: - def test_new(self): - ref_1 = rq.ColumnRef("id") - assert ref_1.name == "id" - assert ref_1.table is None - assert ref_1.schema is None - - ref_2 = rq.ColumnRef("id", "characters", "public") - assert ref_2.name == "id" - assert ref_2.table == "characters" - assert ref_2.schema == "public" - - ref_3 = rq.ColumnRef.parse("public.characters.id") - assert ref_3.name == "id" - assert ref_3.table == "characters" - assert ref_3.schema == "public" - - assert ref_2 == ref_3 - assert ref_1 != ref_2 - - asterisk_1 = rq.ColumnRef("*") - assert asterisk_1.name == "*" - assert asterisk_1.table is None - assert asterisk_1.schema is None - - asterisk_2 = rq.ColumnRef("*", "characters") - assert asterisk_2.name == "*" - assert asterisk_2.table == "characters" - assert asterisk_2.schema is None - - asterisk_3 = rq.ColumnRef.parse("characters.*") - assert asterisk_3.name == "*" - assert asterisk_3.table == "characters" - assert asterisk_3.schema is None - - assert asterisk_2 == asterisk_3 - assert asterisk_1 != asterisk_2 - - def test_copy_with(self): - ref_1 = rq.ColumnRef("id") - assert ref_1.name == "id" - assert ref_1.table is None - assert ref_1.schema is None - - ref_2 = ref_1.copy_with(table="characters", schema="public") - assert ref_2.name == "id" - assert ref_2.table == "characters" - assert ref_2.schema == "public" - - ref_3 = ref_2.copy_with(name="*") - assert ref_3.name == "*" - assert ref_3.table == "characters" - assert ref_3.schema == "public" - - def test_try_from(self): - class Prop: - @property - def __column_ref__(self): - return "characters.id" - - class Var: - __column_ref__ = "characters.id" - - rq.Expr.col("characters.id") - rq.Expr.col(rq.ColumnRef("id", "characters")) - rq.Expr.col(Prop()) - rq.Expr.col(Var) - - def test_to_expr(self): - ref = rq.ColumnRef("id") - assert type(ref.to_expr()) is rq.Expr - - def test_label(self): - ref = rq.ColumnRef("id") - label = ref.label("a") - assert type(label) is rq.SelectLabel - assert label.alias == "a" - assert label.window is None - - -class SelectStatementChild(rq.SelectStatement): - pass - - -class TestExpr: - def test_new(self): - rq.Expr(rq.Expr.custom("WOW")) - rq.Expr(rq.Value(None)) - rq.Expr(rq.ColumnRef("id")) - rq.Expr(rq.Func("NOW")) - rq.Expr(rq.SelectStatement().columns("id")) - rq.Expr(SelectStatementChild().columns("id")) - rq.Expr(rq.CaseStatement().when(rq.Expr.col("aspect").in_([2, 4]), True).else_(False)) - - rq.Expr((rq.Expr.custom("TUPLE"), rq.Expr.custom("TUPLE"))) - - class Prop: - @property - def __expr__(self): - return rq.Expr.custom("PROPERTY") - - rq.Expr(Prop()) - - class ColumnRefProp: - @property - def __column_ref__(self): - return "characters.id" - - rq.Expr(ColumnRefProp()) - - rq.Expr(1) - rq.Expr("a") - rq.Expr(3.4) - rq.Expr(None) - rq.Expr(["a", "b"]) - - def test_val(self): - rq.Expr.val(1) - rq.Expr.val(1.4) - rq.Expr.val("a") - rq.Expr.val(None) - - with pytest.raises(TypeError): - rq.Expr.val("a", rq.sqltypes.Integer()) # type: ignore - - def test_exists(self): - with pytest.raises(TypeError): - rq.Expr.exists(1) # type: ignore - - stmt = rq.SelectStatement().columns("id") - assert "EXISTS" in rq.Expr.exists(stmt)._to_sql("postgres") - - stmt = SelectStatementChild().columns("id") - assert "EXISTS" in rq.Expr.exists(stmt)._to_sql("postgres") - - def test_all(self): - with pytest.raises(TypeError): - rq.Expr.all(1) # type: ignore - - stmt = rq.SelectStatement().columns("id") - assert "ALL" in rq.Expr.all(stmt)._to_sql("postgres") - - stmt = SelectStatementChild().columns("id") - assert "ALL" in rq.Expr.all(stmt)._to_sql("postgres") - - def test_any(self): - with pytest.raises(TypeError): - rq.Expr.any(1) # type: ignore - - stmt = rq.SelectStatement().columns("id") - assert "ANY" in rq.Expr.any(stmt)._to_sql("postgres") - - stmt = SelectStatementChild().columns("id") - assert "ANY" in rq.Expr.any(stmt)._to_sql("postgres") - - def test_some(self): - with pytest.raises(TypeError): - rq.Expr.some(1) # type: ignore - - stmt = rq.SelectStatement().columns("id") - assert "SOME" in rq.Expr.some(stmt)._to_sql("postgres") - - stmt = SelectStatementChild().columns("id") - assert "SOME" in rq.Expr.some(stmt)._to_sql("postgres") - - -class TestForeignKey: - def test_init(self): - rq.ForeignKey( - ["font_id"], - ["id"], - "fonts", - "fk_name", - on_delete="CASCADE", - on_update="NO ACTION", - ) - fk = rq.ForeignKey(["font_id"], ["fonts.id"]) - assert fk.to_table.name == "fonts" - - rq.ForeignKey(["font_id", "font_name"], ["fonts.id", "fonts.name"]) - - with pytest.raises(ValueError): - rq.ForeignKey(["id_1", "id_2"], ["t1.id", "t2.id"]) - - with pytest.raises(ValueError): - rq.ForeignKey(["1", "2"], ["1"]) - - with pytest.raises(ValueError): - rq.ForeignKey(["font_id"], ["fonts.id"], on_delete="AAA") # type: ignore - - with pytest.raises(ValueError): - rq.ForeignKey(["font_id"], ["fonts.id"], on_update="AAA") # type: ignore - - -class TestFunc: - def test_new(self): - rq.Func("CUSTOM", 1, 2) - - -class TestTableName: - def test_new(self): - t_1 = rq.TableName("users") - assert t_1.name == "users" - assert t_1.schema is None - assert t_1.database is None - assert t_1.alias is None - - t_2 = rq.TableName("users", "public", "world") - assert t_2.name == "users" - assert t_2.schema == "public" - assert t_2.database == "world" - assert t_2.alias is None - - t_3 = rq.TableName.parse("world.public.users") - assert t_3.name == "users" - assert t_3.schema == "public" - assert t_3.database == "world" - assert t_3.alias is None - - assert t_3 == t_2 - assert t_3 != t_1 - - def test_copy_with(self): - t_1 = rq.TableName("users") - assert t_1.name == "users" - assert t_1.schema is None - assert t_1.database is None - assert t_1.alias is None - - t_2 = t_1.copy_with(schema="public", database="world") - assert t_2.name == "users" - assert t_2.schema == "public" - assert t_2.database == "world" - assert t_2.alias is None - - t_3 = t_2.copy_with(database=None, alias="t_3") - assert t_3.name == "users" - assert t_3.schema == "public" - assert t_3.database is None - assert t_3.alias == "t_3" - - def test_try_from(self): - class Prop: - @property - def __table_name__(self): - return "world.public.users" - - class Var: - __table_name__ = "world.public.users" - - stmt = rq.AlterTable(rq.TableName("users", "public", "world")) - assert stmt.name == rq.TableName("users", "public", "world") - - stmt = rq.AlterTable("world.public.users") - assert stmt.name == rq.TableName("users", "public", "world") - - stmt = rq.AlterTable(Prop()) - assert stmt.name == rq.TableName("users", "public", "world") - - stmt = rq.AlterTable(Var) - assert stmt.name == rq.TableName("users", "public", "world") - - stmt = rq.AlterTable("world.public.users as t1") - assert stmt.name == rq.TableName("users", "public", "world", "t1") - - stmt = rq.AlterTable("world.public.users AS t1") - assert stmt.name == rq.TableName("users", "public", "world", "t1") - - stmt = rq.AlterTable("world.public.users As t1") - assert stmt.name == rq.TableName("users", "public", "world", "t1") - - stmt = rq.AlterTable("world.public.users As s t1") - assert stmt.name == rq.TableName("users", "public", "world", "s t1") diff --git a/tests/test_dialect.py b/tests/test_dialect.py new file mode 100644 index 0000000..89f55d3 --- /dev/null +++ b/tests/test_dialect.py @@ -0,0 +1,71 @@ +import pytest + +from rapidquery.dialect import ( + OPT_BIT_FORMAT_POSTFIX_BIG_B, + OPT_BOOL_FORMAT_INTEGER, + OPT_BYTES_FORMAT_SLASH_X_BYTEA, + OPT_IDENTIFIER_ESCAPE_BACKSLASH, + OPT_IDENTIFIER_FORMAT_BACKTICK, + OPT_LIKE_ESCAPE_INCLUDING_ANY, + OPT_LIKE_ESCAPE_SCALAR, + OPT_PLACEHOLDER_NUMBERED, + OPT_PLACEHOLDER_SIMPLE, + OPT_STRING_ESCAPE_BACKSLASH_HEX, + OPT_STRING_FORMAT_PREFIX_BIG_E, + Dialect, + PostgreSql, +) + + +def test_getters_round_trip(): + dialect = Dialect( + placeholder_character="$", + placeholder=OPT_PLACEHOLDER_NUMBERED, + like_escape=OPT_LIKE_ESCAPE_INCLUDING_ANY, + bool_format=OPT_BOOL_FORMAT_INTEGER, + supports_double_colon=True, + ) + assert dialect.placeholder_character == "$" + assert dialect.placeholder == OPT_PLACEHOLDER_NUMBERED + assert dialect.like_escape == OPT_LIKE_ESCAPE_INCLUDING_ANY + assert dialect.bool_format == OPT_BOOL_FORMAT_INTEGER + assert dialect.supports_double_colon is True + assert dialect.supports_similar_to is False + assert dialect.parenthesize_in_between is False + + default = Dialect() + assert default.placeholder_character == "?" + assert default.placeholder == OPT_PLACEHOLDER_SIMPLE + assert default.supports_double_colon is False + + +def test_builtin_getters(): + assert PostgreSql.placeholder_character == "$" + assert PostgreSql.placeholder == OPT_PLACEHOLDER_NUMBERED + assert PostgreSql.supports_double_colon is True + assert PostgreSql.like_escape == OPT_LIKE_ESCAPE_SCALAR + + +# Edge cases are tested in Rust +def test_dialect_methods(): + default = Dialect( + bool_format=OPT_BOOL_FORMAT_INTEGER, + string_format=OPT_STRING_FORMAT_PREFIX_BIG_E, + string_escape=OPT_STRING_ESCAPE_BACKSLASH_HEX, + identifier_format=OPT_IDENTIFIER_FORMAT_BACKTICK, + identifier_escape=OPT_IDENTIFIER_ESCAPE_BACKSLASH, + bit_format=OPT_BIT_FORMAT_POSTFIX_BIG_B, + bytes_format=OPT_BYTES_FORMAT_SLASH_X_BYTEA, + ) + assert default.write_boolean(True) == "1" + + assert default.write_string_literal("love") == "E'love'" + + assert default.write_identifier("lov`e") == "`lov\\`e`" + + assert default.write_bits("10101") == "'10101'B" + + with pytest.raises(ValueError): + _ = default.write_bits("123") + + assert default.write_bytes(b"love") == "'\\x6C6F7665'::bytea" diff --git a/tests/test_expr.py b/tests/test_expr.py new file mode 100644 index 0000000..41549eb --- /dev/null +++ b/tests/test_expr.py @@ -0,0 +1,1072 @@ +import pytest + +from rapidquery.dialect import ( + BigQuery, + Dialect, + DuckDb, + MsSql, + MySql, + Oracle, + PostgreSql, + Redshift, + Snowflake, + Spark, + Sqlite, + Teradata, +) +from rapidquery.expr import ( + Asterisk, + BetweenExpr, + BinaryExpr, + CaseExpr, + CastExpr, + CollateExpr, + CustomExpr, + Expr, + Function, + Ident, + InListExpr, + IsDistinctFromExpr, + IsExpr, + LikeExpr, + RLikeExpr, + SimilarToExpr, + TrimExpr, + TupleExpr, + UnaryExpr, + Unquote, + Value, +) + + +class TestExpr: + def test_cannot_instantiate_base(self): + with pytest.raises(TypeError): + Expr() # pyright: ignore + + def test_nodes_are_expr_subclasses(self): + nodes = [ + Ident("id"), + Value(1), + Function("now"), + TupleExpr(1), + UnaryExpr("-", 1), + BinaryExpr(1, "+", 2), + IsExpr(Ident("id"), "NULL"), + IsDistinctFromExpr(1, 2), + InListExpr(Ident("id"), [1]), + BetweenExpr(Ident("id"), 0, 1), + LikeExpr(Ident("name"), "%a%"), + SimilarToExpr(Ident("name"), "%a%"), + RLikeExpr(Ident("name"), "%a%"), + CastExpr(Ident("id"), "int"), + TrimExpr(Ident("s")), + CollateExpr(Ident("s"), "en_US"), + CaseExpr(), + CustomExpr("1"), + ] + for node in nodes: + assert isinstance(node, Expr) + + def test_render_succeeds_on_every_dialect(self, dialect: Dialect): + expr = BinaryExpr(Ident("id"), ">", 10) + sql = expr.to_sql(dialect) + assert "id" in sql + assert "10" in sql + + +def same(actual: object, expected: object) -> bool: + """Structural check. + + ``==`` on an expression builds a SQL comparison and returns that node, so + it is not a boolean. Compare ``repr`` instead. A tuple of nodes is compared + element by element. + """ + + if isinstance(actual, tuple) and isinstance(expected, tuple): + return len(actual) == len(expected) and all( + same(left, right) for left, right in zip(actual, expected, strict=True) + ) + if isinstance(actual, Expr) or isinstance(expected, Expr): + return repr(actual) == repr(expected) + return actual == expected + + +def _assert_binary(expr: Expr, operator: str, left: Expr, right: Expr) -> None: + assert isinstance(expr, BinaryExpr) + assert expr.get_operator() == operator + assert same(expr.get_left(), left) + assert same(expr.get_right(), right) + + +class TestExprMethods: + """Fluent methods build the same nodes as the constructors. + + ``==`` and ``!=`` are SQL on every node, including ``Ident`` and ``Value``. + Python ``&`` / ``|`` are ``AND`` / ``OR``. + """ + + def test_operators(self): + name = Ident("name") + one = Value(1) + two = Value(2) + text = Value("x") + + # A bare string is a literal. ``&`` / ``|`` are logical; bitwise forms + # are named so ``~expr`` is not silently ``bit_not``. + built = [ + (name + "x", "+", name, text), + (name - 1, "-", name, one), + (name * 2, "*", name, two), + (name / 2, "/", name, two), + (name % 2, "%", name, two), + (name < 1, "<", name, one), + (name <= 1, "<=", name, one), + (name > 1, ">", name, one), + (name >= 1, ">=", name, one), + (name == 1, "=", name, one), + (name != 1, "<>", name, one), + (name << 1, "<<", name, one), + (name >> 1, ">>", name, one), + (name & 1, "AND", name, one), + (name | 1, "OR", name, one), + (name.bit_and(1), "&", name, one), + (name.bit_or(1), "|", name, one), + (name.bit_xor(1), "^", name, one), + (name.concat("x"), "||", name, text), + # Reflected: the other operand is on the left. + (1 + name, "+", one, name), + (1 - name, "-", one, name), + (2 * name, "*", two, name), + (2 / name, "/", two, name), + (2 % name, "%", two, name), + (1 << name, "<<", one, name), + (1 >> name, ">>", one, name), + (1 & name, "AND", one, name), + (1 | name, "OR", one, name), + (one + 2, "+", one, two), + (one == 2, "=", one, two), + (one != 2, "<>", one, two), + # Python swaps comparisons. There is no reflected ``<``. + (1 < name, ">", name, one), + ] + for expr, operator, left, right in built: + _assert_binary(expr, operator, left, right) + + assert (+name).get_operator() == "+" + assert same((+name).get_expr(), name) + assert (-name).get_operator() == "-" + assert name.not_().get_operator() == "NOT" + assert name.bit_not().get_operator() == "~" + with pytest.raises(TypeError): + ~name # pyright: ignore + + # ``*`` binds tighter than ``+``, so the sum is parenthesized. + assert ((name + 1) * 2).to_sql(PostgreSql) == '("name" + 1) * 2' + + def test_is_accepts_none_bool_and_string(self): + name = Ident("name") + + # ``None`` / ``True`` / ``False`` are ``IS`` tests, not ``= NULL``. + null = name.is_(None) + assert isinstance(null, IsExpr) + assert same(null.get_expr(), name) + assert null.get_kind() == "NULL" + assert null.get_negated() is False + assert name.is_(True).get_kind() == "TRUE" + assert name.is_(False).get_kind() == "FALSE" + assert name.is_("true").get_kind() == "TRUE" + assert name.is_("unknown").get_kind() == "UNKNOWN" + + negated = name.is_not(False) + assert negated.get_kind() == "FALSE" + assert negated.get_negated() is True + assert name.is_not(None).to_sql(PostgreSql) == '"name" IS NOT NULL' + assert name.is_(True).to_sql(PostgreSql) == '"name" IS TRUE' + + with pytest.raises(TypeError): + name.is_(1) + + with pytest.raises(ValueError, match="unknown IS kind"): + name.is_("MAYBE") + + def test_predicates(self): + name = Ident("name") + + between = name.between(1, 3) + assert isinstance(between, BetweenExpr) + assert same(between.get_low(), Value(1)) + assert same(between.get_high(), Value(3)) + assert between.get_negated() is False + assert name.not_between(1, 3).get_negated() is True + + inside = name.in_([1, name]) + assert isinstance(inside, InListExpr) + assert same(inside.get_list(), (Value(1), name)) + assert name.not_in([1]).get_negated() is True + + distinct = name.is_distinct_from(1) + assert isinstance(distinct, IsDistinctFromExpr) + assert same(distinct.get_right(), Value(1)) + assert distinct.get_negated() is False + assert name.is_not_distinct_from(1).get_negated() is True + + like = name.like("a%", escape="!") + assert isinstance(like, LikeExpr) + assert same(like.get_pattern(), Value("a%")) + assert like.get_escape() == "!" + assert like.get_negated() is False + assert like.get_case_sensitive() is True + assert name.not_like("a%").get_negated() is True + assert name.ilike("a%").get_case_sensitive() is False + assert same(name.not_ilike(["a%", "b%"]).get_pattern(), (Value("a%"), Value("b%"))) + # PyO3 extracts ``escape`` as one character before the node is built. + with pytest.raises(ValueError, match="length 1"): + name.like("a", escape="ab") # pyright: ignore + + similar = name.similar_to("a%") + assert isinstance(similar, SimilarToExpr) + assert same(similar.get_pattern(), Value("a%")) + assert name.not_similar_to("a%").get_negated() is True + + regex = name.rlike("a.*") + assert isinstance(regex, RLikeExpr) + assert same(regex.get_pattern(), Value("a.*")) + assert name.not_rlike("a.*").get_negated() is True + + # One render per family. Dialect spellings stay on the constructor tests. + assert between.to_sql(PostgreSql) == '"name" BETWEEN 1 AND 3' + assert like.to_sql(PostgreSql) == "\"name\" LIKE E'a%' ESCAPE E'!'" + assert similar.to_sql(PostgreSql) == "\"name\" SIMILAR TO E'a%'" + assert regex.to_sql(PostgreSql) == "\"name\" ~ E'a.*'" + + def test_cast_collate_and_trim(self): + name = Ident("name") + + cast = name.cast_as("int") + assert isinstance(cast, CastExpr) + assert cast.get_kind() == "CAST" + assert cast.get_data_type_name() == "INT" + assert cast.to_sql(PostgreSql) == 'CAST("name" AS INT)' + + attempt = name.try_cast_as("int") + assert attempt.get_kind() == "TRY_CAST" + assert attempt.to_sql(BigQuery) == "SAFE_CAST(`name` AS INT)" + + collated = name.collate(Unquote("C")) + assert isinstance(collated, CollateExpr) + assert same(collated.get_collation(), Ident(Unquote("C"))) + assert name.collate("en_US").to_sql(PostgreSql) == '"name" COLLATE "en_US"' + + trimmed = name.trim() + assert isinstance(trimmed, TrimExpr) + assert trimmed.get_where() == "BOTH" + assert trimmed.get_characters() is None + assert trimmed.to_sql(PostgreSql) == 'TRIM("name")' + + leading = name.trim(where="leading", characters="x") + assert leading.get_where() == "LEADING" + assert same(leading.get_characters(), Value("x")) + with pytest.raises(ValueError, match="unknown TRIM where"): + name.trim(where="NOPE") + + +class TestFunction: + def test_new(self): + fn = Function("now") + assert same(fn.get_name(), Ident("now")) + assert fn.get_distinct() is False + assert fn.get_args() == () + assert fn.get_filter() is None + assert fn.get_null_treatment() is None + + fn = Function("example").filter(BinaryExpr(1, ">", 2)) + assert same(fn.get_name(), Ident("example")) + assert fn.get_distinct() is False + assert fn.get_args() == () + assert isinstance(fn.get_filter(), BinaryExpr) + + fn = Function("count", Ident("id"), distinct=False).filter(BinaryExpr(1, ">", 2)) + assert same(fn.get_name(), Ident("count")) + assert fn.get_distinct() is False + assert same(fn.get_args(), (Ident("id"),)) + assert isinstance(fn.get_filter(), BinaryExpr) + + fn = Function(Unquote("NOW")) + assert same(fn.get_name(), Ident(Unquote("NOW"))) + + fn = Function("schema.fn") + assert same(fn.get_name(), Ident("schema", "fn")) + + bare = Function("now") + with_arg = Function("now", 1) + assert with_arg is not bare + assert same(with_arg.get_args(), (Value(1),)) + assert bare.get_args() == () + + def test_to_sql(self): + assert Function("current_timestamp").to_sql(PostgreSql) == '"current_timestamp"()' + assert Function(Unquote("CURRENT_TIMESTAMP")).to_sql(PostgreSql) == "CURRENT_TIMESTAMP()" + assert Function(Unquote("NOW")).to_sql(PostgreSql) == "NOW()" + + fn = Function("count", Asterisk).filter(BinaryExpr(Ident("status"), "=", "completed")) + assert fn.to_sql(PostgreSql) == '"count"(*) FILTER (WHERE "status" = E\'completed\')' + + fn = Function("count", Ident("id"), distinct=False).filter(BinaryExpr(Ident("status"), "=", "completed")) + assert fn.to_sql(PostgreSql) == '"count"("id") FILTER (WHERE "status" = E\'completed\')' + + fn = Function("count", Ident("id"), distinct=True).filter(BinaryExpr(Ident("status"), "=", "completed")) + assert fn.to_sql(PostgreSql) == '"count"(DISTINCT "id") FILTER (WHERE "status" = E\'completed\')' + + assert Function("now", distinct=True).to_sql(PostgreSql) == '"now"()' + assert Function("custom", 1, 2, Ident("id"), distinct=True).to_sql(PostgreSql) == ( + '"custom"(DISTINCT 1, 2, "id")' + ) + + fn = Function("lag", Ident("x")).ignore_nulls() + assert fn.to_sql(Oracle) == '"lag"("x") IGNORE NULLS' + assert fn.to_sql(BigQuery) == "`lag`(`x` IGNORE NULLS)" + assert fn.to_sql(DuckDb) == '"lag"("x" IGNORE NULLS)' + assert fn.get_null_treatment() == "IGNORE NULLS" + with pytest.raises(RuntimeError, match="null_treatment"): + fn.to_sql(PostgreSql) + + fn = Function("lag", Ident("x")).respect_nulls() + assert fn.to_sql(Oracle) == '"lag"("x") RESPECT NULLS' + assert fn.to_sql(BigQuery) == "`lag`(`x` RESPECT NULLS)" + assert fn.to_sql(DuckDb) == '"lag"("x" RESPECT NULLS)' + assert fn.get_null_treatment() == "RESPECT NULLS" + + fn = Function("lag").ignore_nulls() + assert fn.to_sql(Oracle) == '"lag"() IGNORE NULLS' + assert fn.to_sql(BigQuery) == "`lag`(IGNORE NULLS)" + + completed = BinaryExpr(Ident("status"), "=", "completed") + fn = Function("count", Ident("id")).ignore_nulls().filter(completed) + assert fn.to_sql(Oracle) == '"count"("id") FILTER (WHERE "status" = \'completed\') IGNORE NULLS' + assert fn.to_sql(BigQuery) == "`count`(`id` IGNORE NULLS WHERE `status` = 'completed')" + assert fn.to_sql(DuckDb) == '"count"("id" IGNORE NULLS) FILTER ("status" = \'completed\')' + + def test_edge_cases(self): + with pytest.raises(TypeError): + Function(1) # pyright: ignore + + with pytest.raises(ValueError, match="empty"): + Function("") + + Function("now").filter(Ident("x")) + Function("now").filter(1) + + fn = Function("f", "id") + assert same(fn.get_args(), (Value("id"),)) + assert fn.to_sql(PostgreSql) == "\"f\"(E'id')" + + fn = Function(Ident("schema.fn")) + assert same(fn.get_name(), Ident("schema.fn")) + assert fn.to_sql(PostgreSql) == '"schema.fn"()' + + first = Function("f", 1) + second = Function("f", 2, 3) + assert same(first.get_args(), (Value(1),)) + assert same(second.get_args(), (Value(2), Value(3))) + + filtered = Function("f").filter(BinaryExpr(1, ">", 0)) + replaced = filtered.filter(BinaryExpr(2, ">", 0)) + assert filtered.to_sql(PostgreSql) == '"f"() FILTER (WHERE 1 > 0)' + assert replaced.to_sql(PostgreSql) == '"f"() FILTER (WHERE 2 > 0)' + + ignored = Function("lag", Ident("x")).ignore_nulls() + respected = ignored.respect_nulls() + assert ignored.get_null_treatment() == "IGNORE NULLS" + assert respected.get_null_treatment() == "RESPECT NULLS" + assert ignored.to_sql(Oracle) == '"lag"("x") IGNORE NULLS' + assert respected.to_sql(Oracle) == '"lag"("x") RESPECT NULLS' + + fn = Function("count", 1).filter(BinaryExpr(Ident("x"), ">", 2)) + sql, params = fn.prepare(PostgreSql) + assert sql == '"count"($1) FILTER (WHERE "x" > $2)' + assert params == (1, 2) + + assert repr(Function("lag", Ident("x")).ignore_nulls()) == ('Function(Ident("lag"), Ident("x")).ignore_nulls()') + + +class TestTupleExpr: + def test_new(self): + tpl = TupleExpr(1, 2, "A") + assert same(tpl.get_items(), (Value(1), Value(2), Value("A"))) + + tpl = TupleExpr(1, 2, Ident("A")) + assert same(tpl.get_items(), (Value(1), Value(2), Ident("A"))) + + empty = TupleExpr() + assert empty.get_items() == () + + nested = TupleExpr((1, 2), Ident("A")) + assert isinstance(nested.get_items()[0], TupleExpr) + assert same(nested.get_items()[0].get_items(), (Value(1), Value(2))) + assert same(nested.get_items()[1], Ident("A")) + + def test_to_sql(self): + tpl = TupleExpr(1, 2, "A") + assert tpl.to_sql(PostgreSql) == "ROW(1, 2, E'A')" + + sql, params = tpl.prepare(PostgreSql) + assert sql == "ROW($1, $2, $3)" + assert params == (1, 2, "A") + + assert TupleExpr().to_sql(PostgreSql) == "ROW()" + assert TupleExpr(1).to_sql(PostgreSql) == "ROW(1)" + assert TupleExpr((1, 2), Ident("A")).to_sql(PostgreSql) == 'ROW(ROW(1, 2), "A")' + + assert TupleExpr(1, 2).to_sql(Sqlite) == "(1, 2)" + with pytest.raises(RuntimeError, match="at least two elements"): + TupleExpr(1).to_sql(Sqlite) + assert TupleExpr(1, 2).to_sql(Spark) == "struct(1, 2)" + with pytest.raises(RuntimeError, match="tuple values are not supported"): + TupleExpr(1, 2).to_sql(BigQuery) + + +class TestUnaryExpr: + def test_new(self): + un = UnaryExpr("NOT", 1) + assert same(un.get_expr(), Value(1)) + assert un.get_operator() == "NOT" + + un = UnaryExpr("Not", 1) + assert un.get_operator() == "NOT" + + un = UnaryExpr("-", 1) + assert un.get_operator() == "-" + + un = UnaryExpr("+", Ident("id")) + assert un.get_operator() == "+" + assert same(un.get_expr(), Ident("id")) + + un = UnaryExpr("~", 1) + assert un.get_operator() == "~" + + for token in ( + "@-@", + "@@", + "#", + "@", + "||/", + "!", + "!!", + "|/", + "?-", + "?|", + "bang", + ): + with pytest.raises(ValueError, match="unknown unary operator"): + UnaryExpr(token, 1) + + def test_to_sql(self): + un = UnaryExpr("-", Function("now")) + assert un.to_sql(PostgreSql) == '-"now"()' + + assert UnaryExpr("NOT", 1).to_sql(PostgreSql) == "NOT 1" + assert UnaryExpr("NOT", Ident("id")).to_sql(PostgreSql) == 'NOT "id"' + assert UnaryExpr("+", 1).to_sql(PostgreSql) == "+1" + assert UnaryExpr("~", Ident("id")).to_sql(PostgreSql) == '~"id"' + assert UnaryExpr("-", UnaryExpr("-", 1)).to_sql(PostgreSql) == "-(-1)" + assert UnaryExpr("NOT", UnaryExpr("NOT", 1)).to_sql(PostgreSql) == "NOT (NOT 1)" + assert UnaryExpr("-", BinaryExpr(1, "+", 2)).to_sql(PostgreSql) == "-(1 + 2)" + + sql, params = UnaryExpr("-", 1).prepare(PostgreSql) + assert sql == "-$1" + assert params == (1,) + + +class TestBinaryExpr: + def test_new(self): + bin = BinaryExpr(Ident("id"), ">", 10) + assert same(bin.get_left(), Ident("id")) + assert bin.get_operator() == ">" + assert same(bin.get_right(), Value(10)) + + bin = BinaryExpr("str", "-custom-", 10) + assert same(bin.get_left(), Value("str")) + assert bin.get_operator() == "-custom-" + assert same(bin.get_right(), Value(10)) + + bin = BinaryExpr(1, "!=", 2) + assert bin.get_operator() == "<>" + + bin = BinaryExpr(1, "and", 2) + assert bin.get_operator() == "AND" + bin = BinaryExpr(1, "Or", 2) + assert bin.get_operator() == "OR" + bin = BinaryExpr(1, "xor", 2) + assert bin.get_operator() == "xor" + + def test_to_sql(self): + bin = BinaryExpr(Ident("id"), ">", 10) + assert bin.to_sql(PostgreSql) == '"id" > 10' + + bin = BinaryExpr("str", "-custom-", 10) + assert bin.to_sql(PostgreSql) == "E'str' -custom- 10" + + bin = BinaryExpr(Ident("id"), ">", BinaryExpr(456, "&", 10)) + assert bin.to_sql(PostgreSql) == '"id" > 456 & 10' + + bin = BinaryExpr(Ident("id"), ">", BinaryExpr(456, "CusTom", 10)) + assert bin.to_sql(PostgreSql) == '"id" > (456 CusTom 10)' + + bin = BinaryExpr(Ident("id"), ">", BinaryExpr(456, "<", 10)) + assert bin.to_sql(PostgreSql) == '"id" > (456 < 10)' + + assert BinaryExpr(1, "!=", 2).to_sql(PostgreSql) == "1 <> 2" + assert BinaryExpr(1, "AND", 2).to_sql(PostgreSql) == "1 AND 2" + assert BinaryExpr(BinaryExpr(1, "+", 2), "AND", 3).to_sql(PostgreSql) == "1 + 2 AND 3" + assert BinaryExpr(1, "+", BinaryExpr(2, "AND", 3)).to_sql(PostgreSql) == "1 + (2 AND 3)" + + sql, params = BinaryExpr(Ident("id"), ">", 10).prepare(PostgreSql) + assert sql == '"id" > $1' + assert params == (10,) + + concat = BinaryExpr(1, "||", BinaryExpr(2, "+", 3)) + assert concat.to_sql(PostgreSql) == "1 || (2 + 3)" + assert concat.to_sql(Sqlite) == "1 || (2 + 3)" + assert concat.to_sql(MsSql) == "1 + (2 + 3)" + assert concat.to_sql(MySql) == "CONCAT(1, 2 + 3)" + assert BinaryExpr(BinaryExpr(1, "||", 2), "||", 3).to_sql(MySql) == "CONCAT(CONCAT(1, 2), 3)" + + assert BinaryExpr(1, "^", 2).to_sql(PostgreSql) == "1 # 2" + assert BinaryExpr(1, "^", 2).to_sql(DuckDb) == "xor(1, 2)" + assert BinaryExpr(1, "^", 2).to_sql(Snowflake) == "BITXOR(1, 2)" + assert BinaryExpr(1, "^", 2).to_sql(MySql) == "1 ^ 2" + with pytest.raises(RuntimeError, match="bitwise XOR is not supported"): + BinaryExpr(1, "^", 2).to_sql(Sqlite) + + assert BinaryExpr(5, "%", 2).to_sql(Oracle) == "MOD(5, 2)" + assert BinaryExpr(5, "%", 2).to_sql(Teradata) == "5 MOD 2" + assert BinaryExpr(5, "%", 2).to_sql(PostgreSql) == "5 % 2" + + def test_repr(self): + assert repr(BinaryExpr(Ident("id"), ">", 10)) == 'BinaryExpr(Ident("id"), ">", Value(10, IntegerSerializer()))' + + +class TestIsExpr: + def test_new(self): + is_ = IsExpr(Ident("id"), "null") + assert same(is_.get_expr(), Ident("id")) + assert is_.get_kind() == "NULL" + assert is_.get_negated() is False + + is_ = IsExpr(None, "UNKNOWN", negated=True) + assert same(is_.get_expr(), Value(None)) + assert is_.get_kind() == "UNKNOWN" + assert is_.get_negated() is True + + with pytest.raises(ValueError, match="unknown IS kind"): + IsExpr(Ident("id"), "MAYBE") + + def test_to_sql(self): + is_ = IsExpr(Ident("id"), "NUll") + assert is_.to_sql(PostgreSql) == '"id" IS NULL' + + is_ = IsExpr(Ident("id"), "FalSE") + assert is_.to_sql(PostgreSql) == '"id" IS FALSE' + + is_ = IsExpr(Ident("id"), "tRuE") + assert is_.to_sql(PostgreSql) == '"id" IS TRUE' + + is_ = IsExpr(Ident("id"), "unknown", negated=True) + assert is_.to_sql(PostgreSql) == '"id" IS NOT UNKNOWN' + + sql, params = IsExpr(Ident("id"), "NULL").prepare(PostgreSql) + assert sql == '"id" IS NULL' + assert params == () + + sql, params = IsExpr(1, "TRUE").prepare(PostgreSql) + assert sql == "$1 IS TRUE" + assert params == (1,) + + def test_repr(self): + assert repr(IsExpr(Ident("id"), "NULL")) == 'IsExpr(Ident("id"), "NULL")' + assert repr(IsExpr(Ident("id"), "NULL", negated=True)) == 'IsExpr(Ident("id"), "NULL", negated=True)' + + +class TestIsDistinctFromExpr: + def test_new(self): + idf = IsDistinctFromExpr(2, 3) + assert same(idf.get_left(), Value(2)) + assert same(idf.get_right(), Value(3)) + assert idf.get_negated() is False + + idf = IsDistinctFromExpr("A", "B") + assert same(idf.get_left(), Value("A")) + assert same(idf.get_right(), Value("B")) + assert idf.get_negated() is False + + idf = IsDistinctFromExpr(Ident("id"), 10, negated=True) + assert same(idf.get_left(), Ident("id")) + assert same(idf.get_right(), Value(10)) + assert idf.get_negated() is True + + idf = IsDistinctFromExpr(Ident("a"), Ident("b")) + assert same(idf.get_left(), Ident("a")) + assert same(idf.get_right(), Ident("b")) + + def test_to_sql(self): + idf = IsDistinctFromExpr(2, 3) + assert idf.to_sql(PostgreSql) == "2 IS DISTINCT FROM 3" + + idf = IsDistinctFromExpr(Ident("id"), 10, negated=True) + assert idf.to_sql(PostgreSql) == '"id" IS NOT DISTINCT FROM 10' + + sql, params = idf.prepare(PostgreSql) + assert sql == '"id" IS NOT DISTINCT FROM $1' + assert params == (10,) + + assert IsDistinctFromExpr(Ident("a"), Ident("b")).to_sql(PostgreSql) == '"a" IS DISTINCT FROM "b"' + + assert IsDistinctFromExpr(1, 2).to_sql(MySql) == "NOT (1 <=> 2)" + assert IsDistinctFromExpr(1, 2, negated=True).to_sql(MySql) == "1 <=> 2" + with pytest.raises(RuntimeError, match="IS DISTINCT FROM is not supported"): + IsDistinctFromExpr(1, 2).to_sql(Oracle) + + +class TestInListExpr: + def test_new(self): + lst = InListExpr(Ident("id"), [1, 2, 3]) + assert same(lst.get_expr(), Ident("id")) + assert same(lst.get_list(), (Value(1), Value(2), Value(3))) + assert lst.get_negated() is False + + lst = InListExpr("A", [1, Ident("id"), 3], negated=True) + assert same(lst.get_expr(), Value("A")) + assert same(lst.get_list(), (Value(1), Ident("id"), Value(3))) + assert lst.get_negated() is True + + lst = InListExpr(Ident("id"), []) + assert lst.get_list() == () + + with pytest.raises(TypeError): + InListExpr(Ident("id"), 1) # pyright: ignore + + def test_to_sql(self): + lst = InListExpr(Ident("id"), [1, 2, 3]) + assert lst.to_sql(PostgreSql) == '"id" IN (1, 2, 3)' + + sql, params = lst.prepare(PostgreSql) + assert sql == '"id" IN ($1, $2, $3)' + assert params == (1, 2, 3) + + assert InListExpr(Ident("id"), []).to_sql(PostgreSql) == "FALSE" + assert InListExpr(Ident("id"), [], negated=True).to_sql(PostgreSql) == "TRUE" + assert InListExpr(Ident("id"), [], negated=True).to_sql(MySql) == "1" + assert InListExpr(Ident("id"), [1], negated=True).to_sql(PostgreSql) == ('"id" NOT IN (1)') + assert InListExpr(Ident("id"), [1, 2]).to_sql(MsSql) == "([id] IN (1, 2))" + assert ( + BinaryExpr( + InListExpr(Ident("id"), [1]), + "OR", + InListExpr(Ident("id"), [2]), + ).to_sql(MsSql) + == "([id] IN (1)) OR ([id] IN (2))" + ) + + +class TestBetweenExpr: + def test_new(self): + bw = BetweenExpr(Ident("id"), 0, 10) + assert same(bw.get_expr(), Ident("id")) + assert same(bw.get_low(), Value(0)) + assert same(bw.get_high(), Value(10)) + assert bw.get_negated() is False + + bw = BetweenExpr(Ident("id"), BinaryExpr(Ident("old"), "|", 0), 10, negated=True) + assert same(bw.get_expr(), Ident("id")) + assert isinstance(bw.get_low(), BinaryExpr) + assert same(bw.get_high(), Value(10)) + assert bw.get_negated() is True + + def test_to_sql(self): + bw = BetweenExpr(Ident("id"), BinaryExpr(Ident("old"), "|", 0), 10, negated=True) + assert bw.to_sql(PostgreSql) == '"id" NOT BETWEEN "old" | 0 AND 10' + + bw = BetweenExpr(Ident("id"), 1, 10) + assert BetweenExpr(Ident("id"), 1, 10, negated=True).to_sql(MsSql) == ("([id] NOT BETWEEN 1 AND 10)") + assert bw.to_sql(PostgreSql) == '"id" BETWEEN 1 AND 10' + + bw = BetweenExpr(Ident("id"), BinaryExpr(1, "AND", 2), 10) + assert bw.to_sql(PostgreSql) == '"id" BETWEEN (1 AND 2) AND 10' + + sql, params = BetweenExpr(Ident("id"), 1, 10).prepare(PostgreSql) + assert sql == '"id" BETWEEN $1 AND $2' + assert params == (1, 10) + + +class TestLikeExpr: + def test_new(self): + like = LikeExpr(Ident("name"), "%a%") + assert same(like.get_expr(), Ident("name")) + assert same(like.get_pattern(), Value("%a%")) + assert like.get_case_sensitive() is True + assert like.get_negated() is False + assert like.get_escape() is None + + like = LikeExpr( + Ident("name"), + Ident("pattern"), + case_sensitive=False, + negated=True, + escape="!", + ) + assert same(like.get_expr(), Ident("name")) + assert same(like.get_pattern(), Ident("pattern")) + assert like.get_case_sensitive() is False + assert like.get_negated() is True + assert like.get_escape() == "!" + + any_like = LikeExpr(Ident("name"), ["%a%", "%b%"]) + assert same(any_like.get_pattern(), (Value("%a%"), Value("%b%"))) + + one_any = LikeExpr(Ident("name"), ["%a%"]) + assert same(one_any.get_pattern(), (Value("%a%"),)) + + tup = LikeExpr(Ident("name"), ("%a%", Ident("p"))) + assert same(tup.get_pattern(), (Value("%a%"), Ident("p"))) + + with pytest.raises(ValueError, match="empty"): + LikeExpr(Ident("name"), []) + + with pytest.raises((TypeError, ValueError)): + LikeExpr(Ident("n"), "p", escape="ab") # pyright: ignore + + def test_to_sql(self): + like = LikeExpr(Ident("name"), "%a%") + assert like.to_sql(PostgreSql) == "\"name\" LIKE E'%a%'" + + assert LikeExpr(Ident("name"), "%a%", case_sensitive=False).to_sql(PostgreSql) == "\"name\" ILIKE E'%a%'" + assert LikeExpr(Ident("name"), "%a%", negated=True).to_sql(PostgreSql) == "\"name\" NOT LIKE E'%a%'" + assert LikeExpr(Ident("name"), "%a%", escape="!").to_sql(PostgreSql) == "\"name\" LIKE E'%a%' ESCAPE E'!'" + + sql, params = LikeExpr(Ident("name"), "%a%").prepare(PostgreSql) + assert sql == '"name" LIKE $1' + assert params == ("%a%",) + + any_like = LikeExpr(Ident("name"), ["%a%", "%b%"]) + assert any_like.to_sql(PostgreSql) == "\"name\" LIKE ANY (ARRAY[E'%a%', E'%b%'])" + assert any_like.to_sql(Snowflake) == "\"name\" LIKE ANY ('%a%', '%b%')" + assert any_like.to_sql(Sqlite) == "(\"name\" LIKE '%a%' OR \"name\" LIKE '%b%')" + + sql, params = any_like.prepare(PostgreSql) + assert sql == '"name" LIKE ANY (ARRAY[$1, $2])' + assert params == ("%a%", "%b%") + + negated = LikeExpr(Ident("name"), ["%a%", "%b%"], negated=True) + assert negated.to_sql(PostgreSql) == "NOT (\"name\" LIKE ANY (ARRAY[E'%a%', E'%b%']))" + assert negated.to_sql(Snowflake) == "NOT (\"name\" LIKE ANY ('%a%', '%b%'))" + assert negated.to_sql(Sqlite) == "(\"name\" NOT LIKE '%a%' AND \"name\" NOT LIKE '%b%')" + + ilike_any = LikeExpr(Ident("name"), ["%a%", "%b%"], case_sensitive=False) + assert ilike_any.to_sql(PostgreSql) == "\"name\" ILIKE ANY (ARRAY[E'%a%', E'%b%'])" + + with pytest.raises(RuntimeError, match="LIKE ANY ESCAPE"): + LikeExpr(Ident("name"), ["%a%", "%b%"], escape="!").to_sql(PostgreSql) + assert LikeExpr(Ident("name"), ["%a%", "%b%"], escape="!").to_sql(Snowflake) == ( + "\"name\" LIKE ANY ('%a%', '%b%') ESCAPE '!'" + ) + assert LikeExpr(Ident("name"), ["%a%", "%b%"], escape="!").to_sql(Sqlite) == ( + "(\"name\" LIKE '%a%' ESCAPE '!' OR \"name\" LIKE '%b%' ESCAPE '!')" + ) + + assert ( + repr(LikeExpr(Ident("name"), ["%a%", "%b%"], negated=True)) + == "LikeExpr(Ident(\"name\"), [Value('%a%', StringSerializer()), " + "Value('%b%', StringSerializer())], negated=True)" + ) + + +class TestSimilarToExpr: + def test_new(self): + sim = SimilarToExpr(Ident("name"), "%a%") + assert same(sim.get_expr(), Ident("name")) + assert same(sim.get_pattern(), Value("%a%")) + assert sim.get_negated() is False + + sim = SimilarToExpr(Ident("name"), Ident("pattern"), negated=True) + assert same(sim.get_expr(), Ident("name")) + assert same(sim.get_pattern(), Ident("pattern")) + assert sim.get_negated() is True + + def test_to_sql(self): + sim = SimilarToExpr(Ident("name"), "%a%") + assert sim.to_sql(PostgreSql) == "\"name\" SIMILAR TO E'%a%'" + assert sim.to_sql(Redshift) == "\"name\" SIMILAR TO '%a%'" + assert sim.to_sql(DuckDb) == "\"name\" SIMILAR TO '%a%'" + + assert SimilarToExpr(Ident("name"), "%a%", negated=True).to_sql(PostgreSql) == ( + "\"name\" NOT SIMILAR TO E'%a%'" + ) + + sql, params = sim.prepare(PostgreSql) + assert sql == '"name" SIMILAR TO $1' + assert params == ("%a%",) + + with pytest.raises(RuntimeError, match="SIMILAR TO is not supported"): + sim.to_sql(MySql) + with pytest.raises(RuntimeError, match="SIMILAR TO is not supported"): + sim.to_sql(Sqlite) + + assert repr(sim) == "SimilarToExpr(Ident(\"name\"), Value('%a%', StringSerializer()))" + assert ( + repr(SimilarToExpr(Ident("name"), "%a%", negated=True)) + == "SimilarToExpr(Ident(\"name\"), Value('%a%', StringSerializer()), " + "negated=True)" + ) + + +class TestRLikeExpr: + def test_new(self): + rlike = RLikeExpr(Ident("name"), "%a%") + assert same(rlike.get_expr(), Ident("name")) + assert same(rlike.get_pattern(), Value("%a%")) + assert rlike.get_negated() is False + + rlike = RLikeExpr(Ident("name"), Ident("pattern"), negated=True) + assert same(rlike.get_expr(), Ident("name")) + assert same(rlike.get_pattern(), Ident("pattern")) + assert rlike.get_negated() is True + + def test_to_sql(self): + rlike = RLikeExpr(Ident("name"), "%a%") + assert rlike.to_sql(PostgreSql) == "\"name\" ~ E'%a%'" + assert rlike.to_sql(MySql) == "`name` RLIKE '%a%'" + assert rlike.to_sql(Spark) == "`name` RLIKE '%a%'" + assert rlike.to_sql(Snowflake) == "\"name\" RLIKE '%a%'" + assert rlike.to_sql(Oracle) == "REGEXP_LIKE(\"name\", '%a%')" + assert rlike.to_sql(BigQuery) == "REGEXP_CONTAINS(`name`, '%a%')" + assert rlike.to_sql(DuckDb) == "regexp_matches(\"name\", '%a%')" + + assert RLikeExpr(Ident("name"), "%a%", negated=True).to_sql(PostgreSql) == "\"name\" !~ E'%a%'" + assert RLikeExpr(Ident("name"), "%a%", negated=True).to_sql(Oracle) == "NOT REGEXP_LIKE(\"name\", '%a%')" + + sql, params = rlike.prepare(PostgreSql) + assert sql == '"name" ~ $1' + assert params == ("%a%",) + + with pytest.raises(RuntimeError, match="RLIKE is not supported"): + rlike.to_sql(Sqlite) + with pytest.raises(RuntimeError, match="RLIKE is not supported"): + rlike.to_sql(MsSql) + + assert repr(rlike) == "RLikeExpr(Ident(\"name\"), Value('%a%', StringSerializer()))" + assert ( + repr(RLikeExpr(Ident("name"), "%a%", negated=True)) + == "RLikeExpr(Ident(\"name\"), Value('%a%', StringSerializer()), " + "negated=True)" + ) + + +class TestCastExpr: + def test_new(self): + cast = CastExpr(Ident("id"), "int") + assert same(cast.get_expr(), Ident("id")) + assert cast.get_data_type_name() == "INT" + assert cast.get_kind() == "CAST" + + cast = CastExpr(1, "varchar(255)", kind="TRY_CAST") + assert same(cast.get_expr(), Value(1)) + assert cast.get_data_type_name() == "varchar(255)" + assert cast.get_kind() == "TRY_CAST" + + cast = CastExpr(Ident("x"), "some_totally_unknown_type", kind="cAsT") + assert cast.get_data_type_name() == "some_totally_unknown_type" + assert cast.get_kind() == "CAST" + + cast = CastExpr(Ident("x"), "integer", kind="TRY_CAST") + assert cast.get_data_type_name() == "INTEGER" + assert cast.get_kind() == "TRY_CAST" + + cast = CastExpr(Ident("x"), "integer", kind="::") + assert cast.get_kind() == "::" + + with pytest.raises(ValueError, match="unknown cast kind"): + CastExpr(Ident("x"), "integer", kind="SAFE_CAST") + + def test_to_sql(self): + assert CastExpr(Ident("id"), "int").to_sql(PostgreSql) == 'CAST("id" AS INT)' + assert CastExpr(1, "varchar(255)").to_sql(PostgreSql) == "CAST(1 AS varchar(255))" + assert CastExpr(Ident("id"), "INT ARRAY").to_sql(PostgreSql) == 'CAST("id" AS INT ARRAY)' + + with pytest.raises(RuntimeError, match="try cast"): + CastExpr(Ident("id"), "int", kind="TRY_CAST").to_sql(PostgreSql) + assert CastExpr(Ident("id"), "int", kind="TRY_CAST").to_sql(BigQuery) == "SAFE_CAST(`id` AS INT)" + assert CastExpr(Ident("id"), "int", kind="TRY_CAST").to_sql(Teradata) == 'TRYCAST("id" AS INT)' + assert CastExpr(Ident("id"), "int", kind="::").to_sql(DuckDb) == '("id")::INT' + + sql, params = CastExpr(1, "INT").prepare(PostgreSql) + assert sql == "CAST($1 AS INT)" + assert params == (1,) + + assert repr(CastExpr(Ident("id"), "int")) == 'CastExpr(Ident("id"), "INT")' + assert repr(CastExpr(Ident("id"), "int", kind="TRY_CAST")) == 'CastExpr(Ident("id"), "INT", kind="TRY_CAST")' + + +class TestTrimExpr: + def test_new(self): + trim = TrimExpr(Ident("s")) + assert same(trim.get_expr(), Ident("s")) + assert trim.get_where() == "BOTH" + assert trim.get_characters() is None + + trim = TrimExpr(Ident("s"), where="leading", characters="x") + assert trim.get_where() == "LEADING" + assert same(trim.get_characters(), Value("x")) + + trim = TrimExpr(Ident("s"), characters=Ident("b")) + assert same(trim.get_characters(), Ident("b")) + + with pytest.raises(ValueError, match="unknown TRIM where"): + TrimExpr(Ident("s"), where="MIDDLE") + with pytest.raises(TypeError): + TrimExpr(Ident("s"), what="x") # pyright: ignore + + def test_to_sql(self): + assert TrimExpr(Ident("s")).to_sql(PostgreSql) == 'TRIM("s")' + assert TrimExpr(Ident("s"), where="LEADING").to_sql(PostgreSql) == 'TRIM(LEADING FROM "s")' + assert TrimExpr(Ident("s"), characters="x").to_sql(PostgreSql) == "TRIM(E'x' FROM \"s\")" + assert TrimExpr(Ident("s"), where="BOTH", characters="x").to_sql(PostgreSql) == "TRIM(E'x' FROM \"s\")" + assert TrimExpr(Ident("s"), where="LEADING", characters="x").to_sql(Sqlite) == "LTRIM(\"s\", 'x')" + + sql, params = TrimExpr(Ident("s"), characters="x").prepare(PostgreSql) + assert sql == 'TRIM($1 FROM "s")' + assert params == ("x",) + + assert repr(TrimExpr(Ident("s"))) == 'TrimExpr(Ident("s"))' + assert repr(TrimExpr(Ident("s"), where="LEADING", characters="x")) == ( + 'TrimExpr(Ident("s"), where="LEADING", characters=Value(\'x\', StringSerializer()))' + ) + + +class TestCollateExpr: + def test_new(self): + col = CollateExpr(Ident("name"), "en_US") + assert same(col.get_expr(), Ident("name")) + assert same(col.get_collation(), Ident("en_US")) + + col = CollateExpr("café", Ident("pg_catalog", "default")) + assert same(col.get_expr(), Value("café")) + assert same(col.get_collation(), Ident("pg_catalog", "default")) + + col = CollateExpr(Ident("name"), Unquote("C")) + assert same(col.get_collation(), Ident(Unquote("C"))) + + col = CollateExpr(Ident("name"), "pg_catalog.default") + assert same(col.get_collation(), Ident("pg_catalog", "default")) + + def test_to_sql(self): + assert CollateExpr(Ident("name"), "en_US").to_sql(PostgreSql) == ('"name" COLLATE "en_US"') + assert CollateExpr(Ident("name"), Unquote("C")).to_sql(PostgreSql) == ('"name" COLLATE C') + assert ( + CollateExpr(Ident("name"), Ident("pg_catalog", "default")).to_sql(PostgreSql) + == '"name" COLLATE "pg_catalog"."default"' + ) + assert CollateExpr(BinaryExpr(1, "+", 2), "en_US").to_sql(PostgreSql) == ('(1 + 2) COLLATE "en_US"') + assert BinaryExpr(CollateExpr(Ident("a"), "c"), "+", 1).to_sql(PostgreSql) == '"a" COLLATE "c" + 1' + + assert repr(CollateExpr(Ident("name"), "en_US")) == 'CollateExpr(Ident("name"), Ident("en_US"))' + + +class TestCaseExpr: + def test_new(self): + empty = CaseExpr() + assert empty.get_operand() is None + assert empty.get_when_list() == () + assert empty.get_else() is None + + cond = BinaryExpr(Ident("id"), ">", 0) + searched = empty.when(cond, "yes") + assert searched is not empty + assert empty.get_when_list() == () + when_list = searched.get_when_list() + assert len(when_list) == 1 + assert isinstance(when_list[0][0], BinaryExpr) + assert same(when_list[0][1], Value("yes")) + assert searched.get_else() is None + + with_else = searched.else_("no") + assert with_else is not searched + assert searched.get_else() is None + assert same(with_else.get_else(), Value("no")) + + simple = CaseExpr(Ident("status")) + assert same(simple.get_operand(), Ident("status")) + + CaseExpr().when(Ident("x"), 1) + CaseExpr().when(Value(1), 1) + CaseExpr().when(1, 2) + + def test_to_sql(self): + assert CaseExpr().to_sql(PostgreSql) == "CASE END" + + searched = CaseExpr().when(BinaryExpr(Ident("id"), ">", 0), "yes").else_("no") + assert searched.to_sql(PostgreSql) == "CASE WHEN \"id\" > 0 THEN E'yes' ELSE E'no' END" + + simple = CaseExpr(Ident("status")).when(BinaryExpr(Ident("status"), "=", "open"), 1).else_(0) + assert simple.to_sql(PostgreSql) == 'CASE "status" WHEN "status" = E\'open\' THEN 1 ELSE 0 END' + + sql, params = searched.prepare(PostgreSql) + assert sql == 'CASE WHEN "id" > $1 THEN $2 ELSE $3 END' + assert params == (0, "yes", "no") + + +class TestCustomExpr: + def test_new(self): + custom = CustomExpr("1 = 1") + assert custom.get_sql() == "1 = 1" + assert custom.get_args() == () + + custom = CustomExpr("{} = {}", Ident("a"), 1) + assert custom.get_sql() == "{} = {}" + assert same(custom.get_args(), (Ident("a"), Value(1))) + + def test_to_sql(self): + assert CustomExpr("1 = 1").to_sql(PostgreSql) == "1 = 1" + assert CustomExpr("{} = {}", Ident("a"), 1).to_sql(PostgreSql) == '"a" = 1' + assert CustomExpr("{{x}}").to_sql(PostgreSql) == "{x}" + assert CustomExpr("a {{}} b").to_sql(PostgreSql) == "a {} b" + assert CustomExpr("fn({})", Ident("id")).to_sql(PostgreSql) == 'fn("id")' + + with pytest.raises(ValueError, match="placeholders"): + CustomExpr("a {} {}", 1).to_sql(PostgreSql) + + with pytest.raises(ValueError, match="placeholders"): + CustomExpr("a {}", 1, 2).to_sql(PostgreSql) + + sql, params = CustomExpr("x = {}", 1).prepare(PostgreSql) + assert sql == "x = $1" + assert params == (1,) + + with pytest.raises(ValueError, match="placeholders"): + CustomExpr("{} {}", 1).prepare(PostgreSql) + + +class TestExprProtocol: + def test_attribute(self): + + class Eq: + __expr__ = BinaryExpr(1, "=", 2) + + cust = CustomExpr("{}", Eq) + assert isinstance(cust.get_args()[0], BinaryExpr) + assert cust.to_sql(PostgreSql) == "1 = 2" + + def test_property(self): + + class Eq: + def __init__(self, a: object, b: object): + self.a = a + self.b = b + + @property + def __expr__(self): + return BinaryExpr(1, "=", 2) + + cust = CustomExpr("{}", Eq(1, 2)) + assert isinstance(cust.get_args()[0], BinaryExpr) + assert cust.to_sql(PostgreSql) == "1 = 2" diff --git a/tests/test_ident.py b/tests/test_ident.py new file mode 100644 index 0000000..e6e4ab7 --- /dev/null +++ b/tests/test_ident.py @@ -0,0 +1,188 @@ +import typing + +import pytest + +from rapidquery import Asterisk, Ident, Unquote +from rapidquery.dialect import ( + BigQuery, + Databricks, + Dialect, + DuckDb, + Hive, + MsSql, + MySql, + Oracle, + PostgreSql, + Redshift, + Snowflake, + Spark, + Sqlite, + Teradata, +) + +_QUOTED_USERS_ID = { + BigQuery: "`users`.`id`", + Databricks: "`users`.`id`", + DuckDb: '"users"."id"', + Hive: "`users`.`id`", + MsSql: "[users].[id]", + MySql: "`users`.`id`", + Oracle: '"users"."id"', + PostgreSql: '"users"."id"', + Redshift: '"users"."id"', + Snowflake: '"users"."id"', + Spark: "`users`.`id`", + Sqlite: '"users"."id"', + Teradata: '"users"."id"', +} + + +class TestIdentConstruction: + def test_varargs_parts(self): + ident = Ident("table", "id") + assert len(ident) == 2 + assert ident[0] == "table" + assert ident[1] == "id" + + def test_single_str_is_one_quoted_part_even_with_dots(self): + ident = Ident("schema.table") + + assert len(ident) == 1 + assert ident[0] == "schema.table" + assert ident.to_sql(PostgreSql) == '"schema.table"' + + def test_rejects_no_arguments(self): + with pytest.raises(ValueError, match="empty"): + _ = Ident() + + def test_rejects_empty_string_part(self): + with pytest.raises(ValueError, match="empty"): + _ = Ident("") + + def test_rejects_non_ident_argument(self): + with pytest.raises(TypeError): + _ = Ident(1) # pyright: ignore + + def test_flattens_nested_ident(self): + ident = Ident(Ident("t"), "id") + + assert ident.to_list() == ["t", "id"] + assert ident.to_sql(PostgreSql) == '"t"."id"' + + def test_too_many_parts(self): + with pytest.raises(ValueError, match="too many"): + _ = Ident(*[f"p{i}" for i in range(11)]) + + def test_ten_parts_are_allowed(self): + ident = Ident(*[f"p{i}" for i in range(10)]) + assert len(ident) == 10 + + +class TestQuotedStarVersusAsterisk: + def test_str_star_is_quoted_column(self): + ident = Ident("*") + assert len(ident) == 1 + assert ident[0] == "*" + assert ident.to_sql(PostgreSql) == '"*"' + assert ident.to_sql(MySql) == "`*`" + + def test_asterisk_class_is_wildcard(self): + ident = Ident(Asterisk) + assert ident[0] == Asterisk() + assert ident.to_sql(PostgreSql) == "*" + + def test_asterisk_instance_is_wildcard(self): + ident = Ident(Asterisk()) + assert ident.to_sql(MySql) == "*" + + def test_qualified_wildcard(self): + ident = Ident("t", Asterisk) + assert ident.to_sql(PostgreSql) == '"t".*' + assert ident.to_sql(MySql) == "`t`.*" + + def test_asterisk_not_last_is_rejected(self): + with pytest.raises(ValueError, match="asterisk"): + _ = Ident(Asterisk, "id") + + +class TestUnquote: + def test_simple_name(self): + part = Unquote("Users") + assert part.name == "Users" + assert Ident(part).to_sql(PostgreSql) == "Users" + + def test_mixed_with_quoted_parts(self): + ident = Ident("public", Unquote("Users"), "id") + assert ident.to_sql(PostgreSql) == '"public".Users."id"' + + def test_equals_by_name(self): + assert Unquote("id") == Unquote("id") + assert Unquote("id") != Unquote("ID") + assert Unquote("id") != "id" + + def test_rejects_injection_and_non_ascii(self): + for bad in [ + "", + "*", + "a;b", + "a.b", + "1id", + "a-b", + "a b", + "café", + "a; DROP TABLE t--", + ]: + with pytest.raises(ValueError): + _ = Unquote(bad) + + def test_accepts_underscore_and_digits(self): + assert Unquote("_id").name == "_id" + assert Unquote("created_at").name == "created_at" + assert Unquote("a1").name == "a1" + + +class TestEqualityHashReprFrozen: + def test_repr(self): + assert repr(Ident("schema", "table")) == 'Ident("schema", "table")' + assert repr(Ident("t", Asterisk)) == 'Ident("t", Asterisk)' + assert repr(Ident(Unquote("Users"))) == 'Ident(Unquote("Users"))' + assert repr(Unquote("Users")) == 'Unquote("Users")' + assert repr(Asterisk()) == "Asterisk()" + + +class TestRender: + def test_to_sql_quotes_each_part(self, dialect: Dialect): + assert Ident("users", "id").to_sql(dialect) == _QUOTED_USERS_ID[dialect] + + def test_quoted_injection_stays_inside_delimiters(self): + payload = "a; DROP TABLE t--" + sql = Ident(payload).to_sql(PostgreSql) + assert sql == f'"{payload}"' + + +class TestIdentProtocol: + def test_attribute(self): + + class Unk: + __ident__: Ident = Ident("schema", "unk") + + ident = Ident("database", Unk()) + assert ident.to_list() == ["database", "schema", "unk"] + + def test_property(self): + + class Unk: + @property + def __ident__(self) -> Ident: + return Ident("schema", "unk") + + ident = Ident(Unk(), "col") + assert ident.to_list() == ["schema", "unk", "col"] + + def test_invalid_type(self): + + class Unk: + __ident__ = "A" + + with pytest.raises(TypeError): + _ = Ident(Unk()) # pyright: ignore[reportArgumentType] diff --git a/tests/test_query.py b/tests/test_query.py deleted file mode 100644 index 1b1564b..0000000 --- a/tests/test_query.py +++ /dev/null @@ -1,427 +0,0 @@ -import pytest - -import rapidquery as rq - -from .mixin import OrderByMixin, ReturningMixin, TableNameInitMixin, WhereMixin - - -class TestCaseStatement: - def test_init(self): - rq.CaseStatement() - - def test_when_else(self): - stmt = ( - rq.CaseStatement().when(rq.Expr.col("id") == 1, 1).when(rq.Expr.col("name") == "ali", 1) - ) - sql = rq.SelectStatement(rq.SelectLabel(stmt, "test")).to_sql("postgres") - assert '"id"' in sql - assert '"name"' in sql - assert '"test"' in sql - - stmt = ( - rq.CaseStatement().when(rq.Expr.col("id") == 1, 1).when(rq.Expr.col("name") == "ali", 1) - ) - sql = rq.SelectStatement(stmt.label("test")).to_sql("postgres") - assert '"id"' in sql - assert '"name"' in sql - assert '"test"' in sql - - with pytest.raises(TypeError): - rq.CaseStatement().when("Ali", 1) # type: ignore - - rq.CaseStatement().when(rq.Expr("Ali"), 1).else_(1) - - -class TestDeleteStatement(TableNameInitMixin, ReturningMixin, WhereMixin, OrderByMixin): - table_name_instance = rq.DeleteStatement - - def get_statement(self): - return rq.DeleteStatement("users") - - def test_from_table(self): - stmt = rq.DeleteStatement("users") - assert '"users"' in stmt.to_sql("postgres") - - stmt = stmt.from_table("fonts") - assert '"users"' not in stmt.to_sql("postgres") - assert '"fonts"' in stmt.to_sql("postgres") - - def test_limit(self): - stmt = rq.DeleteStatement("users").limit(20) - assert "LIMIT" in stmt.to_sql("postgres") - - -class TestInsertStatement(TableNameInitMixin, ReturningMixin): - table_name_instance = rq.InsertStatement - - def get_statement(self): - return rq.InsertStatement("users") - - def test_replace(self): - stmt = rq.InsertStatement("public.users").replace().to_sql("postgres") - assert "INSERT" not in stmt - assert "REPLACE" in stmt - - def test_columns(self): - stmt = rq.InsertStatement("users").columns("id", "name") - assert '"id"' in stmt.to_sql("postgres") - assert '"name"' in stmt.to_sql("postgres") - - stmt.columns("created_at") - assert '"created_at"' in stmt.to_sql("postgres") - assert '"id"' not in stmt.to_sql("postgres") - assert '"name"' not in stmt.to_sql("postgres") - - def test_values(self): - with pytest.raises(TypeError): - rq.InsertStatement("users").values(1, a=1) # type: ignore - - with pytest.raises(ValueError): - rq.InsertStatement("users").values(name="ali", id=1).values(name="ali") # type: ignore - - with pytest.raises(ValueError): - rq.InsertStatement("users").columns("id").values(1, 2) - - with pytest.raises(ValueError): - rq.InsertStatement("users").columns("id").values() - - stmt = rq.InsertStatement("users").columns("id").values("ali") - assert stmt.to_sql("postgres").count("ali") == 1 - - stmt = rq.InsertStatement("users").columns("id").values("ali").values("ali") - assert stmt.to_sql("postgres").count("ali") == 2 - - stmt = rq.InsertStatement("users").values(name="ali", id=1) - assert "name" in stmt.to_sql("postgres") - assert "id" in stmt.to_sql("postgres") - assert "ali" in stmt.to_sql("postgres") - assert "1" in stmt.to_sql("postgres") - - stmt = rq.InsertStatement("users").values(name="ali", id=1).values(name="ali", id=1) - assert stmt.to_sql("postgres").count("ali") == 2 - assert stmt.to_sql("postgres").count("1") == 2 - - def test_into(self): - stmt = ( - rq.InsertStatement("name_1") - .values(aspect=4.21, image="123") - .into("name_2") - .to_sql("sqlite") - ) - assert "name_2" in stmt - assert "name_1" not in stmt - - def test_or_default_values(self): - stmt = rq.InsertStatement("users").or_default_values() - assert stmt.to_sql("postgres").count("DEFAULT") == 1 - - stmt = rq.InsertStatement("users").or_default_values(4) - assert stmt.to_sql("postgres").count("DEFAULT") == 4 - - stmt = rq.InsertStatement("users").or_default_values(4).values(aspect=3.14) - assert stmt.to_sql("postgres").count("DEFAULT") == 0 - - def test_select_from(self): - valid_cols = rq.SelectStatement().columns("id", "name").from_table("fonts") - invalid_cols = rq.SelectStatement().columns("id").from_table("fonts") - - stmt = rq.InsertStatement("users").columns("id", "name") - - with pytest.raises(ValueError): - stmt.select_from(invalid_cols) - - stmt.select_from(valid_cols) - assert "SELECT" in stmt.to_sql("postgres") - - -class TestOnConflict: - def test_init(self): - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_update(image="ex")) - ) - assert 'ON CONFLICT ("id")' in stmt.to_sql("postgres") - assert "DO UPDATE" in stmt.to_sql("postgres") - - def test_action_where(self): - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict( - rq.OnConflict("id") - .do_update(image="ex") - .action_where(rq.Expr.col("aspect").is_null()) - ) - ) - assert "WHERE" in stmt.to_sql("postgres") - - def test_target_where(self): - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict( - rq.OnConflict("id") - .do_update(image="ex") - .target_where(rq.Expr.col("aspect").is_null()) - ) - ) - assert "WHERE" in stmt.to_sql("postgres") - - def test_do_update(self): - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_update("aspect", image=rq.Expr(1) + 2)) - ) - assert "DO UPDATE" in stmt.to_sql("postgres") - - def test_do_nothing(self): - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_nothing()) - ) - assert "DO NOTHING" in stmt.to_sql("postgres") - - stmt = ( - rq.InsertStatement("glyph") - .values(aspect=3.1415, image="abcd") - .on_conflict(rq.OnConflict("id").do_nothing("id")) - ) - assert "DO NOTHING" in stmt.to_sql("postgres") - - -class TestSelectStatement(WhereMixin, OrderByMixin): - table_name_instance = rq.SelectStatement - - def get_statement(self): - return rq.SelectStatement().columns("character").from_table("characters") - - def test_init(self): - stmt = ( - rq.SelectStatement() - .columns("character", "fonts.name") - .from_table("characters") - .join( - "fonts", - rq.Expr.col("characters.font_id") == rq.Expr.col("fonts.id"), - "LEFT", - ) - .where(rq.Expr.col("size_w").in_((3, 4))) - .where(rq.Expr.col("character").like("A%")) - .to_sql("postgres") - ) - assert 'LEFT JOIN "fonts" ON "characters"."font_id" = "fonts"."id"' in stmt - - stmt = rq.SelectStatement( - rq.SelectLabel(1), - rq.SelectLabel("hello"), - rq.Expr.col("font"), - ).to_sql("postgres") - assert "SELECT 1, 'hello', \"font\"" in stmt - - def test_columns(self): - stmt = ( - rq.SelectStatement() - .columns("character", "size_w", "size_h") - .from_table("characters") - .to_sql("mysql") - ) - assert "`character` AS `character`, `size_w` AS `size_w`, `size_h` AS `size_h`" in stmt - - def test_distinct(self): - stmt = ( - rq.SelectStatement() - .distinct() - .columns("character", "size_w", "size_h") - .from_table("characters") - .to_sql("mysql") - ) - assert "DISTINCT" in stmt - - def test_from_function(self): - stmt = ( - rq.SelectStatement(rq.Expr.asterisk()) - .from_function(rq.Func.random(), "func") - .to_sql("postgres") - ) - assert "FROM RANDOM()" in stmt - - def test_from_subquery(self): - rq.SelectStatement().from_subquery( - rq.SelectStatement().columns("image", "aspect").from_table("glyph"), - "subglyph", - ) - - stmt = rq.SelectStatement() - with pytest.raises(ValueError): - stmt.from_subquery(stmt, "a") - - -class TestUpdateStatement(WhereMixin, OrderByMixin): - table_name_instance = rq.UpdateStatement - - def get_statement(self): - return rq.UpdateStatement("users") - - def test_from_table(self): - stmt = rq.UpdateStatement("archive.users").from_table("public.users").to_sql("postgres") - assert "FROM" in stmt - assert '"public"."users"' in stmt - assert '"archive"."users"' in stmt - - def test_table(self): - stmt = ( - rq.UpdateStatement("name_1") - .values(aspect=1.23, image=123) - .table("name_2") - .to_sql("sqlite") - ) - assert "name_1" not in stmt - assert "name_2" in stmt - - def test_values(self): - stmt = ( - rq.UpdateStatement("glyph") - .values(aspect=1.23, image=123) - .values(font_id=20) - .to_sql("sqlite") - ) - assert "aspect" in stmt - assert "image" in stmt - assert "font_id" in stmt - - -class TestWindowStatement: - def test_basic_partition(self): - """Test a basic OVER clause with PARTITION BY.""" - window = rq.WindowStatement("department").order_by(rq.Ordering("salary", order="DESC")) - - # Using it in a SelectStatement context - stmt = rq.SelectStatement( - rq.Expr.custom("AVG(salary)").label("svg_sal", window) - ).from_table("employees") - - sql = stmt.to_sql("postgres") - assert 'PARTITION BY "department"' in sql - assert 'ORDER BY "salary" DESC' in sql - assert "OVER" in sql - - def test_frame_rows(self): - """Test WINDOW with ROWS framing.""" - window = ( - rq.WindowStatement() - .order_by(rq.Ordering("id")) - .frame("ROWS", rq.Frame.preceding(5), rq.Frame.current_row()) - ) - - stmt = rq.SelectStatement( - rq.Expr.custom("SUM(val)").label("running_sum", window) - ).from_table("data") - - sql = stmt.to_sql("postgres") - assert "ROWS BETWEEN 5PRECEDING AND CURRENT ROW" in sql - - def test_frame_unbounded(self): - """Test WINDOW with UNBOUNDED boundaries.""" - window = rq.WindowStatement().frame( - "RANGE", rq.Frame.unbounded_preceding(), rq.Frame.unbounded_following() - ) - - stmt = rq.SelectStatement( - rq.SelectLabel(rq.Expr.custom("COUNT(*)"), "total", window) - ).from_table("users") - - sql = stmt.to_sql("mysql") - assert "RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING" in sql - - -class TestWith: - def test_simple_cte(self): - """Test a basic CTE using WithClause.""" - cte_query = ( - rq.SelectStatement() - .columns("id", "name") - .from_table("users") - .where(rq.Expr.col("active").is_(True)) - ) - - with_query = ( - rq.WithClause() - .cte("active_users", cte_query) - .query(rq.SelectStatement().columns("*").from_table("active_users")) - ) - - sql = with_query.to_sql("postgres") - assert 'WITH "active_users"' in sql - assert 'SELECT "id" AS "id", "name" AS "name" FROM "users"' in sql - assert 'SELECT * FROM "active_users"' in sql - - def test_multiple_ctes(self): - """Test multiple CTEs in a single WITH clause.""" - q1 = rq.SelectStatement().columns("id").from_table("table_a") - q2 = rq.SelectStatement().columns("id").from_table("table_b") - - with_query = ( - rq.WithClause() - .cte("cte_a", q1) - .cte("cte_b", q2) - .query( - rq.SelectStatement() - .columns("cte_a.id") - .from_table("cte_a") - .join("cte_b", rq.Expr.col("cte_a.id") == rq.Expr.col("cte_b.id")) - ) - ) - - sql = with_query.to_sql("postgres") - assert 'WITH "cte_a"' in sql - assert '"cte_b"' in sql - assert 'JOIN "cte_b"' in sql - - def test_recursive(self): - """Test RECURSIVE WITH clause generation.""" - # A simplified recursive CTE for sequence generation - base_part = rq.SelectStatement(rq.Expr.val(1).label("n")) - recursive_part = ( - rq.SelectStatement((rq.Expr.col("n") + 1).label("n")) - .from_table("nums") - .where(rq.Expr.col("n") < 5) - ) - - # SeaQuery/RapidQuery typically uses Union to combine parts for recursion - union_query = base_part.union(recursive_part) - - with_query = ( - rq.WithClause() - .recursive() - .cte("nums", union_query, columns=["n"]) - .query(rq.SelectStatement().columns("*").from_table("nums")) - ) - - sql = with_query.to_sql("postgres") - assert "WITH RECURSIVE" in sql - assert '"nums" ("n") AS' in sql - - def test_materialized(self): - """Test the MATERIALIZED hint in CTEs (Postgres specific).""" - q = rq.SelectStatement().columns("id").from_table("very_large_table") - - # Materialized True - sql_mat = ( - rq.WithClause() - .cte("cached_data", q, materialized=True) - .query(rq.SelectStatement().from_table("cached_data")) - .to_sql("postgres") - ) - assert "MATERIALIZED" in sql_mat - - # Materialized False (NOT MATERIALIZED) - sql_not_mat = ( - rq.WithClause() - .cte("raw_data", q, materialized=False) - .query(rq.SelectStatement().from_table("raw_data")) - .to_sql("postgres") - ) - assert "NOT MATERIALIZED" in sql_not_mat diff --git a/tests/test_schema.py b/tests/test_schema.py deleted file mode 100644 index 814c6a2..0000000 --- a/tests/test_schema.py +++ /dev/null @@ -1,272 +0,0 @@ -import pytest - -import rapidquery as rq - -from .mixin import TableNameInitMixin - - -class TestAlterTable(TableNameInitMixin): - table_name_instance = rq.AlterTable - - options = [ - rq.AlterTableAddColumnOption(rq.Column("premium", rq.sqltypes.Boolean())), - rq.AlterTableAddForeignKeyOption(rq.ForeignKey(["id"], ["fonts.id"])), - rq.AlterTableDropColumnOption("id"), - rq.AlterTableDropForeignKeyOption("fk_12"), - rq.AlterTableModifyColumnOption(rq.Column("name", rq.sqltypes.Char(64), nullable=True)), - rq.AlterTableRenameColumnOption("id", "user_id"), - ] - - def test_init(self): - rq.AlterTable("users", self.options) - rq.AlterTable("users", []) - - with pytest.raises(TypeError): - rq.AlterTable("users", [1]) # type: ignore - - with pytest.raises(TypeError): - rq.AlterTable("users", ["a"]) # type: ignore - - def test_add_option(self): - stmt = rq.AlterTable("users", []) - - for op in self.options: - stmt.add_option(op) - - def test_properties(self): - stmt = rq.AlterTable("users", self.options) - - assert stmt.name == rq.TableName("users") - assert stmt.options == self.options - - stmt.name = "public.fonts" - assert stmt.name == rq.TableName("fonts", "public") - - stmt.options = [] - assert stmt.options == [] - - -class TestDropIndex(TableNameInitMixin): - def table_name_instance(self, tb): - return rq.DropIndex("idx_1", tb) - - def test_init(self): - stmt = rq.DropIndex("idx_name", "users", if_exists=True) - assert "IF EXISTS" in stmt.to_sql("postgres") - - def test_properties(self): - stmt = rq.DropIndex("idx_1", "fonts") - - assert stmt.name == "idx_1" - assert stmt.table == rq.TableName("fonts") - assert not stmt.if_exists - - stmt.name = "idx_2" - assert stmt.name == "idx_2" - - stmt.table = "users" - assert stmt.table == rq.TableName("users") - - stmt.if_exists = True - assert stmt.if_exists - - -class TestDropTable(TableNameInitMixin): - table_name_instance = rq.DropTable - - def test_init(self): - rq.DropTable("users", if_exists=True, cascade=True, restrict=True) - - def test_properties(self): - stmt = rq.DropTable("fonts") - - assert stmt.name == rq.TableName("fonts") - assert not stmt.if_exists - assert not stmt.cascade - assert not stmt.restrict - - stmt.name = "public.users" - assert stmt.name == rq.TableName("users", "public") - - stmt.if_exists = True - stmt.cascade = True - stmt.restrict = True - assert stmt.if_exists - assert stmt.cascade - assert stmt.restrict - - -class TestIndex(TableNameInitMixin): - def table_name_instance(self, tb): - return rq.Index(None, ["id"], tb) - - def test_init(self): - stmt = rq.Index("idx_glyph_aspect", ["aspect"], "glyph", if_not_exists=True).to_sql( - "sqlite" - ) - assert stmt == 'CREATE INDEX IF NOT EXISTS "idx_glyph_aspect" ON "glyph" ("aspect")' - - stmt = rq.Index("idx_glyph_aspect", [rq.IndexColumn("aspect", "ASC", 128)], "glyph").to_sql( - "mysql" - ) - assert stmt == "CREATE INDEX `idx_glyph_aspect` ON `glyph` (`aspect` (128) ASC)" - - stmt = rq.Index( - "idx_font_name_include_language", - ["name"], - "fonts", - include=["language"], - where=rq.Expr.col("aspect").in_([3, 4]), - ).to_sql("postgresql") - assert ( - stmt - == 'CREATE INDEX "idx_font_name_include_language" ON "fonts" ("name") INCLUDE ("language")' - ) - - stmt = rq.Index( - "idx_name", - ["font_id"], - "fonts", - primary=False, - nulls_not_distinct=True, - unique=True, - index_type="BTREE", - where=None, - include=["language"], - ).to_sql("postgres") - assert ( - stmt - == 'CREATE UNIQUE INDEX "idx_name" ON "fonts" USING BTREE ("font_id") INCLUDE ("language") NULLS NOT DISTINCT' - ) - - -class TestIndexColumn: - def test_new(self): - rq.IndexColumn("name") - rq.IndexColumn("name", "ASC") - rq.IndexColumn("name", prefix=12) - rq.IndexColumn("name", "DESC", prefix=1) - rq.IndexColumn("name", None, None) - - # lowercase orders - rq.IndexColumn("name", "asc") # type: ignore - rq.IndexColumn("name", "desc") # type: ignore - - with pytest.raises(Exception): - rq.IndexColumn("name", "invalid") # type: ignore - - val = rq.IndexColumn("name", "desc", prefix=1) # type: ignore - assert val.name == "name" - assert val.order == "DESC" - assert val.prefix == 1 - - -class TestRenameTable(TableNameInitMixin): - def table_name_instance(self, tb): - return rq.RenameTable(tb, tb) - - def test_properties(self): - stmt = rq.RenameTable("t1", "t2") - - assert stmt.from_name == rq.TableName("t1") - assert stmt.to_name == rq.TableName("t2") - - stmt.from_name = "pub.users" - stmt.to_name = "arc.users" - assert stmt.from_name == rq.TableName("users", "pub") - assert stmt.to_name == rq.TableName("users", "arc") - - -class TestTable: - def test_init(self): - rq.Table("users") - rq.Table("public.users") - rq.Table("db.public.users") - rq.Table(rq.TableName("users")) - rq.Table(rq.TableName("users", "public")) - rq.Table(rq.TableName("users", "public", "db")) - - table = rq.Table( - "pub.users", - rq.Column("id", rq.sqltypes.Integer(), primary_key=True, auto_increment=True), - rq.Column("username", rq.sqltypes.Integer(), unique_key=True, nullable=False), - rq.Column("font_id", rq.sqltypes.Integer(), unique_key=True, nullable=False), - rq.ForeignKey(["font_id"], ["fonts.id"], on_delete="CASCADE"), - rq.Index("idx_name", ["id", "font_id"]), - rq.Expr.col("username").like("A%"), - if_not_exists=True, - extra="WOW", - ) - assert table.name == rq.TableName("users", "pub") - assert len(table.columns) == 3 - assert len(table.foreign_keys) == 1 - assert len(table.indexes) == 1 - assert len(table.checks) == 1 - assert all(isinstance(x, rq.Column) for x in table.columns) - assert all(isinstance(x, rq.ForeignKey) for x in table.foreign_keys) - assert all(isinstance(x, rq.Index) for x in table.indexes) - assert all(isinstance(x, rq.Expr) for x in table.checks) - assert table.if_not_exists - assert not table.temporary - assert table.extra == "WOW" - - stmt = table.to_sql("postgres") - assert stmt.count(";") == 1 - assert "IF NOT EXISTS" in stmt - assert '"id"' in stmt - assert '"username"' in stmt - assert '"font_id"' in stmt - assert "WOW" in stmt - - def test_args(self): - with pytest.raises(TypeError): - rq.Table( - "pub.users", - rq.Column("id", rq.sqltypes.Integer(), primary_key=True, auto_increment=True), - complex(), # type: ignore - ) - - class Col(rq.Column): - pass - - class Fk(rq.ForeignKey): - pass - - class Idx(rq.Index): - pass - - table = rq.Table( - "pub.users", - Col("id", rq.sqltypes.Integer(), primary_key=True, auto_increment=True), - Fk(["font_id"], ["fonts.id"], on_delete="CASCADE"), - Idx("idx_name", ["id", "font_id"]), - ) - - assert table.name == rq.TableName("users", "pub") - assert len(table.columns) == 1 - assert len(table.foreign_keys) == 1 - assert len(table.indexes) == 1 - assert all(isinstance(x, Col) for x in table.columns) - assert all(isinstance(x, Fk) for x in table.foreign_keys) - assert all(isinstance(x, Idx) for x in table.indexes) - - def test_multiple_primary(self): - stmt = rq.Table( - "pub.users", - rq.Column("id", rq.sqltypes.Integer(), primary_key=True), - rq.Column("name", rq.sqltypes.Integer(), primary_key=True), - ) - assert stmt.to_sql("postgres").count("PRIMARY KEY") == 2 - - -class TestTruncateTable: - def table_name_instance(self, tb): - return rq.TruncateTable(tb) - - def test_properties(self): - stmt = rq.TruncateTable("t1") - - assert stmt.name == rq.TableName("t1") - - stmt.name = "pub.users" - assert stmt.name == rq.TableName("users", "pub") diff --git a/tests/test_serializers.py b/tests/test_serializers.py new file mode 100644 index 0000000..d3d8f9e --- /dev/null +++ b/tests/test_serializers.py @@ -0,0 +1,407 @@ +"""Serializer type checks, canonicalization, and literal spelling.""" + +import datetime +import enum +import ipaddress +import typing +import uuid +from dataclasses import dataclass +from decimal import Decimal + +import pytest + +from rapidquery.dialect import ( + BigQuery, + Dialect, + DuckDb, + Hive, + MsSql, + MySql, + Oracle, + PostgreSql, + Snowflake, + Spark, + Sqlite, + Teradata, +) +from rapidquery.serializers import ( + ArraySerializer, + BitSerializer, + BooleanSerializer, + BytesSerializer, + CustomSerializer, + DateSerializer, + DatetimeSerializer, + FloatSerializer, + InetSerializer, + IntegerSerializer, + JSONSerializer, + MacAddressSerializer, + StringSerializer, + TimeSerializer, + UnspecifiedSerializer, + UuidSerializer, +) + +from .conftest import ALL_DIALECTS, USING_ORJSON + +PG = PostgreSql +BOOL_INT = (MsSql, MySql, Oracle, Sqlite) +BIT_SQL = { + PostgreSql: "B'10'", + DuckDb: "B'10'", + MySql: "b'10'", + Teradata: "'10'B", +} +ARRAY_SQL = { + PostgreSql: "ARRAY[1, 2, 3]", + BigQuery: "ARRAY[1, 2, 3]", + DuckDb: "[1, 2, 3]", + Spark: "array(1, 2, 3)", + Hive: "array(1, 2, 3)", +} + + +class InvalidStrEnum(enum.Enum): + Variant = 1 + + +@dataclass(frozen=True, slots=True) +class ExampleDataclass: + x: int + + +def _custom_ok(dialect: Dialect, obj: typing.Any) -> str: + assert isinstance(dialect, Dialect) + return f"CUSTOM:{obj}" + + +def _custom_not_str(dialect: Dialect, obj: typing.Any) -> int: + return 12345 + + +class TestIntegerSerializer: + def test_values(self): + s = IntegerSerializer() + assert s.serialize(PG, 1) == "1" + assert s.serialize(PG, 18_446_744_073_709_551_615) == "18446744073709551615" + assert s.serialize(PG, -9_223_372_036_854_775_808) == "-9223372036854775808" + assert s.serialize(PG, True) == "1" + assert s.serialize(PG, False) == "0" + + @pytest.mark.parametrize("value", [2.5, "invalid"]) + def test_rejects_non_int(self, value: typing.Any): + with pytest.raises(TypeError): + IntegerSerializer().serialize(PG, value) + + +class TestFloatSerializer: + def test_values(self): + s = FloatSerializer() + assert s.serialize(PG, 1.1) == "1.1" + assert s.serialize(PG, 1) == "1" + assert s.serialize(PG, 1.0) == "1.0" + assert s.serialize(PG, Decimal("1.3435")) == "1.3435" + + def test_rejects_non_numeric(self): + with pytest.raises(TypeError): + FloatSerializer().serialize(PG, "invalid") + + @pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")]) + def test_rejects_non_finite(self, value: float): + with pytest.raises(ValueError): + FloatSerializer().serialize(PG, value) + + +@pytest.mark.parametrize("dialect", ALL_DIALECTS) +def test_boolean_spelling(dialect: Dialect): + s = BooleanSerializer() + if dialect in BOOL_INT: + assert s.serialize(dialect, True) == "1" + assert s.serialize(dialect, False) == "0" + else: + assert s.serialize(dialect, True) == "TRUE" + assert s.serialize(dialect, False) == "FALSE" + + +@pytest.mark.parametrize("value", [1, 0, object()]) +def test_boolean_rejects_non_bool(value: typing.Any): + with pytest.raises(TypeError): + BooleanSerializer().serialize(PG, value) + + +@pytest.mark.parametrize("dialect", ALL_DIALECTS) +def test_string_obrien_and_nul(dialect: Dialect): + s = StringSerializer() + sql = s.serialize(dialect, "O'Brien") + assert "O'Brien" not in sql + assert "O''Brien" in sql or "O\\'Brien" in sql + with pytest.raises(ValueError, match="NUL"): + s.serialize(dialect, "a\0b") + + +@pytest.mark.parametrize("value", [InvalidStrEnum.Variant, 1, None, b"bytes"]) +def test_string_rejects_non_str(value: typing.Any): + with pytest.raises(TypeError): + StringSerializer().serialize(PG, value) + + +class TestUuidSerializer: + _TEXT = "103596b1-db75-4ae0-907a-a4a9bc4dab85" + + def test_uuid_and_str(self): + s = UuidSerializer() + expected = f"E'{self._TEXT}'::uuid" + assert s.serialize(PG, self._TEXT) == expected + assert s.serialize(PG, uuid.UUID(self._TEXT)) == expected + assert s.serialize(DuckDb, self._TEXT) == f"UUID '{self._TEXT}'" + assert s.serialize(Sqlite, self._TEXT) == f"'{self._TEXT}'" + + def test_invalid_str(self): + with pytest.raises(ValueError): + UuidSerializer().serialize(PG, "103596-db75-4ae0-907a-a4a9bc4d85") + + def test_rejects_non_uuid(self): + with pytest.raises(TypeError): + UuidSerializer().serialize(PG, 6428739) + + +def test_bytes_pg_snowflake_duckdb(): + dead = b"\xde\xad" + assert BytesSerializer().serialize(PG, dead) == "'\\xDEAD'::bytea" + assert BytesSerializer().serialize(PG, b"") == "'\\x'::bytea" + assert BytesSerializer().serialize(Snowflake, dead) == "TO_BINARY('DEAD', 'HEX')" + assert BytesSerializer().serialize(Snowflake, b"") == "TO_BINARY('', 'HEX')" + assert BytesSerializer().serialize(DuckDb, dead) == "'\\xDE\\xAD'::BLOB" + assert BytesSerializer().serialize(DuckDb, b"") == "''::BLOB" + + +@pytest.mark.parametrize("value", [bytearray(b"abc"), memoryview(b"abc"), "abc", 1, None]) +def test_bytes_rejects_non_exact_bytes(value: typing.Any): + with pytest.raises(TypeError): + BytesSerializer().serialize(PG, value) + + +class TestBitSerializer: + def test_follows_bit_format_when_supported(self, dialect: Dialect): + expected = BIT_SQL.get(dialect) + if expected is None: + with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + BitSerializer().serialize(dialect, "10") + return + assert BitSerializer().serialize(dialect, "10") == expected + + def test_rejects_non_bit_string(self): + with pytest.raises(ValueError): + BitSerializer().serialize(PG, "102") + with pytest.raises(ValueError): + BitSerializer().serialize(PG, "") + with pytest.raises(TypeError): + BitSerializer().serialize(PG, 10) + + +class TestJSONSerializer: + def test_dumps_then_json_format(self): + s = JSONSerializer() + assert s.serialize(PG, 10) == "E'10'" + assert s.serialize(PG, "10") == "E'\"10\"'" + assert s.serialize(BigQuery, {"a": 1}) == "JSON '{\"a\":1}'" + assert s.serialize(Snowflake, {"a": 1}) == "PARSE_JSON('{\"a\":1}')" + assert s.serialize(Sqlite, {"a": 1}) == "'{\"a\":1}'" + + def test_orjson_only_values(self): + s = JSONSerializer() + extras = [ + ( + datetime.datetime(2024, 10, 1, tzinfo=datetime.timezone.utc), + "E'\"2024-10-01T00:00:00+00:00\"'", + ), + ( + uuid.UUID("103596b1-db75-4ae0-907a-a4a9bc4dab85"), + "E'\"103596b1-db75-4ae0-907a-a4a9bc4dab85\"'", + ), + (ExampleDataclass(x=1), "E'{\"x\":1}'"), + ] + for obj, expected in extras: + if USING_ORJSON: + assert s.serialize(PG, obj) == expected + else: + with pytest.raises(TypeError): + s.serialize(PG, obj) + + +class TestDateTimeSerializers: + def test_date(self): + value = datetime.date(2024, 1, 15) + assert DateSerializer().serialize(PG, value) == "DATE E'2024-01-15'" + assert DateSerializer().serialize(Oracle, value) == "DATE '2024-01-15'" + assert DateSerializer().serialize(Sqlite, value) == "'2024-01-15'" + + def test_date_rejects_datetime_and_str(self): + with pytest.raises(TypeError): + DateSerializer().serialize(PG, datetime.datetime(2024, 1, 15, 10, 0)) + with pytest.raises(TypeError): + DateSerializer().serialize(PG, "2024-01-15") + + def test_time(self): + s = TimeSerializer() + assert s.serialize(PG, datetime.time(13, 45, 30)) == "TIME E'13:45:30'" + assert s.serialize(PG, datetime.time(1, 2, 3, 456789)) == "TIME E'01:02:03.456789'" + assert s.serialize(Oracle, datetime.time(13, 45, 30)) == "'13:45:30'" + + def test_time_rejects_date(self): + with pytest.raises(TypeError): + TimeSerializer().serialize(PG, datetime.date(2024, 1, 1)) + + def test_datetime_naive_and_aware(self): + s = DatetimeSerializer() + naive = datetime.datetime(2024, 1, 15, 10, 30, 0) + utc = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc) + offset = datetime.datetime( + 2024, + 1, + 15, + 10, + 30, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=-5)), + ) + assert s.serialize(PG, naive) == "TIMESTAMP E'2024-01-15 10:30:00'" + assert s.serialize(PG, utc) == "TIMESTAMPTZ E'2024-01-15 10:30:00+00:00'" + assert s.serialize(PG, offset) == "TIMESTAMPTZ E'2024-01-15 10:30:00-05:00'" + assert s.serialize(Oracle, naive) == "TIMESTAMP '2024-01-15 10:30:00'" + assert s.serialize(Oracle, offset) == "TIMESTAMP WITH TIMEZONE '2024-01-15 10:30:00-05:00'" + assert s.serialize(Sqlite, naive) == "'2024-01-15 10:30:00'" + assert s.serialize(Sqlite, offset) == "'2024-01-15 10:30:00-05:00'" + + def test_datetime_unix_timestamp(self): + s = DatetimeSerializer() + assert s.serialize(PG, 0) == "TIMESTAMPTZ E'1970-01-01 00:00:00+00:00'" + assert s.serialize(PG, 0.5) == "TIMESTAMPTZ E'1970-01-01 00:00:00.500+00:00'" + + def test_datetime_rejects_date_and_str(self): + with pytest.raises(TypeError): + DatetimeSerializer().serialize(PG, datetime.date(2024, 1, 15)) + with pytest.raises(TypeError): + DatetimeSerializer().serialize(PG, "2024-01-15T10:30:00") + + def test_datetime_overflow(self): + with pytest.raises(OverflowError): + DatetimeSerializer().serialize(PG, 10**20) + + +class TestInetSerializer: + def test_canonical_cidr(self, dialect: Dialect): + if dialect not in (PostgreSql, DuckDb): + with pytest.raises(NotImplementedError, match="does not support InetSerializer"): + InetSerializer().serialize(dialect, "192.168.1.1") + return + s = InetSerializer() + cases = [ + ("192.168.1.1", "192.168.1.1/32"), + (ipaddress.ip_address("192.168.1.1"), "192.168.1.1/32"), + ("192.168.1.0/24", "192.168.1.0/24"), + ("::1", "::1/128"), + ("2001:db8::/32", "2001:db8::/32"), + ] + for obj, inner in cases: + if dialect == PostgreSql: + expected = f"E'{inner}'::inet" + else: + expected = f"'{inner}'::inet" + assert s.serialize(dialect, obj) == expected + + def test_invalid_address(self): + with pytest.raises(ValueError): + InetSerializer().serialize(PG, "not-an-ip") + + +class TestMacAddressSerializer: + def test_canonical_form(self, dialect: Dialect): + if dialect != PostgreSql: + with pytest.raises(NotImplementedError, match="does not support MacAddressSerializer"): + MacAddressSerializer().serialize(dialect, "01:23:45:67:89:ab") + return + s = MacAddressSerializer() + expected = "E'01:23:45:67:89:AB'::macaddr" + assert s.serialize(dialect, "01:23:45:67:89:ab") == expected + assert s.serialize(dialect, "01-23-45-67-89-AB") == expected + + def test_invalid_address(self): + with pytest.raises(ValueError): + MacAddressSerializer().serialize(PG, "not-a-mac") + + +class TestUnspecifiedSerializer: + def test_uses_str(self): + s = UnspecifiedSerializer() + assert s.serialize(PG, 123) == "123" + assert s.serialize(PG, "hello") == "hello" + assert s.serialize(PG, None) == "None" + + +class TestArraySerializer: + def test_renders_array_constructor(self, dialect: Dialect): + s = ArraySerializer(IntegerSerializer()) + expected = ARRAY_SQL.get(dialect) + if expected is None: + with pytest.raises(NotImplementedError, match="does not support ArraySerializer"): + s.serialize(dialect, [1, 2, 3]) + return + assert s.serialize(dialect, [1, 2, 3]) == expected + assert s.serialize(dialect, (1, 2, 3)) == expected + empty = expected.replace("1, 2, 3", "") + assert s.serialize(dialect, []) == empty + nested = ArraySerializer(ArraySerializer(IntegerSerializer())) + if dialect in (PostgreSql, BigQuery): + assert nested.serialize(dialect, [[1, 2], [3]]) == "ARRAY[ARRAY[1, 2], ARRAY[3]]" + elif dialect == DuckDb: + assert nested.serialize(dialect, [[1, 2], [3]]) == "[[1, 2], [3]]" + elif dialect in (Spark, Hive): + assert nested.serialize(dialect, [[1, 2], [3]]) == "array(array(1, 2), array(3))" + else: + raise AssertionError(expected) + + def test_rejects_non_sequence_and_bad_element(self): + s = ArraySerializer(IntegerSerializer()) + with pytest.raises(TypeError): + s.serialize(PG, "not a list") + with pytest.raises(TypeError): + s.serialize(PG, [1, "x"]) + + def test_nested_unsupported_element(self): + with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + ArraySerializer(BitSerializer()).serialize(BigQuery, ["10"]) + + +class TestCustomSerializer: + def test_receives_dialect_instance(self): + dialect = DuckDb + assert CustomSerializer(_custom_ok).serialize(dialect, 1) == "CUSTOM:1" + + def test_must_return_str(self): + with pytest.raises(TypeError): + CustomSerializer(_custom_not_str).serialize(PG, "x") # pyright: ignore + + +def test_unsupported_serializer_error_message(): + with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + BitSerializer().serialize(Sqlite, "10") + with pytest.raises(NotImplementedError, match="does not support ArraySerializer"): + ArraySerializer(IntegerSerializer()).serialize(MySql, [1, 2]) + with pytest.raises(NotImplementedError, match="does not support InetSerializer"): + InetSerializer().serialize(Sqlite, "192.168.1.1") + with pytest.raises(NotImplementedError, match="does not support MacAddressSerializer"): + MacAddressSerializer().serialize(DuckDb, "01:23:45:67:89:ab") + + +class TestLiteralGoldens: + def test_pg_string_uses_e_prefix(self): + assert StringSerializer().serialize(PG, "hello") == "E'hello'" + assert StringSerializer().serialize(PG, "a\nb") == "E'a\\nb'" + + def test_mssql_nvarchar_prefix(self): + assert StringSerializer().serialize(MsSql, "hi") == "N'hi'" + + def test_pg_e_needed_for_backslash(self): + assert StringSerializer().serialize(PG, "a\\b") == "E'a\\\\b'" diff --git a/tests/test_sqltypes.py b/tests/test_sqltypes.py deleted file mode 100644 index 5538402..0000000 --- a/tests/test_sqltypes.py +++ /dev/null @@ -1,262 +0,0 @@ -import datetime -import decimal -import enum -import uuid -from collections import namedtuple - -import pytest - -import rapidquery as rq - -Case = namedtuple( - "Case", - ("data_type", "value", "exc"), -) - - -class Enum(enum.Enum): - FIELD = "field" - - -class StrEnum(str, enum.Enum): - FIELD = "field" - - -class IntEnum(int, enum.Enum): - FIELD = 1 - - -TEST_CASES = [ - # --- BlobType --- - Case(rq.sqltypes.Blob(), b"A", None), - Case(rq.sqltypes.Blob(), 4.5, TypeError), - Case(rq.sqltypes.Blob(), "Ali", TypeError), - # --- BinaryType --- - Case(rq.sqltypes.Binary(), b"A", None), - Case(rq.sqltypes.Binary(), 4.5, TypeError), - Case(rq.sqltypes.Binary(), "Ali", TypeError), - # --- VarBinaryType --- - Case(rq.sqltypes.VarBinary(), b"A", None), - Case(rq.sqltypes.VarBinary(), 4.5, TypeError), - Case(rq.sqltypes.VarBinary(), "Ali", TypeError), - # --- BitType --- - Case(rq.sqltypes.Bit(8), b"A", None), - Case(rq.sqltypes.Bit(8), 4.5, TypeError), - Case(rq.sqltypes.Bit(8), "Ali", TypeError), - # --- VarBitType --- - Case(rq.sqltypes.VarBit(8), b"A", None), - Case(rq.sqltypes.VarBit(8), 4.5, TypeError), - Case(rq.sqltypes.VarBit(8), "Ali", TypeError), - # --- DateTimeType --- - Case(rq.sqltypes.DateTime(), datetime.datetime.now(), None), - Case(rq.sqltypes.DateTime(), datetime.datetime.now(tz=datetime.timezone.utc), None), - Case(rq.sqltypes.DateTime(), datetime.datetime.now(tz=datetime.timezone.min), None), - Case(rq.sqltypes.DateTime(), datetime.datetime.now().date(), TypeError), - Case(rq.sqltypes.DateTime(), datetime.datetime.now().time(), TypeError), - Case(rq.sqltypes.DateTime(), 4, TypeError), - Case(rq.sqltypes.DateTime(), 4.5, TypeError), - Case(rq.sqltypes.DateTime(), "Ali", TypeError), - # --- TimestampType --- - Case(rq.sqltypes.Timestamp(), datetime.datetime.now(), None), - Case( - rq.sqltypes.Timestamp(True), - datetime.datetime.now(tz=datetime.timezone.utc), - None, - ), - Case(rq.sqltypes.Timestamp(), datetime.datetime.now(tz=datetime.timezone.min), None), - Case(rq.sqltypes.Timestamp(), datetime.datetime.now().date(), TypeError), - Case(rq.sqltypes.Timestamp(), datetime.datetime.now().time(), TypeError), - Case(rq.sqltypes.Timestamp(), datetime.datetime.now().timestamp(), None), - Case(rq.sqltypes.Timestamp(), int(datetime.datetime.now().timestamp()), None), - Case(rq.sqltypes.Timestamp(), "Ali", TypeError), - # --- TimeType --- - Case(rq.sqltypes.Time(), datetime.datetime.now(), TypeError), - Case(rq.sqltypes.Time(), datetime.datetime.now().date(), TypeError), - Case(rq.sqltypes.Time(), datetime.datetime.now().time(), None), - Case(rq.sqltypes.Time(), datetime.datetime.now().timestamp(), TypeError), - Case(rq.sqltypes.Time(), int(datetime.datetime.now().timestamp()), TypeError), - Case(rq.sqltypes.Time(), "Ali", TypeError), - # --- DateType --- - Case(rq.sqltypes.Date(), datetime.datetime.now(), TypeError), - Case(rq.sqltypes.Date(), datetime.datetime.now().date(), None), - Case(rq.sqltypes.Date(), datetime.datetime.now().time(), TypeError), - Case(rq.sqltypes.Date(), datetime.datetime.now().timestamp(), TypeError), - Case(rq.sqltypes.Date(), int(datetime.datetime.now().timestamp()), TypeError), - Case(rq.sqltypes.Date(), "Ali", TypeError), - # --- JSONType --- - Case(rq.sqltypes.JSON(), 4.5, None), - Case(rq.sqltypes.JSON(), 4456746532, None), - Case(rq.sqltypes.JSON(), "Ali", None), - Case(rq.sqltypes.JSON(), {"key": "value"}, None), - Case(rq.sqltypes.JSON(), [2, 3], None), - Case(rq.sqltypes.JSON(), b"bytes", TypeError), - Case(rq.sqltypes.JSON(), (1, 2), TypeError), - Case(rq.sqltypes.JSON(), datetime.datetime.now(), TypeError), - # --- JSONBinaryType --- - Case(rq.sqltypes.JSONBinary(), 4.5, None), - Case(rq.sqltypes.JSONBinary(), 4456746532, None), - Case(rq.sqltypes.JSONBinary(), "Ali", None), - Case(rq.sqltypes.JSONBinary(), {"key": "value"}, None), - Case(rq.sqltypes.JSONBinary(), [2, 3], None), - Case(rq.sqltypes.JSONBinary(), b"bytes", TypeError), - Case(rq.sqltypes.JSONBinary(), (1, 2), TypeError), - Case(rq.sqltypes.JSONBinary(), datetime.datetime.now(), TypeError), - # --- DecimalType --- - Case(rq.sqltypes.Decimal(None), 4.5, None), - Case(rq.sqltypes.Decimal((10, 4)), "5.6", None), - Case(rq.sqltypes.Decimal(), 4, None), - Case(rq.sqltypes.Decimal(), "9e-10", None), - Case(rq.sqltypes.Decimal(), decimal.Decimal("1.0"), None), - Case(rq.sqltypes.Decimal(), "Ali", ValueError), - # --- UUIDType --- - Case(rq.sqltypes.UUID(), uuid.uuid4(), None), - Case(rq.sqltypes.UUID(), uuid.uuid4().hex, TypeError), - Case(rq.sqltypes.UUID(), uuid.uuid4().int, TypeError), - # --- INETType --- - Case(rq.sqltypes.INET(), "1.2.3.4", None), - Case(rq.sqltypes.INET(), "1.2.3.4/23", None), - Case(rq.sqltypes.INET(), "127.0.0.1", None), - Case(rq.sqltypes.INET(), "127.0.0.1:8080", ValueError), - Case(rq.sqltypes.INET(), "EA:31:7D:14:D8:40", ValueError), - Case(rq.sqltypes.INET(), "invalid", ValueError), - Case(rq.sqltypes.INET(), uuid.uuid4().hex, ValueError), - Case(rq.sqltypes.INET(), 45, TypeError), - # --- MacAddressType --- - Case(rq.sqltypes.MacAddress(), "EA:31:7D:14:D8:40", None), - Case(rq.sqltypes.MacAddress(), "1.2.3.4", ValueError), - Case(rq.sqltypes.MacAddress(), "invalid", ValueError), - Case(rq.sqltypes.MacAddress(), uuid.uuid4().hex, ValueError), - Case(rq.sqltypes.MacAddress(), 45, TypeError), - # --- EnumType --- - Case(rq.sqltypes.Enum("USER", ("a", "b")), "string", None), - Case(rq.sqltypes.Enum("USER", ("a", "b")), Enum.FIELD, None), - Case(rq.sqltypes.Enum("USER", ("a", "b")), StrEnum.FIELD, None), - Case(rq.sqltypes.Enum("USER", ("a", "b")), IntEnum.FIELD, TypeError), - Case(rq.sqltypes.Enum("USER", ("a", "b")), Enum, TypeError), - Case(rq.sqltypes.Enum("USER", ("a", "b")), 4.5, TypeError), - Case(rq.sqltypes.Enum("USER", ("a", "b")), 45, TypeError), - # --- BigIntegerType --- - Case(rq.sqltypes.BigInteger(), 1638479230, None), - Case(rq.sqltypes.BigInteger(), -1638479230, None), - Case(rq.sqltypes.BigInteger(), 4.5, TypeError), - Case(rq.sqltypes.BigInteger(), "Ali", TypeError), - Case(rq.sqltypes.BigInteger(), 9223372036854775807, None), - Case(rq.sqltypes.BigInteger(), 9223372036854775807 + 1, OverflowError), - # --- IntegerType --- - Case(rq.sqltypes.Integer(), 37, None), - Case(rq.sqltypes.Integer(), -37, None), - Case(rq.sqltypes.Integer(), 4.5, TypeError), - Case(rq.sqltypes.Integer(), "Ali", TypeError), - Case(rq.sqltypes.Integer(), 2147483647, None), - Case(rq.sqltypes.Integer(), 2147483647 + 1, OverflowError), - # --- SmallIntegerType --- - Case(rq.sqltypes.SmallInteger(), 647, None), - Case(rq.sqltypes.SmallInteger(), -647, None), - Case(rq.sqltypes.SmallInteger(), 4.5, TypeError), - Case(rq.sqltypes.SmallInteger(), "Ali", TypeError), - Case(rq.sqltypes.SmallInteger(), 32767, None), - Case(rq.sqltypes.SmallInteger(), 32767 + 1, OverflowError), - # --- TinyIntegerType --- - Case(rq.sqltypes.TinyInteger(), 10, None), - Case(rq.sqltypes.TinyInteger(), -10, None), - Case(rq.sqltypes.TinyInteger(), 4.5, TypeError), - Case(rq.sqltypes.TinyInteger(), "Ali", TypeError), - Case(rq.sqltypes.TinyInteger(), 127, None), - Case(rq.sqltypes.TinyInteger(), 127 + 1, OverflowError), - # --- BigUnsignedType --- - Case(rq.sqltypes.BigUnsigned(), 89438302, None), - Case(rq.sqltypes.BigUnsigned(), -1, OverflowError), - Case(rq.sqltypes.BigUnsigned(), 4.5, TypeError), - Case(rq.sqltypes.BigUnsigned(), "Ali", TypeError), - Case(rq.sqltypes.BigUnsigned(), 18446744073709551615, None), - Case(rq.sqltypes.BigUnsigned(), 18446744073709551615 + 1, OverflowError), - # --- UnsignedType --- - Case(rq.sqltypes.Unsigned(), 89438302, None), - Case(rq.sqltypes.Unsigned(), -1, OverflowError), - Case(rq.sqltypes.Unsigned(), 4.5, TypeError), - Case(rq.sqltypes.Unsigned(), "Ali", TypeError), - Case(rq.sqltypes.Unsigned(), 4294967295, None), - Case(rq.sqltypes.Unsigned(), 4294967295 + 1, OverflowError), - # --- SmallUnsignedType --- - Case(rq.sqltypes.SmallUnsigned(), 978, None), - Case(rq.sqltypes.SmallUnsigned(), -1, OverflowError), - Case(rq.sqltypes.SmallUnsigned(), 4.5, TypeError), - Case(rq.sqltypes.SmallUnsigned(), "Ali", TypeError), - Case(rq.sqltypes.SmallUnsigned(), 65535, None), - Case(rq.sqltypes.SmallUnsigned(), 65535 + 1, OverflowError), - # --- TinyUnsignedType --- - Case(rq.sqltypes.TinyUnsigned(), 20, None), - Case(rq.sqltypes.TinyUnsigned(), -1, OverflowError), - Case(rq.sqltypes.TinyUnsigned(), 4.5, TypeError), - Case(rq.sqltypes.TinyUnsigned(), "Ali", TypeError), - Case(rq.sqltypes.TinyUnsigned(), 255, None), - Case(rq.sqltypes.TinyUnsigned(), 255 + 1, OverflowError), - # --- FloatType --- - Case(rq.sqltypes.Float(), 20.0, None), - Case(rq.sqltypes.Float(), 20, None), - Case(rq.sqltypes.Float(), 4.5, None), - Case(rq.sqltypes.Float(), "Ali", TypeError), - # --- DoubleType --- - Case(rq.sqltypes.Double(), 20.0, None), - Case(rq.sqltypes.Double(), 20, None), - Case(rq.sqltypes.Double(), 4.5, None), - Case(rq.sqltypes.Double(), "Ali", TypeError), - # --- TextType --- - Case(rq.sqltypes.Text(), "A", None), - Case(rq.sqltypes.Text(), "A" * 10000, None), - Case(rq.sqltypes.Text(), b"B", TypeError), - Case(rq.sqltypes.Text(), 1, TypeError), - Case(rq.sqltypes.Text(), 1.4, TypeError), - # --- StringType --- - Case(rq.sqltypes.String(), "A", None), - Case(rq.sqltypes.String(), "A" * 10000, None), - Case(rq.sqltypes.String(), b"B", TypeError), - Case(rq.sqltypes.String(), 1, TypeError), - Case(rq.sqltypes.String(), 1.4, TypeError), - # --- CharType --- - Case(rq.sqltypes.Char(), "A", None), - Case(rq.sqltypes.Char(), "A" * 10000, None), - Case(rq.sqltypes.Char(), b"B", TypeError), - Case(rq.sqltypes.Char(), 1, TypeError), - Case(rq.sqltypes.Char(), 1.4, TypeError), - # --- ArrayType --- - Case(rq.sqltypes.Array(rq.sqltypes.Integer()), [1, 2], None), - Case(rq.sqltypes.Array(rq.sqltypes.Integer()), [1, 2], None), - Case(rq.sqltypes.Array(rq.sqltypes.Integer()), (1, 2, 5), None), - Case(rq.sqltypes.Array(rq.sqltypes.Float()), [1.3, 2.7], None), - Case(rq.sqltypes.Array(rq.sqltypes.Integer()), [1, "A"], TypeError), - Case( - rq.sqltypes.Array(rq.sqltypes.Integer()), - [1, 2, 3, 4, 5, 6, 7, 8, "A", 10], - TypeError, - ), - Case(rq.sqltypes.Array(rq.sqltypes.Text()), ["B", "A"], TypeError), - # --- VectorType --- - Case(rq.sqltypes.Vector(6), [1.4, 2.1], None), - Case(rq.sqltypes.Vector(5), [1, 2], None), - Case(rq.sqltypes.Vector(4), (1, 2, 5.3), None), - Case(rq.sqltypes.Vector(3), [1.3, 2.7], None), - Case(rq.sqltypes.Vector(2), [1, "A"], TypeError), - Case(rq.sqltypes.Vector(1), [1, 2, 3, 4, 5, 6, 7, 8, "A", 10], TypeError), - Case(rq.sqltypes.Vector(), ["B", "A"], TypeError), -] - - -class TestSQLType: - @pytest.mark.parametrize("case", TEST_CASES) - def test_validate_serialize(self, case: Case): - try: - val = rq.Value(case.value, case.data_type) - rq.Expr(val) # Force Value to adapt - except Exception as e: - if case.exc is not None and type(e) is case.exc: - return - - raise e - - assert val.sql_type is case.data_type - assert val.value == case.value - - @pytest.mark.parametrize("case", TEST_CASES) - def test_type_name(self, case: Case): - assert case.data_type.__type_name__ diff --git a/tests/test_value.py b/tests/test_value.py new file mode 100644 index 0000000..ff5f43c --- /dev/null +++ b/tests/test_value.py @@ -0,0 +1,189 @@ +import datetime +import decimal +import enum +import uuid + +import pytest + +from rapidquery.dialect import PostgreSql +from rapidquery.expr import Expr, Value +from rapidquery.serializers import ( + ArraySerializer, + BitSerializer, + BooleanSerializer, + BytesSerializer, + CustomSerializer, + DateSerializer, + DatetimeSerializer, + FloatSerializer, + InetSerializer, + IntegerSerializer, + JSONSerializer, + MacAddressSerializer, + Serializer, + StringSerializer, + TimeSerializer, + UnspecifiedSerializer, + UuidSerializer, +) +from tests.conftest import USING_ORJSON + + +class ValidEnum(enum.StrEnum): + TEST = "test" + + +class InvalidEnum(enum.Enum): + TEST = 1 + + +INFER_TEST_CASES: dict[type[Serializer], list[object]] = { + IntegerSerializer: [1, 18_446_744_073_709_551_615, -1], + FloatSerializer: [1.0, decimal.Decimal("1.0"), decimal.Decimal(1)], + BooleanSerializer: [True, False], + StringSerializer: ["string", ValidEnum.TEST], + UuidSerializer: [uuid.UUID("58073209-2238-4d99-9e74-1bc21110393b")], + BytesSerializer: [b"bytes"], + BitSerializer: [], + JSONSerializer: [[], {}, {"OK": 1}], + DateSerializer: [datetime.date(2026, 10, 3)], + TimeSerializer: [datetime.time(10, 23)], + DatetimeSerializer: [datetime.datetime(2026, 10, 3, 23, 42, 34)], + InetSerializer: [], + MacAddressSerializer: [], + UnspecifiedSerializer: [datetime.timezone.utc, InvalidEnum.TEST], + ArraySerializer: [], +} + +if USING_ORJSON: + INFER_TEST_CASES[JSONSerializer].append({"OK": datetime.datetime(2026, 10, 3, 23, 42, 34)}) + INFER_TEST_CASES[JSONSerializer].append({"OK": uuid.UUID("58073209-2238-4d99-9e74-1bc21110393b")}) + + +class TestValueConstruction: + def test_new_overload_1(self): + val = Value(1) + assert isinstance(val, Expr) + assert val.get_object() == 1 + assert isinstance(val.get_serializer(), IntegerSerializer) + + def test_new_overload_2(self): + val = Value(1, InetSerializer()) + assert isinstance(val, Expr) + assert val.get_object() == 1 + assert isinstance(val.get_serializer(), InetSerializer) + + @pytest.mark.parametrize( + "serializer_cls, value", + [ + pytest.param(cls, value, id=f"{cls.__name__}-{value!r}") + for cls, values in INFER_TEST_CASES.items() + for value in values + ], + ) + def test_infers(self, serializer_cls: type[Serializer], value: object): + val = Value(value) + assert val.get_object() == value + assert isinstance(val.get_serializer(), serializer_cls) + + def test_wrap_inner(self): + inner = Value(object(), UnspecifiedSerializer()) + outer = Value(inner) + + assert inner.get_object() is outer.get_object() + assert inner.get_serializer() == outer.get_serializer() + + def test_wrap_trick_to_change_serializer(self): + inner = Value(object(), InetSerializer()) + outer = Value(inner, UnspecifiedSerializer()) + + assert inner.get_object() is outer.get_object() + assert isinstance(inner.get_serializer(), InetSerializer) + assert isinstance(outer.get_serializer(), UnspecifiedSerializer) + + +class TestValueToSql: + def test_integer_literal(self): + assert Value(42).to_sql(PostgreSql) == "42" + + def test_string_literal_matches_string_serializer(self): + assert Value("hi").to_sql(PostgreSql) == StringSerializer().serialize(PostgreSql, "hi") + + def test_none_is_null(self): + assert Value(None).to_sql(PostgreSql) == "NULL" + + def test_propagates_serializer_type_error(self): + with pytest.raises(TypeError): + Value("not an int", IntegerSerializer()).to_sql(PostgreSql) + + +class TestSerializerProtocol: + def test_classvar_attribute(self): + + class Unk: + __serializer__: Serializer = CustomSerializer["Unk"](lambda _, x: str(x)) + + def __str__(self) -> str: + return "UNK" + + val = Value(Unk()) + assert isinstance(val.get_serializer(), CustomSerializer) + assert val.to_sql(PostgreSql) == "UNK" + + def test_property(self): + + class Unk: + @property + def __serializer__(self): + return CustomSerializer["Unk"](lambda _, x: str(x)) + + def __str__(self) -> str: + return "UNK" + + val = Value(Unk()) + assert isinstance(val.get_serializer(), CustomSerializer) + assert val.to_sql(PostgreSql) == "UNK" + + def test_invalid_attribute(self): + + class Unk: + __serializer__ = 10 + + with pytest.raises(TypeError, match="non-serializer"): + _ = Value(Unk()) + + def test_attribute_error(self): + + class Unk: + def __str__(self) -> str: + return "UNK" + + val = Value(Unk()) + assert isinstance(val.get_serializer(), UnspecifiedSerializer) + assert val.to_sql(PostgreSql) == "UNK" + + def test_builtin_types_priority(self): + + class String(str): + # It must not use this attribute + # We are using PyUnicode_Check to support StrEnum + __serializer__ = IntegerSerializer() + + val = Value(String("test")) + assert isinstance(val.get_serializer(), StringSerializer) + + class Integer(int): + # It must not use this attribute + # We are using PyLong_Check to support IntEnum + __serializer__ = UnspecifiedSerializer() + + val = Value(Integer(1)) + assert isinstance(val.get_serializer(), IntegerSerializer) + + class Blob(bytes): + # It MUST use this attribute + # We are using PyBytes_CheckExact, so we only use BytesSerializer to literaly `bytes`. + __serializer__ = FloatSerializer() + + val = Value(Blob(b"test")) + assert isinstance(val.get_serializer(), FloatSerializer) diff --git a/uv.lock b/uv.lock index de45cac..2059735 100644 --- a/uv.lock +++ b/uv.lock @@ -1,779 +1,894 @@ version = 1 -revision = 3 -requires-python = ">=3.8" +revision = 5 +requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, + { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, + { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, + { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, + { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, + { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, + { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, + { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, + { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, + { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, + { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, + { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] -name = "greenlet" -version = "3.1.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467" } +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563" }, - { url = "https://mirror-pypi.runflare.com/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83" }, - { url = "https://mirror-pypi.runflare.com/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0" }, - { url = "https://mirror-pypi.runflare.com/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc" }, - { url = "https://mirror-pypi.runflare.com/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617" }, - { url = "https://mirror-pypi.runflare.com/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7" }, - { url = "https://mirror-pypi.runflare.com/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6" }, - { url = "https://mirror-pypi.runflare.com/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80" }, - { url = "https://mirror-pypi.runflare.com/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70" }, - { url = "https://mirror-pypi.runflare.com/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159" }, - { url = "https://mirror-pypi.runflare.com/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e" }, - { url = "https://mirror-pypi.runflare.com/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383" }, - { url = "https://mirror-pypi.runflare.com/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a" }, - { url = "https://mirror-pypi.runflare.com/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511" }, - { url = "https://mirror-pypi.runflare.com/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395" }, - { url = "https://mirror-pypi.runflare.com/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39" }, - { url = "https://mirror-pypi.runflare.com/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d" }, - { url = "https://mirror-pypi.runflare.com/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79" }, - { url = "https://mirror-pypi.runflare.com/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa" }, - { url = "https://mirror-pypi.runflare.com/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36" }, - { url = "https://mirror-pypi.runflare.com/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9" }, - { url = "https://mirror-pypi.runflare.com/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0" }, - { url = "https://mirror-pypi.runflare.com/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942" }, - { url = "https://mirror-pypi.runflare.com/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1" }, - { url = "https://mirror-pypi.runflare.com/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff" }, - { url = "https://mirror-pypi.runflare.com/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a" }, - { url = "https://mirror-pypi.runflare.com/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4" }, - { url = "https://mirror-pypi.runflare.com/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e" }, - { url = "https://mirror-pypi.runflare.com/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1" }, - { url = "https://mirror-pypi.runflare.com/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c" }, - { url = "https://mirror-pypi.runflare.com/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761" }, - { url = "https://mirror-pypi.runflare.com/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011" }, - { url = "https://mirror-pypi.runflare.com/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13" }, - { url = "https://mirror-pypi.runflare.com/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b" }, - { url = "https://mirror-pypi.runflare.com/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822" }, - { url = "https://mirror-pypi.runflare.com/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01" }, - { url = "https://mirror-pypi.runflare.com/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6" }, - { url = "https://mirror-pypi.runflare.com/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9" }, - { url = "https://mirror-pypi.runflare.com/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111" }, - { url = "https://mirror-pypi.runflare.com/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81" }, - { url = "https://mirror-pypi.runflare.com/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8" }, - { url = "https://mirror-pypi.runflare.com/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1" }, - { url = "https://mirror-pypi.runflare.com/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd" }, - { url = "https://mirror-pypi.runflare.com/packages/9f/f5/e9b151ddd2ed0508b7a47bef7857e46218dbc3fd10e564617a3865abfaac/greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7" }, - { url = "https://mirror-pypi.runflare.com/packages/86/97/2c86989ca4e0f089fbcdc9229c972a01ef53abdafd5ae89e0f3dcdcd4adb/greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef" }, - { url = "https://mirror-pypi.runflare.com/packages/d3/50/7b7a3e10ed82c760c1fd8d3167a7c95508e9fdfc0b0604f05ed1a9a9efdc/greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d" }, - { url = "https://mirror-pypi.runflare.com/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3" }, - { url = "https://mirror-pypi.runflare.com/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42" }, - { url = "https://mirror-pypi.runflare.com/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" }, - { url = "https://mirror-pypi.runflare.com/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145" }, - { url = "https://mirror-pypi.runflare.com/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c" }, - { url = "https://mirror-pypi.runflare.com/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e" }, - { url = "https://mirror-pypi.runflare.com/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e" }, - { url = "https://mirror-pypi.runflare.com/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c" }, - { url = "https://mirror-pypi.runflare.com/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22" }, -] - -[[package]] -name = "greenlet" -version = "3.2.5" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/b0/f5/3e9eafb4030588337b2a2ae4df46212956854e9069c07b53aa3caabafd47/greenlet-3.2.5.tar.gz", hash = "sha256:c816554eb33e7ecf9ba4defcb1fd8c994e59be6b4110da15480b3e7447ea4286" } + +[[package]] +name = "filelock" +version = "4.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/51/a182494d1d8dde1240bff84dda57d48165d982e59582ce8f167e8e3d7628/filelock-4.0.10.tar.gz", hash = "sha256:00d6a81f976a6332551c2c10f39e12b4abb7e01c64d4c497b81a615bc9186f1f", size = 252487, upload-time = "2026-10-03T23:40:48.884Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/ff/d6/b3db928fc329b1b19ba32ffe143d2305f3aaafc583f5e1074c74ec445189/greenlet-3.2.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:34cc7cf8ab6f4b85298b01e13e881265ee7b3c1daf6bc10a2944abc15d4f87c3" }, - { url = "https://mirror-pypi.runflare.com/packages/b3/ff/ab0ad4ff3d9e1faa266de4f6c79763b33fccd9265995f2940192494cc0ec/greenlet-3.2.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c11fe0cfb0ce33132f0b5d27eeadd1954976a82e5e9b60909ec2c4b884a55382" }, - { url = "https://mirror-pypi.runflare.com/packages/da/dd/7b3ac77099a1671af8077ecedb12c9a1be1310e4c35bb69fd34c18ab6093/greenlet-3.2.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a145f4b1c4ed7a2c94561b7f18b4beec3d3fb6f0580db22f7ed1d544e0620b34" }, - { url = "https://mirror-pypi.runflare.com/packages/0f/36/84630e9ff1dfc8b7690957c0f77834a84eabdbd9c4977c3a2d0cbd5325c2/greenlet-3.2.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1d01bdd67db3e5711e6246e451d7a0f75fae7bbf40adde129296a7f9aa7cc9" }, - { url = "https://mirror-pypi.runflare.com/packages/12/c4/6a2ee6c676dea7a05a3c3c1291fbc8ea44f26456b0accc891471293825af/greenlet-3.2.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd593db7ee1fa8a513a48a404f8cc4126998a48025e3f5cbbc68d51be0a6bf66" }, - { url = "https://mirror-pypi.runflare.com/packages/01/c0/75e75c2c993aa850292561ec80f5c263e3924e5843aa95a38716df69304c/greenlet-3.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac8db07bced2c39b987bba13a3195f8157b0cfbce54488f86919321444a1cc3c" }, - { url = "https://mirror-pypi.runflare.com/packages/ee/03/e38ebf9024a0873fe8f60f5b7bc36bfb3be5e13efe4d798240f2d1f0fb73/greenlet-3.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4544ab2cfd5912e42458b13516429e029f87d8bbcdc8d5506db772941ae12493" }, - { url = "https://mirror-pypi.runflare.com/packages/d8/7b/c6e1192c795c0c12871e199237909a6bd35757d92c8472c7c019959b8637/greenlet-3.2.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acabf468466d18017e2ae5fbf1a5a88b86b48983e550e1ae1437b69a83d9f4ac" }, - { url = "https://mirror-pypi.runflare.com/packages/3e/b6/9887b559f3e1952d23052ec352e9977e808a2246c7cb8282a38337221e88/greenlet-3.2.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:472841de62d60f2cafd60edd4fd4dd7253eb70e6eaf14b8990dcaf177f4af957" }, - { url = "https://mirror-pypi.runflare.com/packages/8a/be/e3e48b63bbc27d660fa1d98aecb64906b90a12e686a436169c1330ef34b2/greenlet-3.2.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d951e7d628a6e8b68af469f0fe4f100ef64c4054abeb9cdafbfaa30a920c950" }, - { url = "https://mirror-pypi.runflare.com/packages/4c/ac/e731ed62576e91e533b36d0d97325adc2786674ab9e48ed8a6a24f4ef4e9/greenlet-3.2.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8317d732e2ae0935d9ed2af2ea876fa714cf6f3b887a31ca150b54329b0a6e9" }, - { url = "https://mirror-pypi.runflare.com/packages/70/64/99e5cdceb494bd4c1341c45b93f322601d2c8a5e1e4d1c7a2d24c5ed0570/greenlet-3.2.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce8aed6fdd5e07d3cbb988cbdc188266a4eb9e1a52db9ef5c6526e59962d3933" }, - { url = "https://mirror-pypi.runflare.com/packages/ee/e9/968e11f388c2b8792d3b8b40a57984c894a3b4745dae3662dce722653bc5/greenlet-3.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:60c06b502d56d5451f60ca665691da29f79ed95e247bcf8ce5024d7bbe64acb9" }, - { url = "https://mirror-pypi.runflare.com/packages/cb/2c/b5f2c4c68d753dce08218dc5a6b21d82238fdfdc44309032f6fe24d285e6/greenlet-3.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d2a78e6f1bf3f1672df91e212a2f8314e1e7c922f065d14cbad4bc815059467" }, - { url = "https://mirror-pypi.runflare.com/packages/ad/32/022b21523eee713e7550162d5ca6aed23f913cc2c6232b154b9fd9badc07/greenlet-3.2.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2acb30e77042f747ca81f0a10cc153296567e92e666c5e1b117f4595afd43352" }, - { url = "https://mirror-pypi.runflare.com/packages/90/c5/8a3b0ed3cc34d8b988a44349437dfa0941f9c23ac108175f7b4ccea97111/greenlet-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:393c03c26c865f17f31d8db2f09603fadbe0581ad85a5d5908b131549fc38217" }, - { url = "https://mirror-pypi.runflare.com/packages/b1/2c/2627bea183554695016af6cae93d7474fa90f61e5a6601a84ae7841cb720/greenlet-3.2.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04e6a202cde56043fd355fefd1552c4caa5c087528121871d950eb4f1b51fa99" }, - { url = "https://mirror-pypi.runflare.com/packages/2f/1b/75a5aeff487a26ba427a3837da6372f1fe6f2a9c6b2898e28ac99d491c11/greenlet-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45fcea7b697b91290b36eafc12fff479aca6ba6500d98ef6f34d5634c7119cbe" }, - { url = "https://mirror-pypi.runflare.com/packages/53/91/9b5dfb4f3c88f8247c7a8f4c3759f0740bfa6bb0c59a9f6bf938e913df56/greenlet-3.2.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96e2bb8a56b7e1aed1dbfbbe0050cb2ecca99c7c91892fd1771e3afab63b3e3" }, - { url = "https://mirror-pypi.runflare.com/packages/b4/8d/d0b086410512d9859c84e9242a9b341de9f5566011ddf3a3f6886b842b61/greenlet-3.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d7456e67b0be653dfe643bb37d9566cd30939c80f858e2ce6d2d54951f75b14a" }, - { url = "https://mirror-pypi.runflare.com/packages/ef/37/59fe12fe456e84ced6ba71781e28cde52a3124d1dd2077bc1727021f49fd/greenlet-3.2.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5ceb29d1f74c7280befbbfa27b9bf91ba4a07a1a00b2179a5d953fc219b16c42" }, - { url = "https://mirror-pypi.runflare.com/packages/dd/95/d5d332fb73affaf7a1fbe80e49c2c7eae4f17c645af24a3b3fa25736d6f0/greenlet-3.2.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f2cc88b50b9006b324c1b9f5f3552f9d4564c78af57cdfb4c7baf4f0aa089146" }, - { url = "https://mirror-pypi.runflare.com/packages/6c/77/89458e20db5a4f1c64f9a0191561227e76d809941ca2d7529006d17d3450/greenlet-3.2.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e66872daffa360b2537170b73ad530f14fa31785b1bc78080125d92edf0a6def" }, - { url = "https://mirror-pypi.runflare.com/packages/90/f8/9962175d2f2eaa629a7fd7545abacc8c4deda3baa4e52c1526d2eb5f5546/greenlet-3.2.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5445ddb7b586d870dad32ca9fc47c287d6022a528d194efdb8912093c5303ad" }, - { url = "https://mirror-pypi.runflare.com/packages/f5/d7/826d0e080f0a7ad5ec47c8d143bbd3ca0887657bb806595fe2434d12938a/greenlet-3.2.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:752c896a8c976548faafe8a306d446c6a4c68d4fd24699b84d4393bd9ac69a8e" }, - { url = "https://mirror-pypi.runflare.com/packages/41/cc/33bd4c2f816be8c8e16f71740c4130adf3a66a3dd2ba29de72b9d8dd1096/greenlet-3.2.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499b809e7738c8af0ff9ac9d5dd821cb93f4293065a9237543217f0b252f950a" }, - { url = "https://mirror-pypi.runflare.com/packages/48/79/f3891dcfc59097474a53cc3c624f2f2465e431ab493bda043b8c873fb20a/greenlet-3.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2c7429f6e9cea7cbf2637d86d3db12806ba970f7f972fcab39d6b54b4457cbaf" }, - { url = "https://mirror-pypi.runflare.com/packages/ca/47/212b47e6d2d7a04c4083db1af2fdd291bc8fe99b7e3571bfa560b65fc361/greenlet-3.2.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e4b25e855800fba17713020c5c33e0a4b7a1829027719344f0c7c8870092a2" }, - { url = "https://mirror-pypi.runflare.com/packages/f6/9d/4e9b941be05f8da7ba804c6413761d2c11cca05994cbf0a015bd729419f0/greenlet-3.2.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7123b29e6bad2f3f89681be4ef316480fca798ebe8d22fbaced9cc3775007a4f" }, - { url = "https://mirror-pypi.runflare.com/packages/23/cb/a73625c9a35138330014ecf3740c0d62e0c2b5e7279bb7f2586b1b199fac/greenlet-3.2.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e8fe0c72603201a86b2e038daf9b6c8570715f8779566419cff543b6ace88de" }, - { url = "https://mirror-pypi.runflare.com/packages/83/49/6d1531109507bce7dfb23acf57a87013627ed3ac058851176e443a6a9134/greenlet-3.2.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:050703a60603db0e817364d69e048c70af299040c13a7e67792b9e62d4571196" }, - { url = "https://mirror-pypi.runflare.com/packages/f7/38/f958ee90fab93529b30cc1e4a59b27c1112b640570043a84af84da3b3b98/greenlet-3.2.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6712bfd520530eb67331813f7112d3ee18e206f48b3d026d8a96cd2d2ad20251" }, - { url = "https://mirror-pypi.runflare.com/packages/51/c1/a603906e79716d61f08afedaf8aed62017661457aef233d62d6e57ecd511/greenlet-3.2.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc06a78fa3ffbe2a75f1ebc7e040eacf6fa1050a9432953ab111fbbbf0d03c1" }, - { url = "https://mirror-pypi.runflare.com/packages/3f/8f/f880ff4587d236b4d06893fb34da6b299aa0d00f6c8259673f80e1b6d63c/greenlet-3.2.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:dbe0e81e24982bb45907ca20152b31c2e3300ca352fdc4acbd4956e4a2cbc195" }, - { url = "https://mirror-pypi.runflare.com/packages/3c/50/f6c78b8420187fdfe97fcf2e6d1dd243a7742d272c32fd4d4b1095474b37/greenlet-3.2.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15871afc0d78ec87d15d8412b337f287fc69f8f669346e391585824970931c48" }, - { url = "https://mirror-pypi.runflare.com/packages/26/d6/3277f92e1961e6e9f41d9f173ea74b5c1f7065072637669f761626f26cc0/greenlet-3.2.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5bf0d7d62e356ef2e87e55e46a4e930ac165f9372760fb983b5631bb479e9d3a" }, - { url = "https://mirror-pypi.runflare.com/packages/2a/6a/4f79d2e7b5ef3723fc5ffea0d6cb22627e5f95e0f19c973fa12bf1cf7891/greenlet-3.2.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dff6433742073e5b6ad40953a78a0e8cddcb3f6869e5ea635d29a810ca5e7d0" }, - { url = "https://mirror-pypi.runflare.com/packages/4d/59/7aadf33f23c65dbf4db27e7f5b60c414797a61e954352ae4a86c5c8b0553/greenlet-3.2.5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdd67619cefe1cc9fcab57c8853d2bb36eca9f166c0058cc0d428d471f7c785c" }, - { url = "https://mirror-pypi.runflare.com/packages/1d/46/b3422959f830de28a4eea447414e6bd7b980d755892f66ab52ad805da1c4/greenlet-3.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3828b309dfb1f117fe54867512a8265d8d4f00f8de6908eef9b885f4d8789062" }, - { url = "https://mirror-pypi.runflare.com/packages/54/4a/3d1c9728f093415637cf3696909fa10852632e33e68238fb8ca60eb90de1/greenlet-3.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67725ae9fea62c95cf1aa230f1b8d4dc38f7cd14f6103d1df8a5a95657eb8e54" }, -] - -[[package]] -name = "greenlet" -version = "3.3.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", + { url = "https://files.pythonhosted.org/packages/5b/2a/4b1ea3f2180be251a5cff1e70c8a5b7994ffa5c8eab8937901f8a9db58fe/filelock-4.0.10-py3-none-any.whl", hash = "sha256:698048d158dc3c9e18cc5e88b3efdd4bd0e77d76686b22adb4511cc4b2c2f0db", size = 111193, upload-time = "2026-10-03T23:40:47.334Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2" } +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d" }, - { url = "https://mirror-pypi.runflare.com/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13" }, - { url = "https://mirror-pypi.runflare.com/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e" }, - { url = "https://mirror-pypi.runflare.com/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f" }, - { url = "https://mirror-pypi.runflare.com/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef" }, - { url = "https://mirror-pypi.runflare.com/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca" }, - { url = "https://mirror-pypi.runflare.com/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86" }, - { url = "https://mirror-pypi.runflare.com/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f" }, - { url = "https://mirror-pypi.runflare.com/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55" }, - { url = "https://mirror-pypi.runflare.com/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358" }, - { url = "https://mirror-pypi.runflare.com/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99" }, - { url = "https://mirror-pypi.runflare.com/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be" }, - { url = "https://mirror-pypi.runflare.com/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5" }, - { url = "https://mirror-pypi.runflare.com/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd" }, - { url = "https://mirror-pypi.runflare.com/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd" }, - { url = "https://mirror-pypi.runflare.com/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd" }, - { url = "https://mirror-pypi.runflare.com/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac" }, - { url = "https://mirror-pypi.runflare.com/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070" }, - { url = "https://mirror-pypi.runflare.com/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79" }, - { url = "https://mirror-pypi.runflare.com/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395" }, - { url = "https://mirror-pypi.runflare.com/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f" }, - { url = "https://mirror-pypi.runflare.com/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643" }, - { url = "https://mirror-pypi.runflare.com/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4" }, - { url = "https://mirror-pypi.runflare.com/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986" }, - { url = "https://mirror-pypi.runflare.com/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92" }, - { url = "https://mirror-pypi.runflare.com/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab" }, - { url = "https://mirror-pypi.runflare.com/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a" }, - { url = "https://mirror-pypi.runflare.com/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b" }, - { url = "https://mirror-pypi.runflare.com/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124" }, - { url = "https://mirror-pypi.runflare.com/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327" }, - { url = "https://mirror-pypi.runflare.com/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab" }, - { url = "https://mirror-pypi.runflare.com/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082" }, - { url = "https://mirror-pypi.runflare.com/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9" }, - { url = "https://mirror-pypi.runflare.com/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506" }, - { url = "https://mirror-pypi.runflare.com/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce" }, - { url = "https://mirror-pypi.runflare.com/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492" }, - { url = "https://mirror-pypi.runflare.com/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71" }, - { url = "https://mirror-pypi.runflare.com/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54" }, - { url = "https://mirror-pypi.runflare.com/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4" }, - { url = "https://mirror-pypi.runflare.com/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff" }, - { url = "https://mirror-pypi.runflare.com/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e" }, - { url = "https://mirror-pypi.runflare.com/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/bd/80/4efe245037b2d2ff64e220fecb4656f41a3d5e296a65daa6d6e4245e1400/hypothesis-6.168.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2", size = 792323, upload-time = "2026-09-28T05:19:31.294Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/0f96010943e93d07fe0c064a6c1812b33d305f29fa43606a8b863ba806af/hypothesis-6.168.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742", size = 788335, upload-time = "2026-09-28T05:20:29.748Z" }, + { url = "https://files.pythonhosted.org/packages/12/a9/95ebd471169fef0ccb7c9194ed643797eab67703b927efb6e76c046a78ce/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05", size = 1124433, upload-time = "2026-09-28T05:19:01.479Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3a/153bf04b7ce7f14d563b943af5f05347d5f91948f66f59655b9913b97531/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74", size = 1170640, upload-time = "2026-09-28T05:18:40.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/3d5ca014d00897dc5f69fa45374d86014fde89938e381303d597275b4ad3/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c", size = 1300447, upload-time = "2026-09-28T05:18:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/18/76/38fb90efa47cab7d93b2794624a8177533bc748ee4666681a0ece1e3c23b/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083", size = 1336918, upload-time = "2026-09-28T05:18:45.505Z" }, + { url = "https://files.pythonhosted.org/packages/78/44/92b10c9df3029379cc67ad2d3af17cd1b35a72516b0ef37d5b55a125caec/hypothesis-6.168.3-cp310-cp310-win_amd64.whl", hash = "sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0", size = 684844, upload-time = "2026-09-28T05:19:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, ] [[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7" } +name = "identify" +version = "2.6.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" }, + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73" } +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc" }, - { url = "https://mirror-pypi.runflare.com/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7" }, - { url = "https://mirror-pypi.runflare.com/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6" }, - { url = "https://mirror-pypi.runflare.com/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0" }, - { url = "https://mirror-pypi.runflare.com/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6" }, - { url = "https://mirror-pypi.runflare.com/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71" }, - { url = "https://mirror-pypi.runflare.com/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7" }, - { url = "https://mirror-pypi.runflare.com/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05" }, - { url = "https://mirror-pypi.runflare.com/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891" }, - { url = "https://mirror-pypi.runflare.com/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7" }, - { url = "https://mirror-pypi.runflare.com/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2" }, - { url = "https://mirror-pypi.runflare.com/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd" }, - { url = "https://mirror-pypi.runflare.com/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965" }, - { url = "https://mirror-pypi.runflare.com/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da" }, - { url = "https://mirror-pypi.runflare.com/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0" }, - { url = "https://mirror-pypi.runflare.com/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e" }, - { url = "https://mirror-pypi.runflare.com/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3" }, - { url = "https://mirror-pypi.runflare.com/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac" }, - { url = "https://mirror-pypi.runflare.com/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596" }, - { url = "https://mirror-pypi.runflare.com/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99" }, - { url = "https://mirror-pypi.runflare.com/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe" }, - { url = "https://mirror-pypi.runflare.com/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb" }, - { url = "https://mirror-pypi.runflare.com/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b" }, - { url = "https://mirror-pypi.runflare.com/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9" }, - { url = "https://mirror-pypi.runflare.com/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a" }, - { url = "https://mirror-pypi.runflare.com/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9" }, - { url = "https://mirror-pypi.runflare.com/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb" }, - { url = "https://mirror-pypi.runflare.com/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d" }, - { url = "https://mirror-pypi.runflare.com/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7" }, - { url = "https://mirror-pypi.runflare.com/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440" }, - { url = "https://mirror-pypi.runflare.com/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9" }, - { url = "https://mirror-pypi.runflare.com/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972" }, - { url = "https://mirror-pypi.runflare.com/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921" }, - { url = "https://mirror-pypi.runflare.com/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0" }, - { url = "https://mirror-pypi.runflare.com/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a" }, - { url = "https://mirror-pypi.runflare.com/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444" }, - { url = "https://mirror-pypi.runflare.com/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d" }, - { url = "https://mirror-pypi.runflare.com/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35" }, - { url = "https://mirror-pypi.runflare.com/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583" }, - { url = "https://mirror-pypi.runflare.com/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04" }, - { url = "https://mirror-pypi.runflare.com/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363" }, - { url = "https://mirror-pypi.runflare.com/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0" }, - { url = "https://mirror-pypi.runflare.com/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012" }, - { url = "https://mirror-pypi.runflare.com/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb" }, - { url = "https://mirror-pypi.runflare.com/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b" }, - { url = "https://mirror-pypi.runflare.com/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d" }, - { url = "https://mirror-pypi.runflare.com/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a" }, - { url = "https://mirror-pypi.runflare.com/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79" }, - { url = "https://mirror-pypi.runflare.com/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0" }, - { url = "https://mirror-pypi.runflare.com/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f" }, - { url = "https://mirror-pypi.runflare.com/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c" }, - { url = "https://mirror-pypi.runflare.com/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc" }, - { url = "https://mirror-pypi.runflare.com/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c" }, - { url = "https://mirror-pypi.runflare.com/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3" }, - { url = "https://mirror-pypi.runflare.com/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14" }, - { url = "https://mirror-pypi.runflare.com/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7" }, - { url = "https://mirror-pypi.runflare.com/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6" }, - { url = "https://mirror-pypi.runflare.com/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071" }, - { url = "https://mirror-pypi.runflare.com/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78" }, - { url = "https://mirror-pypi.runflare.com/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023" }, - { url = "https://mirror-pypi.runflare.com/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730" }, - { url = "https://mirror-pypi.runflare.com/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3" }, - { url = "https://mirror-pypi.runflare.com/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1" }, - { url = "https://mirror-pypi.runflare.com/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee" }, - { url = "https://mirror-pypi.runflare.com/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7" }, - { url = "https://mirror-pypi.runflare.com/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040" }, - { url = "https://mirror-pypi.runflare.com/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e" }, - { url = "https://mirror-pypi.runflare.com/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732" }, - { url = "https://mirror-pypi.runflare.com/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624" }, - { url = "https://mirror-pypi.runflare.com/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4" }, - { url = "https://mirror-pypi.runflare.com/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382" }, - { url = "https://mirror-pypi.runflare.com/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994" }, - { url = "https://mirror-pypi.runflare.com/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a" }, - { url = "https://mirror-pypi.runflare.com/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4" }, - { url = "https://mirror-pypi.runflare.com/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61" }, - { url = "https://mirror-pypi.runflare.com/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac" }, - { url = "https://mirror-pypi.runflare.com/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed" }, - { url = "https://mirror-pypi.runflare.com/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd" }, - { url = "https://mirror-pypi.runflare.com/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851" }, - { url = "https://mirror-pypi.runflare.com/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128" }, - { url = "https://mirror-pypi.runflare.com/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac" }, - { url = "https://mirror-pypi.runflare.com/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551" }, - { url = "https://mirror-pypi.runflare.com/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5" }, - { url = "https://mirror-pypi.runflare.com/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6" }, - { url = "https://mirror-pypi.runflare.com/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed" }, - { url = "https://mirror-pypi.runflare.com/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc" }, - { url = "https://mirror-pypi.runflare.com/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7" }, + { url = "https://files.pythonhosted.org/packages/ec/77/900294b3c9a81415323d81b3f3f271a7f5d7ed9d01e824fc38ffa49470b6/librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", size = 148668, upload-time = "2026-09-29T00:44:20.282Z" }, + { url = "https://files.pythonhosted.org/packages/55/3b/c301e8cc47758e4518b93e926a0111080155cc79fa03651d202ac2025065/librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", size = 153562, upload-time = "2026-09-29T00:44:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/62/b1/4d5b98114a58bf17e3832cd33400e40f2582f68c2fb7ce25b4468cb1afd3/librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", size = 494337, upload-time = "2026-09-29T00:44:23.096Z" }, + { url = "https://files.pythonhosted.org/packages/88/36/14223c9b0dc38982eb4f399732c8cb27b19c7e027ae0faef5ca51d08d19f/librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", size = 485440, upload-time = "2026-09-29T00:44:24.555Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/d7155e6800d0c33e297e529b217649f7361d706491ebf9d2fc0fc627309e/librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", size = 498363, upload-time = "2026-09-29T00:44:26.03Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ce/d5f1fb9f913ac0979d6fc5db9fb9c0bf28cc3470988528957922f15ed40c/librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", size = 515453, upload-time = "2026-09-29T00:44:27.566Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/ca0fca1a27c26080d1a25b2b31f271e532078a5719878c3daca0c3d4c539/librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", size = 509537, upload-time = "2026-09-29T00:44:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c0/26807bf2221a53a2c66f2e23a5c4d57f1c0eaa4afed7cb81d841cf3d1903/librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", size = 532541, upload-time = "2026-09-29T00:44:30.745Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/c329d509907e1bc061abeec3796f94d255d43ba25d28c3854d2138ed5ab2/librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", size = 537059, upload-time = "2026-09-29T00:44:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/4571b9c9e65d98e5503734166dce2fd3e216f526dc3c59a3773faa8a60e3/librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", size = 536614, upload-time = "2026-09-29T00:44:33.653Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d3/a2ed8a292e4ab7889792a9c3f4ad5d1caf6557afce7f6b452e77f8ed20b7/librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", size = 517199, upload-time = "2026-09-29T00:44:35.115Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/276e5218b4b7adb6258a42b7403cb1b7cef25616238d0c2152288a9d2f53/librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", size = 558746, upload-time = "2026-09-29T00:44:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5c/9156df81a1726e1efc9567379def2b8777a6d56f464d2f1b760bf951b8af/librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", size = 104424, upload-time = "2026-09-29T00:44:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/4fd172cb3f49ab2cf5ef17de519a93cfb935202c1270ba9d44c6a64178c3/librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", size = 124267, upload-time = "2026-09-29T00:44:39.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] [[package]] name = "maturin" -version = "1.12.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/ae/13/aeff8a21835ed0e40c329c286750fcdcdcbf231f1a5cb327378666c5def6/maturin-1.12.2.tar.gz", hash = "sha256:d6253079f53dbb692395a13abddc0f2d3d96af32f8c0b32e2912849713c55794" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/8a/9d/4811e1fcaa346a0b9fad6aee0ac0eec9eb376a24fe27c66d5d4fe975586e/maturin-1.12.2-py3-none-linux_armv6l.whl", hash = "sha256:0ed31b6a392928ad23645a470edc4f3814b952a416e41f8e5daac42d7bfbabc6" }, - { url = "https://mirror-pypi.runflare.com/packages/69/db/74d582af74c32bbda12e4d7e153b389884409a1c5cd31edc9d3194d515f7/maturin-1.12.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1c2e4ee43bf286b052091a3b2356a157978985837c7aed42354deb2947a4006" }, - { url = "https://mirror-pypi.runflare.com/packages/9d/6f/71be226c6780387f032c0b4ab791c390c7162ed62f93a11e600f9266dafd/maturin-1.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:04c9c4f9c9f904f007cbfcd4640c406e53f19d04c220f5940d1537edb914d325" }, - { url = "https://mirror-pypi.runflare.com/packages/6a/cc/989dce6140227277b4184aab248d07fe67fa11f95411ccf90e272542287d/maturin-1.12.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:4bdc486b9ab80d8b50143ecc9a1924b890866fe95be150dd9a59fa22a6b37238" }, - { url = "https://mirror-pypi.runflare.com/packages/a1/e8/02bb64f7150013d8af3ca622944e22f550beb312b6d5cf8760dc2896cce8/maturin-1.12.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:134e895578258a693ba1d55b166c2ba96e9f51067e106b8a74d422432653d45b" }, - { url = "https://mirror-pypi.runflare.com/packages/84/81/b603a74bef68fabd402d1e54f43560213ea69c3c01467610d0256eea013b/maturin-1.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:39665d622dcc950ab17b9569e8cab84a4d64eea6a18b540a8b49e00c0f7dda02" }, - { url = "https://mirror-pypi.runflare.com/packages/70/a5/387c7bced34f7fd8d08d399c6b1ac3d94d7ca50c9f87db9e1bc0dd8c8d08/maturin-1.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ca3b20bcc3aff115c9eaf97340e78bff58829ea1efa16764940dd0d858dcf6af" }, - { url = "https://mirror-pypi.runflare.com/packages/6d/30/d5ae812c54a70d5d3a5b67b073e92d1d14d36675242e2d00e6a175fa6117/maturin-1.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:d1617989b4a5dc543fea6d23c28b2f07fadb2c726ff00fe959538ee71a301384" }, - { url = "https://mirror-pypi.runflare.com/packages/84/f4/7baac2fa5324ccdc3f888ff5f6a793f3eb5a7805d89bc17a8bacbe9fc566/maturin-1.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6af778e7ee048612e55a1255488db7678741bea2ba881e66a19712f59f2534cb" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/ed/5680efbb1becb4f47da3ada8ea4eb6844d2fd91ae558287e1dd0871cb603/maturin-1.12.2-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:72aad9efe09a6392de9930f2bea80bfcc36fd98e18caa621f512571179c02d41" }, - { url = "https://mirror-pypi.runflare.com/packages/86/20/7e27e07dd2270b707dd0124256cd46bef7c8832476b0aefa2ecd74835365/maturin-1.12.2-py3-none-win32.whl", hash = "sha256:9763d277e143409cf0ce309eb1a493fc4e1e75777364d67ccac39a161b51b5b0" }, - { url = "https://mirror-pypi.runflare.com/packages/3b/6e/9cc0e19c9a336fbc1b9664c1a7955caa6d8fd510c0047ace9be66a33704a/maturin-1.12.2-py3-none-win_amd64.whl", hash = "sha256:c06d218931985035d7ab4d0211ba96027e1bc7e4b01a87c8c4e30a57790403ec" }, - { url = "https://mirror-pypi.runflare.com/packages/2e/67/07ea2c991ca1a55c6b08cd821710736276af7a3e160e1f869ea5c41c78c3/maturin-1.12.2-py3-none-win_arm64.whl", hash = "sha256:a882cc80c241b1e2c27bd1acd713b09e9ac9266a3159cc1e34e8c7b77f049bba" }, + { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, + { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, + { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, ] [[package]] name = "mypy" -version = "1.14.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6" } +sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb" }, - { url = "https://mirror-pypi.runflare.com/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0" }, - { url = "https://mirror-pypi.runflare.com/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d" }, - { url = "https://mirror-pypi.runflare.com/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b" }, - { url = "https://mirror-pypi.runflare.com/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427" }, - { url = "https://mirror-pypi.runflare.com/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f" }, - { url = "https://mirror-pypi.runflare.com/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c" }, - { url = "https://mirror-pypi.runflare.com/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1" }, - { url = "https://mirror-pypi.runflare.com/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8" }, - { url = "https://mirror-pypi.runflare.com/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f" }, - { url = "https://mirror-pypi.runflare.com/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1" }, - { url = "https://mirror-pypi.runflare.com/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae" }, - { url = "https://mirror-pypi.runflare.com/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14" }, - { url = "https://mirror-pypi.runflare.com/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9" }, - { url = "https://mirror-pypi.runflare.com/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11" }, - { url = "https://mirror-pypi.runflare.com/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e" }, - { url = "https://mirror-pypi.runflare.com/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b" }, - { url = "https://mirror-pypi.runflare.com/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255" }, - { url = "https://mirror-pypi.runflare.com/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34" }, - { url = "https://mirror-pypi.runflare.com/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a" }, - { url = "https://mirror-pypi.runflare.com/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9" }, - { url = "https://mirror-pypi.runflare.com/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd" }, - { url = "https://mirror-pypi.runflare.com/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107" }, - { url = "https://mirror-pypi.runflare.com/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31" }, - { url = "https://mirror-pypi.runflare.com/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6" }, - { url = "https://mirror-pypi.runflare.com/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319" }, - { url = "https://mirror-pypi.runflare.com/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac" }, - { url = "https://mirror-pypi.runflare.com/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b" }, - { url = "https://mirror-pypi.runflare.com/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837" }, - { url = "https://mirror-pypi.runflare.com/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35" }, - { url = "https://mirror-pypi.runflare.com/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc" }, - { url = "https://mirror-pypi.runflare.com/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9" }, - { url = "https://mirror-pypi.runflare.com/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb" }, - { url = "https://mirror-pypi.runflare.com/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60" }, - { url = "https://mirror-pypi.runflare.com/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c" }, - { url = "https://mirror-pypi.runflare.com/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/78c9ac93933e2ecd51d807021f78c03ffe98640f33951a8be159f5fd55bc/mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2", size = 14408618, upload-time = "2026-10-01T20:39:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4f/5f5f4429fd5a4218ce6268164a66afeed75c7e2ce5522a4d163faaa5fe85/mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23", size = 14552749, upload-time = "2026-10-01T20:40:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/fa1c319cd51db7a6027b46117460d5a361c6bba0c0f0f0670a2dab8ce2a5/mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080", size = 15505846, upload-time = "2026-10-01T20:39:15.475Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/14c02690776fb04e9fa60b81173cb9ae6315e0745fad3813b303737f2f0c/mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f", size = 15823059, upload-time = "2026-10-01T20:39:35.429Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/d29afa819f8cb765018d6e2773e390a41ea4592f75abe417135969646857/mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb", size = 11312927, upload-time = "2026-10-01T20:39:28.509Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c8/966dec4d80303855d824e6bf67fe1da812d88adfe4d75e24d8545dab4826/mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220", size = 10879252, upload-time = "2026-10-01T20:38:41.545Z" }, + { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, + { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, ] [[package]] -name = "mypy" -version = "1.19.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, - { name = "pathspec", marker = "python_full_version >= '3.9'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.9'" }, +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba" } + +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec" }, - { url = "https://mirror-pypi.runflare.com/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b" }, - { url = "https://mirror-pypi.runflare.com/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6" }, - { url = "https://mirror-pypi.runflare.com/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74" }, - { url = "https://mirror-pypi.runflare.com/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1" }, - { url = "https://mirror-pypi.runflare.com/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac" }, - { url = "https://mirror-pypi.runflare.com/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288" }, - { url = "https://mirror-pypi.runflare.com/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab" }, - { url = "https://mirror-pypi.runflare.com/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6" }, - { url = "https://mirror-pypi.runflare.com/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331" }, - { url = "https://mirror-pypi.runflare.com/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925" }, - { url = "https://mirror-pypi.runflare.com/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042" }, - { url = "https://mirror-pypi.runflare.com/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1" }, - { url = "https://mirror-pypi.runflare.com/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e" }, - { url = "https://mirror-pypi.runflare.com/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2" }, - { url = "https://mirror-pypi.runflare.com/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8" }, - { url = "https://mirror-pypi.runflare.com/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13" }, - { url = "https://mirror-pypi.runflare.com/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250" }, - { url = "https://mirror-pypi.runflare.com/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b" }, - { url = "https://mirror-pypi.runflare.com/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e" }, - { url = "https://mirror-pypi.runflare.com/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef" }, - { url = "https://mirror-pypi.runflare.com/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75" }, - { url = "https://mirror-pypi.runflare.com/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd" }, - { url = "https://mirror-pypi.runflare.com/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1" }, - { url = "https://mirror-pypi.runflare.com/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718" }, - { url = "https://mirror-pypi.runflare.com/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b" }, - { url = "https://mirror-pypi.runflare.com/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045" }, - { url = "https://mirror-pypi.runflare.com/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957" }, - { url = "https://mirror-pypi.runflare.com/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f" }, - { url = "https://mirror-pypi.runflare.com/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3" }, - { url = "https://mirror-pypi.runflare.com/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a" }, - { url = "https://mirror-pypi.runflare.com/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67" }, - { url = "https://mirror-pypi.runflare.com/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e" }, - { url = "https://mirror-pypi.runflare.com/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376" }, - { url = "https://mirror-pypi.runflare.com/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24" }, - { url = "https://mirror-pypi.runflare.com/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247" }, + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, ] [[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" } +name = "orjson" +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505" }, + { url = "https://files.pythonhosted.org/packages/cf/35/819eeb4fa8ee676d38fdbb8213a76fd496f7dbbfdfafa89d34e02b22dfac/orjson-3.12.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:747843254519dd43b93eee3153a19e5a509334320c4d2f823ec879232db5c796", size = 224133, upload-time = "2026-08-14T16:12:00.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/d9221d4a2b085b073fcddc91728d490f20b9cf010c62c2f42371ab997695/orjson-3.12.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7c2ad193c8004254f34b499f3bd2c80f043d10754aff2b38f93da574f4883f98", size = 113669, upload-time = "2026-08-14T16:12:02.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/644cbbcabb26df61d9ef0c66e6f2bf8b687cc7b66137597f2858951f1952/orjson-3.12.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bc7a872f03522d90e0429e6c0c5cd23084f767bedcb4c58048eec19294613344", size = 130410, upload-time = "2026-08-14T16:12:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/14/6d/e3a8c34d687895aecd8b267a01c46106eb98d8424a83bfa7bacb723854f6/orjson-3.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18a87929f31d94a77f7dc93cf527e91f39ce7fe7813d588a4de2507efd32a387", size = 131101, upload-time = "2026-08-14T16:12:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/930824c07685c22af23f26818ed3853b0270488a412b6ab757904b7f787b/orjson-3.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9683ee9ea0659da64f36574ef675b8a86330c34c19ea75db1fb93c3ff99e0ef", size = 131479, upload-time = "2026-08-14T16:12:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a6/22e863bbbe8917aa292e33e0db597000f9a07eb5e6f52efed623fa16bae1/orjson-3.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:103b5db66aa53c1f9e88c2524be4f383e831ba7dfd5f9f5af6336a177c622f11", size = 135865, upload-time = "2026-08-14T16:12:07.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/ceb5008914a65e9a19a46a09d94bc67a74d120209fdfa772750023ceb377/orjson-3.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd57d79aefa3f84eec851d6de7a366795b9345cfaf17f82b4820430a7a5fa241", size = 127843, upload-time = "2026-08-14T16:12:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/3d/61c6b3b84c250cb09cb7229701ff77e4d763773ad7f577d0b6abf2892664/orjson-3.12.0-cp310-cp310-win32.whl", hash = "sha256:3dbce9b6b3074b31a5d5dd322a9c4e5b16f206091ece4194c2e36952847a105e", size = 128293, upload-time = "2026-08-14T16:12:09.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/ea0f4a563253b6363195a4f704123c6bfbf156641bd3be5a75de81c5e917/orjson-3.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bb17a06f9bd15237b3216c044209fe92597379124018cfc196fbb846cde64df", size = 122216, upload-time = "2026-08-14T16:12:11.261Z" }, + { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, ] [[package]] name = "packaging" -version = "26.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4" } +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645" } +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1" } +name = "platformdirs" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/23/4a86fc741c38c5b69792a4ef954b281afa69bea9f083f881de1b0d23bc07/platformdirs-4.12.3.tar.gz", hash = "sha256:427fc0bb321ae0c5b037fa03238ca74820437be162e78b4848c4d4055b9b766c", size = 59538, upload-time = "2026-10-03T23:40:46.595Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/6ce1ead737fe496611bda600c263b9a11ae7bd8f41bb13f9bd7e0a2c37a4/platformdirs-4.12.3-py3-none-any.whl", hash = "sha256:080f3b39423b5abfca9a23d84c4e9795f54d395cd8459867a8ded44084fcd5f8", size = 32493, upload-time = "2026-10-03T23:40:45.104Z" }, ] [[package]] name = "pluggy" version = "1.6.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] name = "pygments" -version = "2.19.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887" } +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] -name = "pypika" -version = "0.51.1" -source = { registry = "https://mirror-pypi.runflare.com/simple" } +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "packaging", marker = "python_full_version < '3.9'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" } + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820" }, + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", -] +name = "python-discovery" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "packaging", marker = "python_full_version == '3.9.*'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pygments", marker = "python_full_version == '3.9.*'" }, - { name = "tomli", marker = "python_full_version == '3.9.*'" }, -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01" } + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/57/250bd238b966cece44328235eb85290045d059265fdaf7527a3a958123db/python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e", size = 84338, upload-time = "2026-09-18T01:31:53.971Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" }, + { url = "https://files.pythonhosted.org/packages/16/7d/e9ffbadfbf89c93848412d04594135c4ae8c1d37d9e053b9c3ed718fabc4/python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3", size = 38664, upload-time = "2026-09-18T01:31:52.448Z" }, ] [[package]] -name = "pytest" -version = "9.0.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" } +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b" }, + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "rapidquery" source = { editable = "." } +[package.optional-dependencies] +orjson = [ + { name = "orjson" }, +] + [package.dev-dependencies] +build = [ + { name = "maturin" }, +] dev = [ + { name = "hypothesis" }, { name = "maturin" }, - { name = "mypy", version = "1.14.1", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy", version = "1.19.1", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pypika" }, - { name = "pytest", version = "8.3.5", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "sqlalchemy" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-xdist" }, + { name = "ruff" }, +] +lint = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, +] +test = [ + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-xdist" }, ] [package.metadata] +requires-dist = [{ name = "orjson", marker = "extra == 'orjson'", specifier = ">=3.0.0" }] +provides-extras = ["orjson"] [package.metadata.requires-dev] +build = [{ name = "maturin", specifier = "==1.15.0" }] dev = [ - { name = "maturin", specifier = ">=1.9.4" }, - { name = "mypy", specifier = ">=1.14.1" }, - { name = "pypika", specifier = ">=0.51.1" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "sqlalchemy", specifier = ">=2.0.47" }, + { name = "hypothesis", specifier = ">=6.0.0" }, + { name = "maturin", specifier = "==1.15.0" }, + { name = "mypy", specifier = ">=2.3.1" }, + { name = "pre-commit", specifier = ">=4.6.2" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "ruff", specifier = ">=0.16.9" }, +] +lint = [ + { name = "mypy", specifier = ">=2.3.1" }, + { name = "pre-commit", specifier = ">=4.6.2" }, + { name = "ruff", specifier = ">=0.16.9" }, +] +test = [ + { name = "hypothesis", specifier = ">=6.0.0" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, ] [[package]] -name = "sqlalchemy" -version = "2.0.47" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -dependencies = [ - { name = "greenlet", version = "3.1.1", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "(python_full_version < '3.9' and platform_machine == 'AMD64') or (python_full_version < '3.9' and platform_machine == 'WIN32') or (python_full_version < '3.9' and platform_machine == 'aarch64') or (python_full_version < '3.9' and platform_machine == 'amd64') or (python_full_version < '3.9' and platform_machine == 'ppc64le') or (python_full_version < '3.9' and platform_machine == 'win32') or (python_full_version < '3.9' and platform_machine == 'x86_64')" }, - { name = "greenlet", version = "3.2.5", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "(python_full_version == '3.9.*' and platform_machine == 'AMD64') or (python_full_version == '3.9.*' and platform_machine == 'WIN32') or (python_full_version == '3.9.*' and platform_machine == 'aarch64') or (python_full_version == '3.9.*' and platform_machine == 'amd64') or (python_full_version == '3.9.*' and platform_machine == 'ppc64le') or (python_full_version == '3.9.*' and platform_machine == 'win32') or (python_full_version == '3.9.*' and platform_machine == 'x86_64')" }, - { name = "greenlet", version = "3.3.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirror-pypi.runflare.com/simple" }, marker = "python_full_version >= '3.9'" }, +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] -sdist = { url = "https://mirror-pypi.runflare.com/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d" } + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/ec/75/17db77c57129c223c7d98518ad1e1faa24ee350c22a44b55390d8463c28c/sqlalchemy-2.0.47-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33a917ede39406ddb93c3e642b5bc480be7c5fd0f3d0d6ae1036d466fb963f1a" }, - { url = "https://mirror-pypi.runflare.com/packages/b0/d6/3658f7e5c376de774c009f2bb9c0ddf88a35b89c5bfb15ee7174a17b1a5f/sqlalchemy-2.0.47-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:561d027c829b01e040bdade6b6f5b429249d056ef95d7bdcb9211539ecc82803" }, - { url = "https://mirror-pypi.runflare.com/packages/4e/38/f4b94f85d1c26cb9ee0e57449754de816c326f9586b9a8c5247eb49146de/sqlalchemy-2.0.47-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa5072a37e68c565363c009b7afa5b199b488c87940ec02719860093a08f34ca" }, - { url = "https://mirror-pypi.runflare.com/packages/94/f2/36714f1de01e135a2bf142b662e416e5338ab63c47878e31051338c66e2d/sqlalchemy-2.0.47-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e7ed17dd4312a298b6024bfd1baf51654bc49e3f03c798005babf0c7922d6a7" }, - { url = "https://mirror-pypi.runflare.com/packages/ab/94/fcd978e7625cd1c97d9f1d7363e18e37d24314e572acd7c091e3a4210106/sqlalchemy-2.0.47-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6992e353fcb0593eb42d95ad84b3e58fe40b5e37fd332b9ccba28f4b2f36d1fc" }, - { url = "https://mirror-pypi.runflare.com/packages/23/29/c633202b9900ab65f0162f59df737b57f30010f44d892b186810c9ed58b7/sqlalchemy-2.0.47-cp310-cp310-win32.whl", hash = "sha256:05a6d58ed99ebd01303c92d29a0c9cbf70f637b3ddd155f5172c5a7239940998" }, - { url = "https://mirror-pypi.runflare.com/packages/00/39/54acf13913932b8508058d47a169e6fcde9adaa4cbfa16cbf30da1f6a482/sqlalchemy-2.0.47-cp310-cp310-win_amd64.whl", hash = "sha256:4a7aa4a584cc97e268c11e700dea0b763874eaebb435e75e7d0ffee5d90f5030" }, - { url = "https://mirror-pypi.runflare.com/packages/94/13/886338d3e8ab5ddcfe84d54302c749b1793e16c4bba63d7004e3f7baa8ec/sqlalchemy-2.0.47-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a1dbf0913879c443617d6b64403cf2801c941651db8c60e96d204ed9388d6b0" }, - { url = "https://mirror-pypi.runflare.com/packages/b6/bb/a897f6a66c9986aa9f27f5cf8550637d8a5ea368fd7fb42f6dac3105b4dc/sqlalchemy-2.0.47-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:775effbb97ea3b00c4dd3aeaf3ba8acba6e3e2b4b41d17d67a27e696843dbc95" }, - { url = "https://mirror-pypi.runflare.com/packages/59/fb/69bfae022b681507565ab0d34f0c80aa1e9f954a5a7cbfb0ed054966ac8d/sqlalchemy-2.0.47-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56cc834a3ffac34270cc2a41875e0f40e97aa651f4f3ca1cfbbf421c044cb62b" }, - { url = "https://mirror-pypi.runflare.com/packages/04/f3/0eba329f7c182d53205a228c4fd24651b95489b431ea2bd830887b4c13c4/sqlalchemy-2.0.47-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49b5e0c7244262f39e767c018e4fdb5e5dbc23cd54c5ddac8eea8f0ba32ef890" }, - { url = "https://mirror-pypi.runflare.com/packages/5c/06/654edc084b3b46ac79e04200d7c46467ae80c759c4ee41c897f9272b036f/sqlalchemy-2.0.47-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cd822a3f1f6f77b5b841a30c1a07a07f7dee3385f17e638e1722de9ab683be" }, - { url = "https://mirror-pypi.runflare.com/packages/78/33/c18c8f63b61981219d3aa12321bb7ccee605034d195e868ed94f9727b27c/sqlalchemy-2.0.47-cp311-cp311-win32.whl", hash = "sha256:9847a19548cd283a65e1ce0afd54016598d55ff72682d6fd3e493af6fc044064" }, - { url = "https://mirror-pypi.runflare.com/packages/f5/c6/a59e3f9796fff844e16afbd821db9abfd6e12698db9441a231a96193a100/sqlalchemy-2.0.47-cp311-cp311-win_amd64.whl", hash = "sha256:722abf1c82aeca46a1a0803711244a48a298279eeaec9e02f7bfee9e064182e5" }, - { url = "https://mirror-pypi.runflare.com/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1" }, - { url = "https://mirror-pypi.runflare.com/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356" }, - { url = "https://mirror-pypi.runflare.com/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7" }, - { url = "https://mirror-pypi.runflare.com/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec" }, - { url = "https://mirror-pypi.runflare.com/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602" }, - { url = "https://mirror-pypi.runflare.com/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9" }, - { url = "https://mirror-pypi.runflare.com/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb" }, - { url = "https://mirror-pypi.runflare.com/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143" }, - { url = "https://mirror-pypi.runflare.com/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6" }, - { url = "https://mirror-pypi.runflare.com/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5" }, - { url = "https://mirror-pypi.runflare.com/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092" }, - { url = "https://mirror-pypi.runflare.com/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01" }, - { url = "https://mirror-pypi.runflare.com/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476" }, - { url = "https://mirror-pypi.runflare.com/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66" }, - { url = "https://mirror-pypi.runflare.com/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003" }, - { url = "https://mirror-pypi.runflare.com/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487" }, - { url = "https://mirror-pypi.runflare.com/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb" }, - { url = "https://mirror-pypi.runflare.com/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e" }, - { url = "https://mirror-pypi.runflare.com/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc" }, - { url = "https://mirror-pypi.runflare.com/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42" }, - { url = "https://mirror-pypi.runflare.com/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6" }, - { url = "https://mirror-pypi.runflare.com/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44" }, - { url = "https://mirror-pypi.runflare.com/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6" }, - { url = "https://mirror-pypi.runflare.com/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f" }, - { url = "https://mirror-pypi.runflare.com/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f" }, - { url = "https://mirror-pypi.runflare.com/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db" }, - { url = "https://mirror-pypi.runflare.com/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb" }, - { url = "https://mirror-pypi.runflare.com/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64" }, - { url = "https://mirror-pypi.runflare.com/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172" }, - { url = "https://mirror-pypi.runflare.com/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4" }, - { url = "https://mirror-pypi.runflare.com/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706" }, - { url = "https://mirror-pypi.runflare.com/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff" }, - { url = "https://mirror-pypi.runflare.com/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7" }, - { url = "https://mirror-pypi.runflare.com/packages/ed/44/52c490a81d747e10c6c3427d2169b551ae9a8c5059c0d792418fead6921a/sqlalchemy-2.0.47-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fe3f8519a52ca5032015780de3fc4e6ab42c6e0bcf9d807143a3d17b3350d546" }, - { url = "https://mirror-pypi.runflare.com/packages/05/1a/431489c8991f5a06d9b55f0999df221488a2725c2cccb661d21ac7dc315b/sqlalchemy-2.0.47-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6acfc1f95ed0369e0c4100d98870c9c4bfd56818ddc825357a0a979d5973195" }, - { url = "https://mirror-pypi.runflare.com/packages/cb/b6/e13f0136b21e98d3385554712c9a0a85fb0ebab2502f784b587474bd3fd4/sqlalchemy-2.0.47-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f530b939eca775f6f77fa359a3e7039209a96958c1aa28c1b796f600e0fee7cd" }, - { url = "https://mirror-pypi.runflare.com/packages/e6/f1/77dcffeeb622c4cd9083a4ddc314d5cd9b7caca4ef88ee487e3b79f7bcd0/sqlalchemy-2.0.47-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0a6fdf665268dfe0ba52fb2d8d62deee96b297d460e2797bdd52d2d1941dd8cd" }, - { url = "https://mirror-pypi.runflare.com/packages/6f/f3/0c7222af667c8c8307d4ed365e5eb3ead2f1375afe03c8992c500e56dd21/sqlalchemy-2.0.47-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:25b3c189dab94dedb6db9d4e06476ce955182e7f45412b096ae9033519e33ce8" }, - { url = "https://mirror-pypi.runflare.com/packages/ae/d4/b813da53629aadb97101d091bcae54b29735d82e74f85d1a8988fd788ba3/sqlalchemy-2.0.47-cp38-cp38-win32.whl", hash = "sha256:a8f991cac31b673aff1648cafb8b10022719e5a632bbadaa9c5d41511bd507a5" }, - { url = "https://mirror-pypi.runflare.com/packages/ba/1d/e992dbafa8a2189021683416d84cc325e81a2b29abc5c11586002f3c58eb/sqlalchemy-2.0.47-cp38-cp38-win_amd64.whl", hash = "sha256:52be08b31f70bed2ed05c5c4b8237cf361a8581f32a5e89f9dfc295f795db10f" }, - { url = "https://mirror-pypi.runflare.com/packages/e3/3f/2a9d99e811ada3d82bb13beaed2a0b1294de931ebb5590de18b8bce59a5b/sqlalchemy-2.0.47-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d7477190e90852c00bf654b70ae21e5b85b5ac4d09094cf82e84eb3abdb6c5a7" }, - { url = "https://mirror-pypi.runflare.com/packages/39/8f/dd79902a3e0e0a8b8d75727cdad2f2ac0d299ced73cd815f3b716d853079/sqlalchemy-2.0.47-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb0b3e4946bf05d68673a1857db1a16bd58207c83ebc4ed5732a6e60029bac2d" }, - { url = "https://mirror-pypi.runflare.com/packages/52/e1/f3524df09fecfa4b122b3d1318f1d242e61ed9b04715498dabe76be58980/sqlalchemy-2.0.47-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bc111a5b98881b7e1ab108921f2fcc09fa06abbd98f4f0ed6cb2c23e70cdd23" }, - { url = "https://mirror-pypi.runflare.com/packages/54/cf/e3a13f04a775e87a967386d2b00caafd36fe7b6ac8f477cfd29c2270ad0f/sqlalchemy-2.0.47-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:97bf49caf4e405c18f3b9f695751c5bf14a9d8899c6e54eaeb49dda0d4fa009d" }, - { url = "https://mirror-pypi.runflare.com/packages/b4/4d/83d84cc4b4ad459d88e0d6b20614d84a26704ca42e41cba07d97fd8ffca8/sqlalchemy-2.0.47-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2fa029051d3db77ad79a55c3ddf005bd7c038a111af0db9b56158857702aef6" }, - { url = "https://mirror-pypi.runflare.com/packages/b3/b7/19a24d27718fa06225828871be5e4308904fd9965fe38015288ce1016f0a/sqlalchemy-2.0.47-cp39-cp39-win32.whl", hash = "sha256:6e547682d508d141de942484b9976cbee91b7a50739d4ee25b3d0a62dd71a954" }, - { url = "https://mirror-pypi.runflare.com/packages/7d/6e/b6317aa4af77f5506a70dda83e39a2ac4043e4cf735737778024f3c56805/sqlalchemy-2.0.47-cp39-cp39-win_amd64.whl", hash = "sha256:bb833131169444c87160aa95fcdd22ae86d0fa4ef174d36b3dfb9be363b4e574" }, - { url = "https://mirror-pypi.runflare.com/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93" }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "tomli" version = "2.4.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -sdist = { url = "https://mirror-pypi.runflare.com/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867" }, - { url = "https://mirror-pypi.runflare.com/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9" }, - { url = "https://mirror-pypi.runflare.com/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95" }, - { url = "https://mirror-pypi.runflare.com/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76" }, - { url = "https://mirror-pypi.runflare.com/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d" }, - { url = "https://mirror-pypi.runflare.com/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576" }, - { url = "https://mirror-pypi.runflare.com/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a" }, - { url = "https://mirror-pypi.runflare.com/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa" }, - { url = "https://mirror-pypi.runflare.com/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614" }, - { url = "https://mirror-pypi.runflare.com/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1" }, - { url = "https://mirror-pypi.runflare.com/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8" }, - { url = "https://mirror-pypi.runflare.com/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a" }, - { url = "https://mirror-pypi.runflare.com/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1" }, - { url = "https://mirror-pypi.runflare.com/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b" }, - { url = "https://mirror-pypi.runflare.com/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51" }, - { url = "https://mirror-pypi.runflare.com/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729" }, - { url = "https://mirror-pypi.runflare.com/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da" }, - { url = "https://mirror-pypi.runflare.com/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3" }, - { url = "https://mirror-pypi.runflare.com/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0" }, - { url = "https://mirror-pypi.runflare.com/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e" }, - { url = "https://mirror-pypi.runflare.com/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4" }, - { url = "https://mirror-pypi.runflare.com/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e" }, - { url = "https://mirror-pypi.runflare.com/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c" }, - { url = "https://mirror-pypi.runflare.com/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f" }, - { url = "https://mirror-pypi.runflare.com/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86" }, - { url = "https://mirror-pypi.runflare.com/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87" }, - { url = "https://mirror-pypi.runflare.com/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6" }, - { url = "https://mirror-pypi.runflare.com/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc" }, - { url = "https://mirror-pypi.runflare.com/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66" }, - { url = "https://mirror-pypi.runflare.com/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d" }, - { url = "https://mirror-pypi.runflare.com/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702" }, - { url = "https://mirror-pypi.runflare.com/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8" }, - { url = "https://mirror-pypi.runflare.com/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776" }, - { url = "https://mirror-pypi.runflare.com/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475" }, - { url = "https://mirror-pypi.runflare.com/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2" }, - { url = "https://mirror-pypi.runflare.com/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9" }, - { url = "https://mirror-pypi.runflare.com/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0" }, - { url = "https://mirror-pypi.runflare.com/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df" }, - { url = "https://mirror-pypi.runflare.com/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d" }, - { url = "https://mirror-pypi.runflare.com/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f" }, - { url = "https://mirror-pypi.runflare.com/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b" }, - { url = "https://mirror-pypi.runflare.com/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087" }, - { url = "https://mirror-pypi.runflare.com/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd" }, - { url = "https://mirror-pypi.runflare.com/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4" }, - { url = "https://mirror-pypi.runflare.com/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a" }, + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" } +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://mirror-pypi.runflare.com/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://mirror-pypi.runflare.com/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } +name = "virtualenv" +version = "21.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/f9/f323b3b6058cff3853b31cf6a49c0425ed797bf9611572ec10d065e08bac/virtualenv-21.14.5.tar.gz", hash = "sha256:c4cb6c13e46b57225a999c7e22a09b163393878facc7ac4c059a57f46faa1647", size = 5467127, upload-time = "2026-10-02T17:42:25.613Z" } wheels = [ - { url = "https://mirror-pypi.runflare.com/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, + { url = "https://files.pythonhosted.org/packages/df/93/084f10ebbecdedf7902ed68fbecc903f0e9c5742155bfffb05eccbc071da/virtualenv-21.14.5-py3-none-any.whl", hash = "sha256:b0651e0174982bba17cc6f0aaa2ef496730cb85a3f35eb5d1119d1c904f7d1da", size = 5487482, upload-time = "2026-10-02T17:42:23.38Z" }, ]