├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── Cargo.toml ├── LICENSE ├── README.md ├── pyproject.toml ├── python ├── scyllapy │ ├── __init__.py │ ├── _internal │ │ ├── __init__.pyi │ │ ├── exceptions.pyi │ │ ├── extra_types.pyi │ │ ├── load_balancing.pyi │ │ └── query_builder.pyi │ ├── exceptions.py │ ├── extra_types.py │ ├── load_balancing.py │ ├── py.typed │ └── query_builder.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── query_builders │ ├── __init__.py │ ├── test_delete.py │ ├── test_inserts.py │ ├── test_select.py │ └── test_update.py │ ├── test_batches.py │ ├── test_bindings.py │ ├── test_extra_types.py │ ├── test_pagination.py │ ├── test_parsing.py │ ├── test_prepared.py │ ├── test_profiles.py │ ├── test_queries.py │ ├── test_query_res.py │ └── utils.py ├── scripts └── version_bumper.py ├── src ├── batches.rs ├── consistencies.rs ├── exceptions │ ├── mod.rs │ ├── py_err.rs │ └── rust_err.rs ├── execution_profiles.rs ├── extra_types.rs ├── inputs.rs ├── lib.rs ├── load_balancing.rs ├── prepared_queries.rs ├── queries.rs ├── query_builder │ ├── delete.rs │ ├── insert.rs │ ├── mod.rs │ ├── select.rs │ ├── update.rs │ └── utils.rs ├── query_results.rs ├── scylla_cls.rs └── utils.rs └── tox.ini /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | linux: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | target: 17 | - x86 18 | - x86_64 19 | - aarch64 20 | - armv7 21 | - ppc64le 22 | # - s390x # We cannot build it because of some illegal instructions. 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-python@v4 26 | with: 27 | python-version: '3.11' 28 | - name: Bumping version 29 | run: | 30 | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" 31 | - name: Build wheels 32 | uses: PyO3/maturin-action@v1 33 | with: 34 | target: ${{ matrix.target }} 35 | manylinux: auto 36 | args: --release --out dist 37 | before-script-linux: | 38 | # If we're running on rhel centos, install needed packages. 39 | if command -v yum &> /dev/null; then 40 | yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic 41 | 42 | # If we're running on i686 we need to symlink libatomic 43 | # in order to build openssl with -latomic flag. 44 | if [[ ! -d "/usr/lib64" ]]; then 45 | ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so 46 | fi 47 | else 48 | # If we're running on debian-based system. 49 | apt update -y && apt-get install -y libssl-dev openssl pkg-config 50 | fi 51 | - name: Upload wheels 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: wheels 55 | path: dist 56 | - name: Releasing assets 57 | uses: softprops/action-gh-release@v1 58 | with: 59 | files: | 60 | dist/* 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | windows: 65 | runs-on: windows-latest 66 | strategy: 67 | matrix: 68 | target: [x64, x86] 69 | steps: 70 | - uses: actions/checkout@v3 71 | - uses: actions/setup-python@v4 72 | with: 73 | python-version: '3.10' 74 | architecture: ${{ matrix.target }} 75 | - name: Bumping version 76 | run: | 77 | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" 78 | - name: Install OpenSSL 79 | run: vcpkg install openssl:x64-windows-static-md 80 | - name: Build wheels 81 | uses: PyO3/maturin-action@v1 82 | with: 83 | target: ${{ matrix.target }} 84 | args: --release --out dist 85 | sccache: 'true' 86 | - name: Upload wheels 87 | uses: actions/upload-artifact@v3 88 | with: 89 | name: wheels 90 | path: dist 91 | - name: Releasing assets 92 | uses: softprops/action-gh-release@v1 93 | with: 94 | files: | 95 | dist/* 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | 99 | macos: 100 | runs-on: macos-latest 101 | strategy: 102 | matrix: 103 | target: [x86_64, aarch64] 104 | steps: 105 | - uses: actions/checkout@v3 106 | - uses: actions/setup-python@v4 107 | with: 108 | python-version: '3.10' 109 | - name: Bumping version 110 | run: | 111 | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" 112 | - name: Build wheels 113 | uses: PyO3/maturin-action@v1 114 | with: 115 | target: ${{ matrix.target }} 116 | args: --release --out dist 117 | sccache: 'true' 118 | - name: Upload wheels 119 | uses: actions/upload-artifact@v3 120 | with: 121 | name: wheels 122 | path: dist 123 | - name: Releasing assets 124 | uses: softprops/action-gh-release@v1 125 | with: 126 | files: | 127 | dist/* 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | 131 | sdist: 132 | runs-on: ubuntu-latest 133 | steps: 134 | - uses: actions/checkout@v3 135 | - uses: actions/setup-python@v4 136 | with: 137 | python-version: '3.10' 138 | - name: Bumping version 139 | run: | 140 | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" 141 | - name: Build sdist 142 | uses: PyO3/maturin-action@v1 143 | with: 144 | command: sdist 145 | args: --out dist 146 | - name: Upload sdist 147 | uses: actions/upload-artifact@v3 148 | with: 149 | name: wheels 150 | path: dist 151 | - name: Releasing assets 152 | uses: softprops/action-gh-release@v1 153 | with: 154 | files: | 155 | dist/* 156 | env: 157 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 158 | 159 | musllinux: 160 | runs-on: ubuntu-latest 161 | strategy: 162 | matrix: 163 | target: 164 | - x86_64-unknown-linux-musl 165 | - i686-unknown-linux-musl 166 | steps: 167 | - uses: actions/checkout@v3 168 | - uses: actions/setup-python@v4 169 | with: 170 | python-version: '3.11' 171 | architecture: x64 172 | - name: Bumping version 173 | run: | 174 | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" 175 | - name: Instal OpenSSL 176 | run: sudo apt-get update && sudo apt-get install libssl-dev openssl 177 | - name: Build wheels 178 | uses: messense/maturin-action@v1 179 | with: 180 | target: ${{ matrix.target }} 181 | args: --release --out dist 182 | manylinux: musllinux_1_2 183 | - name: Upload wheels 184 | uses: actions/upload-artifact@v3 185 | with: 186 | name: wheels 187 | path: dist 188 | - name: Releasing assets 189 | uses: softprops/action-gh-release@v1 190 | with: 191 | files: | 192 | dist/* 193 | env: 194 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 195 | 196 | release: 197 | name: Release 198 | runs-on: ubuntu-latest 199 | needs: [linux, windows, macos, musllinux, sdist] 200 | steps: 201 | - uses: actions/download-artifact@v3 202 | with: 203 | name: wheels 204 | - name: Publish to PyPI 205 | uses: PyO3/maturin-action@v1 206 | env: 207 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 208 | with: 209 | command: upload 210 | args: --skip-existing * 211 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: 'Testing package' 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | py-lint: 8 | strategy: 9 | matrix: 10 | cmd: 11 | - black 12 | - isort 13 | - ruff 14 | - mypy 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.11" 22 | - name: Run lint check 23 | uses: pre-commit/action@v3.0.0 24 | with: 25 | extra_args: -a ${{ matrix.cmd }} 26 | fmt: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v1 30 | - uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: stable 33 | components: rustfmt 34 | override: true 35 | - name: Check code format 36 | run: cargo fmt -- --check --config use_try_shorthand=true,imports_granularity=Crate 37 | 38 | clippy: 39 | permissions: 40 | checks: write 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v1 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: stable 47 | components: clippy 48 | override: true 49 | - uses: actions-rs/clippy-check@v1 50 | with: 51 | token: ${{ secrets.GITHUB_TOKEN }} 52 | args: -p scyllapy --all-features -- -W clippy::all -W clippy::pedantic -D warnings 53 | pytest: 54 | name: ${{matrix.job.os}}-${{matrix.py_version}} 55 | services: 56 | scylla: 57 | image: scylladb/scylla:5.2 58 | options: >- 59 | --health-cmd="cqlsh -e 'select * from system.local' " 60 | --health-interval=5s 61 | --health-timeout=5s 62 | --health-retries=60 63 | ports: 64 | - 9042:9042 65 | strategy: 66 | matrix: 67 | py_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 68 | job: 69 | - os: ubuntu-latest 70 | ssl_cmd: sudo apt-get update && sudo apt-get install libssl-dev openssl 71 | # Uncomment when containerss become available 72 | # on these systems. 73 | # - os: windows-latest 74 | # ssl_cmd: vcpkg install openssl:x64-windows-static-md 75 | # - os: macos-latest 76 | # ssl_cmd: echo "Already installed" 77 | runs-on: ${{matrix.job.os}} 78 | steps: 79 | - uses: actions/checkout@v1 80 | - uses: actions-rs/toolchain@v1 81 | with: 82 | toolchain: stable 83 | components: clippy 84 | override: true 85 | - name: Setup OpenSSL 86 | run: ${{matrix.job.ssl_cmd}} 87 | - name: Setup python for test ${{ matrix.py_version }} 88 | uses: actions/setup-python@v4 89 | with: 90 | python-version: ${{ matrix.py_version }} 91 | - name: Install tox 92 | run: pip install "tox-gh>=1.2,<2" 93 | - name: Run pytest 94 | run: tox -v 95 | 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.venv 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | .idea/ 165 | .vscode/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.1.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - repo: https://github.com/pycqa/isort 7 | rev: 5.12.0 8 | hooks: 9 | - id: isort 10 | name: python isort 11 | pass_filenames: false 12 | always_run: true 13 | args: ["python"] 14 | - repo: https://github.com/psf/black 15 | rev: 23.9.1 16 | hooks: 17 | - id: black 18 | name: python black 19 | pass_filenames: false 20 | always_run: true 21 | args: ["python"] 22 | - repo: https://github.com/pre-commit/mirrors-mypy 23 | rev: v1.9.0 24 | hooks: 25 | - id: mypy 26 | name: python mypy 27 | always_run: true 28 | additional_dependencies: 29 | - "types-python-dateutil" 30 | pass_filenames: false 31 | args: ["python"] 32 | - repo: https://github.com/astral-sh/ruff-pre-commit 33 | rev: v0.0.291 34 | hooks: 35 | - id: ruff 36 | name: ruff 37 | pass_filenames: false 38 | always_run: true 39 | args: ["python", "--fix"] 40 | - repo: local 41 | hooks: 42 | - id: fmt 43 | types: 44 | - rust 45 | name: rust fmt 46 | language: system 47 | entry: cargo 48 | pass_filenames: false 49 | args: 50 | - fmt 51 | - -- 52 | - --config 53 | - use_try_shorthand=true,imports_granularity=Crate 54 | 55 | - id: clippy 56 | types: 57 | - rust 58 | name: rust clippy 59 | language: system 60 | pass_filenames: false 61 | entry: cargo 62 | args: 63 | - clippy 64 | - -p 65 | - scyllapy 66 | - -- 67 | - -W 68 | - clippy::all 69 | - -W 70 | - clippy::pedantic 71 | - -D 72 | - warnings 73 | 74 | - id: check 75 | types: 76 | - rust 77 | name: rust cargo check 78 | language: system 79 | entry: cargo 80 | pass_filenames: false 81 | args: 82 | - check 83 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.2 2 | 3.11.4 3 | 3.10.12 4 | 3.9.17 5 | 3.8.17 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scyllapy" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "scyllapy" 8 | crate-type = ["cdylib"] 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | chrono = "0.4.31" 13 | eq-float = "0.1.0" 14 | futures = "0.3.28" 15 | log = "0.4.20" 16 | openssl = { version = "0.10.57", features = ["vendored"] } 17 | pyo3 = { version = "0.20.0", features = [ 18 | "auto-initialize", 19 | "abi3-py38", 20 | "extension-module", 21 | "chrono", 22 | ] } 23 | pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime"] } 24 | pyo3-log = "0.9.0" 25 | rustc-hash = "1.1.0" 26 | scylla = { version = "0.12.0", features = ["ssl", "full-serialization"] } 27 | bigdecimal-04 = { package = "bigdecimal", version = "0.4" } 28 | thiserror = "1.0.48" 29 | tokio = { version = "1.32.0", features = ["bytes"] } 30 | uuid = { version = "1.4.1", features = ["v4"] } 31 | time = { version = "*", features = ["formatting", "macros"] } 32 | 33 | [profile.release] 34 | lto = "fat" 35 | codegen-units = 1 36 | opt-level = 3 37 | debug = false 38 | panic = "abort" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Intree Aps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/scyllapy?style=for-the-badge)](https://pypi.org/project/scyllapy/) 2 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/scyllapy?style=for-the-badge)](https://pypistats.org/packages/scyllapy) 3 | 4 | 5 | # Async Scylla driver for python 6 | 7 | Python driver for ScyllaDB written in Rust. Though description says it's for scylla, 8 | however it can be used with Cassandra and AWS keyspaces as well. 9 | 10 | This driver uses official [ScyllaDB driver](https://github.com/scylladb/scylla-rust-driver) for [Rust](https://github.com/rust-lang/rust/) and exposes python API to interact with it. 11 | 12 | ## Installation 13 | 14 | To install it, use your favorite package manager for python packages: 15 | 16 | ```bash 17 | pip install scyllapy 18 | ``` 19 | 20 | Also, you can build from sources. To do it, install stable rust, [maturin](https://github.com/PyO3/maturin) and openssl libs. 21 | 22 | ```bash 23 | maturin build --release --out dist 24 | # Then install whl file from dist folder. 25 | pip install dist/* 26 | ``` 27 | 28 | ## Usage 29 | 30 | The usage is pretty straitforward. Create a Scylla instance, run startup and start executing queries. 31 | 32 | ```python 33 | import asyncio 34 | 35 | from scyllapy import Scylla 36 | 37 | 38 | async def main(): 39 | scylla = Scylla(["localhost:9042"], keyspace="keyspace") 40 | await scylla.startup() 41 | await scylla.execute("SELECT * FROM table") 42 | await scylla.shutdown() 43 | 44 | if __name__ == "__main__": 45 | asyncio.run(main()) 46 | 47 | ``` 48 | 49 | ## Parametrizing queries 50 | 51 | While executing queries sometimes you may want to fine-tune some parameters, or dynamically pass values to the query. 52 | 53 | Passing parameters is simple. You need to add a paramters list to the query. 54 | 55 | ```python 56 | await scylla.execute( 57 | "INSERT INTO otps(id, otp) VALUES (?, ?)", 58 | [uuid.uuid4(), uuid.uuid4().hex], 59 | ) 60 | ``` 61 | 62 | Queries can be modified further by using `Query` class. It allows you to define 63 | consistency for query or enable tracing. 64 | 65 | ```python 66 | from scyllapy import Scylla, Query, Consistency, SerialConsistency 67 | 68 | async def make_query(scylla: Scylla) -> None: 69 | query = Query( 70 | "SELECT * FROM table", 71 | consistency=Consistency.ALL, 72 | serial_consistency=SerialConsistency.LOCAL_SERIAL, 73 | request_timeout=1, 74 | timestamp=int(time.time()), 75 | is_idempotent=False, 76 | tracing=True, 77 | ) 78 | result = await scylla.execute(query) 79 | print(result.all()) 80 | ``` 81 | 82 | Also, with queries you can tweak random parameters for a specific execution. 83 | 84 | ```python 85 | query = Query("SELECT * FROM table") 86 | 87 | new_query = query.with_consistency(Consistency.ALL) 88 | ``` 89 | 90 | All `with_` methods create new query, copying all other parameters. 91 | 92 | Here's the list of scylla types and corresponding python types that you should use while passing parameters to queries: 93 | 94 | | Scylla type | Python type | 95 | | ----------- | ---------------------- | 96 | | int | int | 97 | | tinyint | extra_types.TinyInt | 98 | | bigint | extra_types.BigInt | 99 | | varint | any int type | 100 | | float | float | 101 | | double | extra_types.Double | 102 | | decimal | decimal.Decimal | 103 | | ascii | str | 104 | | text | str | 105 | | varchar | str | 106 | | blob | bytes | 107 | | boolean | bool | 108 | | counter | extra_types.Counter | 109 | | date | datetime.date | 110 | | uuid | uuid.UUID | 111 | | inet | ipaddress | 112 | | time | datetime.time | 113 | | timestamp | datetime.datetime | 114 | | duration | dateutil.relativedelta | 115 | 116 | All types from `extra_types` module are used to eliminate any possible ambiguity while passing parameters to queries. You can find more information about them in `Extra types` section. 117 | 118 | We use relative delta from `dateutil` for duration, because it's the only way to represent it in python. Since scylla operates with months, days and nanosecond, there's no way we can represent it in python, becuase months are variable length. 119 | 120 | 121 | ## Named parameters 122 | 123 | Also, you can provide named parameters to querties, by using name 124 | placeholders instead of `?`. 125 | 126 | For example: 127 | 128 | ```python 129 | async def insert(scylla: Scylla): 130 | await scylla.execute( 131 | "INSERT INTO table(id, name) VALUES (:id, :name)", 132 | params={"id": uuid.uuid4(), "name": uuid.uuid4().hex} 133 | ) 134 | ``` 135 | 136 | Important note: All variables should be in snake_case. 137 | Otherwise the error may be raised or parameter may not be placed in query correctly. 138 | This happens, because scylla makes all parameters in query lowercase. 139 | 140 | The scyllapy makes all parameters lowercase, but you may run into problems, 141 | if you use multiple parameters that differ only in cases of some letters. 142 | 143 | 144 | ## Preparing queries 145 | 146 | Also, queries can be prepared. You can either prepare raw strings, or `Query` objects. 147 | 148 | ```python 149 | from scyllapy import Scylla, Query, PreparedQuery 150 | 151 | 152 | async def prepare(scylla: Scylla, query: str | Query) -> PreparedQuery: 153 | return await scylla.prepare(query) 154 | ``` 155 | 156 | You can execute prepared queries by passing them to `execute` method. 157 | 158 | ```python 159 | async def run_prepared(scylla: Scylla) -> None: 160 | prepared = await scylla.prepare("INSERT INTO memse(title) VALUES (?)") 161 | await scylla.execute(prepared, ("American joke",)) 162 | ``` 163 | 164 | ### Batching 165 | 166 | We support batches. Batching can help a lot when you have lots of queries that you want to execute at the same time. 167 | 168 | ```python 169 | from scyllapy import Scylla, Batch 170 | 171 | 172 | async def run_batch(scylla: Scylla, num_queries: int) -> None: 173 | batch = Batch() 174 | for _ in range(num_queries): 175 | batch.add_query("SELECT * FROM table WHERE id = ?") 176 | await scylla.batch(batch, [(i,) for i in range(num_queries)]) 177 | ``` 178 | 179 | Here we pass query as strings. But you can also add Prepared statements or Query objects. 180 | 181 | Also, note that we pass list of lists as parametes for execute. Each element of 182 | the list is going to be used in the query with the same index. But named parameters 183 | are not supported for batches. 184 | 185 | ```python 186 | async def run_batch(scylla: Scylla, num_queries: int) -> None: 187 | batch = Batch() 188 | batch.add_query("SELECT * FROM table WHERE id = :id") 189 | await scylla.batch(batch, [{"id": 1}]) # Will rase an error! 190 | ``` 191 | 192 | ## Pagination 193 | 194 | Sometimes you want to query lots of data. For such cases it's better not to 195 | fetch all results at once, but fetch them using pagination. It reduces load 196 | not only on your application, but also on a cluster. 197 | 198 | To execute query with pagination, simply add `paged=True` in execute method. 199 | After doing so, `execute` method will return `IterableQueryResult`, instead of `QueryResult`. 200 | Instances of `IterableQueryResult` can be iterated with `async for` statements. 201 | You, as a client, won't see any information about pages, it's all handeled internally within a driver. 202 | 203 | Please note, that paginated queries are slower to fetch all rows, but much more memory efficent for large datasets. 204 | 205 | ```python 206 | result = await scylla.execute("SELECT * FROM table", paged=True) 207 | async for row in result: 208 | print(row) 209 | 210 | ``` 211 | 212 | Of course, you can change how results returned to you, by either using `scalars` or 213 | `as_cls`. For example: 214 | 215 | ```python 216 | async def func(scylla: Scylla) -> None: 217 | rows = await scylla.execute("SELECT id FROM table", paged=True) 218 | # Will print ids of each returned row. 219 | async for test_id in rows.scalars(): 220 | print(test_id) 221 | 222 | ``` 223 | 224 | ```python 225 | from dataclasses import dataclass 226 | 227 | @dataclass 228 | class MyDTO: 229 | id: int 230 | val: int 231 | 232 | async def func(scylla: Scylla) -> None: 233 | rows = await scylla.execute("SELECT * FROM table", paged=True) 234 | # Will print ids of each returned row. 235 | async for my_dto in rows.as_cls(MyDTO): 236 | print(my_dto.id, my_dto.val) 237 | 238 | ``` 239 | 240 | ## Execution profiles 241 | 242 | You can define profiles using `ExecutionProfile` class. After that the 243 | profile can be used while creating a cluster or when defining queries. 244 | 245 | ```python 246 | from scyllapy import Consistency, ExecutionProfile, Query, Scylla, SerialConsistency 247 | from scyllapy.load_balancing import LoadBalancingPolicy, LatencyAwareness 248 | 249 | default_profile = ExecutionProfile( 250 | consistency=Consistency.LOCAL_QUORUM, 251 | serial_consistency=SerialConsistency.LOCAL_SERIAL, 252 | request_timeout=2, 253 | ) 254 | 255 | async def main(): 256 | query_profile = ExecutionProfile( 257 | consistency=Consistency.ALL, 258 | serial_consistency=SerialConsistency.SERIAL, 259 | # Load balancing cannot be constructed without running event loop. 260 | # If you won't do it inside async funcion, it will result in error. 261 | load_balancing_policy=await LoadBalancingPolicy.build( 262 | token_aware=True, 263 | prefer_rack="rack1", 264 | prefer_datacenter="dc1", 265 | permit_dc_failover=True, 266 | shuffling_replicas=True, 267 | latency_awareness=LatencyAwareness( 268 | minimum_measurements=10, 269 | retry_period=1000, 270 | exclusion_threshold=1.4, 271 | update_rate=1000, 272 | scale=2, 273 | ), 274 | ), 275 | ) 276 | 277 | scylla = Scylla( 278 | ["192.168.32.4"], 279 | default_execution_profile=default_profile, 280 | ) 281 | await scylla.startup() 282 | await scylla.execute( 283 | Query( 284 | "SELECT * FROM system_schema.keyspaces;", 285 | profile=query_profile, 286 | ) 287 | ) 288 | ``` 289 | 290 | ### Results 291 | 292 | Every query returns a class that represents returned rows. It allows you to not fetch 293 | and parse actual data if you don't need it. **Please be aware** that if your query was 294 | not expecting any rows in return. Like for `Update` or `Insert` queries. The `RuntimeError` is raised when you call `all` or `first`. 295 | 296 | ```python 297 | result = await scylla.execute("SELECT * FROM table") 298 | print(result.all()) 299 | ``` 300 | 301 | If you were executing query with tracing, you can get tracing id from results. 302 | 303 | ```python 304 | result = await scylla.execute(Query("SELECT * FROM table", tracing=True)) 305 | print(result.trace_id) 306 | ``` 307 | 308 | Also it's possible to parse your data using custom classes. You 309 | can use dataclasses or Pydantic. 310 | 311 | ```python 312 | from dataclasses import dataclass 313 | 314 | @dataclass 315 | class MyDTO: 316 | id: uuid.UUID 317 | name: str 318 | 319 | result = await scylla.execute("SELECT * FROM inbox") 320 | print(result.all(as_class=MyDTO)) 321 | ``` 322 | 323 | Or with pydantic. 324 | 325 | ```python 326 | from pydantic import BaseModel 327 | 328 | class MyDTO(BaseModel): 329 | user_id: uuid.UUID 330 | chat_id: uuid.UUID 331 | 332 | result = await scylla.execute("SELECT * FROM inbox") 333 | print(result.all(as_class=MyDTO)) 334 | ``` 335 | 336 | ## Extra types 337 | 338 | Since Rust enforces typing, it's hard to identify which value 339 | user tries to pass as a parameter. For example, `1` that comes from python 340 | can be either `tinyint`, `smallint` or even `bigint`. But we cannot say for sure 341 | how many bytes should we send to server. That's why we created some extra_types to 342 | eliminate any possible ambigousnity. 343 | 344 | You can find these types in `extra_types` module from scyllapy. 345 | 346 | ```python 347 | from scyllapy import Scylla, extra_types 348 | 349 | async def execute(scylla: Scylla) -> None: 350 | await scylla.execute( 351 | "INSERT INTO table(id, name) VALUES (?, ?)", 352 | [extra_types.BigInt(1), "memelord"], 353 | ) 354 | ``` 355 | 356 | ## User defined types 357 | 358 | We also support user defined types. You can pass them as a parameter to query. 359 | Or parse it as a model in response. 360 | 361 | Here's binding example. Imagine we have defined a type in scylla like this: 362 | 363 | ```cql 364 | CREATE TYPE IF NOT EXISTS test ( 365 | id int, 366 | name text 367 | ); 368 | ``` 369 | 370 | Now we need to define a model for it in python. 371 | 372 | ```python 373 | from dataclasses import dataclass 374 | from scyllapy.extra_types import ScyllaPyUDT 375 | 376 | @dataclass 377 | class TestUDT(ScyllaPyUDT): 378 | # Always define fields in the same order as in scylla. 379 | # Otherwise you will get an error, or wrong data. 380 | id: int 381 | name: str 382 | 383 | async def execute(scylla: Scylla) -> None: 384 | await scylla.execute( 385 | "INSERT INTO table(id, udt_col) VALUES (?, ?)", 386 | [1, TestUDT(id=1, name="test")], 387 | ) 388 | 389 | ``` 390 | 391 | We also support pydantic based models. Decalre them like this: 392 | 393 | ```python 394 | from pydantic import BaseModel 395 | from scyllapy.extra_types import ScyllaPyUDT 396 | 397 | 398 | class TestUDT(BaseModel, ScyllaPyUDT): 399 | # Always define fields in the same order as in scylla. 400 | # Otherwise you will get an error, or wrong data. 401 | id: int 402 | name: str 403 | 404 | ``` 405 | 406 | # Query building 407 | 408 | ScyllaPy gives you ability to build queries, 409 | instead of working with raw cql. The main advantage that it's harder to make syntax error, 410 | while creating queries. 411 | 412 | Base classes for Query building can be found in `scyllapy.query_builder`. 413 | 414 | Usage example: 415 | 416 | ```python 417 | from scyllapy import Scylla 418 | from scyllapy.query_builder import Insert, Select, Update, Delete 419 | 420 | 421 | async def main(scylla: Scylla): 422 | await scylla.execute("CREATE TABLE users(id INT PRIMARY KEY, name TEXT)") 423 | 424 | user_id = 1 425 | 426 | # We create a user with id and name. 427 | await Insert("users").set("id", user_id).set( 428 | "name", "user" 429 | ).if_not_exists().execute(scylla) 430 | 431 | # We update it's name to be user2 432 | await Update("users").set("name", "user2").where("id = ?", [user_id]).execute( 433 | scylla 434 | ) 435 | 436 | # We select all users with id = user_id; 437 | res = await Select("users").where("id = ?", [user_id]).execute(scylla) 438 | # Verify that it's correct. 439 | assert res.first() == {"id": 1, "name": "user2"} 440 | 441 | # We delete our user. 442 | await Delete("users").where("id = ?", [user_id]).if_exists().execute(scylla) 443 | 444 | res = await Select("users").where("id = ?", [user_id]).execute(scylla) 445 | 446 | # Verify that user is deleted. 447 | assert not res.all() 448 | 449 | await scylla.execute("DROP TABLE users") 450 | 451 | ``` 452 | 453 | Also, you can pass built queries into InlineBatches. You cannot use queries built with query_builder module with default batches. This constraint is exists, because we 454 | need to use values from within your queries and should ignore all parameters passed in 455 | `batch` method of scylla. 456 | 457 | Here's batch usage example. 458 | 459 | ```python 460 | from scyllapy import Scylla, InlineBatch 461 | from scyllapy.query_builder import Insert 462 | 463 | 464 | async def execute_batch(scylla: Scylla) -> None: 465 | batch = InlineBatch() 466 | for i in range(10): 467 | Insert("users").set("id", i).set("name", "test").add_to_batch(batch) 468 | await scylla.batch(batch) 469 | 470 | ``` 471 | 472 | ## Paging 473 | 474 | Queries that were built with QueryBuilder also support paged returns. 475 | But it supported only for select, because update, delete and insert should 476 | not return anything and it makes no sense implementing it. 477 | To make built `Select` query return paginated iterator, add paged parameter in execute method. 478 | 479 | ```python 480 | rows = await Select("test").execute(scylla, paged=True) 481 | async for row in rows: 482 | print(row['id']) 483 | ``` 484 | 485 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "scyllapy" 3 | description = "Async scylla driver for python" 4 | requires-python = ">=3.8,<4" 5 | readme = "README.md" 6 | license = { file = "LICENSE" } 7 | keywords = ["scylla", "cassandra", "async-driver", "scylla-driver"] 8 | classifiers = [ 9 | "Typing :: Typed", 10 | "Topic :: Database", 11 | "Development Status :: 4 - Beta", 12 | "Programming Language :: Rust", 13 | "Operating System :: MacOS", 14 | "Operating System :: Microsoft", 15 | "Operating System :: POSIX :: Linux", 16 | "Intended Audience :: Developers", 17 | "Topic :: Database :: Front-Ends", 18 | ] 19 | dependencies = ["python-dateutil"] 20 | 21 | [tool.maturin] 22 | python-source = "python" 23 | module-name = "scyllapy._internal" 24 | features = ["pyo3/extension-module"] 25 | 26 | [tool.isort] 27 | profile = "black" 28 | multi_line_output = 3 29 | 30 | [tool.mypy] 31 | strict = true 32 | mypy_path = "python" 33 | ignore_missing_imports = true 34 | allow_subclassing_any = true 35 | allow_untyped_calls = true 36 | pretty = true 37 | show_error_codes = true 38 | implicit_reexport = true 39 | allow_untyped_decorators = true 40 | warn_return_any = false 41 | warn_unused_ignores = false 42 | 43 | [build-system] 44 | requires = ["maturin>=1.0,<2.0"] 45 | build-backend = "maturin" 46 | 47 | [tool.ruff] 48 | # List of enabled rulsets. 49 | # See https://docs.astral.sh/ruff/rules/ for more information. 50 | select = [ 51 | "E", # Error 52 | "F", # Pyflakes 53 | "W", # Pycodestyle 54 | "C90", # McCabe complexity 55 | "N", # pep8-naming 56 | "D", # Pydocstyle 57 | "ANN", # Pytype annotations 58 | "S", # Bandit 59 | "B", # Bugbear 60 | "COM", # Commas 61 | "C4", # Comprehensions 62 | "ISC", # Implicit string concat 63 | "PIE", # Unnecessary code 64 | "T20", # Catch prints 65 | "PYI", # validate pyi files 66 | "Q", # Checks for quotes 67 | "RSE", # Checks raise statements 68 | "RET", # Checks return statements 69 | "SLF", # Self checks 70 | "SIM", # Simplificator 71 | "PTH", # Pathlib checks 72 | "ERA", # Checks for commented out code 73 | "PL", # PyLint checks 74 | "RUF", # Specific to Ruff checks 75 | ] 76 | ignore = [ 77 | "D105", # Missing docstring in magic method 78 | "D107", # Missing docstring in __init__ 79 | "D211", # No blank lines allowed before class docstring 80 | "D212", # Multi-line docstring summary should start at the first line 81 | "D401", # First line should be in imperative mood 82 | "D104", # Missing docstring in public package 83 | "D100", # Missing docstring in public module 84 | "ANN102", # Missing type annotation for self in method 85 | "ANN101", # Missing type annotation for argument 86 | "ANN401", # typing.Any are disallowed in `**kwargs 87 | "PLR0913", # Too many arguments for function call 88 | "D106", # Missing docstring in public nested class 89 | ] 90 | exclude = [".venv/"] 91 | mccabe = { max-complexity = 10 } 92 | line-length = 88 93 | 94 | [tool.ruff.per-file-ignores] 95 | "python/scyllapy/*" = ["PYI021"] 96 | "python/tests/*" = [ 97 | "S101", # Use of assert detected 98 | "S608", # Possible SQL injection vector through string-based query construction 99 | "D103", # Missing docstring in public function 100 | "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes 101 | ] 102 | 103 | [tool.ruff.pydocstyle] 104 | convention = "pep257" 105 | ignore-decorators = ["typing.overload"] 106 | 107 | [tool.ruff.pylint] 108 | allow-magic-value-types = ["int", "str", "float"] 109 | -------------------------------------------------------------------------------- /python/scyllapy/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | from . import extra_types 4 | from ._internal import ( 5 | Batch, 6 | BatchType, 7 | Consistency, 8 | ExecutionProfile, 9 | InlineBatch, 10 | PreparedQuery, 11 | Query, 12 | QueryResult, 13 | Scylla, 14 | SerialConsistency, 15 | SSLVerifyMode, 16 | ) 17 | 18 | __version__ = version("scyllapy") 19 | 20 | __all__ = [ 21 | "__version__", 22 | "Scylla", 23 | "Consistency", 24 | "Query", 25 | "SerialConsistency", 26 | "PreparedQuery", 27 | "Batch", 28 | "BatchType", 29 | "QueryResult", 30 | "SSLVerifyMode", 31 | "extra_types", 32 | "InlineBatch", 33 | "ExecutionProfile", 34 | ] 35 | -------------------------------------------------------------------------------- /python/scyllapy/_internal/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | Callable, 4 | Generic, 5 | Iterable, 6 | Literal, 7 | Optional, 8 | TypeVar, 9 | overload, 10 | ) 11 | 12 | from scyllapy._internal.load_balancing import LoadBalancingPolicy 13 | 14 | _T = TypeVar("_T") 15 | _T2 = TypeVar("_T2") 16 | 17 | class SSLVerifyMode: 18 | """ 19 | SSL Verify modes. 20 | 21 | Used for SSL/TLS connection verification. 22 | Read mode: https://docs.rs/openssl/0.10.68/openssl/ssl/struct.SslVerifyMode.html 23 | """ 24 | 25 | NONE: SSLVerifyMode 26 | PEER: SSLVerifyMode 27 | FAIL_IF_NO_PEER_CERT: SSLVerifyMode 28 | 29 | class Scylla: 30 | """ 31 | Scylla class. 32 | 33 | This class represents scylla cluster. 34 | And has internal connection pool. 35 | 36 | Everything that can beconfigured, shown below. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | contact_points: list[str], 42 | *, 43 | username: str | None = None, 44 | password: str | None = None, 45 | keyspace: str | None = None, 46 | ssl_cert: str | None = None, 47 | ssl_key: str | None = None, 48 | ssl_ca_file: str | None = None, 49 | ssl_verify_mode: SSLVerifyMode | None = None, 50 | conn_timeout: int | None = None, 51 | write_coalescing: bool | None = None, 52 | pool_size_per_host: int | None = None, 53 | pool_size_per_shard: int | None = None, 54 | keepalive_interval: int | None = None, 55 | keepalive_timeout: int | None = None, 56 | tcp_keepalive_interval: int | None = None, 57 | tcp_nodelay: bool | None = None, 58 | disallow_shard_aware_port: bool | None = None, 59 | default_execution_profile: ExecutionProfile | None = None, 60 | ) -> None: 61 | """ 62 | Configure cluster for later use. 63 | 64 | :param contact_points: List of known nodes. (Hosts and ports). 65 | ["192.168.1.1:9042", "my_keyspace.node:9042"] 66 | :param username: Plain text auth username. 67 | :param password: Plain text auth password. 68 | :param ssl_cert: Certficiate string to use for connection. 69 | Should be PEM encoded x509 certificate string. 70 | :param ssl_key: Key string to use for connection. 71 | Should be RSA private key PEM encoded string. 72 | :param ssl_ca_file: CA file to use for connection. This parameter 73 | should be a path to the CA file (which is PEM encoded CA). 74 | :param ssl_verify_mode: tells server on how to validate client's certificate. 75 | :param conn_timeout: Timeout in seconds. 76 | :param write_coalescing: 77 | If true, the driver will inject a small delay before flushing data 78 | to the socket - by rescheduling the task that writes data to the socket. 79 | This gives the task an opportunity to collect more write requests 80 | and write them in a single syscall, increasing the efficiency. 81 | :param pool_size_per_host: how many connections should be established 82 | to the node. 83 | :param pool_size_per_host: how many connections should be established 84 | to each shard of the node. 85 | :param keepalive_interval: How ofter to send keepalive messages, 86 | when connection is idling. In seconds. 87 | :param keepalive_timeout: sets keepalive timeout. 88 | :param tcp_keepalive_interval: Sets TCP keepalive interval. 89 | :param tcp_nodelay: sets TCP nodelay flag. 90 | :param disallow_shard_aware_port: If true, prevents the driver from connecting 91 | to the shard-aware port, even if the node supports it. 92 | """ 93 | async def startup(self) -> None: 94 | """Initialize the custer.""" 95 | async def shutdown(self) -> None: 96 | """Shutdown the cluster.""" 97 | async def prepare(self, query: str | Query) -> PreparedQuery: ... 98 | @overload 99 | async def execute( # type: ignore 100 | self, 101 | query: str | Query | PreparedQuery, 102 | params: Iterable[Any] | dict[str, Any] | None = None, 103 | *, 104 | paged: Literal[False] = False, 105 | ) -> QueryResult: 106 | """ 107 | Execute a query. 108 | 109 | This function takes a query string, 110 | and list of parameters. 111 | 112 | Parameters in query can be specified as ? signs. 113 | 114 | await scylla.execute("SELECT * FROM table WHERE id = ?", [11]) 115 | 116 | Or you can use named parameters and pass dict to execute. Like this: 117 | 118 | await scylla.execute("SELECT * FROM table WHERE id = :id", {"id": 11}) 119 | 120 | :param query: query to use. 121 | :param params: list of query parameters. 122 | :param as_class: DTO class to use for parsing rows 123 | (Can be pydantic model or dataclass). 124 | :param paged: Whether to use paging. Default if false. 125 | """ 126 | @overload 127 | async def execute( 128 | self, 129 | query: str | Query | PreparedQuery, 130 | params: Iterable[Any] | dict[str, Any] | None = None, 131 | *, 132 | paged: Literal[True] = ..., 133 | ) -> IterableQueryResult[dict[str, Any]]: ... 134 | async def batch( 135 | self, 136 | batch: Batch | InlineBatch, 137 | params: Optional[Iterable[Iterable[Any] | dict[str, Any]]] = None, 138 | ) -> QueryResult: 139 | """ 140 | Execute a batch statement. 141 | 142 | Batch statements are useful for grouping multiple queries 143 | together and executing them in one query. 144 | 145 | Each element of a list associated 146 | 147 | It may speed up you application. 148 | """ 149 | async def use_keyspace(self, keyspace: str) -> None: 150 | """Change current keyspace for all connections.""" 151 | async def get_keyspace(self) -> str | None: 152 | """Get current keyspace.""" 153 | 154 | class ExecutionProfile: 155 | def __init__( 156 | self, 157 | *, 158 | consistency: Consistency | None = None, 159 | serial_consistency: SerialConsistency | None = None, 160 | request_timeout: int | None = None, 161 | load_balancing_policy: LoadBalancingPolicy | None = None, 162 | ) -> None: ... 163 | 164 | class QueryResult: 165 | trace_id: str | None 166 | 167 | @overload 168 | def all(self, as_class: Literal[None] = None) -> list[dict[str, Any]]: ... 169 | @overload 170 | def all(self, as_class: Callable[..., _T] | None = None) -> list[_T]: ... 171 | @overload 172 | def first(self, as_class: Literal[None] = None) -> dict[str, Any] | None: ... 173 | @overload 174 | def first(self, as_class: Callable[..., _T] | None = None) -> _T | None: ... 175 | def scalars(self) -> list[Any]: ... 176 | def scalar(self) -> Any | None: ... 177 | def __len__(self) -> int: ... 178 | 179 | class IterableQueryResult(Generic[_T]): 180 | def as_cls( 181 | self: IterableQueryResult[_T], 182 | as_class: Callable[..., _T2], 183 | ) -> IterableQueryResult[_T2]: ... 184 | def scalars(self) -> IterableQueryResult[Any]: ... 185 | def __aiter__(self) -> IterableQueryResult[_T]: ... 186 | async def __anext__(self) -> _T: ... 187 | 188 | class Query: 189 | """ 190 | Query class. 191 | 192 | It's used for fine-tuning specific queries. 193 | If you don't need a specific consistency, or 194 | any other parameter, you can pass a string instead. 195 | """ 196 | 197 | query: str 198 | consistency: Consistency | None 199 | serial_consistency: SerialConsistency | None 200 | request_timeout: int | None 201 | is_idempotent: bool | None 202 | tracing: bool | None 203 | profile: ExecutionProfile 204 | 205 | def __init__( 206 | self, 207 | query: str, 208 | consistency: Consistency | None = None, 209 | serial_consistency: SerialConsistency | None = None, 210 | request_timeout: int | None = None, 211 | timestamp: int | None = None, 212 | is_idempotent: bool | None = None, 213 | tracing: bool | None = None, 214 | profile: ExecutionProfile | None = None, 215 | ) -> None: ... 216 | def with_consistency(self, consistency: Consistency | None) -> Query: ... 217 | def with_serial_consistency( 218 | self, 219 | serial_consistency: SerialConsistency | None, 220 | ) -> Query: ... 221 | def with_request_timeout(self, request_timeout: int | None) -> Query: ... 222 | def with_timestamp(self, timestamp: int | None) -> Query: ... 223 | def with_is_idempotent(self, is_idempotent: bool | None) -> Query: ... 224 | def with_tracing(self, tracing: bool | None) -> Query: ... 225 | def with_profile(self, profile: ExecutionProfile | None) -> Query: ... 226 | 227 | class BatchType: 228 | """Possible BatchTypes.""" 229 | 230 | COUNTER: BatchType 231 | LOGGED: BatchType 232 | UNLOGGED: BatchType 233 | 234 | class Batch: 235 | """Class for batching queries together.""" 236 | 237 | def __init__( 238 | self, 239 | batch_type: BatchType = ..., 240 | consistency: Consistency | None = None, 241 | serial_consistency: SerialConsistency | None = None, 242 | request_timeout: int | None = None, 243 | timestamp: int | None = None, 244 | is_idempotent: bool | None = None, 245 | tracing: bool | None = None, 246 | ) -> None: ... 247 | def add_query(self, query: Query | PreparedQuery | str) -> None: ... 248 | 249 | class InlineBatch: 250 | def __init__( 251 | self, 252 | batch_type: BatchType = ..., 253 | consistency: Consistency | None = None, 254 | serial_consistency: SerialConsistency | None = None, 255 | request_timeout: int | None = None, 256 | timestamp: int | None = None, 257 | is_idempotent: bool | None = None, 258 | tracing: bool | None = None, 259 | ) -> None: ... 260 | def add_query( 261 | self, 262 | query: Query | PreparedQuery | str, 263 | values: list[Any] | None = None, 264 | ) -> None: ... 265 | 266 | class Consistency: 267 | """Consistency for query.""" 268 | 269 | ANY: Consistency 270 | ONE: Consistency 271 | TWO: Consistency 272 | THREE: Consistency 273 | QUORUM: Consistency 274 | ALL: Consistency 275 | LOCAL_QUORUM: Consistency 276 | EACH_QUORUM: Consistency 277 | LOCAL_ONE: Consistency 278 | SERIAL: Consistency 279 | LOCAL_SERIAL: Consistency 280 | 281 | class SerialConsistency: 282 | """Serial consistency for query.""" 283 | 284 | SERIAL: SerialConsistency 285 | LOCAL_SERIAL: SerialConsistency 286 | 287 | class PreparedQuery: 288 | """Class that represents prepared statement.""" 289 | -------------------------------------------------------------------------------- /python/scyllapy/_internal/exceptions.pyi: -------------------------------------------------------------------------------- 1 | class ScyllaPyBaseError(Exception): 2 | """Base scyllapy exception.""" 3 | 4 | class ScyllaPyBindingError(ScyllaPyBaseError): 5 | """ 6 | Error that occurs during parameter binding. 7 | 8 | This error can be thrown if a parameter 9 | is not of the correct type and therefore cannot 10 | be bound. 11 | """ 12 | 13 | class ScyllaPyDBError(ScyllaPyBaseError): 14 | """ 15 | Database related exception. 16 | 17 | This exception can be thrown when 18 | the database returns an error. 19 | """ 20 | 21 | class ScyllaPySessionError(ScyllaPyDBError): 22 | """ 23 | Error related to database session. 24 | 25 | This exception can be thrown when 26 | session was not properly initialized, 27 | or if it was closed by some reason. 28 | """ 29 | 30 | class ScyllaPyMappingError(ScyllaPyBaseError): 31 | """ 32 | Exception that occurs during mapping results back to python. 33 | 34 | It can be thrown if you request row fetching, 35 | but query didn't return any rows. 36 | 37 | Also it occurs if rows cannot be mapped to python types. 38 | """ 39 | 40 | class ScyllaPyQueryBuiderError(ScyllaPyBaseError): 41 | """ 42 | Error that is thrown if Query cannot be built. 43 | 44 | When using query builder you can try to execute 45 | partialy built query that is guaranteed to 46 | have syntax errors. In order to avoid 47 | such situations we introduced another type of error, 48 | that is thrown before query is executed. 49 | """ 50 | -------------------------------------------------------------------------------- /python/scyllapy/_internal/extra_types.pyi: -------------------------------------------------------------------------------- 1 | class BigInt: 2 | def __init__(self, val: int) -> None: ... 3 | 4 | class SmallInt: 5 | def __init__(self, val: int) -> None: ... 6 | 7 | class TinyInt: 8 | def __init__(self, val: int) -> None: ... 9 | 10 | class Double: 11 | def __init__(self, val: float) -> None: ... 12 | 13 | class Counter: 14 | def __init__(self, val: int) -> None: ... 15 | 16 | class Unset: 17 | """ 18 | Class for unsetting the variable. 19 | 20 | If you want to set NULL to a column, 21 | when performing INSERT statements, 22 | it's better to use Unset instead of setting 23 | NULL, because it may result in better performance. 24 | 25 | https://rust-driver.docs.scylladb.com/stable/queries/values.html#unset-values 26 | """ 27 | 28 | def __init__(self) -> None: ... 29 | -------------------------------------------------------------------------------- /python/scyllapy/_internal/load_balancing.pyi: -------------------------------------------------------------------------------- 1 | class LatencyAwareness: 2 | def __init__( 3 | self, 4 | *, 5 | minimum_measurements: int | None = None, 6 | retry_period: int | None = None, 7 | exclusion_threshold: float | None = None, 8 | update_rate: int | None = None, 9 | scale: int | None = None, 10 | ) -> None: ... 11 | """ 12 | Build latency awareness for balancing policy. 13 | 14 | :param minimum_measurements: Minimum number of measurements to consider a host 15 | as eligible for query plans. 16 | :param retry_period: Number of milliseconds to wait before attempting to refresh 17 | the latency information of a host. 18 | :param exclusion_threshold: Maximum ratio of hosts to exclude from query plans. 19 | For example, if set to 2, the resulting policy excludes nodes that are 20 | more than twice slower than the fastest node. 21 | :param update_rate: Number of milliseconds between measurements. 22 | :param scale: provides control on how the weight given to older latencies decreases 23 | over time. 24 | """ 25 | 26 | class LoadBalancingPolicy: 27 | """ 28 | Load balancing policy. 29 | 30 | Useful to control how the driver distributes queries among nodes. 31 | Can be applied to profiles. 32 | """ 33 | 34 | @classmethod 35 | async def build( 36 | cls, 37 | *, 38 | token_aware: bool | None = None, 39 | prefer_rack: str | None = None, 40 | prefer_datacenter: str | None = None, 41 | permit_dc_failover: bool | None = None, 42 | shuffling_replicas: bool | None = None, 43 | ) -> LoadBalancingPolicy: ... 44 | """ 45 | Construct load balancing policy. 46 | 47 | It requires to be async, becausse it needs to start a background task. 48 | 49 | :param token_aware: Whether to use token aware routing. 50 | :param prefer_rack: Name of the rack to prefer. 51 | :param prefer_datacenter: Name of the datacenter to prefer. 52 | :param permit_dc_failover: Whether to allow datacenter failover. 53 | :param shuffling_replicas: Whether to shuffle replicas. 54 | """ 55 | 56 | async def with_latency_awareness( 57 | self, 58 | latency_awareness: LatencyAwareness, 59 | ) -> None: ... 60 | -------------------------------------------------------------------------------- /python/scyllapy/_internal/query_builder.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal, overload 2 | 3 | from scyllapy._internal import ( 4 | Consistency, 5 | ExecutionProfile, 6 | InlineBatch, 7 | IterableQueryResult, 8 | QueryResult, 9 | Scylla, 10 | SerialConsistency, 11 | ) 12 | 13 | class Select: 14 | def __init__(self, table: str) -> None: ... 15 | def only(self, *columns: str) -> Select: ... 16 | def where(self, clause: str, params: list[Any] | None = None) -> Select: ... 17 | def group_by(self, group: str) -> Select: ... 18 | def order_by(self, order: str, desc: bool = False) -> Select: ... 19 | def per_partition_limit(self, per_partition_limit: int) -> Select: ... 20 | def limit(self, limit: int) -> Select: ... 21 | def allow_filtering(self) -> Select: ... 22 | def distinct(self) -> Select: ... 23 | def timeout(self, timeout: int | str) -> Select: ... 24 | def request_params( 25 | self, 26 | consistency: Consistency | None = None, 27 | serial_consistency: SerialConsistency | None = None, 28 | request_timeout: int | None = None, 29 | timestamp: int | None = None, 30 | is_idempotent: bool | None = None, 31 | tracing: bool | None = None, 32 | profile: ExecutionProfile | None = None, 33 | ) -> Select: ... 34 | def add_to_batch(self, batch: InlineBatch) -> None: ... 35 | @overload 36 | async def execute( # type: ignore 37 | self, 38 | scylla: Scylla, 39 | *, 40 | paged: Literal[False] = False, 41 | ) -> QueryResult: ... 42 | @overload 43 | async def execute( 44 | self, 45 | scylla: Scylla, 46 | *, 47 | paged: Literal[True] = True, 48 | ) -> IterableQueryResult[dict[str, Any]]: ... 49 | @overload 50 | async def execute(self, scylla: Scylla, *, paged: bool = False) -> Any: ... 51 | 52 | class Insert: 53 | def __init__(self, table: str) -> None: ... 54 | def if_not_exists(self) -> Insert: ... 55 | def set(self, name: str, value: Any) -> Insert: ... 56 | def timeout(self, timeout: int | str) -> Insert: ... 57 | def timestamp(self, timestamp: int) -> Insert: ... 58 | def ttl(self, ttl: int) -> Insert: ... 59 | def request_params( 60 | self, 61 | consistency: Consistency | None = None, 62 | serial_consistency: SerialConsistency | None = None, 63 | request_timeout: int | None = None, 64 | timestamp: int | None = None, 65 | is_idempotent: bool | None = None, 66 | tracing: bool | None = None, 67 | profile: ExecutionProfile | None = None, 68 | ) -> Insert: ... 69 | def add_to_batch(self, batch: InlineBatch) -> None: ... 70 | async def execute(self, scylla: Scylla) -> QueryResult: ... 71 | 72 | class Delete: 73 | def __init__(self, table: str) -> None: ... 74 | def cols(self, *cols: str) -> Delete: ... 75 | def where(self, clause: str, values: list[Any] | None = None) -> Delete: ... 76 | def timeout(self, timeout: int | str) -> Delete: ... 77 | def timestamp(self, timestamp: int) -> Delete: ... 78 | def if_exists(self) -> Delete: ... 79 | def if_(self, clause: str, values: list[Any] | None = None) -> Delete: ... 80 | def request_params( 81 | self, 82 | consistency: Consistency | None = None, 83 | serial_consistency: SerialConsistency | None = None, 84 | request_timeout: int | None = None, 85 | timestamp: int | None = None, 86 | is_idempotent: bool | None = None, 87 | tracing: bool | None = None, 88 | profile: ExecutionProfile | None = None, 89 | ) -> Delete: ... 90 | def add_to_batch(self, batch: InlineBatch) -> None: ... 91 | async def execute(self, scylla: Scylla) -> QueryResult: ... 92 | 93 | class Update: 94 | def __init__(self, table: str) -> None: ... 95 | def set(self, name: str, value: Any) -> Update: ... 96 | def inc(self, column: str, value: Any) -> Update: ... 97 | def dec(self, column: str, value: Any) -> Update: ... 98 | def where(self, clause: str, values: list[Any] | None = None) -> Update: ... 99 | def timeout(self, timeout: int | str) -> Update: ... 100 | def timestamp(self, timestamp: int) -> Update: ... 101 | def ttl(self, ttl: int) -> Update: ... 102 | def request_params( 103 | self, 104 | consistency: Consistency | None = None, 105 | serial_consistency: SerialConsistency | None = None, 106 | request_timeout: int | None = None, 107 | timestamp: int | None = None, 108 | is_idempotent: bool | None = None, 109 | tracing: bool | None = None, 110 | profile: ExecutionProfile | None = None, 111 | ) -> Update: ... 112 | def if_exists(self) -> Update: ... 113 | def if_(self, clause: str, values: list[Any] | None = None) -> Update: ... 114 | def add_to_batch(self, batch: InlineBatch) -> None: ... 115 | async def execute(self, scylla: Scylla) -> QueryResult: ... 116 | -------------------------------------------------------------------------------- /python/scyllapy/exceptions.py: -------------------------------------------------------------------------------- 1 | from ._internal.exceptions import ( 2 | ScyllaPyBaseError, 3 | ScyllaPyBindingError, 4 | ScyllaPyDBError, 5 | ScyllaPyMappingError, 6 | ScyllaPyQueryBuiderError, 7 | ScyllaPySessionError, 8 | ) 9 | 10 | __all__ = ( 11 | "ScyllaPyBaseError", 12 | "ScyllaPyDBError", 13 | "ScyllaPySessionError", 14 | "ScyllaPyMappingError", 15 | "ScyllaPyQueryBuiderError", 16 | "ScyllaPyBindingError", 17 | ) 18 | -------------------------------------------------------------------------------- /python/scyllapy/extra_types.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from typing import Any, List 3 | 4 | from ._internal.extra_types import BigInt, Counter, Double, SmallInt, TinyInt, Unset 5 | 6 | try: 7 | import pydantic 8 | except ImportError: 9 | pydantic = None 10 | 11 | 12 | class ScyllaPyUDT: 13 | """ 14 | Class for declaring UDT models. 15 | 16 | This class is a mixin for models like dataclasses and pydantic models, 17 | or classes that have `__slots__` attribute. 18 | 19 | It can be further extended to support other model types. 20 | """ 21 | 22 | def __dump_udt__(self) -> List[Any]: 23 | """ 24 | Method to dump UDT models to a dict. 25 | 26 | This method returns a list of values in the order of the UDT fields. 27 | Because in the protocol, UDT fields should be sent in the same order as 28 | they were declared. 29 | """ 30 | if dataclasses.is_dataclass(self): 31 | values = [] 32 | for field in dataclasses.fields(self): 33 | values.append(getattr(self, field.name)) 34 | return values 35 | if pydantic is not None and isinstance(self, pydantic.BaseModel): 36 | values = [] 37 | for param in self.__class__.__signature__.parameters: 38 | values.append(getattr(self, param)) 39 | return values 40 | if hasattr(self, "__slots__"): 41 | values = [] 42 | for slot in self.__slots__: 43 | values.append(getattr(self, slot)) 44 | return values 45 | raise ValueError("Unsupported model type") 46 | 47 | 48 | __all__ = ("BigInt", "Counter", "Double", "SmallInt", "TinyInt", "Unset", "ScyllaPyUDT") 49 | -------------------------------------------------------------------------------- /python/scyllapy/load_balancing.py: -------------------------------------------------------------------------------- 1 | from ._internal.load_balancing import LatencyAwareness, LoadBalancingPolicy 2 | 3 | __all__ = ("LatencyAwareness", "LoadBalancingPolicy") 4 | -------------------------------------------------------------------------------- /python/scyllapy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intreecom/scyllapy/c2af00f13220c8a8261e51681db37501199d5513/python/scyllapy/py.typed -------------------------------------------------------------------------------- /python/scyllapy/query_builder.py: -------------------------------------------------------------------------------- 1 | from ._internal.query_builder import Delete, Insert, Select, Update 2 | 3 | __all__ = ["Select", "Delete", "Insert", "Update"] 4 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intreecom/scyllapy/c2af00f13220c8a8261e51681db37501199d5513/python/tests/__init__.py -------------------------------------------------------------------------------- /python/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import AsyncGenerator 3 | 4 | import pytest 5 | from tests.utils import random_string 6 | 7 | from scyllapy import Scylla 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def anyio_backend() -> str: 12 | """ 13 | Anyio backend. 14 | 15 | Backend for anyio pytest plugin. 16 | :return: backend name. 17 | """ 18 | return "asyncio" 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | def scylla_url() -> str: 23 | return os.environ.get("SCYLLA_URL", "localhost:9042") 24 | 25 | 26 | @pytest.fixture(scope="session") 27 | async def keyspace(scylla_url: str) -> AsyncGenerator[str, None]: 28 | keyspace_name = random_string(5) 29 | scylla = Scylla(contact_points=[scylla_url]) 30 | await scylla.startup() 31 | await scylla.execute( 32 | f"CREATE keyspace {keyspace_name} WITH replication = " 33 | "{'class': 'SimpleStrategy', 'replication_factor': 1}", 34 | ) 35 | 36 | yield keyspace_name 37 | 38 | await scylla.execute(f"DROP KEYSPACE {keyspace_name}") 39 | 40 | 41 | @pytest.fixture(scope="session") 42 | async def scylla(scylla_url: str, keyspace: str) -> AsyncGenerator[Scylla, None]: 43 | scylla = Scylla( 44 | contact_points=[scylla_url], 45 | keyspace=keyspace, 46 | ) 47 | await scylla.startup() 48 | 49 | yield scylla 50 | 51 | await scylla.shutdown() 52 | -------------------------------------------------------------------------------- /python/tests/query_builders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Intreecom/scyllapy/c2af00f13220c8a8261e51681db37501199d5513/python/tests/query_builders/__init__.py -------------------------------------------------------------------------------- /python/tests/query_builders/test_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | from scyllapy.query_builder import Delete 6 | 7 | 8 | @pytest.mark.anyio 9 | async def test_success(scylla: Scylla) -> None: 10 | table_name = random_string(4) 11 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 12 | await scylla.execute( 13 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 14 | [1, "meme"], 15 | ) 16 | await Delete(table_name).where("id = ?", [1]).execute(scylla) 17 | res = await scylla.execute(f"SELECT * FROM {table_name}") 18 | assert not res.all() 19 | 20 | 21 | @pytest.mark.anyio 22 | async def test_if_exists(scylla: Scylla) -> None: 23 | table_name = random_string(4) 24 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 25 | await scylla.execute( 26 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 27 | [1, "meme"], 28 | ) 29 | await Delete(table_name).where("id = ?", [1]).if_exists().execute(scylla) 30 | res = await scylla.execute(f"SELECT * FROM {table_name}") 31 | assert not res.all() 32 | 33 | 34 | @pytest.mark.anyio 35 | async def test_custom_if(scylla: Scylla) -> None: 36 | table_name = random_string(4) 37 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 38 | await scylla.execute( 39 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 40 | [1, "meme"], 41 | ) 42 | await Delete(table_name).where("id = ?", [1]).if_("name != ?", [None]).execute( 43 | scylla, 44 | ) 45 | res = await scylla.execute(f"SELECT * FROM {table_name}") 46 | assert not res.all() 47 | 48 | 49 | @pytest.mark.anyio 50 | async def test_custom_custom_if(scylla: Scylla) -> None: 51 | table_name = random_string(4) 52 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 53 | await scylla.execute( 54 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 55 | [1, "meme"], 56 | ) 57 | await Delete(table_name).where("id = ?", [1]).if_("name != ?", [None]).execute( 58 | scylla, 59 | ) 60 | res = await scylla.execute(f"SELECT * FROM {table_name}") 61 | assert not res.all() 62 | -------------------------------------------------------------------------------- /python/tests/query_builders/test_inserts.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | from scyllapy.query_builder import Insert 6 | 7 | 8 | @pytest.mark.anyio 9 | async def test_insert_success(scylla: Scylla) -> None: 10 | table_name = random_string(4) 11 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 12 | await Insert(table_name).set("id", 1).set("name", "random").execute(scylla) 13 | result = await scylla.execute(f"SELECT * FROM {table_name}") 14 | assert result.all() == [{"id": 1, "name": "random"}] 15 | 16 | 17 | @pytest.mark.anyio 18 | async def test_insert_if_not_exists(scylla: Scylla) -> None: 19 | table_name = random_string(4) 20 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 21 | await Insert(table_name).set("id", 1).set("name", "random").execute(scylla) 22 | await Insert(table_name).set("id", 1).set( 23 | "name", 24 | "random2", 25 | ).if_not_exists().execute(scylla) 26 | res = await scylla.execute(f"SELECT * FROM {table_name}") 27 | assert res.all() == [{"id": 1, "name": "random"}] 28 | 29 | 30 | @pytest.mark.anyio 31 | async def test_insert_request_params(scylla: Scylla) -> None: 32 | table_name = random_string(4) 33 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 34 | await Insert(table_name).set("id", 1).set("name", "random").execute(scylla) 35 | res = ( 36 | await Insert(table_name) 37 | .set("id", 1) 38 | .set("name", "random2") 39 | .request_params( 40 | tracing=True, 41 | ) 42 | .execute(scylla) 43 | ) 44 | assert res.trace_id 45 | -------------------------------------------------------------------------------- /python/tests/query_builders/test_select.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | from tests.utils import random_string 5 | 6 | from scyllapy import Scylla 7 | from scyllapy.query_builder import Select 8 | 9 | 10 | @pytest.mark.anyio 11 | async def test_select_success(scylla: Scylla) -> None: 12 | table_name = random_string(4) 13 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 14 | await scylla.execute( 15 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 16 | [1, "meme"], 17 | ) 18 | res = await Select(table_name).execute(scylla) 19 | assert res.all() == [{"id": 1, "name": "meme"}] 20 | 21 | 22 | @pytest.mark.anyio 23 | async def test_select_aliases(scylla: Scylla) -> None: 24 | table_name = random_string(4) 25 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 26 | name = uuid.uuid4().hex 27 | await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, name]) 28 | res = await Select(table_name).only("name as testname").execute(scylla) 29 | assert res.all() == [{"testname": name}] 30 | 31 | 32 | @pytest.mark.anyio 33 | async def test_select_simple_where(scylla: Scylla) -> None: 34 | table_name = random_string(4) 35 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 36 | name = uuid.uuid4().hex 37 | await scylla.execute( 38 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 39 | [1, uuid.uuid4().hex], 40 | ) 41 | await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [2, name]) 42 | 43 | res = await Select(table_name).where("id = ?", [2]).execute(scylla) 44 | assert res.all() == [{"id": 2, "name": name}] 45 | 46 | 47 | @pytest.mark.anyio 48 | async def test_select_multiple_filters(scylla: Scylla) -> None: 49 | table_name = random_string(4) 50 | await scylla.execute( 51 | f"CREATE TABLE {table_name} (id INT, name TEXT, PRIMARY KEY (id, name))", 52 | ) 53 | name = uuid.uuid4().hex 54 | await scylla.execute( 55 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 56 | [1, uuid.uuid4().hex], 57 | ) 58 | await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [2, name]) 59 | 60 | res = ( 61 | await Select(table_name) 62 | .where("id = ?", [2]) 63 | .where("name = ?", [name]) 64 | .execute(scylla) 65 | ) 66 | assert res.all() == [{"id": 2, "name": name}] 67 | 68 | 69 | @pytest.mark.anyio 70 | async def test_allow_filtering(scylla: Scylla) -> None: 71 | table_name = random_string(4) 72 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 73 | name = uuid.uuid4().hex 74 | await scylla.execute( 75 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 76 | [1, uuid.uuid4().hex], 77 | ) 78 | await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [2, name]) 79 | 80 | res = ( 81 | await Select(table_name) 82 | .where("id = ?", [2]) 83 | .where("name = ?", [name]) 84 | .allow_filtering() 85 | .execute(scylla) 86 | ) 87 | assert res.all() == [{"id": 2, "name": name}] 88 | 89 | 90 | @pytest.mark.anyio 91 | async def test_limit(scylla: Scylla) -> None: 92 | table_name = random_string(4) 93 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 94 | for i in range(10): 95 | await scylla.execute( 96 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 97 | [i, uuid.uuid4().hex], 98 | ) 99 | res = await Select(table_name).limit(3).execute(scylla) 100 | assert len(res.all()) == 3 101 | 102 | 103 | @pytest.mark.anyio 104 | async def test_order_by(scylla: Scylla) -> None: 105 | table_name = random_string(4) 106 | await scylla.execute( 107 | f"CREATE TABLE {table_name} (id INT, iid INT, PRIMARY KEY(id, iid))", 108 | ) 109 | for i in range(10): 110 | await scylla.execute( 111 | f"INSERT INTO {table_name}(id, iid) VALUES (?, ?)", 112 | [0, i], 113 | ) 114 | res = ( 115 | await Select(table_name) 116 | .only("iid") 117 | .where("id = ?", [0]) 118 | .order_by("iid") 119 | .execute(scylla) 120 | ) 121 | ids = [row["iid"] for row in res.all()] 122 | assert ids == list(range(10)) 123 | -------------------------------------------------------------------------------- /python/tests/query_builders/test_update.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | from scyllapy.query_builder import Update 6 | 7 | 8 | @pytest.mark.anyio 9 | async def test_success(scylla: Scylla) -> None: 10 | table_name = random_string(4) 11 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 12 | await scylla.execute( 13 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 14 | [1, "meme"], 15 | ) 16 | await Update(table_name).set("name", "meme2").where("id = ?", [1]).execute(scylla) 17 | res = await scylla.execute(f"SELECT * FROM {table_name}") 18 | assert res.all() == [{"id": 1, "name": "meme2"}] 19 | 20 | 21 | @pytest.mark.anyio 22 | async def test_ifs(scylla: Scylla) -> None: 23 | table_name = random_string(4) 24 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 25 | await scylla.execute( 26 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 27 | [1, "meme"], 28 | ) 29 | await Update(table_name).set("name", "meme2").if_("name = ?", ["meme"]).where( 30 | "id = ?", 31 | [1], 32 | ).execute(scylla) 33 | res = await scylla.execute(f"SELECT * FROM {table_name}") 34 | assert res.all() == [{"id": 1, "name": "meme2"}] 35 | 36 | 37 | @pytest.mark.anyio 38 | async def test_if_exists(scylla: Scylla) -> None: 39 | table_name = random_string(4) 40 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 41 | await Update(table_name).set("name", "meme2").if_exists().where( 42 | "id = ?", 43 | [1], 44 | ).execute(scylla) 45 | res = await scylla.execute(f"SELECT * FROM {table_name}") 46 | assert res.all() == [] 47 | -------------------------------------------------------------------------------- /python/tests/test_batches.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Batch, Scylla 5 | 6 | 7 | @pytest.mark.anyio 8 | async def test_batches(scylla: Scylla) -> None: 9 | table_name = random_string(4) 10 | await scylla.execute(f"CREATE TABLE {table_name}(id INT, PRIMARY KEY (id))") 11 | 12 | batch = Batch() 13 | num_queries = 10 14 | for _ in range(num_queries): 15 | batch.add_query(f"INSERT INTO {table_name}(id) VALUES (?)") 16 | 17 | await scylla.batch(batch, [[i] for i in range(num_queries)]) 18 | 19 | res = await scylla.execute(f"SELECT id FROM {table_name}") 20 | assert set(res.scalars()) == set(range(num_queries)) 21 | -------------------------------------------------------------------------------- /python/tests/test_bindings.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import ipaddress 3 | import random 4 | import uuid 5 | from decimal import Decimal 6 | from typing import Any, Callable 7 | 8 | import pytest 9 | from dateutil.relativedelta import relativedelta 10 | from tests.utils import random_string 11 | 12 | from scyllapy import Scylla 13 | 14 | 15 | @pytest.mark.anyio 16 | @pytest.mark.parametrize( 17 | ("type_name", "test_val"), 18 | [ 19 | ("INT", 1), 20 | ("TEXT", "mytext"), 21 | ("VARCHAR", "text2"), 22 | ("ASCII", "randomtext"), 23 | ("BLOB", b"random_bytes"), 24 | ("BOOLEAN", True), 25 | ("BOOLEAN", False), 26 | ("DATE", datetime.date.today()), 27 | ("TIME", datetime.time(22, 30, 11, 403)), 28 | ("TIMEUUID", uuid.uuid1()), 29 | ("UUID", uuid.uuid1()), 30 | ("UUID", uuid.uuid3(uuid.uuid4(), "name")), 31 | ("UUID", uuid.uuid4()), 32 | ("UUID", uuid.uuid5(uuid.uuid4(), "name")), 33 | ("INET", ipaddress.ip_address("192.168.1.1")), 34 | ("INET", ipaddress.ip_address("2001:db8::8a2e:370:7334")), 35 | ("DECIMAL", Decimal("1.1")), 36 | ("DECIMAL", Decimal("1.112e10")), 37 | ("DURATION", relativedelta(months=1, days=2, microseconds=10)), 38 | ("VARINT", 1000), 39 | ], 40 | ) 41 | async def test_bindings( 42 | scylla: Scylla, 43 | type_name: str, 44 | test_val: Any, 45 | ) -> None: 46 | table_name = random_string(4) 47 | await scylla.execute( 48 | f"CREATE TABLE {table_name} (id INT, value {type_name}, PRIMARY KEY (id))", 49 | ) 50 | insert_query = f"INSERT INTO {table_name}(id, value) VALUES (?, ?)" 51 | await scylla.execute(insert_query, [1, test_val]) 52 | 53 | result = await scylla.execute(f"SELECT * FROM {table_name}") 54 | rows = result.all() 55 | assert rows == [{"id": 1, "value": test_val}] 56 | 57 | 58 | @pytest.mark.anyio 59 | @pytest.mark.parametrize( 60 | ("type_name", "test_val", "cast_func"), 61 | [ 62 | ("SET", ["one", "two"], set), 63 | ("SET", {"one", "two"}, set), 64 | ("SET", ("one", "two"), set), 65 | ("LIST", ("1", "2"), list), 66 | ("LIST", ["1", "2"], list), 67 | ("LIST", {"1", "2"}, list), 68 | ("MAP", {"one": "two"}, dict), 69 | ], 70 | ) 71 | async def test_collections( 72 | scylla: Scylla, 73 | type_name: str, 74 | test_val: Any, 75 | cast_func: Callable[[Any], Any], 76 | ) -> None: 77 | table_name = random_string(4) 78 | await scylla.execute( 79 | f"CREATE TABLE {table_name} (id INT, coll {type_name}, PRIMARY KEY (id))", 80 | ) 81 | insert_query = f"INSERT INTO {table_name}(id, coll) VALUES (?, ?)" 82 | 83 | await scylla.execute(insert_query, [1, test_val]) 84 | 85 | result = await scylla.execute(f"SELECT * FROM {table_name}") 86 | rows = result.all() 87 | assert len(rows) == 1 88 | assert rows[0] == {"id": 1, "coll": cast_func(test_val)} 89 | 90 | 91 | @pytest.mark.anyio 92 | async def test_named_parameters(scylla: Scylla) -> None: 93 | table_name = random_string(4) 94 | await scylla.execute( 95 | f"CREATE TABLE {table_name} (id INT, name TEXT, age INT, PRIMARY KEY (id))", 96 | ) 97 | to_insert = { 98 | "id": random.randint(0, 100), 99 | "name": random_string(5), 100 | "age": random.randint(0, 100), 101 | } 102 | await scylla.execute( 103 | f"INSERT INTO {table_name}(id, name, age) VALUES (:id, :name, :age)", 104 | params=to_insert, 105 | ) 106 | 107 | res = await scylla.execute(f"SELECT * FROM {table_name}") 108 | assert res.first() == to_insert 109 | 110 | 111 | @pytest.mark.anyio 112 | async def test_floats(scylla: Scylla) -> None: 113 | table_name = random_string(4) 114 | my_float = 1.234 115 | await scylla.execute( 116 | f"CREATE TABLE {table_name} (id INT, fl FLOAT, PRIMARY KEY (id))", 117 | ) 118 | insert_query = f"INSERT INTO {table_name}(id, fl) VALUES (?, ?)" 119 | 120 | await scylla.execute(insert_query, [1, my_float]) 121 | 122 | res = await scylla.execute(f"SELECT fl FROM {table_name}") 123 | scalar = res.scalar() 124 | assert scalar 125 | assert abs(scalar - my_float) < 0.001 126 | 127 | 128 | @pytest.mark.anyio 129 | async def test_timestamps(scylla: Scylla) -> None: 130 | table_name = random_string(4) 131 | now = datetime.datetime.now() 132 | # We do replace this, because scylla ony has millisecond percision. 133 | now = now.replace(microsecond=now.microsecond - (now.microsecond % 1000)) 134 | await scylla.execute( 135 | f"CREATE TABLE {table_name} (id INT, time TIMESTAMP, PRIMARY KEY (id))", 136 | ) 137 | insert_query = f"INSERT INTO {table_name}(id, time) VALUES (?, ?)" 138 | 139 | await scylla.execute(insert_query, [1, now]) 140 | 141 | res = await scylla.execute(f"SELECT time FROM {table_name}") 142 | assert res.scalar() == now 143 | 144 | 145 | @pytest.mark.anyio 146 | async def test_none_vals(scylla: Scylla) -> None: 147 | table_name = random_string(4) 148 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 149 | await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, None]) 150 | results = await scylla.execute(f"SELECT * FROM {table_name}") 151 | assert results.first() == {"id": 1, "name": None} 152 | 153 | 154 | @pytest.mark.anyio 155 | async def test_cases(scylla: Scylla) -> None: 156 | table_name = random_string(4) 157 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 158 | await scylla.execute( 159 | f"INSERT INTO {table_name}(id, name) VALUES (:Id, :NaMe)", 160 | {"Id": 1, "NaMe": 2}, 161 | ) 162 | -------------------------------------------------------------------------------- /python/tests/test_extra_types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass 2 | from typing import Any, Callable 3 | 4 | import pytest 5 | from tests.utils import random_string 6 | 7 | from scyllapy import Scylla, extra_types 8 | from scyllapy.exceptions import ScyllaPyDBError 9 | 10 | 11 | @pytest.mark.anyio 12 | @pytest.mark.parametrize( 13 | ("type_cls", "type_name", "test_val"), 14 | [ 15 | (extra_types.TinyInt, "TINYINT", 1), 16 | (extra_types.SmallInt, "SMALLINT", 1), 17 | (extra_types.BigInt, "BIGINT", 1), 18 | (extra_types.Double, "DOUBLE", 1.0), 19 | ], 20 | ) 21 | async def test_int_types( 22 | scylla: Scylla, 23 | type_cls: Any, 24 | type_name: str, 25 | test_val: Any, 26 | ) -> None: 27 | table_name = random_string(4) 28 | 29 | await scylla.execute( 30 | f"CREATE TABLE {table_name} (id {type_name}, PRIMARY KEY (id))", 31 | ) 32 | insert_query = f"INSERT INTO {table_name}(id) VALUES (?)" 33 | with pytest.raises(ScyllaPyDBError): 34 | await scylla.execute(insert_query, [test_val]) 35 | 36 | await scylla.execute(insert_query, [type_cls(test_val)]) 37 | 38 | result = await scylla.execute(f"SELECT * FROM {table_name}") 39 | rows = result.all() 40 | assert len(rows) == 1 41 | assert rows[0] == {"id": test_val} 42 | 43 | 44 | @pytest.mark.anyio 45 | async def test_counter(scylla: Scylla) -> None: 46 | table_name = random_string(4) 47 | await scylla.execute( 48 | f"CREATE TABLE {table_name} (id INT, count COUNTER, PRIMARY KEY (id))", 49 | ) 50 | 51 | query = f"UPDATE {table_name} SET count = count + ? WHERE id = ?" 52 | 53 | with pytest.raises(ScyllaPyDBError): 54 | await scylla.execute(query, [1, 1]) 55 | 56 | await scylla.execute(query, [extra_types.Counter(1), 1]) 57 | 58 | res = await scylla.execute(f"SELECT * FROM {table_name}") 59 | rows = res.all() 60 | assert len(rows) == 1 61 | assert rows[0] == {"id": 1, "count": 1} 62 | 63 | 64 | @pytest.mark.anyio 65 | async def test_unset(scylla: Scylla) -> None: 66 | table_name = random_string(4) 67 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") 68 | 69 | await scylla.execute( 70 | f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", 71 | [1, extra_types.Unset()], 72 | ) 73 | 74 | 75 | @pytest.mark.anyio 76 | async def test_udts(scylla: Scylla) -> None: 77 | @dataclass 78 | class TestUDT(extra_types.ScyllaPyUDT): 79 | id: int 80 | name: str 81 | 82 | table_name = random_string(4) 83 | 84 | udt_val = TestUDT(id=1, name="test") 85 | await scylla.execute(f"CREATE TYPE test_udt{table_name} (id int, name text)") 86 | await scylla.execute( 87 | f"CREATE TABLE {table_name} " 88 | f"(id INT PRIMARY KEY, udt_col frozen)", 89 | ) 90 | await scylla.execute( 91 | f"INSERT INTO {table_name} (id, udt_col) VALUES (?, ?)", 92 | [1, udt_val], 93 | ) 94 | 95 | res = await scylla.execute(f"SELECT * FROM {table_name}") 96 | assert res.all() == [{"id": 1, "udt_col": asdict(udt_val)}] 97 | 98 | 99 | @pytest.mark.anyio 100 | async def test_nested_udts(scylla: Scylla) -> None: 101 | @dataclass 102 | class NestedUDT(extra_types.ScyllaPyUDT): 103 | one: int 104 | two: str 105 | 106 | @dataclass 107 | class TestUDT(extra_types.ScyllaPyUDT): 108 | id: int 109 | name: str 110 | nested: NestedUDT 111 | 112 | table_name = random_string(4) 113 | 114 | udt_val = TestUDT(id=1, name="test", nested=NestedUDT(one=1, two="2")) 115 | await scylla.execute(f"CREATE TYPE nested_udt{table_name} (one int, two text)") 116 | await scylla.execute( 117 | f"CREATE TYPE test_udt{table_name} " 118 | f"(id int, name text, nested frozen)", 119 | ) 120 | await scylla.execute( 121 | f"CREATE TABLE {table_name} " 122 | f"(id INT PRIMARY KEY, udt_col frozen)", 123 | ) 124 | await scylla.execute( 125 | f"INSERT INTO {table_name} (id, udt_col) VALUES (?, ?)", 126 | [1, udt_val], 127 | ) 128 | 129 | res = await scylla.execute(f"SELECT * FROM {table_name}") 130 | assert res.all() == [{"id": 1, "udt_col": asdict(udt_val)}] 131 | 132 | 133 | @pytest.mark.parametrize( 134 | ["typ", "val"], 135 | [ 136 | ("BIGINT", 1), 137 | ("TINYINT", 1), 138 | ("SMALLINT", 1), 139 | ("INT", 1), 140 | ("FLOAT", 1.0), 141 | ("DOUBLE", 1.0), 142 | ], 143 | ) 144 | @pytest.mark.anyio 145 | async def test_autocast_positional(scylla: Scylla, typ: str, val: Any) -> None: 146 | table_name = random_string(4) 147 | await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY, val {typ})") 148 | prepared = await scylla.prepare(f"INSERT INTO {table_name}(id, val) VALUES (?, ?)") 149 | await scylla.execute(prepared, [1, val]) 150 | 151 | 152 | @pytest.mark.parametrize( 153 | ["cast_func", "val"], 154 | [ 155 | (extra_types.BigInt, 1000000), 156 | (extra_types.SmallInt, 10), 157 | (extra_types.TinyInt, 1), 158 | (int, 1), 159 | ], 160 | ) 161 | @pytest.mark.anyio 162 | async def test_varint( 163 | scylla: Scylla, 164 | cast_func: Callable[[Any], Any], 165 | val: Any, 166 | ) -> None: 167 | table_name = random_string(4) 168 | await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY, val VARINT)") 169 | await scylla.execute( 170 | f"INSERT INTO {table_name}(id, val) VALUES (?, ?)", 171 | (1, cast_func(val)), 172 | ) 173 | res = await scylla.execute(f"SELECT * FROM {table_name}") 174 | assert res.all() == [{"id": 1, "val": val}] 175 | -------------------------------------------------------------------------------- /python/tests/test_pagination.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | from tests.utils import random_string 5 | 6 | from scyllapy import Scylla 7 | from scyllapy.query_builder import Select 8 | 9 | 10 | @pytest.mark.anyio 11 | async def test_scalars(scylla: Scylla) -> None: 12 | table_name = random_string(4) 13 | await scylla.execute( 14 | f"CREATE TABLE IF NOT EXISTS {table_name} (id INT PRIMARY KEY)", 15 | ) 16 | vals = list(range(10)) 17 | for i in vals: 18 | await scylla.execute(f"INSERT INTO {table_name}(id) VALUES (?)", [i]) 19 | res = await scylla.execute(f"SELECT id FROM {table_name}", paged=True) 20 | async for col in res.scalars(): 21 | assert col in vals 22 | 23 | 24 | @pytest.mark.anyio 25 | async def test_dicts(scylla: Scylla) -> None: 26 | table_name = random_string(4) 27 | await scylla.execute( 28 | f"CREATE TABLE IF NOT EXISTS {table_name} (id INT PRIMARY KEY, val INT)", 29 | ) 30 | vals = list(range(10)) 31 | for i in vals: 32 | await scylla.execute( 33 | f"INSERT INTO {table_name}(id, val) VALUES (?, ?)", 34 | [i, -i], 35 | ) 36 | res = await scylla.execute(f"SELECT id, val FROM {table_name}", paged=True) 37 | async for row in res: 38 | assert row["id"] in vals 39 | assert row["val"] == -row["id"] 40 | 41 | 42 | @pytest.mark.anyio 43 | async def test_dtos(scylla: Scylla) -> None: 44 | @dataclass 45 | class TestDTO: 46 | id: int 47 | val: int 48 | 49 | table_name = random_string(4) 50 | await scylla.execute( 51 | f"CREATE TABLE IF NOT EXISTS {table_name} (id INT PRIMARY KEY, val INT)", 52 | ) 53 | vals = list(range(10)) 54 | for i in vals: 55 | await scylla.execute( 56 | f"INSERT INTO {table_name}(id, val) VALUES (?, ?)", 57 | [i, -i], 58 | ) 59 | res = await scylla.execute(f"SELECT id, val FROM {table_name}", paged=True) 60 | async for row in res.as_cls(TestDTO): 61 | assert row.id in vals 62 | assert row.val == -row.id 63 | 64 | 65 | @pytest.mark.anyio 66 | async def test_paged_select_qb(scylla: Scylla) -> None: 67 | table_name = random_string(4) 68 | await scylla.execute( 69 | f"CREATE TABLE IF NOT EXISTS {table_name} (id INT PRIMARY KEY, val INT)", 70 | ) 71 | vals = list(range(10)) 72 | for i in vals: 73 | await scylla.execute( 74 | f"INSERT INTO {table_name}(id, val) VALUES (?, ?)", 75 | [i, -i], 76 | ) 77 | res = await Select(table_name).execute(scylla, paged=True) 78 | async for row in res: 79 | assert row["id"] in vals 80 | assert row["val"] == -row["id"] 81 | -------------------------------------------------------------------------------- /python/tests/test_parsing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | 6 | 7 | @pytest.mark.anyio 8 | async def test_udt_parsing(scylla: Scylla) -> None: 9 | table_name = random_string(4) 10 | await scylla.execute(f"CREATE TYPE test_udt{table_name} (id int, name text)") 11 | await scylla.execute( 12 | f"CREATE TABLE {table_name} " 13 | f"(id int PRIMARY KEY, udt_col frozen)", 14 | ) 15 | await scylla.execute( 16 | f"INSERT INTO {table_name} (id, udt_col) VALUES (1, {{id: 1, name: 'test'}})", 17 | ) 18 | res = await scylla.execute(f"SELECT * FROM {table_name}") 19 | assert res.all() == [{"id": 1, "udt_col": {"id": 1, "name": "test"}}] 20 | -------------------------------------------------------------------------------- /python/tests/test_prepared.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | 6 | 7 | @pytest.mark.anyio 8 | async def test_prepared(scylla: Scylla) -> None: 9 | table_name = random_string(4) 10 | await scylla.execute(f"CREATE TABLE {table_name}(id INT, PRIMARY KEY (id))") 11 | await scylla.execute(f"INSERT INTO {table_name}(id) VALUES (?)", [1]) 12 | 13 | query = f"SELECT * FROM {table_name}" 14 | prepared = await scylla.prepare(query) 15 | res = await scylla.execute(query) 16 | prepared_res = await scylla.execute(prepared) 17 | 18 | assert res.all() == prepared_res.all() 19 | -------------------------------------------------------------------------------- /python/tests/test_profiles.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Consistency, ExecutionProfile, Query, Scylla 5 | from scyllapy.exceptions import ScyllaPyDBError 6 | 7 | 8 | @pytest.mark.anyio 9 | async def test_wrong_consistency(scylla: Scylla) -> None: 10 | profile = ExecutionProfile(consistency=Consistency.ANY) 11 | table_name = random_string(4) 12 | await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY)") 13 | query = Query(f"SELECT * FROM {table_name} WHERE id = ?", profile=profile) 14 | with pytest.raises(ScyllaPyDBError, match=".*only supported for writes.*"): 15 | await scylla.execute(query, [1]) 16 | 17 | await scylla.execute(query.with_profile(None), [1]) 18 | -------------------------------------------------------------------------------- /python/tests/test_queries.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | from tests.utils import random_string 5 | 6 | from scyllapy import Scylla 7 | 8 | 9 | @pytest.mark.anyio 10 | async def test_empty_scalars(scylla: Scylla) -> None: 11 | table_name = random_string(4) 12 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY)") 13 | res = await scylla.execute(f"SELECT id FROM {table_name}") 14 | 15 | assert res.all() == [] 16 | assert res.scalars() == [] 17 | 18 | 19 | @pytest.mark.anyio 20 | async def test_as_class(scylla: Scylla) -> None: 21 | @dataclass 22 | class TestDTO: 23 | id: int 24 | 25 | table_name = random_string(4) 26 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY)") 27 | await scylla.execute(f"INSERT INTO {table_name}(id) VALUES (?)", [42]) 28 | res = await scylla.execute(f"SELECT id FROM {table_name}") 29 | 30 | assert res.all(as_class=TestDTO) == [TestDTO(id=42)] 31 | 32 | 33 | @pytest.mark.anyio 34 | async def test_udt_as_dataclass(scylla: Scylla) -> None: 35 | @dataclass 36 | class UDTType: 37 | id: int 38 | name: str 39 | 40 | @dataclass 41 | class TestDTO: 42 | id: int 43 | udt_col: UDTType 44 | 45 | def __post_init__(self) -> None: 46 | if not isinstance(self.udt_col, UDTType): 47 | self.udt_col = UDTType(**self.udt_col) 48 | 49 | table_name = random_string(4) 50 | await scylla.execute(f"CREATE TYPE test_udt{table_name} (id int, name text)") 51 | await scylla.execute( 52 | f"CREATE TABLE {table_name} " 53 | f"(id int PRIMARY KEY, udt_col frozen)", 54 | ) 55 | await scylla.execute( 56 | f"INSERT INTO {table_name} (id, udt_col) VALUES (1, {{id: 1, name: 'test'}})", 57 | ) 58 | res = await scylla.execute(f"SELECT * FROM {table_name}") 59 | assert res.all(as_class=TestDTO) == [ 60 | TestDTO(id=1, udt_col=UDTType(id=1, name="test")), 61 | ] 62 | -------------------------------------------------------------------------------- /python/tests/test_query_res.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.utils import random_string 3 | 4 | from scyllapy import Scylla 5 | 6 | 7 | @pytest.mark.anyio 8 | async def test_results_len(scylla: Scylla) -> None: 9 | table_name = random_string(4) 10 | await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY)") 11 | for i in range(10): 12 | await scylla.execute(f"INSERT INTO {table_name}(id) VALUES (?)", [i]) 13 | res = await scylla.execute(f"SELECT id FROM {table_name}") 14 | 15 | assert len(res) == 10 16 | -------------------------------------------------------------------------------- /python/tests/utils.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | 4 | 5 | def random_string(length: int) -> str: 6 | return "".join([secrets.choice(string.ascii_lowercase) for _ in range(length)]) 7 | -------------------------------------------------------------------------------- /scripts/version_bumper.py: -------------------------------------------------------------------------------- 1 | import re 2 | import argparse 3 | from pathlib import Path 4 | 5 | 6 | def parse_args() -> argparse.Namespace: 7 | """Parse command line arguments.""" 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | "--target", 11 | "-t", 12 | dest="target", 13 | type=Path, 14 | default="Cargo.toml", 15 | ) 16 | parser.add_argument("version", type=str) 17 | return parser.parse_args() 18 | 19 | 20 | def main() -> None: 21 | """Main function.""" 22 | args = parse_args() 23 | with args.target.open("r") as f: 24 | contents = f.read() 25 | 26 | contents = re.sub( 27 | r"version\s*=\s*\"(.*)\"", 28 | f'version = "{args.version}"', 29 | contents, 30 | count=1, 31 | ) 32 | 33 | with args.target.open("w") as f: 34 | f.write(contents) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /src/batches.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{pyclass, pymethods, types::PyDict, PyAny}; 2 | use scylla::{ 3 | batch::{Batch, BatchStatement, BatchType}, 4 | frame::value::LegacySerializedValues, 5 | }; 6 | 7 | use crate::{ 8 | exceptions::rust_err::ScyllaPyResult, inputs::BatchQueryInput, queries::ScyllaPyRequestParams, 9 | utils::parse_python_query_params, 10 | }; 11 | 12 | #[pyclass(name = "BatchType")] 13 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 14 | pub enum ScyllaPyBatchType { 15 | COUNTER, 16 | LOGGED, 17 | UNLOGGED, 18 | } 19 | 20 | #[pyclass(name = "Batch")] 21 | #[derive(Clone)] 22 | pub struct ScyllaPyBatch { 23 | inner: Batch, 24 | request_params: ScyllaPyRequestParams, 25 | } 26 | 27 | #[pyclass(name = "InlineBatch")] 28 | #[derive(Clone)] 29 | pub struct ScyllaPyInlineBatch { 30 | inner: Batch, 31 | request_params: ScyllaPyRequestParams, 32 | values: Vec, 33 | } 34 | 35 | impl From for Batch { 36 | fn from(value: ScyllaPyBatch) -> Self { 37 | let mut inner = value.inner; 38 | value.request_params.apply_to_batch(&mut inner); 39 | inner 40 | } 41 | } 42 | 43 | impl From for (Batch, Vec) { 44 | fn from(mut value: ScyllaPyInlineBatch) -> Self { 45 | value.request_params.apply_to_batch(&mut value.inner); 46 | (value.inner, value.values) 47 | } 48 | } 49 | 50 | #[pymethods] 51 | impl ScyllaPyBatch { 52 | /// Create new batch. 53 | /// 54 | /// # Errors 55 | /// 56 | /// Can return an error in case if 57 | /// wrong type for parameters were passed. 58 | #[new] 59 | #[pyo3(signature = ( 60 | batch_type = ScyllaPyBatchType::UNLOGGED, 61 | **params 62 | ))] 63 | pub fn py_new(batch_type: ScyllaPyBatchType, params: Option<&PyDict>) -> ScyllaPyResult { 64 | Ok(Self { 65 | inner: Batch::new(batch_type.into()), 66 | request_params: ScyllaPyRequestParams::from_dict(params)?, 67 | }) 68 | } 69 | 70 | pub fn add_query(&mut self, query: BatchQueryInput) { 71 | self.inner.append_statement(query); 72 | } 73 | } 74 | 75 | impl ScyllaPyInlineBatch { 76 | pub fn add_query_inner( 77 | &mut self, 78 | query: impl Into, 79 | values: impl Into, 80 | ) { 81 | self.inner.append_statement(query); 82 | self.values.push(values.into()); 83 | } 84 | } 85 | 86 | #[pymethods] 87 | impl ScyllaPyInlineBatch { 88 | /// Create new batch. 89 | /// 90 | /// # Errors 91 | /// 92 | /// Can return an error in case if 93 | /// wrong type for parameters were passed. 94 | #[new] 95 | #[pyo3(signature = ( 96 | batch_type = ScyllaPyBatchType::UNLOGGED, 97 | **params 98 | ))] 99 | pub fn py_new(batch_type: ScyllaPyBatchType, params: Option<&PyDict>) -> ScyllaPyResult { 100 | Ok(Self { 101 | inner: Batch::new(batch_type.into()), 102 | request_params: ScyllaPyRequestParams::from_dict(params)?, 103 | values: vec![], 104 | }) 105 | } 106 | 107 | /// Add query to batch. 108 | /// 109 | /// This function appends query to batch. 110 | /// along with values, so you don't need to 111 | /// pass values in execute. 112 | /// 113 | /// # Errors 114 | /// 115 | /// Will result in an error, if 116 | /// values are incorrect. 117 | #[pyo3(signature = (query, values = None))] 118 | pub fn add_query( 119 | &mut self, 120 | query: BatchQueryInput, 121 | values: Option<&PyAny>, 122 | ) -> ScyllaPyResult<()> { 123 | self.inner.append_statement(query); 124 | if let Some(passed_params) = values { 125 | self.values 126 | .push(parse_python_query_params(Some(passed_params), false, None)?); 127 | } else { 128 | self.values.push(LegacySerializedValues::new()); 129 | } 130 | Ok(()) 131 | } 132 | } 133 | 134 | impl From for BatchType { 135 | fn from(value: ScyllaPyBatchType) -> Self { 136 | match value { 137 | ScyllaPyBatchType::COUNTER => Self::Counter, 138 | ScyllaPyBatchType::LOGGED => Self::Logged, 139 | ScyllaPyBatchType::UNLOGGED => Self::Unlogged, 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/consistencies.rs: -------------------------------------------------------------------------------- 1 | use pyo3::pyclass; 2 | use scylla::statement::{Consistency, SerialConsistency}; 3 | 4 | /// Consistency levels for queries. 5 | /// 6 | /// This class allows to run queries 7 | /// with specific consistency levels. 8 | #[pyclass(name = "Consistency")] 9 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 10 | #[allow(non_camel_case_types)] 11 | pub enum ScyllaPyConsistency { 12 | ANY, 13 | ONE, 14 | TWO, 15 | THREE, 16 | QUORUM, 17 | ALL, 18 | LOCAL_QUORUM, 19 | EACH_QUORUM, 20 | LOCAL_ONE, 21 | SERIAL, 22 | LOCAL_SERIAL, 23 | } 24 | 25 | #[pyclass(name = "SerialConsistency")] 26 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 27 | #[allow(non_camel_case_types)] 28 | pub enum ScyllaPySerialConsistency { 29 | SERIAL, 30 | LOCAL_SERIAL, 31 | } 32 | 33 | /// Here we define how to convert our Consistency, 34 | /// to the type that is used by scylla library. 35 | impl From for Consistency { 36 | fn from(value: ScyllaPyConsistency) -> Self { 37 | match value { 38 | ScyllaPyConsistency::ANY => Self::Any, 39 | ScyllaPyConsistency::ONE => Self::One, 40 | ScyllaPyConsistency::TWO => Self::Two, 41 | ScyllaPyConsistency::THREE => Self::Three, 42 | ScyllaPyConsistency::QUORUM => Self::Quorum, 43 | ScyllaPyConsistency::ALL => Self::All, 44 | ScyllaPyConsistency::LOCAL_QUORUM => Self::LocalQuorum, 45 | ScyllaPyConsistency::EACH_QUORUM => Self::EachQuorum, 46 | ScyllaPyConsistency::LOCAL_ONE => Self::LocalOne, 47 | ScyllaPyConsistency::SERIAL => Self::Serial, 48 | ScyllaPyConsistency::LOCAL_SERIAL => Self::LocalSerial, 49 | } 50 | } 51 | } 52 | 53 | /// Convertion between python serial consistency 54 | /// and scylla serial consistency. 55 | impl From for SerialConsistency { 56 | fn from(value: ScyllaPySerialConsistency) -> Self { 57 | match value { 58 | ScyllaPySerialConsistency::SERIAL => Self::Serial, 59 | ScyllaPySerialConsistency::LOCAL_SERIAL => Self::LocalSerial, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/exceptions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod py_err; 2 | pub mod rust_err; 3 | -------------------------------------------------------------------------------- /src/exceptions/py_err.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{create_exception, types::PyModule, PyResult, Python}; 2 | 3 | create_exception!( 4 | scyllapy.exceptions, 5 | ScyllaPyBaseError, 6 | pyo3::exceptions::PyException 7 | ); 8 | create_exception!(scyllapy.exceptions, ScyllaPyBindingError, ScyllaPyBaseError); 9 | create_exception!(scyllapy.exceptions, ScyllaPyDBError, ScyllaPyBaseError); 10 | create_exception!(scyllapy.exceptions, ScyllaPySessionError, ScyllaPyDBError); 11 | create_exception!(scyllapy.exceptions, ScyllaPyMappingError, ScyllaPyBaseError); 12 | create_exception!( 13 | scyllapy.exceptions, 14 | ScyllaPyQueryBuiderError, 15 | ScyllaPyBaseError 16 | ); 17 | 18 | /// Create module with exceptions. 19 | /// 20 | /// This method adds custom exceptions 21 | /// to scyllapy python module. 22 | /// 23 | /// # Errors 24 | /// 25 | /// May throw an error, if module cannot be constructed. 26 | pub fn setup_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { 27 | module.add("ScyllaPyBaseError", py.get_type::())?; 28 | module.add("ScyllaPyDBError", py.get_type::())?; 29 | module.add( 30 | "ScyllaPySessionError", 31 | py.get_type::(), 32 | )?; 33 | module.add( 34 | "ScyllaPyBindingError", 35 | py.get_type::(), 36 | )?; 37 | module.add( 38 | "ScyllaPyMappingError", 39 | py.get_type::(), 40 | )?; 41 | module.add( 42 | "ScyllaPyQueryBuiderError", 43 | py.get_type::(), 44 | )?; 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/exceptions/rust_err.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use super::py_err::{ 4 | ScyllaPyBaseError, ScyllaPyBindingError, ScyllaPyDBError, ScyllaPyMappingError, 5 | ScyllaPyQueryBuiderError, ScyllaPySessionError, 6 | }; 7 | 8 | pub type ScyllaPyResult = Result; 9 | 10 | /// Error type for internal use. 11 | /// 12 | /// Used only inside Rust application. 13 | #[derive(Error, Debug)] 14 | pub enum ScyllaPyError { 15 | #[error("Session error: {0}.")] 16 | SessionError(String), 17 | #[error("Binding error. Cause: {0}.")] 18 | BindingError(String), 19 | 20 | // Derived exception. 21 | #[error("{0}")] 22 | QueryError(#[from] scylla::transport::errors::QueryError), 23 | #[error("{0}")] 24 | DBError(#[from] scylla::transport::errors::DbError), 25 | #[error("Python exception: {0}.")] 26 | PyError(#[from] pyo3::PyErr), 27 | #[error("OpenSSL error: {0}.")] 28 | SSLError(#[from] openssl::error::ErrorStack), 29 | #[error("Cannot construct new session: {0}.")] 30 | ScyllaSessionError(#[from] scylla::transport::errors::NewSessionError), 31 | 32 | // Binding errors 33 | #[error("Binding error. Cannot build values for query: {0},")] 34 | ScyllaValueError(#[from] scylla::frame::value::SerializeValuesError), 35 | #[error("Binding error. Cannot parse time, because of: {0}.")] 36 | DateParseError(#[from] chrono::ParseError), 37 | #[error("Binding error. Cannot parse ip address, because of: {0}.")] 38 | IpParseError(#[from] std::net::AddrParseError), 39 | #[error("Binding error. Cannot parse uuid, because of: {0}.")] 40 | UuidParseError(#[from] uuid::Error), 41 | 42 | // Mapping errors 43 | #[error("Cannot map rows: {0}.")] 44 | RowsDowncastError(String), 45 | #[error("Cannot parse value of column {0} as {1}.")] 46 | ValueDowncastError(String, &'static str), 47 | #[error("Cannot downcast UDT {0} of column {1}. Reason: {2}.")] 48 | UDTDowncastError(String, String, String), 49 | #[error("Query didn't suppose to return anything.")] 50 | NoReturnsError, 51 | #[error("Query doesn't have columns.")] 52 | NoColumns, 53 | 54 | // QueryBuilder errors 55 | #[error("Query builder error: {0}.")] 56 | QueryBuilderError(&'static str), 57 | } 58 | 59 | impl From for pyo3::PyErr { 60 | fn from(error: ScyllaPyError) -> Self { 61 | let err_desc = error.to_string(); 62 | match error { 63 | ScyllaPyError::PyError(err) => err, 64 | ScyllaPyError::SSLError(_) => ScyllaPyBaseError::new_err((err_desc,)), 65 | ScyllaPyError::QueryError(_) | ScyllaPyError::DBError(_) => { 66 | ScyllaPyDBError::new_err((err_desc,)) 67 | } 68 | ScyllaPyError::SessionError(_) | ScyllaPyError::ScyllaSessionError(_) => { 69 | ScyllaPySessionError::new_err((err_desc,)) 70 | } 71 | ScyllaPyError::BindingError(_) 72 | | ScyllaPyError::ScyllaValueError(_) 73 | | ScyllaPyError::DateParseError(_) 74 | | ScyllaPyError::UuidParseError(_) 75 | | ScyllaPyError::IpParseError(_) => ScyllaPyBindingError::new_err((err_desc,)), 76 | ScyllaPyError::RowsDowncastError(_) 77 | | ScyllaPyError::ValueDowncastError(_, _) 78 | | ScyllaPyError::UDTDowncastError(_, _, _) 79 | | ScyllaPyError::NoReturnsError 80 | | ScyllaPyError::NoColumns => ScyllaPyMappingError::new_err((err_desc,)), 81 | ScyllaPyError::QueryBuilderError(_) => ScyllaPyQueryBuiderError::new_err((err_desc,)), 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/execution_profiles.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use pyo3::{pyclass, pymethods}; 4 | use scylla::{execution_profile::ExecutionProfileHandle, statement::SerialConsistency}; 5 | 6 | use crate::{ 7 | consistencies::{ScyllaPyConsistency, ScyllaPySerialConsistency}, 8 | load_balancing::ScyllaPyLoadBalancingPolicy, 9 | }; 10 | 11 | #[pyclass(name = "ExecutionProfile")] 12 | #[derive(Clone, Debug)] 13 | pub struct ScyllaPyExecutionProfile { 14 | inner: scylla::ExecutionProfile, 15 | } 16 | 17 | #[pymethods] 18 | impl ScyllaPyExecutionProfile { 19 | #[new] 20 | #[pyo3(signature = (*, 21 | consistency=None, 22 | serial_consistency=None, 23 | request_timeout=None, 24 | load_balancing_policy = None 25 | ))] 26 | fn py_new( 27 | consistency: Option, 28 | serial_consistency: Option, 29 | request_timeout: Option, 30 | load_balancing_policy: Option, 31 | ) -> Self { 32 | let mut profile_builder = scylla::ExecutionProfile::builder(); 33 | if let Some(consistency) = consistency { 34 | profile_builder = profile_builder.consistency(consistency.into()); 35 | } 36 | if let Some(load_balancing_policy) = load_balancing_policy { 37 | profile_builder = profile_builder.load_balancing_policy(load_balancing_policy.into()); 38 | } 39 | profile_builder = profile_builder 40 | .serial_consistency(serial_consistency.map(SerialConsistency::from)) 41 | .request_timeout(request_timeout.map(Duration::from_secs)); 42 | Self { 43 | inner: profile_builder.build(), 44 | } 45 | } 46 | } 47 | 48 | impl From<&ScyllaPyExecutionProfile> for ExecutionProfileHandle { 49 | fn from(value: &ScyllaPyExecutionProfile) -> Self { 50 | value.inner.clone().into_handle() 51 | } 52 | } 53 | 54 | impl From for ExecutionProfileHandle { 55 | fn from(value: ScyllaPyExecutionProfile) -> Self { 56 | value.inner.into_handle() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/extra_types.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{pyclass, pymethods, types::PyModule, PyResult, Python}; 2 | 3 | macro_rules! simple_wrapper { 4 | ($name:ident, $ttype:ty) => { 5 | #[pyclass] 6 | #[derive(Clone)] 7 | pub struct $name { 8 | inner: $ttype, 9 | } 10 | 11 | impl $name { 12 | #[must_use] 13 | pub fn get_value(&self) -> $ttype { 14 | self.inner 15 | } 16 | } 17 | 18 | #[pymethods] 19 | impl $name { 20 | #[new] 21 | #[must_use] 22 | pub fn py_new(val: $ttype) -> Self { 23 | Self { inner: val } 24 | } 25 | 26 | #[must_use] 27 | pub fn __str__(&self) -> String { 28 | format!("{}({})", stringify!($name), self.inner) 29 | } 30 | } 31 | }; 32 | } 33 | 34 | simple_wrapper!(SmallInt, i16); 35 | simple_wrapper!(TinyInt, i8); 36 | simple_wrapper!(BigInt, i64); 37 | simple_wrapper!(Double, f64); 38 | simple_wrapper!(Counter, i64); 39 | 40 | #[pyclass(name = "Unset")] 41 | #[derive(Clone, Copy)] 42 | pub struct ScyllaPyUnset {} 43 | 44 | #[pymethods] 45 | impl ScyllaPyUnset { 46 | #[new] 47 | #[must_use] 48 | pub fn py_new() -> Self { 49 | Self {} 50 | } 51 | } 52 | 53 | /// Create new module for extra types. 54 | /// 55 | /// # Errors 56 | /// 57 | /// May return error if module cannot be created, 58 | /// or any of classes cannot be added. 59 | pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { 60 | module.add_class::()?; 61 | module.add_class::()?; 62 | module.add_class::()?; 63 | module.add_class::()?; 64 | module.add_class::()?; 65 | module.add_class::()?; 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /src/inputs.rs: -------------------------------------------------------------------------------- 1 | use pyo3::FromPyObject; 2 | 3 | use crate::{ 4 | batches::{ScyllaPyBatch, ScyllaPyInlineBatch}, 5 | prepared_queries::ScyllaPyPreparedQuery, 6 | queries::ScyllaPyQuery, 7 | }; 8 | use scylla::{batch::BatchStatement, query::Query}; 9 | 10 | #[derive(Clone, FromPyObject)] 11 | pub enum ExecuteInput { 12 | #[pyo3(transparent, annotation = "str")] 13 | Text(String), 14 | #[pyo3(transparent, annotation = "Query")] 15 | Query(ScyllaPyQuery), 16 | #[pyo3(transparent, annotation = "PreparedQuery")] 17 | PreparedQuery(ScyllaPyPreparedQuery), 18 | } 19 | 20 | #[derive(Clone, FromPyObject)] 21 | pub enum BatchQueryInput { 22 | #[pyo3(transparent, annotation = "str")] 23 | Text(String), 24 | #[pyo3(transparent, annotation = "Query")] 25 | Query(ScyllaPyQuery), 26 | #[pyo3(transparent, annotation = "PreparedQuery")] 27 | PreparedQuery(ScyllaPyPreparedQuery), 28 | } 29 | 30 | impl From for BatchStatement { 31 | fn from(value: BatchQueryInput) -> Self { 32 | match value { 33 | BatchQueryInput::Text(text) => Self::Query(text.into()), 34 | BatchQueryInput::Query(query) => Self::Query(query.into()), 35 | BatchQueryInput::PreparedQuery(prepared) => Self::PreparedStatement(prepared.into()), 36 | } 37 | } 38 | } 39 | 40 | #[derive(Clone, FromPyObject)] 41 | pub enum PrepareInput { 42 | #[pyo3(transparent, annotation = "str")] 43 | Text(String), 44 | #[pyo3(transparent, annotation = "Query")] 45 | Query(ScyllaPyQuery), 46 | } 47 | 48 | impl From for Query { 49 | fn from(value: PrepareInput) -> Self { 50 | match value { 51 | PrepareInput::Text(text) => Self::new(text), 52 | PrepareInput::Query(query) => Self::from(query), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, FromPyObject)] 58 | pub enum BatchInput { 59 | #[pyo3(transparent, annotation = "Batch")] 60 | Batch(ScyllaPyBatch), 61 | #[pyo3(transparent, annotation = "InlineBatch")] 62 | InlineBatch(ScyllaPyInlineBatch), 63 | } 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod batches; 2 | pub mod consistencies; 3 | pub mod exceptions; 4 | pub mod execution_profiles; 5 | pub mod extra_types; 6 | pub mod inputs; 7 | pub mod load_balancing; 8 | pub mod prepared_queries; 9 | pub mod queries; 10 | pub mod query_builder; 11 | pub mod query_results; 12 | pub mod scylla_cls; 13 | pub mod utils; 14 | 15 | use pyo3::{pymodule, types::PyModule, PyResult, Python}; 16 | 17 | use crate::utils::add_submodule; 18 | 19 | #[pymodule] 20 | #[pyo3(name = "_internal")] 21 | fn _internal(py: Python<'_>, pymod: &PyModule) -> PyResult<()> { 22 | pyo3_log::init(); 23 | pymod.add_class::()?; 24 | pymod.add_class::()?; 25 | pymod.add_class::()?; 26 | pymod.add_class::()?; 27 | pymod.add_class::()?; 28 | pymod.add_class::()?; 29 | pymod.add_class::()?; 30 | pymod.add_class::()?; 31 | pymod.add_class::()?; 32 | pymod.add_class::()?; 33 | pymod.add_class::()?; 34 | add_submodule(py, pymod, "extra_types", extra_types::setup_module)?; 35 | add_submodule(py, pymod, "query_builder", query_builder::setup_module)?; 36 | add_submodule(py, pymod, "exceptions", exceptions::py_err::setup_module)?; 37 | add_submodule(py, pymod, "load_balancing", load_balancing::setup_module)?; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/load_balancing.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use pyo3::{ 4 | pyclass, pymethods, 5 | types::{PyModule, PyType}, 6 | PyAny, PyResult, Python, 7 | }; 8 | use scylla::load_balancing::{DefaultPolicy, LatencyAwarenessBuilder, LoadBalancingPolicy}; 9 | use std::time::Duration; 10 | 11 | use crate::{exceptions::rust_err::ScyllaPyResult, utils::scyllapy_future}; 12 | 13 | #[pyclass(name = "LoadBalancingPolicy")] 14 | #[derive(Clone, Debug)] 15 | pub struct ScyllaPyLoadBalancingPolicy { 16 | inner: Arc, 17 | } 18 | 19 | #[pymethods] 20 | impl ScyllaPyLoadBalancingPolicy { 21 | #[classmethod] 22 | #[pyo3(signature = ( 23 | *, 24 | token_aware = None, 25 | prefer_rack = None, 26 | prefer_datacenter = None, 27 | permit_dc_failover = None, 28 | shuffling_replicas = None, 29 | latency_awareness = None, 30 | ) 31 | )] 32 | fn build( 33 | cls: &PyType, 34 | token_aware: Option, 35 | prefer_rack: Option, 36 | prefer_datacenter: Option, 37 | permit_dc_failover: Option, 38 | shuffling_replicas: Option, 39 | latency_awareness: Option, 40 | ) -> ScyllaPyResult<&PyAny> { 41 | scyllapy_future(cls.py(), async move { 42 | let mut policy_builer = DefaultPolicy::builder(); 43 | if let Some(permit) = permit_dc_failover { 44 | policy_builer = policy_builer.permit_dc_failover(permit); 45 | } 46 | if let Some(token) = token_aware { 47 | policy_builer = policy_builer.token_aware(token); 48 | } 49 | if let Some(dc) = prefer_datacenter { 50 | if let Some(rack) = prefer_rack { 51 | policy_builer = policy_builer.prefer_datacenter_and_rack(dc, rack); 52 | } else { 53 | policy_builer = policy_builer.prefer_datacenter(dc); 54 | } 55 | } 56 | if let Some(shufle) = shuffling_replicas { 57 | policy_builer = policy_builer.enable_shuffling_replicas(shufle); 58 | } 59 | if let Some(latency_awareness) = latency_awareness { 60 | policy_builer = policy_builer.latency_awareness(latency_awareness.into()); 61 | } 62 | Ok(Self { 63 | inner: policy_builer.build(), 64 | }) 65 | }) 66 | } 67 | } 68 | 69 | #[pyclass(name = "LatencyAwareness")] 70 | #[derive(Clone, Debug)] 71 | pub struct ScyllaPyLatencyAwareness { 72 | inner: LatencyAwarenessBuilder, 73 | } 74 | 75 | #[pymethods] 76 | impl ScyllaPyLatencyAwareness { 77 | #[new] 78 | #[pyo3(signature = ( 79 | *, 80 | minimum_measurements = None, 81 | retry_period = None, 82 | exclusion_threshold = None, 83 | update_rate = None, 84 | scale = None, 85 | ))] 86 | fn new( 87 | minimum_measurements: Option, 88 | retry_period: Option, 89 | exclusion_threshold: Option, 90 | update_rate: Option, 91 | scale: Option, 92 | ) -> Self { 93 | let mut builder = LatencyAwarenessBuilder::new(); 94 | if let Some(minimum_measurements) = minimum_measurements { 95 | builder = builder.minimum_measurements(minimum_measurements); 96 | } 97 | if let Some(retry_period) = retry_period { 98 | builder = builder.retry_period(Duration::from_millis(retry_period)); 99 | } 100 | if let Some(exclusion_threshold) = exclusion_threshold { 101 | builder = builder.exclusion_threshold(exclusion_threshold); 102 | } 103 | if let Some(update_rate) = update_rate { 104 | builder = builder.update_rate(Duration::from_millis(update_rate)); 105 | } 106 | if let Some(scale) = scale { 107 | builder = builder.scale(Duration::from_millis(scale)); 108 | } 109 | Self { inner: builder } 110 | } 111 | } 112 | 113 | impl From for LatencyAwarenessBuilder { 114 | fn from(value: ScyllaPyLatencyAwareness) -> Self { 115 | value.inner 116 | } 117 | } 118 | 119 | impl From for Arc { 120 | fn from(value: ScyllaPyLoadBalancingPolicy) -> Self { 121 | value.inner 122 | } 123 | } 124 | 125 | /// Setup load balancing module. 126 | /// 127 | /// This function adds `LoadBalancingPolicy` and `LatencyAwareness` classes to the module. 128 | /// 129 | /// # Errors 130 | /// 131 | /// If cannot add class to the module. 132 | pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { 133 | module.add_class::()?; 134 | module.add_class::()?; 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /src/prepared_queries.rs: -------------------------------------------------------------------------------- 1 | use pyo3::pyclass; 2 | use scylla::prepared_statement::PreparedStatement; 3 | 4 | #[pyclass(name = "PreparedQuery")] 5 | #[derive(Clone, Debug)] 6 | pub struct ScyllaPyPreparedQuery { 7 | pub inner: PreparedStatement, 8 | } 9 | 10 | impl From for ScyllaPyPreparedQuery { 11 | fn from(value: PreparedStatement) -> Self { 12 | Self { inner: value } 13 | } 14 | } 15 | 16 | impl From for PreparedStatement { 17 | fn from(value: ScyllaPyPreparedQuery) -> Self { 18 | value.inner 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/queries.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{ 4 | consistencies::{ScyllaPyConsistency, ScyllaPySerialConsistency}, 5 | exceptions::rust_err::ScyllaPyResult, 6 | execution_profiles::ScyllaPyExecutionProfile, 7 | }; 8 | use pyo3::{pyclass, pymethods, types::PyDict, FromPyObject, Python}; 9 | use scylla::{batch::Batch, execution_profile::ExecutionProfileHandle, statement::query::Query}; 10 | 11 | #[derive(Clone, Debug, Default, FromPyObject)] 12 | pub struct ScyllaPyRequestParams { 13 | pub consistency: Option, 14 | pub serial_consistency: Option, 15 | pub request_timeout: Option, 16 | pub timestamp: Option, 17 | pub is_idempotent: Option, 18 | pub tracing: Option, 19 | pub profile: Option, 20 | } 21 | 22 | impl ScyllaPyRequestParams { 23 | /// Apply parameters to scylla's query. 24 | pub fn apply_to_query(&self, query: &mut Query) { 25 | if let Some(consistency) = self.consistency { 26 | query.set_consistency(consistency.into()); 27 | } 28 | if let Some(is_idempotent) = self.is_idempotent { 29 | query.set_is_idempotent(is_idempotent); 30 | } 31 | if let Some(tracing) = self.tracing { 32 | query.set_tracing(tracing); 33 | } 34 | query.set_execution_profile_handle(self.profile.as_ref().map(ExecutionProfileHandle::from)); 35 | query.set_timestamp(self.timestamp); 36 | query.set_request_timeout(self.request_timeout.map(Duration::from_secs)); 37 | query.set_serial_consistency(self.serial_consistency.map(Into::into)); 38 | } 39 | 40 | pub fn apply_to_batch(&self, batch: &mut Batch) { 41 | if let Some(consistency) = self.consistency { 42 | batch.set_consistency(consistency.into()); 43 | } 44 | if let Some(is_idempotent) = self.is_idempotent { 45 | batch.set_is_idempotent(is_idempotent); 46 | } 47 | if let Some(tracing) = self.tracing { 48 | batch.set_tracing(tracing); 49 | } 50 | batch.set_timestamp(self.timestamp); 51 | batch.set_serial_consistency(self.serial_consistency.map(Into::into)); 52 | } 53 | 54 | /// Parse dict to query parameters. 55 | /// 56 | /// This function takes dict and 57 | /// tries to construct `ScyllaPyRequestParams`. 58 | /// 59 | /// # Errors 60 | /// 61 | /// May result in an error if 62 | /// incorrect type passed. 63 | pub fn from_dict(params: Option<&PyDict>) -> ScyllaPyResult { 64 | let Some(params) = params else { 65 | return Ok(Self::default()); 66 | }; 67 | Ok(Self { 68 | consistency: params 69 | .get_item("consistency")? 70 | .map(pyo3::FromPyObject::extract) 71 | .transpose()?, 72 | serial_consistency: params 73 | .get_item("serial_consistency")? 74 | .map(pyo3::FromPyObject::extract) 75 | .transpose()?, 76 | request_timeout: params 77 | .get_item("request_timeout")? 78 | .map(pyo3::FromPyObject::extract) 79 | .transpose()?, 80 | timestamp: params 81 | .get_item("timestamp")? 82 | .map(pyo3::FromPyObject::extract) 83 | .transpose()?, 84 | is_idempotent: params 85 | .get_item("is_idempotent")? 86 | .map(pyo3::FromPyObject::extract) 87 | .transpose()?, 88 | tracing: params 89 | .get_item("tracing")? 90 | .map(pyo3::FromPyObject::extract) 91 | .transpose()?, 92 | profile: params 93 | .get_item("profile")? 94 | .map(pyo3::FromPyObject::extract) 95 | .transpose()?, 96 | }) 97 | } 98 | } 99 | 100 | #[pyclass(name = "Query")] 101 | #[derive(Clone, Debug)] 102 | pub struct ScyllaPyQuery { 103 | #[pyo3(get)] 104 | pub query: String, 105 | pub params: ScyllaPyRequestParams, 106 | } 107 | 108 | impl From<&ScyllaPyQuery> for ScyllaPyQuery { 109 | fn from(value: &ScyllaPyQuery) -> Self { 110 | ScyllaPyQuery { 111 | query: value.query.clone(), 112 | params: ScyllaPyRequestParams::default(), 113 | } 114 | } 115 | } 116 | 117 | #[pymethods] 118 | impl ScyllaPyQuery { 119 | #[new] 120 | #[pyo3(signature = (query,**kwargs))] 121 | #[allow(clippy::too_many_arguments)] 122 | /// Creates new query. 123 | /// 124 | /// # Errors 125 | /// May raise an error if incorrect type passed in kwargs. 126 | pub fn py_new(_py: Python<'_>, query: String, kwargs: Option<&PyDict>) -> ScyllaPyResult { 127 | Ok(Self { 128 | query, 129 | params: ScyllaPyRequestParams::from_dict(kwargs)?, 130 | }) 131 | } 132 | 133 | #[must_use] 134 | pub fn __str__(&self) -> String { 135 | format!("{self:?}") 136 | } 137 | 138 | #[must_use] 139 | pub fn with_consistency(&self, consistency: Option) -> Self { 140 | let mut query = Self::from(self); 141 | query.params.consistency = consistency; 142 | query 143 | } 144 | 145 | #[must_use] 146 | pub fn with_serial_consistency( 147 | &self, 148 | serial_consistency: Option, 149 | ) -> Self { 150 | let mut query = Self::from(self); 151 | query.params.serial_consistency = serial_consistency; 152 | query 153 | } 154 | 155 | #[must_use] 156 | pub fn with_request_timeout(&self, request_timeout: Option) -> Self { 157 | let mut query = Self::from(self); 158 | query.params.request_timeout = request_timeout; 159 | query 160 | } 161 | 162 | #[must_use] 163 | pub fn with_timestamp(&self, timestamp: Option) -> Self { 164 | let mut query = Self::from(self); 165 | query.params.timestamp = timestamp; 166 | query 167 | } 168 | 169 | #[must_use] 170 | pub fn with_is_idempotent(&self, is_idempotent: Option) -> Self { 171 | let mut query = Self::from(self); 172 | query.params.is_idempotent = is_idempotent; 173 | query 174 | } 175 | 176 | #[must_use] 177 | pub fn with_tracing(&self, tracing: Option) -> Self { 178 | let mut query = Self::from(self); 179 | query.params.tracing = tracing; 180 | query 181 | } 182 | 183 | #[must_use] 184 | pub fn with_profile(&self, profile: Option) -> Self { 185 | let mut query = Self::from(self); 186 | query.params.profile = profile; 187 | query 188 | } 189 | } 190 | 191 | impl From for Query { 192 | fn from(value: ScyllaPyQuery) -> Self { 193 | let mut query = Self::new(value.query); 194 | value.params.apply_to_query(&mut query); 195 | query 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/query_builder/delete.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{pyclass, pymethods, types::PyDict, PyAny, PyRefMut, Python}; 2 | use scylla::{frame::value::LegacySerializedValues, query::Query}; 3 | 4 | use super::utils::{pretty_build, IfCluase, Timeout}; 5 | use crate::{ 6 | batches::ScyllaPyInlineBatch, 7 | exceptions::rust_err::{ScyllaPyError, ScyllaPyResult}, 8 | queries::ScyllaPyRequestParams, 9 | scylla_cls::Scylla, 10 | utils::{py_to_value, ScyllaPyCQLDTO}, 11 | }; 12 | 13 | #[pyclass] 14 | #[derive(Clone, Debug, Default)] 15 | pub struct Delete { 16 | table_: String, 17 | columns: Option>, 18 | timeout_: Option, 19 | timestamp_: Option, 20 | if_clause_: Option, 21 | where_clauses_: Vec, 22 | values_: Vec, 23 | request_params_: ScyllaPyRequestParams, 24 | } 25 | 26 | impl Delete { 27 | fn build_query(&self) -> ScyllaPyResult { 28 | if self.where_clauses_.is_empty() { 29 | return Err(ScyllaPyError::QueryBuilderError( 30 | "At least one where clause should be specified.", 31 | )); 32 | } 33 | let columns = self 34 | .columns 35 | .as_ref() 36 | .map_or(String::new(), |cols| cols.join(", ")); 37 | let params = [ 38 | self.timestamp_ 39 | .map(|timestamp| format!("TIMESTAMP {timestamp}")), 40 | self.timeout_.as_ref().map(|timeout| match timeout { 41 | Timeout::Int(int) => format!("TIMEOUT {int}"), 42 | Timeout::Str(string) => format!("TIMEOUT {string}"), 43 | }), 44 | ]; 45 | let prepared_params = params 46 | .iter() 47 | .map(|item| item.as_ref().map_or("", String::as_str)) 48 | .filter(|item| !item.is_empty()) 49 | .collect::>(); 50 | let usings = if prepared_params.is_empty() { 51 | String::new() 52 | } else { 53 | format!("USING {}", prepared_params.join(" AND ")) 54 | }; 55 | let where_clause = format!("WHERE {}", self.where_clauses_.join(" AND ")); 56 | let if_conditions = self 57 | .if_clause_ 58 | .as_ref() 59 | .map_or(String::default(), |cond| match cond { 60 | IfCluase::Exists => String::from("IF EXISTS"), 61 | IfCluase::Condition { clauses, values: _ } => { 62 | format!("IF {}", clauses.join(" AND ")) 63 | } 64 | }); 65 | Ok(pretty_build([ 66 | "DELETE", 67 | columns.as_str(), 68 | "FROM", 69 | self.table_.as_str(), 70 | usings.as_str(), 71 | where_clause.as_str(), 72 | if_conditions.as_str(), 73 | ])) 74 | } 75 | } 76 | 77 | #[pymethods] 78 | impl Delete { 79 | #[new] 80 | #[must_use] 81 | pub fn py_new(table: String) -> Self { 82 | Self { 83 | table_: table, 84 | ..Default::default() 85 | } 86 | } 87 | 88 | #[must_use] 89 | #[pyo3(signature = (*cols))] 90 | pub fn cols(mut slf: PyRefMut<'_, Self>, cols: Vec) -> PyRefMut<'_, Self> { 91 | slf.columns = Some(cols); 92 | slf 93 | } 94 | 95 | /// Add where clause. 96 | /// 97 | /// This function adds where with values. 98 | /// 99 | /// # Errors 100 | /// 101 | /// Can return an error, if values 102 | /// cannot be parsed. 103 | #[pyo3(signature = (clause, values = None))] 104 | pub fn r#where<'a>( 105 | mut slf: PyRefMut<'a, Self>, 106 | clause: String, 107 | values: Option>, 108 | ) -> ScyllaPyResult> { 109 | slf.where_clauses_.push(clause); 110 | if let Some(vals) = values { 111 | for value in vals { 112 | slf.values_.push(py_to_value(value, None)?); 113 | } 114 | } 115 | Ok(slf) 116 | } 117 | 118 | #[must_use] 119 | pub fn timeout(mut slf: PyRefMut<'_, Self>, timeout: Timeout) -> PyRefMut<'_, Self> { 120 | slf.timeout_ = Some(timeout); 121 | slf 122 | } 123 | 124 | #[must_use] 125 | pub fn timestamp(mut slf: PyRefMut<'_, Self>, timestamp: u64) -> PyRefMut<'_, Self> { 126 | slf.timestamp_ = Some(timestamp); 127 | slf 128 | } 129 | 130 | #[must_use] 131 | pub fn if_exists(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { 132 | slf.if_clause_ = Some(IfCluase::Exists); 133 | slf 134 | } 135 | 136 | /// Add if clause. 137 | /// 138 | /// # Errors 139 | /// 140 | /// May return an error, if values 141 | /// cannot be converted to rust types. 142 | #[pyo3(signature = (clause, values = None))] 143 | pub fn if_<'a>( 144 | mut slf: PyRefMut<'a, Self>, 145 | clause: String, 146 | values: Option>, 147 | ) -> ScyllaPyResult> { 148 | let parsed_values = if let Some(vals) = values { 149 | vals.iter() 150 | .map(|item| py_to_value(item, None)) 151 | .collect::, _>>()? 152 | } else { 153 | vec![] 154 | }; 155 | match slf.if_clause_.as_mut() { 156 | Some(IfCluase::Condition { clauses, values }) => { 157 | clauses.push(clause); 158 | values.extend(parsed_values); 159 | } 160 | None | Some(IfCluase::Exists) => { 161 | slf.if_clause_ = Some(IfCluase::Condition { 162 | clauses: vec![clause], 163 | values: parsed_values, 164 | }); 165 | } 166 | } 167 | Ok(slf) 168 | } 169 | 170 | /// Add parameters to the request. 171 | /// 172 | /// These parameters are used by scylla. 173 | /// 174 | /// # Errors 175 | /// 176 | /// May return an error, if request parameters 177 | /// cannot be built. 178 | #[pyo3(signature = (**params))] 179 | pub fn request_params<'a>( 180 | mut slf: PyRefMut<'a, Self>, 181 | params: Option<&'a PyDict>, 182 | ) -> ScyllaPyResult> { 183 | slf.request_params_ = ScyllaPyRequestParams::from_dict(params)?; 184 | Ok(slf) 185 | } 186 | 187 | /// Execute a query. 188 | /// 189 | /// # Errors 190 | /// 191 | /// May return an error, if something goes wrong 192 | /// during query building 193 | /// or during query execution. 194 | pub fn execute<'a>(&'a self, py: Python<'a>, scylla: &'a Scylla) -> ScyllaPyResult<&'a PyAny> { 195 | let mut query = Query::new(self.build_query()?); 196 | self.request_params_.apply_to_query(&mut query); 197 | 198 | let values = if let Some(if_clause) = &self.if_clause_ { 199 | if_clause.extend_values(self.values_.clone()) 200 | } else { 201 | self.values_.clone() 202 | }; 203 | scylla.native_execute(py, Some(query), None, values, false) 204 | } 205 | 206 | /// Add to batch 207 | /// 208 | /// Adds current query to batch. 209 | /// 210 | /// # Errors 211 | /// 212 | /// May result into error if query cannot be build. 213 | /// Or values cannot be passed to batch. 214 | pub fn add_to_batch(&self, batch: &mut ScyllaPyInlineBatch) -> ScyllaPyResult<()> { 215 | let mut query = Query::new(self.build_query()?); 216 | self.request_params_.apply_to_query(&mut query); 217 | 218 | let values = if let Some(if_clause) = &self.if_clause_ { 219 | if_clause.extend_values(self.values_.clone()) 220 | } else { 221 | self.values_.clone() 222 | }; 223 | let mut serialized = LegacySerializedValues::new(); 224 | for val in values { 225 | serialized.add_value(&val)?; 226 | } 227 | batch.add_query_inner(query, serialized); 228 | Ok(()) 229 | } 230 | 231 | #[must_use] 232 | pub fn __repr__(&self) -> String { 233 | format!("{self:?}") 234 | } 235 | 236 | /// Convert query to string. 237 | /// 238 | /// # Errors 239 | /// 240 | /// May return an error if something 241 | /// goes wrong during query building. 242 | pub fn __str__(&self) -> ScyllaPyResult { 243 | self.build_query() 244 | } 245 | 246 | #[must_use] 247 | pub fn __copy__(&self) -> Self { 248 | self.clone() 249 | } 250 | 251 | #[must_use] 252 | pub fn __deepcopy__(&self, _memo: &PyDict) -> Self { 253 | self.clone() 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/query_builder/insert.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{pyclass, pymethods, types::PyDict, PyAny, PyRefMut, Python}; 2 | use scylla::{frame::value::LegacySerializedValues, query::Query}; 3 | 4 | use crate::{ 5 | batches::ScyllaPyInlineBatch, 6 | exceptions::rust_err::{ScyllaPyError, ScyllaPyResult}, 7 | queries::ScyllaPyRequestParams, 8 | scylla_cls::Scylla, 9 | utils::{py_to_value, ScyllaPyCQLDTO}, 10 | }; 11 | 12 | use super::utils::{pretty_build, Timeout}; 13 | 14 | #[pyclass] 15 | #[derive(Clone, Debug, Default)] 16 | pub struct Insert { 17 | table_: String, 18 | if_not_exists_: bool, 19 | names_: Vec, 20 | values_: Vec, 21 | 22 | timeout_: Option, 23 | ttl_: Option, 24 | timestamp_: Option, 25 | 26 | request_params_: ScyllaPyRequestParams, 27 | } 28 | 29 | impl Insert { 30 | /// Build a statement. 31 | /// 32 | /// # Errors 33 | /// If no values was set. 34 | pub fn build_query(&self) -> ScyllaPyResult { 35 | if self.names_.is_empty() { 36 | return Err(ScyllaPyError::QueryBuilderError( 37 | "`set` method should be called at least one time", 38 | )); 39 | } 40 | let names = self.names_.join(","); 41 | let values = self 42 | .names_ 43 | .iter() 44 | .map(|_| "?") 45 | .collect::>() 46 | .join(","); 47 | let names_values = format!("({names}) VALUES ({values})"); 48 | let ifnexist = if self.if_not_exists_ { 49 | "IF NOT EXISTS" 50 | } else { 51 | "" 52 | }; 53 | let params = [ 54 | self.timestamp_ 55 | .map(|timestamp| format!("TIMESTAMP {timestamp}")), 56 | self.ttl_.map(|ttl| format!("TTL {ttl}")), 57 | self.timeout_.as_ref().map(|timeout| match timeout { 58 | Timeout::Int(int) => format!("TIMEOUT {int}"), 59 | Timeout::Str(string) => format!("TIMEOUT {string}"), 60 | }), 61 | ]; 62 | let prepared_params = params 63 | .iter() 64 | .map(|item| item.as_ref().map_or("", String::as_str)) 65 | .filter(|item| !item.is_empty()) 66 | .collect::>(); 67 | let usings = if prepared_params.is_empty() { 68 | String::new() 69 | } else { 70 | format!("USING {}", prepared_params.join(" AND ")) 71 | }; 72 | 73 | Ok(pretty_build([ 74 | "INSERT INTO", 75 | self.table_.as_str(), 76 | names_values.as_str(), 77 | ifnexist, 78 | usings.as_str(), 79 | ])) 80 | } 81 | } 82 | 83 | #[pymethods] 84 | impl Insert { 85 | #[new] 86 | #[must_use] 87 | pub fn py_new(table: String) -> Self { 88 | Self { 89 | table_: table, 90 | ..Default::default() 91 | } 92 | } 93 | 94 | #[must_use] 95 | pub fn if_not_exists(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { 96 | slf.if_not_exists_ = true; 97 | slf 98 | } 99 | 100 | /// Set value to column. 101 | /// 102 | /// # Errors 103 | /// 104 | /// If value cannot be translated 105 | /// into `Rust` type. 106 | pub fn set<'a>( 107 | mut slf: PyRefMut<'a, Self>, 108 | name: String, 109 | value: &'a PyAny, 110 | ) -> ScyllaPyResult> { 111 | slf.names_.push(name); 112 | // Small optimization to speedup inserts. 113 | if value.is_none() { 114 | slf.values_.push(ScyllaPyCQLDTO::Unset); 115 | } else { 116 | slf.values_.push(py_to_value(value, None)?); 117 | } 118 | Ok(slf) 119 | } 120 | 121 | #[must_use] 122 | pub fn timeout(mut slf: PyRefMut<'_, Self>, timeout: Timeout) -> PyRefMut<'_, Self> { 123 | slf.timeout_ = Some(timeout); 124 | slf 125 | } 126 | 127 | #[must_use] 128 | pub fn timestamp(mut slf: PyRefMut<'_, Self>, timestamp: u64) -> PyRefMut<'_, Self> { 129 | slf.timestamp_ = Some(timestamp); 130 | slf 131 | } 132 | 133 | #[must_use] 134 | pub fn ttl(mut slf: PyRefMut<'_, Self>, ttl: i32) -> PyRefMut<'_, Self> { 135 | slf.ttl_ = Some(ttl); 136 | slf 137 | } 138 | 139 | /// Add parameters to the request. 140 | /// 141 | /// These parameters are used by scylla. 142 | /// 143 | /// # Errors 144 | /// 145 | /// May return an error, if request parameters 146 | /// cannot be built. 147 | #[pyo3(signature = (**params))] 148 | pub fn request_params<'a>( 149 | mut slf: PyRefMut<'a, Self>, 150 | params: Option<&'a PyDict>, 151 | ) -> ScyllaPyResult> { 152 | slf.request_params_ = ScyllaPyRequestParams::from_dict(params)?; 153 | Ok(slf) 154 | } 155 | 156 | /// Execute a query. 157 | /// 158 | /// This function is used to execute built query. 159 | /// 160 | /// # Errors 161 | /// 162 | /// If query cannot be built. 163 | /// Also proxies errors from `native_execute`. 164 | pub fn execute<'a>(&'a self, py: Python<'a>, scylla: &'a Scylla) -> ScyllaPyResult<&'a PyAny> { 165 | let mut query = Query::new(self.build_query()?); 166 | self.request_params_.apply_to_query(&mut query); 167 | scylla.native_execute(py, Some(query), None, self.values_.clone(), false) 168 | } 169 | 170 | /// Add to batch 171 | /// 172 | /// Adds current query to batch. 173 | /// 174 | /// # Errors 175 | /// 176 | /// May result into error if query cannot be build. 177 | /// Or values cannot be passed to batch. 178 | pub fn add_to_batch(&self, batch: &mut ScyllaPyInlineBatch) -> ScyllaPyResult<()> { 179 | let mut query = Query::new(self.build_query()?); 180 | self.request_params_.apply_to_query(&mut query); 181 | 182 | let mut serialized = LegacySerializedValues::new(); 183 | for val in self.values_.clone() { 184 | serialized.add_value(&val)?; 185 | } 186 | batch.add_query_inner(query, serialized); 187 | Ok(()) 188 | } 189 | 190 | #[must_use] 191 | pub fn __repr__(&self) -> String { 192 | format!("{self:?}") 193 | } 194 | 195 | /// Returns string part of a query. 196 | /// 197 | /// # Errors 198 | /// If cannot construct query. 199 | pub fn __str__(&self) -> ScyllaPyResult { 200 | self.build_query() 201 | } 202 | 203 | #[must_use] 204 | pub fn __copy__(&self) -> Self { 205 | self.clone() 206 | } 207 | 208 | #[must_use] 209 | pub fn __deepcopy__(&self, _memo: &PyDict) -> Self { 210 | self.clone() 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/query_builder/mod.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{types::PyModule, PyResult, Python}; 2 | 3 | use self::{delete::Delete, insert::Insert, select::Select, update::Update}; 4 | 5 | pub mod delete; 6 | pub mod insert; 7 | pub mod select; 8 | pub mod update; 9 | mod utils; 10 | 11 | /// Create `QueryBuilder` module. 12 | /// 13 | /// This function creates a module with a 14 | /// given name and adds classes to it. 15 | /// 16 | /// # Errors 17 | /// 18 | /// * Cannot create module by any reason. 19 | /// * Cannot add class by some reason. 20 | pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { 21 | module.add_class::