diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index f36ff004b2..7fed7200e4 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -24,7 +24,7 @@ jobs: image: scylladb/scylla ports: - 9042:9042 - options: --health-cmd "cqlsh --debug" --health-interval 5s --health-retries 10 + options: --health-cmd "cqlsh --debug scylladb" --health-interval 5s --health-retries 10 steps: - uses: actions/checkout@v3 - name: Install mdbook @@ -33,7 +33,5 @@ jobs: run: cargo build --verbose --examples - name: Build the book run: mdbook build docs - - name: Build the book using the script - run: python3 docs/build_book.py - name: Run book tests run: mdbook test -L target/debug/deps docs diff --git a/.github/workflows/cassandra.yml b/.github/workflows/cassandra.yml index e22b915d46..712099ee69 100644 --- a/.github/workflows/cassandra.yml +++ b/.github/workflows/cassandra.yml @@ -26,10 +26,10 @@ jobs: docker compose -f test/cluster/cassandra/docker-compose.yml up -d --wait # A separate step for building to separate measuring time of compilation and testing - name: Build the project - run: cargo build --verbose --tests + run: cargo build --verbose --tests --features "full-serialization" - name: Run tests on cassandra run: | - CDC='disabled' SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose -- --skip test_views_in_schema_info --skip test_large_batch_statements + CDC='disabled' SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose --features "full-serialization" -- --skip test_views_in_schema_info --skip test_large_batch_statements - name: Stop the cluster if: ${{ always() }} run: docker compose -f test/cluster/cassandra/docker-compose.yml stop diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 843be6f100..845ac87a2e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,17 +29,23 @@ jobs: - name: Format check run: cargo fmt --verbose --all -- --check - name: Clippy check - run: cargo clippy --verbose --examples --tests -- -Aclippy::uninlined_format_args + run: cargo clippy --verbose --all-targets -- -Aclippy::uninlined_format_args - name: Cargo check without features - run: cargo check --manifest-path "scylla/Cargo.toml" --features "" - - name: Cargo check with secrecy feature - run: cargo check --manifest-path "scylla/Cargo.toml" --features "secret" + run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "" + - name: Cargo check with all serialization features + run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "full-serialization" + - name: Cargo check with secret feature + run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "secret" + - name: Cargo check with chrono feature + run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "chrono" + - name: Cargo check with time feature + run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "time" - name: Build scylla-cql - run: cargo build --verbose --all-targets --manifest-path "scylla-cql/Cargo.toml" + run: cargo build --verbose --all-targets --manifest-path "scylla-cql/Cargo.toml" --features "full-serialization" - name: Build - run: cargo build --verbose --examples + run: cargo build --verbose --all-targets --features "full-serialization" - name: Run tests - run: SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose + run: SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose --features "full-serialization" - name: Stop the cluster if: ${{ always() }} run: docker compose -f test/cluster/docker-compose.yml stop @@ -60,12 +66,14 @@ jobs: rustup override set ${{ env.rust_min }} - name: Print Rust version run: rustc --version + - name: Use MSRV Cargo.lock + run: mv Cargo.lock.msrv Cargo.lock - name: MSRV cargo check with features - run: cargo check --verbose --examples --tests + run: cargo check --verbose --all-targets --all-features --locked - name: MSRV cargo check without features - run: cargo check --verbose --manifest-path "scylla/Cargo.toml" + run: cargo check --verbose --all-targets --locked --manifest-path "scylla/Cargo.toml" - name: MSRV cargo check scylla-cql - run: cargo check --verbose --all-targets --manifest-path "scylla-cql/Cargo.toml" + run: cargo check --verbose --all-targets --locked --manifest-path "scylla-cql/Cargo.toml" # Tests that docstrings generate docs without warnings cargo_docs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd2debba32..174c11cb8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,17 +72,20 @@ But they are removed when building the book cargo install mdbook ``` -Build the book (simple method, contains `Sphinx` artifacts): +Book build process uses preprocessor to remove Sphinx artifacts. +Due to limitation of mdbook, it can only be built either from main directory, +using `mdbook X docs` or from `docs` directory, using `mdbook X`, where +`X` is mdbook command such as `build` / `serve` / `test` etc. + +If the book is built from another directory (e.g. scylla, using `mdbook build ../docs`), +preprocessor won't be found, so the result will contain Sphinx artifacts. + +Build the book. ```bash mdbook build docs # HTML will be in docs/book ``` -To build the release version use a script which automatically removes `Sphinx` chunks: -```bash -python3 docs/build_book.py -# HTML will be in docs/book/scriptbuild/book -``` Or serve it on a local http server (automatically refreshes on changes) ```bash diff --git a/Cargo.lock.msrv b/Cargo.lock.msrv new file mode 100644 index 0000000000..b013de3de5 --- /dev/null +++ b/Cargo.lock.msrv @@ -0,0 +1,2232 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bigdecimal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e50562e37200edf7c6c43e54a08e64a5553bfb59d9c297d5572512aa517256" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.38", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.2", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "examples" +version = "0.0.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "futures", + "openssl", + "rustyline", + "rustyline-derive", + "scylla", + "stats_alloc", + "time", + "tokio", + "tower", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lz4_flex" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea9b256699eda7b0387ffbc776dd625e28bde3918446381781245b7a50349d8" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "ntest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da8ec6d2b73d45307e926f5af46809768581044384637af6b3f3fe7c3c88f512" +dependencies = [ + "ntest_test_cases", + "ntest_timeout", +] + +[[package]] +name = "ntest_test_cases" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7d33be719c6f4d09e64e27c1ef4e73485dc4cc1f4d22201f89860a7fe22e22" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ntest_timeout" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066b468120587a402f0b47d8f80035c921f6a46f8209efd0632a89a16f5188a4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rustyline" +version = "9.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb35a55ab810b5c0fe31606fe9b47d1354e4dc519bec0a102655f78ea2b38057" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scylla" +version = "0.11.1" +dependencies = [ + "arc-swap", + "assert_matches", + "async-trait", + "base64", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "criterion", + "dashmap", + "futures", + "histogram", + "itertools 0.11.0", + "lz4_flex", + "ntest", + "num-bigint", + "num_enum 0.6.1", + "openssl", + "rand", + "rand_chacha", + "rand_pcg", + "scylla-cql", + "scylla-macros", + "scylla-proxy", + "serde", + "serde_yaml", + "smallvec", + "snap", + "socket2", + "strum", + "strum_macros", + "thiserror", + "time", + "tokio", + "tokio-openssl", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "scylla-cql" +version = "0.0.11" +dependencies = [ + "async-trait", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "criterion", + "lz4_flex", + "num-bigint", + "num_enum 0.6.1", + "scylla-macros", + "secrecy", + "serde", + "snap", + "thiserror", + "time", + "tokio", + "uuid", +] + +[[package]] +name = "scylla-macros" +version = "0.3.0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "scylla-proxy" +version = "0.0.3" +dependencies = [ + "assert_matches", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "futures", + "ntest", + "num-bigint", + "num_enum 0.5.11", + "rand", + "scylla-cql", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "secrecy" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0673d6a6449f5e7d12a1caf424fd9363e2af3a4953023ed455e3c4beef4597c0" +dependencies = [ + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "snap" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stats_alloc" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e04424e733e69714ca1bbb9204c1a57f09f5493439520f9f68c132ad25eec" + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.0.2", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Makefile b/Makefile index 7cdaeb2d13..e09ee6bc60 100644 --- a/Makefile +++ b/Makefile @@ -19,15 +19,15 @@ fmt-check: .PHONY: check check: - cargo check --examples --tests + cargo check --all-targets .PHONY: check-without-features check-without-features: - cargo check --manifest-path "scylla/Cargo.toml" --features "" + cargo check --manifest-path "scylla/Cargo.toml" --features "" --all-targets .PHONY: clippy clippy: - RUSTFLAGS=-Dwarnings cargo clippy --examples --tests -- -Aclippy::uninlined_format_args + RUSTFLAGS=-Dwarnings cargo clippy --all-targets -- -Aclippy::uninlined_format_args .PHONY: test test: up @@ -42,11 +42,11 @@ dockerized-test: up .PHONY: build build: - cargo build --examples + cargo build --examples --benches .PHONY: docs docs: - ./docs/build_book.py + mdbook build docs .PHONY: up up: diff --git a/docs/book.toml b/docs/book.toml index 91309c263f..f1726787a9 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -11,4 +11,13 @@ edition = "2021" [output.html] default-theme = "ayu" -git-repository-url = "https://github.com/scylladb/scylla-rust-driver" \ No newline at end of file +git-repository-url = "https://github.com/scylladb/scylla-rust-driver" + +[preprocessor.sphinx] +command = '''bash -c ' +if test -f "./docs/sphinx_preprocessor.py"; then + python ./docs/sphinx_preprocessor.py "$@" +else + python ./sphinx_preprocessor.py "$@" +fi' run_preprocessor +''' diff --git a/docs/build_book.py b/docs/build_book.py deleted file mode 100755 index 726f33a8d3..0000000000 --- a/docs/build_book.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -# A script which removes sphinx markdown and builds the documentation book -# It copies all files to working directory, modifies the .md files -# to remove all ```eval_rst parts and builds the book -# The generated HTML will be in docs/book/scriptbuild/book - -import os -import shutil -import pathlib - -def remove_sphinx_markdown(md_file_text): - text_split = md_file_text.split("```eval_rst") - - result = text_split[0] - - for i in range(1, len(text_split)): - cur_chunk = text_split[i] - result += cur_chunk[cur_chunk.find("```") + 3:] - - return result - -# Working directory, normally book builds in docs/book -# so we can make our a folder in this build directory -work_dir = os.path.join("docs", "book", "scriptbuild") -os.makedirs(work_dir, exist_ok = True) - -# All modified sources will be put in work_dir/source -src_dir = os.path.join(work_dir, "source") -os.makedirs(src_dir, exist_ok = True) - -# Generated HTML will be put in work_dir/book -build_dir = os.path.join(work_dir, "book") - - - -# Copy all mdbook files to work_dir before modifying - -# Copy book.toml -shutil.copyfile(os.path.join("docs", "book.toml"), os.path.join(work_dir, "book.toml")) - -# Go over all .md files, remove the ``` sphinx parts and put them in work_dir/source -for mdfile_path in pathlib.Path(os.path.join("docs", "source")).rglob("*.md"): - - # Path in the book structure ex. queries/queries.md instead of docs/source/queries/queries.md - relative_path = mdfile_path.relative_to(os.path.join("docs", "source")) - - # Read the current file - mdfile = open(mdfile_path, "r").read() - - # Remove sphinx markdown - new_mdfile = remove_sphinx_markdown(mdfile) - - # Write the modified file to src_dir - new_mdfile_path = os.path.join(src_dir, relative_path) - os.makedirs(os.path.dirname(new_mdfile_path), exist_ok = True) - open(new_mdfile_path, "w").write(new_mdfile) - -build_result = os.system(f"mdbook build {work_dir}") - -if build_result == 0: - print(f"OK Done - rendered HTML is in {build_dir}") - exit(0) -else: - print(f"ERROR: mdbook build returned: {build_result}") - exit(1) diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 6d2b3bb240..2a2ff113a1 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "sphinx-docs" description = "ScyllaDB Documentation" -version = "0.9.0" +version = "0.11.1" authors = ["ScyllaDB Documentation Contributors"] [tool.poetry.dependencies] diff --git a/docs/source/SUMMARY.md b/docs/source/SUMMARY.md index 471e8efdad..051aca6062 100644 --- a/docs/source/SUMMARY.md +++ b/docs/source/SUMMARY.md @@ -7,6 +7,9 @@ - [Running Scylla using Docker](quickstart/scylla-docker.md) - [Connecting and running a simple query](quickstart/example.md) +- [Migration guides](migration-guides/migration-guides.md) + - [Adjusting code to changes in serialization API introduced in 0.11](migration-guides/0.11-serialization.md) + - [Connecting to the cluster](connecting/connecting.md) - [Compression](connecting/compression.md) - [Authentication](connecting/authentication.md) diff --git a/docs/source/conf.py b/docs/source/conf.py index c65e86b5ee..9e2215e647 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,14 +14,14 @@ # -- Global variables # Build documentation for the following tags and branches -TAGS = ['v0.9.1', 'v0.10.1'] +TAGS = ['v0.10.1', 'v0.11.1'] BRANCHES = ['main'] # Set the latest version. -LATEST_VERSION = 'v0.10.1' +LATEST_VERSION = 'v0.11.1' # Set which versions are not released yet. UNSTABLE_VERSIONS = ['main'] # Set which versions are deprecated -DEPRECATED_VERSIONS = ['v0.9.1'] +DEPRECATED_VERSIONS = ['v0.10.1'] # -- General configuration diff --git a/docs/source/contents.rst b/docs/source/contents.rst index 0e0446baf7..5bc4a37c9e 100644 --- a/docs/source/contents.rst +++ b/docs/source/contents.rst @@ -13,6 +13,7 @@ retry-policy/retry-policy speculative-execution/speculative metrics/metrics + migration-guides/migration-guides logging/logging tracing/tracing schema/schema diff --git a/docs/source/data-types/data-types.md b/docs/source/data-types/data-types.md index ec5cac41f3..6fe7c70e07 100644 --- a/docs/source/data-types/data-types.md +++ b/docs/source/data-types/data-types.md @@ -21,9 +21,9 @@ Database types and their Rust equivalents: * `Blob` <----> `Vec` * `Inet` <----> `std::net::IpAddr` * `Uuid`, `Timeuuid` <----> `uuid::Uuid` -* `Date` <----> `chrono::NaiveDate`, `u32` -* `Time` <----> `chrono::Duration` -* `Timestamp` <----> `chrono::Duration` +* `Date` <----> `value::CqlDate`, `chrono::NaiveDate`, `time::Date` +* `Time` <----> `value::CqlTime`, `chrono::NaiveTime`, `time::Time` +* `Timestamp` <----> `value::CqlTimestamp`, `chrono::DateTime`, `time::OffsetDateTime` * `Duration` <----> `value::CqlDuration` * `Decimal` <----> `bigdecimal::Decimal` * `Varint` <----> `num_bigint::BigInt` @@ -55,4 +55,4 @@ Database types and their Rust equivalents: tuple udt -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/date.md b/docs/source/data-types/date.md index 6d3384c6af..6e59457f2e 100644 --- a/docs/source/data-types/date.md +++ b/docs/source/data-types/date.md @@ -1,29 +1,77 @@ # Date -For most use cases `Date` can be represented as -[`chrono::NaiveDate`](https://docs.rs/chrono/0.4.19/chrono/naive/struct.NaiveDate.html).\ -`NaiveDate` supports dates from -262145-1-1 to 262143-12-31. +Depending on feature flags, three different types can be used to interact with date. -For dates outside of this range you can use the raw `u32` representation. +Internally [date](https://docs.scylladb.com/stable/cql/types.html#dates) is represented as number of days since +-5877641-06-23 i.e. 2^31 days before unix epoch. + +## CqlDate + +Without any extra features enabled, only `frame::value::CqlDate` is available. It's an +[`u32`](https://doc.rust-lang.org/std/primitive.u32.html) wrapper and it matches the internal date representation. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. -### Using `chrono::NaiveDate`: ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlDate; +use scylla::IntoTypedRows; + +// 1970-01-08 +let to_insert = CqlDate((1 << 31) + 7); + +// Insert date into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read raw Date from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlDate,)>() { + let (date_value,): (CqlDate,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::NaiveDate + +If full range is not required and `chrono` feature is enabled, +[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) can be used. +[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) supports dates from +-262145-01-01 to 262143-12-31. + +```rust # extern crate chrono; +# extern crate scylla; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::NaiveDate; use scylla::IntoTypedRows; -use chrono::naive::NaiveDate; -// Insert some date into the table -let to_insert: NaiveDate = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap(); +// 2021-03-24 +let to_insert = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap(); + +// Insert date into the table session .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; // Read NaiveDate from the table -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ for row in rows.into_typed::<(NaiveDate,)>() { let (date_value,): (NaiveDate,) = row?; } @@ -32,32 +80,40 @@ if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.ro # } ``` -### Using raw `u32` representation -Internally `Date` is represented as number of days since -5877641-06-23 i.e. 2^31 days before unix epoch. +## time::Date + +Alternatively, `time` feature can be used to enable support of +[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html). +[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html)'s value range depends on feature flags, see its +documentation to get more info. ```rust # extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { -use scylla::frame::value::Date; -use scylla::frame::response::result::CqlValue; +use scylla::IntoTypedRows; +use time::{Date, Month}; -// Insert date using raw u32 representation -let to_insert: Date = Date(2_u32.pow(31)); // 1970-01-01 +// 2021-03-24 +let to_insert = Date::from_calendar_date(2021, Month::March, 24).unwrap(); + +// Insert date into the table session .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read raw Date from the table -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows { - let date_value: u32 = match row.columns[0] { - Some(CqlValue::Date(date_value)) => date_value, - _ => panic!("Should be a date!") - }; +// Read Date from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(Date,)>() { + let (date_value,): (Date,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/time.md b/docs/source/data-types/time.md index 6f46f9dae1..3faddaf7a3 100644 --- a/docs/source/data-types/time.md +++ b/docs/source/data-types/time.md @@ -1,33 +1,117 @@ # Time -`Time` is represented as [`chrono::Duration`](https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html) -Internally `Time` is represented as number of nanoseconds since midnight. -It can't be negative or exceed `86399999999999` (24 hours). +Depending on feature flags used, three different types can be used to interact with time. -When sending in a query it needs to be wrapped in `value::Time` to differentiate from [`Timestamp`](timestamp.md) +Internally [time](https://docs.scylladb.com/stable/cql/types.html#times) is represented as number of nanoseconds since +midnight. It can't be negative or exceed `86399999999999` (23:59:59.999999999). + +## CqlTime + +Without any extra features enabled, only `frame::value::CqlTime` is available. It's an +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlTime; +use scylla::IntoTypedRows; + +// 64 seconds since midnight +let to_insert = CqlTime(64 * 1_000_000_000); + +// Insert time into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlTime,)>() { + let (time_value,): (CqlTime,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::NaiveTime + +If `chrono` feature is enabled, [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) +can be used to interact with the database. Although chrono can represent leap seconds, they are not supported. +Attempts to convert [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) with leap +second to `CqlTime` or write it to the database will return an error. + +```rust # extern crate chrono; +# extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::NaiveTime; +use scylla::IntoTypedRows; + +// 01:02:03.456,789,012 +let to_insert = NaiveTime::from_hms_nano_opt(1, 2, 3, 456_789_012); + +// Insert time into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(NaiveTime,)>() { + let (time_value,): (NaiveTime,) = row?; + } +} +# Ok(()) +# } +``` + +## time::Time + +If `time` feature is enabled, [`time::Time`](https://docs.rs/time/0.3/time/struct.Time.html) can be used to interact +with the database. + +```rust +# extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { use scylla::IntoTypedRows; -use scylla::frame::value::Time; -use chrono::Duration; +use time::Time; + +// 01:02:03.456,789,012 +let to_insert = Time::from_hms_nano(1, 2, 3, 456_789_012).unwrap(); -// Insert some time into the table -let to_insert: Duration = Duration::seconds(64); +// Insert time into the table session - .query("INSERT INTO keyspace.table (a) VALUES(?)", (Time(to_insert),)) + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read time from the table, no need for a wrapper here -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (time_value,): (Duration,) = row?; +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(Time,)>() { + let (time_value,): (Time,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/timestamp.md b/docs/source/data-types/timestamp.md index d61aec2aec..1be843754d 100644 --- a/docs/source/data-types/timestamp.md +++ b/docs/source/data-types/timestamp.md @@ -1,33 +1,129 @@ # Timestamp -`Timestamp` is represented as [`chrono::Duration`](https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html) -Internally `Timestamp` is represented as `i64` describing number of milliseconds since unix epoch. -Driver converts this to `chrono::Duration` +Depending on feature flags, three different types can be used to interact with timestamps. -When sending in a query it needs to be wrapped in `value::Timestamp` to differentiate from [`Time`](time.md) +Internally [timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) is represented as +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) describing number of milliseconds since unix epoch. + +## CqlTimestamp + +Without any extra features enabled, only `frame::value::CqlTimestamp` is available. It's an +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation. It's +the only type that supports full range of values that database accepts. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlTimestamp; +use scylla::IntoTypedRows; + +// 64 seconds since unix epoch, 1970-01-01 00:01:04 +let to_insert = CqlTimestamp(64 * 1000); + +// Write timestamp to the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlTimestamp,)>() { + let (timestamp_value,): (CqlTimestamp,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::DateTime + +If full value range is not required, `chrono` feature can be used to enable support of +[`chrono::DateTime`](https://docs.rs/chrono/0.4/chrono/struct.DateTime.html). All values are expected to be converted +to UTC timezone explicitly, as [timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) doesn't store +timezone information. Any precision finer than 1ms will be lost. + +```rust # extern crate chrono; +# extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; +use scylla::IntoTypedRows; + +// 64.123 seconds since unix epoch, 1970-01-01 00:01:04.123 +let to_insert = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + NaiveTime::from_hms_milli_opt(0, 1, 4, 123).unwrap(), +) +.and_utc(); + +// Write timestamp to the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(DateTime,)>() { + let (timestamp_value,): (DateTime,) = row?; + } +} +# Ok(()) +# } +``` + +## time::OffsetDateTime + +Alternatively, `time` feature can be used to enable support of +[`time::OffsetDateTime`](https://docs.rs/time/0.3/time/struct.OffsetDateTime.html). As +[timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) doesn't support timezone information, time will +be corrected to UTC and timezone info will be erased on write. On read, UTC timestamp is returned. Any precision finer +than 1ms will also be lost. + +```rust +# extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { use scylla::IntoTypedRows; -use scylla::frame::value::Timestamp; -use chrono::Duration; +use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; + +// 64.123 seconds since unix epoch, 1970-01-01 00:01:04.123 +let to_insert = PrimitiveDateTime::new( + Date::from_calendar_date(1970, Month::January, 1).unwrap(), + Time::from_hms_milli(0, 1, 4, 123).unwrap(), +) +.assume_utc(); -// Insert some timestamp into the table -let to_insert: Duration = Duration::seconds(64); +// Write timestamp to the table session - .query("INSERT INTO keyspace.table (a) VALUES(?)", (Timestamp(to_insert),)) + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read timestamp from the table, no need for a wrapper here -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (timestamp_value,): (Duration,) = row?; +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(OffsetDateTime,)>() { + let (timestamp_value,): (OffsetDateTime,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/udt.md b/docs/source/data-types/udt.md index ceb8ae8ca6..ac5b134a62 100644 --- a/docs/source/data-types/udt.md +++ b/docs/source/data-types/udt.md @@ -8,17 +8,25 @@ For example let's say `my_type` was created using this query: CREATE TYPE ks.my_type (int_val int, text_val text) ``` -To use this type in the driver, create a matching struct and derive `IntoUserType` and `FromUserType`: +To use this type in the driver, create a matching struct and derive: +- `SerializeCql`: in order to be able to use this struct in query parameters. \ + This macro requires fields of UDT and struct to have matching names, but the order + of the fields is not required to be the same. \ + Note: you can use different name using `rename` attribute - see `SerializeCql` macro documentation. +- `FromUserType`: in order to be able to use this struct in query results. \ + This macro requires fields of UDT and struct to be in the same *ORDER*. \ + This mismatch between `SerializeCql` and `FromUserType` requirements is a temporary situation - in the future `FromUserType` (or the macro that replaces it) will also require matching names. ```rust # extern crate scylla; # async fn check_only_compiles() { -use scylla::macros::{FromUserType, IntoUserType}; +use scylla::macros::{FromUserType, SerializeCql}; // Define a custom struct that matches the User Defined Type created earlier. -// Fields must be in the same order as they are in the database. +// Fields must be in the same order as they are in the database and also +// have the same names. // Wrapping a field in Option will gracefully handle null field values. -#[derive(Debug, IntoUserType, FromUserType)] +#[derive(Debug, FromUserType, SerializeCql)] struct MyType { int_val: i32, text_val: Option, @@ -27,8 +35,13 @@ struct MyType { ``` > ***Important***\ -> Fields in the Rust struct must be defined in the same order as they are in the database. -> When sending and receiving values, the driver will (de)serialize fields one after another, without looking at field names. +> For deserialization, fields in the Rust struct must be defined in the same order as they are in the database. +> When receiving values, the driver will (de)serialize fields one after another, without looking at field names. + +> ***Important***\ +> For serialization, by default fields in the Rust struct must be defined with the same names as they are in the database. +> The driver will serialize the fields in the order defined by the UDT, matching Rust fields by name. +> You can change this behaviour using macro attributes, see `SerializeCql` macro documentation for more information. Now it can be sent and received just like any other CQL value: ```rust @@ -37,10 +50,10 @@ Now it can be sent and received just like any other CQL value: # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { use scylla::IntoTypedRows; -use scylla::macros::{FromUserType, IntoUserType}; +use scylla::macros::{FromUserType, SerializeCql}; use scylla::cql_to_rust::FromCqlVal; -#[derive(Debug, IntoUserType, FromUserType)] +#[derive(Debug, FromUserType, SerializeCql)] struct MyType { int_val: i32, text_val: Option, diff --git a/docs/source/execution-profiles/priority.md b/docs/source/execution-profiles/priority.md index 92a46d50c4..4ae22d2c86 100644 --- a/docs/source/execution-profiles/priority.md +++ b/docs/source/execution-profiles/priority.md @@ -1,6 +1,6 @@ # Priorities of execution settings -You always have a default execution profile set for the `Session`, either the default one or overriden upon `Session` creation. Moreover, you can set a profile for specific statements, in which case the statement's profile has higher priority. Some options are also available for specific statements to be set directly on them, such as request timeout and consistency. In such case, the directly set options are preferred over those specified in execution profiles. +You always have a default execution profile set for the `Session`, either the default one or overridden upon `Session` creation. Moreover, you can set a profile for specific statements, in which case the statement's profile has higher priority. Some options are also available for specific statements to be set directly on them, such as request timeout and consistency. In such case, the directly set options are preferred over those specified in execution profiles. > **Recap**\ > Priorities are as follows:\ diff --git a/docs/source/index.md b/docs/source/index.md index c5e1191b1f..d2a6b79313 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -13,6 +13,7 @@ Although optimized for Scylla, the driver is also compatible with [Apache Cassan ## Contents * [Quick start](quickstart/quickstart.md) - Setting up a Rust project using `scylla-rust-driver` and running a few queries +* [Migration guides](migration-guides/migration-guides.md) - How to update the code that used an older version of this driver * [Connecting to the cluster](connecting/connecting.md) - Configuring a connection to scylla cluster * [Making queries](queries/queries.md) - Making different types of queries (simple, prepared, batch, paged) * [Execution profiles](execution-profiles/execution-profiles.md) - Grouping query execution configuration options together and switching them all at once diff --git a/docs/source/migration-guides/0.11-serialization.md b/docs/source/migration-guides/0.11-serialization.md new file mode 100644 index 0000000000..4df222592c --- /dev/null +++ b/docs/source/migration-guides/0.11-serialization.md @@ -0,0 +1,106 @@ +# Adjusting code to changes in serialization API introduced in 0.11 + +## Background + +When executing a statement through the CQL protocol, values for the bind markers are sent in a serialized, untyped form. In order to implement a safer and more robust interface, drivers can use the information returned after preparing a statement to check the type of data provided by the user against the actual types of the bind markers. + +Before 0.11, the driver couldn't do this kind of type checking. For example, in the case of non-batch queries, the only information about the user data it has is that it implements `ValueList` - defined as follows: + +```rust +# extern crate scylla; +# extern crate bytes; +# use scylla::frame::value::{SerializedResult, SerializeValuesError}; +# use bytes::BufMut; + +pub trait ValueList { + fn serialized(&self) -> SerializedResult<'_>; + fn write_to_request(&self, buf: &mut impl BufMut) -> Result<(), SerializeValuesError>; +} +``` + +The driver would naively serialize the data and hope that the user took care to send correct types of values. Failing to do so would, in the best case, fail on the DB-side validation; in the worst case, the data in its raw form may be reinterpreted as another type in an unintended manner. + +Another problem is that the information from the prepared statement response is required to robustly serialize user defined types, as UDTs require their fields to be serialized in the same order as they are defined in the database schema. The `IntoUserType` macro which implements Rust struct -> UDT serialization just expects that the order of the Rust struct fields matches the schema, but ensuring this can be very cumbersome for the users. + +In version 0.11, a new set of traits is introduced and the old ones are deprecated. The new traits receive more information during serialization such as names of the column/bind markers and their types, which allows to fix the issues mentioned in the previous section. + +## Old vs. new + +Both the old and the new APIs are based on three core traits: + +- `Value` - called `SerializeCql` in the new API. A type that can serialize itself to a single CQL value. For example, `i32` serializes itself into a representation that is compatible with the CQL `int` type. +- `ValueList` - called `SerializeRow` in the new API. A type that can serialize itself as a list of values for a CQL statement. For example, a `(i32, &str)` produces a list of two values which can be used in a query with two bind markers, e.g. `SELECT * FROM table WHERE pk = ? AND ck = ?`. Optionally, values in the produced list may be associated with names which is useful when using it with a query with named bind markers, e.g. `SELECT * FROM table WHERE pk = :pk AND ck = :ck`. +- `LegacyBatchValues`, previously named `BatchValues` - in new API replaced with new trait called (again) `BatchValues`. Represents a source of data for a batch request. It is essentially equivalent to a list of `ValueList`, one for each statement in the batch. For example, `((1, 2), (3, 4, 5))` can be used for a batch with two statements, the first one having two bind markers and the second one having three. + +All methods which take one of the old traits were changed to take the new trait - notably, this includes `Session::query`, `(Caching)Session::execute`, `(Caching)Session::batch`. + +The driver comes a set of `impl`s of those traits which allow to represent any CQL type (for example, see [Data Types](../data-types/data-types.md) page for a list of for which `Value` and `SerializeCql` is implemented). If the driver implements an old trait for some type, then it also provides implements the new trait for the same type. + +## Migration scenarios + +### Different default behavior in `SerializeRow`/`SerializeCql` macros + +By default, the `SerializeRow` and `SerializeCql` **will match the fields in the Rust struct by name to bind marker names** (in case of `SerializeRow`) **or UDT field names** (in case of `SerializeCql`). This is different from the old `ValueList` and `IntoUserType` macros which did not look at the field names at all and would expect the user to order the fields correctly. While the new behavior is much more ergonomic, you might have reasons not to use it. + +> **NOTE:** The deserialization macro counterparts `FromRow` and `FromUserType` have the same limitation as the old serialization macros - they require struct fields to be properly ordered. While a similar rework is planned for the deserialization traits in a future release, for the time being it might not be worth keeping the column names in sync with the database. + +In order to bring the old behavior to the new macros (the only difference being type checking which cannot be disabled right now) you can configure it using attributes, as shown in the snippet below: + +```rust +# extern crate scylla; +use scylla::SerializeCql; + +// The exact same attributes apply to the `SerializeRow` macro and their +// effect is completely analogous. +#[derive(SerializeCql)] +#[scylla(flavor = "enforce_order", skip_name_checks)] +struct Person { + name: String, + surname: String, + age: i16, +} +``` + +Refer to the API reference page for the `SerializeRow` and `SerializeCql` macros in the `scylla` crate to learn more about the supported attributes and their meaning. + +### Preparing is mandatory with a non-empty list of values + +> **NOTE:** The considerations in this section only concerns users of the `Session` API, `CachingSession` is not affected as it already does preparation before execute and caches the result. + +As explained in the [Background](#background) section, the driver uses data returned from the database after preparing a statement in order to implement type checking. As the new API makes type checking mandatory, **the driver must prepare the statement** so that the data for the bind markers can be type checked. It is done in case of the existing methods which used to send unprepared statements: `Session::query` and `Session::batch`. + +> **NOTE:** The driver will skip preparation if it detects that the list of values for the statement is empty, as there is nothing to be type checked. + +If you send simple statements along with non-empty lists of values, the slowdown will be as follows: + +- For `Session::query`, the driver will prepare the statement before sending it, incurring an additional round-trip. +- For `Session::batch`, the driver will send a prepare request for each *unique* unprepared statement with a non-empty list of values. **This is done serially!** + +In both cases, if the additional roundtrips are unacceptable, you should prepare the statements beforehand and reuse them - which aligns with our general recommendation against use of simple statements in performance sensitive scenarios. + +### Migrating from old to new traits *gradually* + +In some cases, migration will be as easy as changing occurrences of `IntoUserType` to `SerializeCql` and `ValueList` to `SerializeRow` and adding some atributes for procedural macros. However, if you have a large enough codebase or some custom, complicated implementations of the old traits then you might not want to migrate everything at once. To support gradual migration, the old traits were not removed but rather deprecated, and we introduced some additional utilities. + +#### Converting an object implementing an old trait to a new trait + +We provide a number of newtype wrappers: + +- `ValueAdapter` - implements `SerializeCql` if the type wrapped over implements `Value`, +- `ValueListAdapter` - implements `SerializeRow` if the type wrapped over implements `ValueList`, +- `LegacyBatchValuesAdapter` - implements `BatchValues` if the type wrapped over implements `LegacyBatchValues`. + +Note that these wrappers are not zero cost and incur some overhead: in case of `ValueAdapter` and `ValueListAdapter`, the data is first written into a newly allocated buffer and then rewritten to the final buffer. In case of `LegacyBatchValuesAdapter` there shouldn't be any additional allocations unless the implementation has an efficient, non-default `Self::LegacyBatchValuesIterator::write_next_to_request` implementation (which is not the case for the built-in `impl`s). + +Naturally, the implementations provided by the wrappers are not type safe as they directly use methods from the old traits. + +Conversion in the other direction is not possible. + +#### Custom implementations of old traits + +It is possible to directly generate an `impl` of `SerializeRow` and `SerializeCql` on a type which implements, respectively, `ValueList` or `Value`, without using the wrappers from the previous section. The following macros are provided: + +- `impl_serialize_cql_via_value` - implements `SerializeCql` if the type wrapped over implements `Value`, +- `impl_serialize_row_via_value_list` - implements `SerializeRow` if the type wrapped over implements `ValueList`, + +The implementations are practically as those generated by the wrappers described in the previous section. diff --git a/docs/source/migration-guides/migration-guides.md b/docs/source/migration-guides/migration-guides.md new file mode 100644 index 0000000000..554af6e41a --- /dev/null +++ b/docs/source/migration-guides/migration-guides.md @@ -0,0 +1,11 @@ +# Migration guides + +- [Serialization changes in version 0.11](0.11-serialization.md) + +```eval_rst +.. toctree:: + :hidden: + :glob: + + 0.11-serialization +``` diff --git a/docs/source/queries/batch.md b/docs/source/queries/batch.md index e316d4243f..4d9694c45e 100644 --- a/docs/source/queries/batch.md +++ b/docs/source/queries/batch.md @@ -17,7 +17,7 @@ use scylla::prepared_statement::PreparedStatement; let mut batch: Batch = Default::default(); // Add a simple statement to the batch using its text -batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(?, ?)"); +batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(1, 2)"); // Add a simple statement created manually to the batch let simple: Query = Query::new("INSERT INTO ks.tab (a, b) VALUES(3, 4)"); @@ -30,7 +30,7 @@ let prepared: PreparedStatement = session batch.append_statement(prepared); // Specify bound values to use with each statement -let batch_values = ((1_i32, 2_i32), +let batch_values = ((), (), (5_i32,)); @@ -40,6 +40,13 @@ session.batch(&batch, batch_values).await?; # } ``` +> ***Warning***\ +> Using simple statements with bind markers in batches is strongly discouraged. +> For each simple statement with a non-empty list of values in the batch, +> the driver will send a prepare request, and it will be done **sequentially**. +> Results of preparation are not cached between `Session::batch` calls. +> Consider preparing the statements before putting them into the batch. + ### Preparing a batch Instead of preparing each statement individually, it's possible to prepare a whole batch at once: @@ -129,6 +136,8 @@ let batch_values = ((1_i32, 2_i32), // Tuple with two values for the first state ()); // Empty tuple/unit for the third statement // Run the batch +// Note that the driver will prepare the first two statements, due to them +// not being prepared and having a non-empty list of values. session.batch(&batch, batch_values).await?; # Ok(()) # } diff --git a/docs/source/queries/paged.md b/docs/source/queries/paged.md index dab3672210..8112c9308b 100644 --- a/docs/source/queries/paged.md +++ b/docs/source/queries/paged.md @@ -5,6 +5,14 @@ allow to receive the whole result page by page. `Session::query_iter` and `Session::execute_iter` take a [simple query](simple.md) or a [prepared query](prepared.md) and return an `async` iterator over result `Rows`. +> ***Warning***\ +> In case of unprepared variant (`Session::query_iter`) if the values are not empty +> driver will first fully prepare a query (which means issuing additional request to each +> node in a cluster). This will have a performance penalty - how big it is depends on +> the size of your cluster (more nodes - more requests) and the size of returned +> result (more returned pages - more amortized penalty). In any case, it is preferable to +> use `Session::execute_iter`. + ### Examples Use `query_iter` to perform a [simple query](simple.md) with paging: ```rust @@ -119,6 +127,11 @@ let res2 = session # } ``` +> ***Warning***\ +> If the values are not empty, driver first needs to send a `PREPARE` request +> in order to fetch information required to serialize values. This will affect +> performance because 2 round trips will be required instead of 1. + On a `PreparedStatement`: ```rust # extern crate scylla; diff --git a/docs/source/queries/simple.md b/docs/source/queries/simple.md index 25190338dd..5b668013a1 100644 --- a/docs/source/queries/simple.md +++ b/docs/source/queries/simple.md @@ -22,6 +22,11 @@ session > > When page size is set, `query` will return only the first page of results. +> ***Warning***\ +> If the values are not empty, driver first needs to send a `PREPARE` request +> in order to fetch information required to serialize values. This will affect +> performance because 2 round trips will be required instead of 1. + ### First argument - the query As the first argument `Session::query` takes anything implementing `Into`.\ You can create a query manually to set custom options. For example to change query consistency: diff --git a/docs/source/queries/values.md b/docs/source/queries/values.md index 09b369b689..a8ba9dcf71 100644 --- a/docs/source/queries/values.md +++ b/docs/source/queries/values.md @@ -5,14 +5,14 @@ Each `?` in query text will be filled with the matching value. > **Never** pass values by adding strings, this could lead to [SQL Injection](https://en.wikipedia.org/wiki/SQL_injection) -Each list of values to send in a query must implement the trait `ValueList`.\ +Each list of values to send in a query must implement the trait `SerializeRow`.\ By default this can be a slice `&[]`, a tuple `()` (max 16 elements) of values to send, -or a custom struct which derives from `ValueList`. +or a custom struct which derives from `SerializeRow`. A few examples: ```rust # extern crate scylla; -# use scylla::{Session, ValueList, frame::response::result::CqlValue}; +# use scylla::{Session, SerializeRow, frame::response::result::CqlValue}; # use std::error::Error; # use std::collections::HashMap; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { @@ -33,22 +33,45 @@ session .await?; // Sending an integer and a string using a named struct. -// The values will be passed in the order from the struct definition -#[derive(ValueList)] +// Names of fields must match names of columns in request, +// but having them in the same order is not required. +// If the fields are in the same order, you can use attribute: +// `#[scylla(flavor = "enforce_order")]` +// in order to skip sorting the fields and just check if they +// are in the same order. See documentation of this macro +// for more information. +#[derive(SerializeRow)] struct IntString { - first_col: i32, - second_col: String, + a: i32, + b: String, } let int_string = IntString { - first_col: 42_i32, - second_col: "hello".to_owned(), + a: 42_i32, + b: "hello".to_owned(), }; session .query("INSERT INTO ks.tab (a, b) VALUES(?, ?)", int_string) .await?; +// You can use named bind markers in query if you want +// your names in struct to be different than column names. +#[derive(SerializeRow)] +struct IntStringCustom { + first_value: i32, + second_value: String, +} + +let int_string_custom = IntStringCustom { + first_value: 42_i32, + second_value: "hello".to_owned(), +}; + +session + .query("INSERT INTO ks.tab (a, b) VALUES(:first_value, :second_value)", int_string_custom) + .await?; + // Sending a single value as a tuple requires a trailing coma (Rust syntax): session.query("INSERT INTO ks.tab (a) VALUES(?)", (2_i32,)).await?; diff --git a/docs/source/quickstart/create-project.md b/docs/source/quickstart/create-project.md index 96bfad98f3..c6ee6bc949 100644 --- a/docs/source/quickstart/create-project.md +++ b/docs/source/quickstart/create-project.md @@ -8,7 +8,7 @@ cargo new myproject In `Cargo.toml` add useful dependencies: ```toml [dependencies] -scylla = "0.8" +scylla = "0.11" tokio = { version = "1.12", features = ["full"] } futures = "0.3.6" uuid = "1.0" diff --git a/docs/source/tracing/tracing.md b/docs/source/tracing/tracing.md index 2d54fb333c..dbf50ce2c0 100644 --- a/docs/source/tracing/tracing.md +++ b/docs/source/tracing/tracing.md @@ -1,6 +1,6 @@ # Query tracing -The driver has utilites for monitoring the execution of queries. +The driver has utilities for monitoring the execution of queries. There are two separate ways to get information about what happened with a query: `Tracing` and `Query Execution History`. ### Tracing diff --git a/docs/sphinx_preprocessor.py b/docs/sphinx_preprocessor.py new file mode 100644 index 0000000000..fc85848e11 --- /dev/null +++ b/docs/sphinx_preprocessor.py @@ -0,0 +1,34 @@ +import json +import sys + +def remove_sphinx_markdown(md_file_text): + text_split = md_file_text.split("```eval_rst") + + result = text_split[0] + + for i in range(1, len(text_split)): + cur_chunk = text_split[i] + result += cur_chunk[cur_chunk.find("```") + 3:] + + return result + +def process_section(section): + if 'Chapter' in section: + section['Chapter']['content'] = remove_sphinx_markdown(section['Chapter']['content']) + for s in section['Chapter']['sub_items']: + process_section(s) + +if __name__ == '__main__': + if len(sys.argv) > 1: # we check if we received any argument + if sys.argv[1] == "supports": + # then we are good to return an exit status code of 0, since the other argument will just be the renderer's name + sys.exit(0) + + # load both the context and the book representations from stdin + context, book = json.load(sys.stdin) + + for section in book['sections']: + process_section(section) + + # we are done with the book's modification, we can just print it to stdout + print(json.dumps(book)) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 84ac516db9..88a7dbda62 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,11 +10,12 @@ futures = "0.3.6" openssl = "0.10.32" rustyline = "9" rustyline-derive = "0.6" -scylla = {path = "../scylla", features = ["ssl", "cloud"]} +scylla = {path = "../scylla", features = ["ssl", "cloud", "chrono", "time"]} tokio = {version = "1.1.0", features = ["full"]} tracing = "0.1.25" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } chrono = { version = "0.4", default-features = false } +time = { version = "0.3.22" } uuid = "1.0" tower = "0.4" stats_alloc = "0.1" diff --git a/examples/allocations.rs b/examples/allocations.rs index deac274a31..3148bb51c2 100644 --- a/examples/allocations.rs +++ b/examples/allocations.rs @@ -131,12 +131,12 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(args.node).build().await?; let session = Arc::new(session); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session.await_schema_agreement().await.unwrap(); session .query( - "CREATE TABLE IF NOT EXISTS ks.alloc_test (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.allocations (a int, b int, c text, primary key (a, b))", &[], ) .await?; @@ -145,13 +145,13 @@ async fn main() -> Result<()> { let prepared_inserts = Arc::new( session - .prepare("INSERT INTO ks.alloc_test (a, b, c) VALUES (?, ?, 'abc')") + .prepare("INSERT INTO examples_ks.allocations (a, b, c) VALUES (?, ?, 'abc')") .await?, ); let prepared_selects = Arc::new( session - .prepare("SELECT * FROM ks.alloc_test WHERE a = ? and b = ?") + .prepare("SELECT * FROM examples_ks.allocations WHERE a = ? and b = ?") .await?, ); diff --git a/examples/auth.rs b/examples/auth.rs index 9b5953b9c0..982ccf5d3a 100644 --- a/examples/auth.rs +++ b/examples/auth.rs @@ -14,9 +14,9 @@ async fn main() -> Result<()> { .await .unwrap(); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await.unwrap(); + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await.unwrap(); session - .query("DROP TABLE IF EXISTS ks.t;", &[]) + .query("DROP TABLE IF EXISTS examples_ks.auth;", &[]) .await .unwrap(); diff --git a/examples/basic.rs b/examples/basic.rs index 01fb4848b0..ecae95068b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -12,25 +12,31 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.basic (a int, b int, c text, primary key (a, b))", &[], ) .await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (?, ?, ?)", (3, 4, "def")) + .query( + "INSERT INTO examples_ks.basic (a, b, c) VALUES (?, ?, ?)", + (3, 4, "def"), + ) .await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (1, 2, 'abc')", &[]) + .query( + "INSERT INTO examples_ks.basic (a, b, c) VALUES (1, 2, 'abc')", + &[], + ) .await?; let prepared = session - .prepare("INSERT INTO ks.t (a, b, c) VALUES (?, 7, ?)") + .prepare("INSERT INTO examples_ks.basic (a, b, c) VALUES (?, 7, ?)") .await?; session .execute(&prepared, (42_i32, "I'm prepared!")) @@ -43,7 +49,11 @@ async fn main() -> Result<()> { .await?; // Rows can be parsed as tuples - if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT a, b, c FROM examples_ks.basic", &[]) + .await? + .rows + { for row in rows.into_typed::<(i32, i32, String)>() { let (a, b, c) = row?; println!("a, b, c: {}, {}, {}", a, b, c); @@ -58,7 +68,11 @@ async fn main() -> Result<()> { _c: String, } - if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT a, b, c FROM examples_ks.basic", &[]) + .await? + .rows + { for row_data in rows.into_typed::() { let row_data = row_data?; println!("row_data: {:?}", row_data); @@ -66,7 +80,11 @@ async fn main() -> Result<()> { } // Or simply as untyped rows - if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT a, b, c FROM examples_ks.basic", &[]) + .await? + .rows + { for row in rows { let a = row.columns[0].as_ref().unwrap().as_int().unwrap(); let b = row.columns[1].as_ref().unwrap().as_int().unwrap(); diff --git a/examples/cloud.rs b/examples/cloud.rs index 99bb85314d..e469ae3241 100644 --- a/examples/cloud.rs +++ b/examples/cloud.rs @@ -16,10 +16,10 @@ async fn main() -> Result<()> { .await .unwrap(); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await.unwrap(); session - .query("DROP TABLE IF EXISTS ks.t;", &[]) + .query("DROP TABLE IF EXISTS examples_ks.cloud;", &[]) .await .unwrap(); diff --git a/examples/compare-tokens.rs b/examples/compare-tokens.rs index 5c56aa5f4d..294dc7842b 100644 --- a/examples/compare-tokens.rs +++ b/examples/compare-tokens.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use scylla::frame::value::ValueList; use scylla::routing::Token; use scylla::transport::NodeAddr; use scylla::{Session, SessionBuilder}; @@ -13,41 +12,51 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (pk bigint primary key)", + "CREATE TABLE IF NOT EXISTS examples_ks.compare_tokens (pk bigint primary key)", &[], ) .await?; - let prepared = session.prepare("INSERT INTO ks.t (pk) VALUES (?)").await?; + let prepared = session + .prepare("INSERT INTO examples_ks.compare_tokens (pk) VALUES (?)") + .await?; for pk in (0..100_i64).chain(99840..99936_i64) { session - .query("INSERT INTO ks.t (pk) VALUES (?)", (pk,)) + .query( + "INSERT INTO examples_ks.compare_tokens (pk) VALUES (?)", + (pk,), + ) .await?; - let serialized_pk = (pk,).serialized()?.into_owned(); - let t = prepared.calculate_token(&serialized_pk)?.unwrap().value; + let t = prepared.calculate_token(&(pk,))?.unwrap().value; println!( "Token endpoints for query: {:?}", session .get_cluster_data() - .get_token_endpoints("ks", Token { value: t }) + .get_token_endpoints("examples_ks", Token { value: t }) .iter() .map(|n| n.address) .collect::>() ); let qt = session - .query(format!("SELECT token(pk) FROM ks.t where pk = {}", pk), &[]) + .query( + format!( + "SELECT token(pk) FROM examples_ks.compare_tokens where pk = {}", + pk + ), + &[], + ) .await? .rows .unwrap() - .get(0) + .first() .expect("token query no rows!") .columns[0] .as_ref() diff --git a/examples/cql-time-types.rs b/examples/cql-time-types.rs index ac4da4831e..fed03f3949 100644 --- a/examples/cql-time-types.rs +++ b/examples/cql-time-types.rs @@ -2,9 +2,9 @@ // Date, Time, Timestamp use anyhow::Result; -use chrono::{Duration, NaiveDate}; +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use scylla::frame::response::result::CqlValue; -use scylla::frame::value::{Date, Time, Timestamp}; +use scylla::frame::value::{CqlDate, CqlTime, CqlTimestamp}; use scylla::transport::session::{IntoTypedRows, Session}; use scylla::SessionBuilder; use std::env; @@ -17,46 +17,85 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; // Date // Date is a year, month and day in the range -5877641-06-23 to -5877641-06-23 session .query( - "CREATE TABLE IF NOT EXISTS ks.dates (d date primary key)", + "CREATE TABLE IF NOT EXISTS examples_ks.dates (d date primary key)", &[], ) .await?; - // Dates in the range -262145-1-1 to 262143-12-31 can be represented using chrono::NaiveDate - let example_date: NaiveDate = NaiveDate::from_ymd_opt(2020, 2, 20).unwrap(); + // If 'chrono' feature is enabled, dates in the range -262145-1-1 to 262143-12-31 can be represented using + // chrono::NaiveDate + let chrono_date = NaiveDate::from_ymd_opt(2020, 2, 20).unwrap(); session - .query("INSERT INTO ks.dates (d) VALUES (?)", (example_date,)) + .query( + "INSERT INTO examples_ks.dates (d) VALUES (?)", + (chrono_date,), + ) .await?; - if let Some(rows) = session.query("SELECT d from ks.dates", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT d from examples_ks.dates", &[]) + .await? + .rows + { for row in rows.into_typed::<(NaiveDate,)>() { let (read_date,): (NaiveDate,) = match row { Ok(read_date) => read_date, Err(_) => continue, // We might read a date that does not fit in NaiveDate, skip it }; - println!("Read a date: {:?}", read_date); + println!("Parsed a date into chrono::NaiveDate: {:?}", read_date); + } + } + + // Alternatively, you can enable 'time' feature and use `time::Date` to represent date. `time::Date` only allows + // dates in range -9999-1-1 to 9999-12-31. Or, if you have 'time/large-dates' feature enabled, this range changes + // to -999999-1-1 to 999999-12-31 + let time_date = time::Date::from_calendar_date(2020, time::Month::March, 21).unwrap(); + + session + .query("INSERT INTO examples_ks.dates (d) VALUES (?)", (time_date,)) + .await?; + + if let Some(rows) = session + .query("SELECT d from examples_ks.dates", &[]) + .await? + .rows + { + for row in rows.into_typed::<(time::Date,)>() { + let (read_date,) = match row { + Ok(read_date) => read_date, + Err(_) => continue, // We might read a date that does not fit in time::Date, skip it + }; + + println!("Parsed a date into time::Date: {:?}", read_date); } } // Dates outside this range must be represented in the raw form - an u32 describing days since -5877641-06-23 - let example_big_date: Date = Date(u32::MAX); + let example_big_date: CqlDate = CqlDate(u32::MAX); session - .query("INSERT INTO ks.dates (d) VALUES (?)", (example_big_date,)) + .query( + "INSERT INTO examples_ks.dates (d) VALUES (?)", + (example_big_date,), + ) .await?; - if let Some(rows) = session.query("SELECT d from ks.dates", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT d from examples_ks.dates", &[]) + .await? + .rows + { for row in rows { let read_days: u32 = match row.columns[0] { - Some(CqlValue::Date(days)) => days, + Some(CqlValue::Date(CqlDate(days))) => days, _ => panic!("oh no"), }; @@ -64,56 +103,161 @@ async fn main() -> Result<()> { } } - // Time - nanoseconds since midnight in range 0..=86399999999999 - let example_time: Duration = Duration::hours(1); + // Time + // Time is represented as nanosecond count since midnight in range 0..=86399999999999 session .query( - "CREATE TABLE IF NOT EXISTS ks.times (t time primary key)", + "CREATE TABLE IF NOT EXISTS examples_ks.times (t time primary key)", &[], ) .await?; - // Time as bound value must be wrapped in value::Time to differentiate from Timestamp + // Time can be represented using 3 different types, chrono::NaiveTime, time::Time and CqlTime. All types support + // full value range + + // chrono::NaiveTime + let chrono_time = NaiveTime::from_hms_nano_opt(1, 2, 3, 456_789_012).unwrap(); + + session + .query( + "INSERT INTO examples_ks.times (t) VALUES (?)", + (chrono_time,), + ) + .await?; + + if let Some(rows) = session + .query("SELECT t from examples_ks.times", &[]) + .await? + .rows + { + for row in rows.into_typed::<(NaiveTime,)>() { + let (read_time,) = row?; + + println!("Parsed a time into chrono::NaiveTime: {:?}", read_time); + } + } + + // time::Time + let time_time = time::Time::from_hms_nano(2, 3, 4, 567_890_123).unwrap(); + + session + .query("INSERT INTO examples_ks.times (t) VALUES (?)", (time_time,)) + .await?; + + if let Some(rows) = session + .query("SELECT t from examples_ks.times", &[]) + .await? + .rows + { + for row in rows.into_typed::<(time::Time,)>() { + let (read_time,) = row?; + + println!("Parsed a time into time::Time: {:?}", read_time); + } + } + + // CqlTime + let time_time = CqlTime(((3 * 60 + 4) * 60 + 5) * 1_000_000_000 + 678_901_234); + session - .query("INSERT INTO ks.times (t) VALUES (?)", (Time(example_time),)) + .query("INSERT INTO examples_ks.times (t) VALUES (?)", (time_time,)) .await?; - if let Some(rows) = session.query("SELECT t from ks.times", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (read_time,): (Duration,) = row?; + if let Some(rows) = session + .query("SELECT t from examples_ks.times", &[]) + .await? + .rows + { + for row in rows.into_typed::<(CqlTime,)>() { + let (read_time,) = row?; - println!("Read a time: {:?}", read_time); + println!("Read a time as raw nanos: {:?}", read_time); } } - // Timestamp - milliseconds since unix epoch - 1970-01-01 - let example_timestamp: Duration = Duration::hours(1); // This will describe 1970-01-01 01:00:00 + // Timestamp + // Timestamp is represented as milliseconds since unix epoch - 1970-01-01. Negative values are also possible session .query( - "CREATE TABLE IF NOT EXISTS ks.timestamps (t timestamp primary key)", + "CREATE TABLE IF NOT EXISTS examples_ks.timestamps (t timestamp primary key)", &[], ) .await?; - // Timestamp as bound value must be wrapped in value::Timestamp to differentiate from Time + // Timestamp can also be represented using 3 different types, chrono::DateTime, time::OffsetDateTime and + // CqlTimestamp. Only CqlTimestamp allows full range. + + // chrono::DateTime + let chrono_datetime = Utc::now(); + + session + .query( + "INSERT INTO examples_ks.timestamps (t) VALUES (?)", + (chrono_datetime,), + ) + .await?; + + if let Some(rows) = session + .query("SELECT t from examples_ks.timestamps", &[]) + .await? + .rows + { + for row in rows.into_typed::<(DateTime,)>() { + let (read_time,) = row?; + + println!( + "Parsed a timestamp into chrono::DateTime: {:?}", + read_time + ); + } + } + + // time::OffsetDateTime + let time_datetime = time::OffsetDateTime::now_utc(); + + session + .query( + "INSERT INTO examples_ks.timestamps (t) VALUES (?)", + (time_datetime,), + ) + .await?; + + if let Some(rows) = session + .query("SELECT t from examples_ks.timestamps", &[]) + .await? + .rows + { + for row in rows.into_typed::<(time::OffsetDateTime,)>() { + let (read_time,) = row?; + + println!( + "Parsed a timestamp into time::OffsetDateTime: {:?}", + read_time + ); + } + } + + // CqlTimestamp + let cql_datetime = CqlTimestamp(1 << 31); + session .query( - "INSERT INTO ks.timestamps (t) VALUES (?)", - (Timestamp(example_timestamp),), + "INSERT INTO examples_ks.timestamps (t) VALUES (?)", + (cql_datetime,), ) .await?; if let Some(rows) = session - .query("SELECT t from ks.timestamps", &[]) + .query("SELECT t from examples_ks.timestamps", &[]) .await? .rows { - for row in rows.into_typed::<(Duration,)>() { - let (read_time,): (Duration,) = row?; + for row in rows.into_typed::<(CqlTimestamp,)>() { + let (read_time,) = row?; - println!("Read a timestamp: {:?}", read_time); + println!("Read a timestamp as raw millis: {:?}", read_time); } } diff --git a/examples/custom_deserialization.rs b/examples/custom_deserialization.rs index 122c842bb8..ea9a0d6284 100644 --- a/examples/custom_deserialization.rs +++ b/examples/custom_deserialization.rs @@ -13,16 +13,19 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (pk int PRIMARY KEY, v text)", + "CREATE TABLE IF NOT EXISTS examples_ks.custom_deserialization (pk int primary key, v text)", &[], ) .await?; session - .query("INSERT INTO ks.t (pk, v) VALUES (1, 'asdf')", ()) + .query( + "INSERT INTO examples_ks.custom_deserialization (pk, v) VALUES (1, 'asdf')", + (), + ) .await?; // You can implement FromCqlVal for your own types @@ -38,7 +41,10 @@ async fn main() -> Result<()> { } let (v,) = session - .query("SELECT v FROM ks.t WHERE pk = 1", ()) + .query( + "SELECT v FROM examples_ks.custom_deserialization WHERE pk = 1", + (), + ) .await? .single_row_typed::<(MyType,)>()?; assert_eq!(v, MyType("asdf".to_owned())); @@ -62,7 +68,10 @@ async fn main() -> Result<()> { impl_from_cql_value_from_method!(MyOtherType, into_my_other_type); let (v,) = session - .query("SELECT v FROM ks.t WHERE pk = 1", ()) + .query( + "SELECT v FROM examples_ks.custom_deserialization WHERE pk = 1", + (), + ) .await? .single_row_typed::<(MyOtherType,)>()?; assert_eq!(v, MyOtherType("asdf".to_owned())); diff --git a/examples/execution_profile.rs b/examples/execution_profile.rs index 13bb44e0e6..317604d9b0 100644 --- a/examples/execution_profile.rs +++ b/examples/execution_profile.rs @@ -59,16 +59,17 @@ async fn main() -> Result<()> { session_3_config.add_known_node(uri); let session3: Session = Session::connect(session_3_config).await?; - session1.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session1.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session2 .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.execution_profile (a int, b int, c text, primary key (a, b))", &[], ) .await?; - let mut query_insert: Query = "INSERT INTO ks.t (a, b, c) VALUES (?, ?, ?)".into(); + let mut query_insert: Query = + "INSERT INTO examples_ks.execution_profile (a, b, c) VALUES (?, ?, ?)".into(); // As `query_insert` is set another handle than session1, the execution profile pointed by query's handle // will be preferred, so the query below will be executed with `profile2`, even though `session1` is set `profile1`. @@ -79,12 +80,18 @@ async fn main() -> Result<()> { handle2.map_to_another_profile(profile1); // And now the following queries are executed with profile1: session1.query(query_insert.clone(), (3, 4, "def")).await?; - session2.query("SELECT * FROM ks.t", ()).await?; + session2 + .query("SELECT * FROM examples_ks.execution_profile", ()) + .await?; // One can unset a profile handle from a statement and, since then, execute it with session's default profile. query_insert.set_execution_profile_handle(None); - session3.query("SELECT * FROM ks.t", ()).await?; // This executes with default session profile. - session2.query("SELECT * FROM ks.t", ()).await?; // This executes with profile1. + session3 + .query("SELECT * FROM examples_ks.execution_profile", ()) + .await?; // This executes with default session profile. + session2 + .query("SELECT * FROM examples_ks.execution_profile", ()) + .await?; // This executes with profile1. Ok(()) } diff --git a/examples/get_by_name.rs b/examples/get_by_name.rs index 0b94b1e898..03c14b8721 100644 --- a/examples/get_by_name.rs +++ b/examples/get_by_name.rs @@ -12,31 +12,31 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.hello (pk int, ck int, value text, primary key (pk, ck))", + "CREATE TABLE IF NOT EXISTS examples_ks.get_by_name (pk int, ck int, value text, primary key (pk, ck))", &[], ) .await?; session .query( - "INSERT INTO ks.hello (pk, ck, value) VALUES (?, ?, ?)", + "INSERT INTO examples_ks.get_by_name (pk, ck, value) VALUES (?, ?, ?)", (3, 4, "def"), ) .await?; session .query( - "INSERT INTO ks.hello (pk, ck, value) VALUES (1, 2, 'abc')", + "INSERT INTO examples_ks.get_by_name (pk, ck, value) VALUES (1, 2, 'abc')", &[], ) .await?; let query_result = session - .query("SELECT pk, ck, value FROM ks.hello", &[]) + .query("SELECT pk, ck, value FROM examples_ks.get_by_name", &[]) .await?; let (ck_idx, _) = query_result .get_column_spec("ck") diff --git a/examples/logging.rs b/examples/logging.rs index 0ee57340c7..19b22d73cb 100644 --- a/examples/logging.rs +++ b/examples/logging.rs @@ -17,9 +17,9 @@ async fn main() -> Result<()> { info!("Connecting to {}", uri); let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; - session.query("USE ks", &[]).await?; + session.query("USE examples_ks", &[]).await?; Ok(()) } diff --git a/examples/parallel-prepared.rs b/examples/parallel-prepared.rs index abab78af64..ae2a11e3f0 100644 --- a/examples/parallel-prepared.rs +++ b/examples/parallel-prepared.rs @@ -14,18 +14,18 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; let session = Arc::new(session); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t2 (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.parallel_prepared (a int, b int, c text, primary key (a, b))", &[], ) .await?; let prepared = Arc::new( session - .prepare("INSERT INTO ks.t2 (a, b, c) VALUES (?, ?, 'abc')") + .prepare("INSERT INTO examples_ks.parallel_prepared (a, b, c) VALUES (?, ?, 'abc')") .await?, ); println!("Prepared statement: {:#?}", prepared); diff --git a/examples/parallel.rs b/examples/parallel.rs index e23c560f41..63b22225c9 100644 --- a/examples/parallel.rs +++ b/examples/parallel.rs @@ -14,11 +14,11 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; let session = Arc::new(session); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t2 (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.parallel (a int, b int, c text, primary key (a, b))", &[], ) .await?; @@ -36,7 +36,7 @@ async fn main() -> Result<()> { session .query( format!( - "INSERT INTO ks.t2 (a, b, c) VALUES ({}, {}, 'abc')", + "INSERT INTO examples_ks.parallel (a, b, c) VALUES ({}, {}, 'abc')", i, 2 * i ), diff --git a/examples/query_history.rs b/examples/query_history.rs index f95001c2e3..e75a30514f 100644 --- a/examples/query_history.rs +++ b/examples/query_history.rs @@ -17,11 +17,11 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.query_history (a int, b int, c text, primary key (a, b))", &[], ) .await?; @@ -47,11 +47,14 @@ async fn main() -> Result<()> { // The same works for other types of queries, e.g iterators for i in 0..32 { session - .query("INSERT INTO ks.t (a, b, c) VALUES (?, ?, 't')", (i, i)) + .query( + "INSERT INTO examples_ks.query_history (a, b, c) VALUES (?, ?, 't')", + (i, i), + ) .await?; } - let mut iter_query: Query = Query::new("SELECT * FROM ks.t"); + let mut iter_query: Query = Query::new("SELECT * FROM examples_ks.query_history"); iter_query.set_page_size(8); let iter_history_listener = Arc::new(HistoryCollector::new()); iter_query.set_history_listener(iter_history_listener.clone()); diff --git a/examples/schema_agreement.rs b/examples/schema_agreement.rs index 08e5a59384..2f7189c1f1 100644 --- a/examples/schema_agreement.rs +++ b/examples/schema_agreement.rs @@ -22,7 +22,7 @@ async fn main() -> Result<()> { println!("Schema version: {}", schema_version); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; match session.await_schema_agreement().await { Ok(_schema_version) => println!("Schema is in agreement in time"), @@ -31,23 +31,29 @@ async fn main() -> Result<()> { }; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.schema_agreement (a int, b int, c text, primary key (a, b))", &[], ) .await?; session.await_schema_agreement().await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (?, ?, ?)", (3, 4, "def")) + .query( + "INSERT INTO examples_ks.schema_agreement (a, b, c) VALUES (?, ?, ?)", + (3, 4, "def"), + ) .await?; session.await_schema_agreement().await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (1, 2, 'abc')", &[]) + .query( + "INSERT INTO examples_ks.schema_agreement (a, b, c) VALUES (1, 2, 'abc')", + &[], + ) .await?; let prepared = session - .prepare("INSERT INTO ks.t (a, b, c) VALUES (?, 7, ?)") + .prepare("INSERT INTO examples_ks.schema_agreement (a, b, c) VALUES (?, 7, ?)") .await?; session .execute(&prepared, (42_i32, "I'm prepared!")) @@ -60,7 +66,11 @@ async fn main() -> Result<()> { .await?; // Rows can be parsed as tuples - if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT a, b, c FROM examples_ks.schema_agreement", &[]) + .await? + .rows + { for row in rows.into_typed::<(i32, i32, String)>() { let (a, b, c) = row?; println!("a, b, c: {}, {}, {}", a, b, c); diff --git a/examples/select-paging.rs b/examples/select-paging.rs index 5386dcf881..8cee4742cb 100644 --- a/examples/select-paging.rs +++ b/examples/select-paging.rs @@ -11,11 +11,11 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.select_paging (a int, b int, c text, primary key (a, b))", &[], ) .await?; @@ -23,7 +23,7 @@ async fn main() -> Result<()> { for i in 0..16_i32 { session .query( - "INSERT INTO ks.t (a, b, c) VALUES (?, ?, 'abc')", + "INSERT INTO examples_ks.select_paging (a, b, c) VALUES (?, ?, 'abc')", (i, 2 * i), ) .await?; @@ -31,7 +31,7 @@ async fn main() -> Result<()> { // Iterate through select result with paging let mut rows_stream = session - .query_iter("SELECT a, b, c FROM ks.t", &[]) + .query_iter("SELECT a, b, c FROM examples_ks.select_paging", &[]) .await? .into_typed::<(i32, i32, String)>(); @@ -40,7 +40,7 @@ async fn main() -> Result<()> { println!("a, b, c: {}, {}, {}", a, b, c); } - let paged_query = Query::new("SELECT a, b, c FROM ks.t").with_page_size(6); + let paged_query = Query::new("SELECT a, b, c FROM examples_ks.select_paging").with_page_size(6); let res1 = session.query(paged_query.clone(), &[]).await?; println!( "Paging state: {:#?} ({} rows)", @@ -65,7 +65,7 @@ async fn main() -> Result<()> { ); let paged_prepared = session - .prepare(Query::new("SELECT a, b, c FROM ks.t").with_page_size(7)) + .prepare(Query::new("SELECT a, b, c FROM examples_ks.select_paging").with_page_size(7)) .await?; let res4 = session.execute(&paged_prepared, &[]).await?; println!( diff --git a/examples/speculative-execution.rs b/examples/speculative-execution.rs index 4f0b3445f9..792b325c6d 100644 --- a/examples/speculative-execution.rs +++ b/examples/speculative-execution.rs @@ -26,16 +26,18 @@ async fn main() -> Result<()> { .build() .await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.speculative_execution (a int, b int, c text, primary key (a, b))", &[], ) .await?; - let mut select_stmt = session.prepare("SELECT a, b, c FROM ks.t").await?; + let mut select_stmt = session + .prepare("SELECT a, b, c FROM examples_ks.speculative_execution") + .await?; // This will allow for speculative execution select_stmt.set_is_idempotent(true); diff --git a/examples/tls.rs b/examples/tls.rs index 49006c1602..4ecfc62fa4 100644 --- a/examples/tls.rs +++ b/examples/tls.rs @@ -49,25 +49,31 @@ async fn main() -> Result<()> { .build() .await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + "CREATE TABLE IF NOT EXISTS examples_ks.tls (a int, b int, c text, primary key (a, b))", &[], ) .await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (?, ?, ?)", (3, 4, "def")) + .query( + "INSERT INTO examples_ks.tls (a, b, c) VALUES (?, ?, ?)", + (3, 4, "def"), + ) .await?; session - .query("INSERT INTO ks.t (a, b, c) VALUES (1, 2, 'abc')", &[]) + .query( + "INSERT INTO examples_ks.tls (a, b, c) VALUES (1, 2, 'abc')", + &[], + ) .await?; let prepared = session - .prepare("INSERT INTO ks.t (a, b, c) VALUES (?, 7, ?)") + .prepare("INSERT INTO examples_ks.tls (a, b, c) VALUES (?, 7, ?)") .await?; session .execute(&prepared, (42_i32, "I'm prepared!")) @@ -80,7 +86,11 @@ async fn main() -> Result<()> { .await?; // Rows can be parsed as tuples - if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT a, b, c FROM examples_ks.tls", &[]) + .await? + .rows + { for row in rows.into_typed::<(i32, i32, String)>() { let (a, b, c) = row?; println!("a, b, c: {}, {}, {}", a, b, c); diff --git a/examples/tracing.rs b/examples/tracing.rs index bff0bca94a..e4c9eb8047 100644 --- a/examples/tracing.rs +++ b/examples/tracing.rs @@ -26,18 +26,18 @@ async fn main() -> Result<()> { .build() .await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.tracing_example (val text primary key)", + "CREATE TABLE IF NOT EXISTS examples_ks.tracing (val text primary key)", &[], ) .await?; // QUERY // Create a simple query and enable tracing for it - let mut query: Query = Query::new("SELECT val from ks.tracing_example"); + let mut query: Query = Query::new("SELECT val from examples_ks.tracing"); query.set_tracing(true); query.set_serial_consistency(Some(SerialConsistency::LocalSerial)); @@ -101,7 +101,7 @@ async fn main() -> Result<()> { // BATCH // Create a simple batch and enable tracing let mut batch: Batch = Batch::default(); - batch.append_statement("INSERT INTO ks.tracing_example (val) VALUES('val')"); + batch.append_statement("INSERT INTO examples_ks.tracing (val) VALUES('val')"); batch.set_tracing(true); // Run the batch and print its tracing_id diff --git a/examples/user-defined-type.rs b/examples/user-defined-type.rs index 53465b2f10..4ed2304a6b 100644 --- a/examples/user-defined-type.rs +++ b/examples/user-defined-type.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use scylla::macros::{FromUserType, IntoUserType}; -use scylla::{IntoTypedRows, Session, SessionBuilder}; +use scylla::macros::FromUserType; +use scylla::{IntoTypedRows, SerializeCql, Session, SessionBuilder}; use std::env; #[tokio::main] @@ -11,25 +11,25 @@ async fn main() -> Result<()> { let session: Session = SessionBuilder::new().known_node(uri).build().await?; - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; session .query( - "CREATE TYPE IF NOT EXISTS ks.my_type (int_val int, text_val text)", + "CREATE TYPE IF NOT EXISTS examples_ks.my_type (int_val int, text_val text)", &[], ) .await?; session .query( - "CREATE TABLE IF NOT EXISTS ks.udt_tab (k int, my my_type, primary key (k))", + "CREATE TABLE IF NOT EXISTS examples_ks.user_defined_type_table (k int, my my_type, primary key (k))", &[], ) .await?; // Define custom struct that matches User Defined Type created earlier // wrapping field in Option will gracefully handle null field values - #[derive(Debug, IntoUserType, FromUserType)] + #[derive(Debug, FromUserType, SerializeCql)] struct MyType { int_val: i32, text_val: Option, @@ -42,11 +42,18 @@ async fn main() -> Result<()> { // It can be inserted like a normal value session - .query("INSERT INTO ks.udt_tab (k, my) VALUES (5, ?)", (to_insert,)) + .query( + "INSERT INTO examples_ks.user_defined_type_table (k, my) VALUES (5, ?)", + (to_insert,), + ) .await?; // And read like any normal value - if let Some(rows) = session.query("SELECT my FROM ks.udt_tab", &[]).await?.rows { + if let Some(rows) = session + .query("SELECT my FROM examples_ks.user_defined_type_table", &[]) + .await? + .rows + { for row in rows.into_typed::<(MyType,)>() { let (my_type_value,): (MyType,) = row?; println!("{:?}", my_type_value) diff --git a/examples/value_list.rs b/examples/value_list.rs index 44b388dcbc..fe03b154bf 100644 --- a/examples/value_list.rs +++ b/examples/value_list.rs @@ -9,17 +9,17 @@ async fn main() { let session: Session = SessionBuilder::new().known_node(uri).build().await.unwrap(); - session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await.unwrap(); + session.query("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await.unwrap(); session .query( - "CREATE TABLE IF NOT EXISTS ks.my_type (k int, my text, primary key (k))", + "CREATE TABLE IF NOT EXISTS examples_ks.my_type (k int, my text, primary key (k))", &[], ) .await .unwrap(); - #[derive(scylla::ValueList)] + #[derive(scylla::SerializeRow)] struct MyType<'a> { k: i32, my: Option<&'a str>, @@ -31,13 +31,16 @@ async fn main() { }; session - .query("INSERT INTO ks.my_type (k, my) VALUES (?, ?)", to_insert) + .query( + "INSERT INTO examples_ks.my_type (k, my) VALUES (?, ?)", + to_insert, + ) .await .unwrap(); // You can also use type generics: - #[derive(scylla::ValueList)] - struct MyTypeWithGenerics { + #[derive(scylla::SerializeRow)] + struct MyTypeWithGenerics { k: i32, my: Option, } @@ -48,12 +51,15 @@ async fn main() { }; session - .query("INSERT INTO ks.my_type (k, my) VALUES (?, ?)", to_insert_2) + .query( + "INSERT INTO examples_ks.my_type (k, my) VALUES (?, ?)", + to_insert_2, + ) .await .unwrap(); let q = session - .query("SELECT * FROM ks.my_type", &[]) + .query("SELECT * FROM examples_ks.my_type", &[]) .await .unwrap(); diff --git a/scylla-cql/Cargo.toml b/scylla-cql/Cargo.toml index d8a4fafd7e..c919ec4787 100644 --- a/scylla-cql/Cargo.toml +++ b/scylla-cql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scylla-cql" -version = "0.0.8" +version = "0.0.11" edition = "2021" description = "CQL data types and primitives, for interacting with Scylla." repository = "https://github.com/scylladb/scylla-rust-driver" @@ -10,7 +10,7 @@ categories = ["database"] license = "MIT OR Apache-2.0" [dependencies] -scylla-macros = { version = "0.2.0", path = "../scylla-macros" } +scylla-macros = { version = "0.3.0", path = "../scylla-macros" } byteorder = "1.3.4" bytes = "1.0.1" num_enum = "0.6" @@ -21,13 +21,16 @@ uuid = "1.0" thiserror = "1.0" bigdecimal = "0.2.0" num-bigint = "0.3" -chrono = { version = "0.4.27", default-features = false } +chrono = { version = "0.4.27", default-features = false, optional = true } lz4_flex = { version = "0.11.1" } async-trait = "0.1.57" -serde = { version = "1.0", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +time = { version = "0.3", optional = true } [dev-dependencies] criterion = "0.4" # Note: v0.5 needs at least rust 1.70.0 +# Use large-dates feature to test potential edge cases +time = { version = "0.3.21", features = ["large-dates"] } [[bench]] name = "benchmark" @@ -35,3 +38,6 @@ harness = false [features] secret = ["secrecy"] +time = ["dep:time"] +chrono = ["dep:chrono"] +full-serialization = ["chrono", "time", "secret"] diff --git a/scylla-cql/benches/benchmark.rs b/scylla-cql/benches/benchmark.rs index 0aa6c89102..2ab15f5051 100644 --- a/scylla-cql/benches/benchmark.rs +++ b/scylla-cql/benches/benchmark.rs @@ -3,17 +3,17 @@ use std::borrow::Cow; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use scylla_cql::frame::request::SerializableRequest; -use scylla_cql::frame::value::SerializedValues; -use scylla_cql::frame::value::ValueList; +use scylla_cql::frame::response::result::ColumnType; use scylla_cql::frame::{request::query, Compression, SerializedRequest}; +use scylla_cql::types::serialize::row::SerializedValues; -fn make_query<'a>(contents: &'a str, values: &'a SerializedValues) -> query::Query<'a> { +fn make_query(contents: &str, values: SerializedValues) -> query::Query<'_> { query::Query { contents: Cow::Borrowed(contents), parameters: query::QueryParameters { consistency: scylla_cql::Consistency::LocalQuorum, serial_consistency: None, - values: Cow::Borrowed(values), + values: Cow::Owned(values), page_size: None, paging_state: None, timestamp: None, @@ -22,13 +22,24 @@ fn make_query<'a>(contents: &'a str, values: &'a SerializedValues) -> query::Que } fn serialized_request_make_bench(c: &mut Criterion) { + let mut values = SerializedValues::new(); let mut group = c.benchmark_group("LZ4Compression.SerializedRequest"); let query_args = [ - ("INSERT foo INTO ks.table_name (?)", &(1234,).serialized().unwrap()), - ("INSERT foo, bar, baz INTO ks.table_name (?, ?, ?)", &(1234, "a value", "i am storing a string").serialized().unwrap()), + ("INSERT foo INTO ks.table_name (?)", { + values.add_value(&1234, &ColumnType::Int).unwrap(); + values.clone() + }), + ("INSERT foo, bar, baz INTO ks.table_name (?, ?, ?)", { + values.add_value(&"a value", &ColumnType::Text).unwrap(); + values.add_value(&"i am storing a string", &ColumnType::Text).unwrap(); + values.clone() + }), ( "INSERT foo, bar, baz, boop, blah INTO longer_keyspace.a_big_table_name (?, ?, ?, ?, 1000)", - &(1234, "a value", "i am storing a string", "dc0c8cd7-d954-47c1-8722-a857941c43fb").serialized().unwrap() + { + values.add_value(&"dc0c8cd7-d954-47c1-8722-a857941c43fb", &ColumnType::Text).unwrap(); + values.clone() + } ), ]; let queries = query_args.map(|(q, v)| make_query(q, v)); diff --git a/scylla-cql/src/errors.rs b/scylla-cql/src/errors.rs index 9e80247e20..e884e37ad5 100644 --- a/scylla-cql/src/errors.rs +++ b/scylla-cql/src/errors.rs @@ -3,6 +3,7 @@ use crate::frame::frame_errors::{FrameError, ParseError}; use crate::frame::protocol_features::ProtocolFeatures; use crate::frame::value::SerializeValuesError; +use crate::types::serialize::SerializationError; use crate::Consistency; use bytes::Bytes; use std::io::ErrorKind; @@ -340,6 +341,9 @@ pub enum BadQuery { #[error("Serializing values failed: {0} ")] SerializeValuesError(#[from] SerializeValuesError), + #[error("Serializing values failed: {0} ")] + SerializationError(#[from] SerializationError), + /// Serialized values are too long to compute partition key #[error("Serialized values are too long to compute partition key! Length: {0}, Max allowed length: {1}")] ValuesTooLongForKey(usize, usize), @@ -443,6 +447,12 @@ impl From for QueryError { } } +impl From for QueryError { + fn from(serialized_err: SerializationError) -> QueryError { + QueryError::BadQuery(BadQuery::SerializationError(serialized_err)) + } +} + impl From for QueryError { fn from(parse_error: ParseError) -> QueryError { QueryError::InvalidMessage(format!("Error parsing message: {}", parse_error)) diff --git a/scylla-cql/src/frame/frame_errors.rs b/scylla-cql/src/frame/frame_errors.rs index 3da4e26d01..9a3b228505 100644 --- a/scylla-cql/src/frame/frame_errors.rs +++ b/scylla-cql/src/frame/frame_errors.rs @@ -1,6 +1,7 @@ use super::response; use crate::cql_to_rust::CqlTypeError; use crate::frame::value::SerializeValuesError; +use crate::types::serialize::SerializationError; use thiserror::Error; #[derive(Error, Debug)] @@ -44,5 +45,7 @@ pub enum ParseError { #[error(transparent)] SerializeValuesError(#[from] SerializeValuesError), #[error(transparent)] + SerializationError(#[from] SerializationError), + #[error(transparent)] CqlTypeError(#[from] CqlTypeError), } diff --git a/scylla-cql/src/frame/request/auth_response.rs b/scylla-cql/src/frame/request/auth_response.rs index aa515b8aed..dabbf20d34 100644 --- a/scylla-cql/src/frame/request/auth_response.rs +++ b/scylla-cql/src/frame/request/auth_response.rs @@ -1,5 +1,4 @@ use crate::frame::frame_errors::ParseError; -use bytes::BufMut; use crate::frame::request::{RequestOpcode, SerializableRequest}; use crate::frame::types::write_bytes_opt; @@ -12,7 +11,7 @@ pub struct AuthResponse { impl SerializableRequest for AuthResponse { const OPCODE: RequestOpcode = RequestOpcode::AuthResponse; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { write_bytes_opt(self.response.as_ref(), buf) } } diff --git a/scylla-cql/src/frame/request/batch.rs b/scylla-cql/src/frame/request/batch.rs index 35dd8c3c3b..7f7895e1de 100644 --- a/scylla-cql/src/frame/request/batch.rs +++ b/scylla-cql/src/frame/request/batch.rs @@ -1,11 +1,18 @@ use bytes::{Buf, BufMut}; use std::{borrow::Cow, convert::TryInto}; -use crate::frame::{ - frame_errors::ParseError, - request::{RequestOpcode, SerializableRequest}, - types::{self, SerialConsistency}, - value::{BatchValues, BatchValuesIterator, SerializedValues}, +use crate::{ + frame::{ + frame_errors::ParseError, + request::{RequestOpcode, SerializableRequest}, + types::{self, SerialConsistency}, + value::SerializeValuesError, + }, + types::serialize::{ + raw_batch::{RawBatchValues, RawBatchValuesIterator}, + row::SerializedValues, + RowWriter, SerializationError, + }, }; use super::DeserializableRequest; @@ -20,7 +27,7 @@ pub struct Batch<'b, Statement, Values> where BatchStatement<'b>: From<&'b Statement>, Statement: Clone, - Values: BatchValues, + Values: RawBatchValues, { pub statements: Cow<'b, [Statement]>, pub batch_type: BatchType, @@ -72,11 +79,11 @@ impl SerializableRequest for Batch<'_, Statement, Values> where for<'s> BatchStatement<'s>: From<&'s Statement>, Statement: Clone, - Values: BatchValues, + Values: RawBatchValues, { const OPCODE: RequestOpcode = RequestOpcode::Batch; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { // Serializing type of batch buf.put_u8(self.batch_type as u8); @@ -93,9 +100,23 @@ where let mut value_lists = self.values.batch_values_iter(); for (idx, statement) in self.statements.iter().enumerate() { BatchStatement::from(statement).serialize(buf)?; + + // Reserve two bytes for length + let length_pos = buf.len(); + buf.extend_from_slice(&[0, 0]); + let mut row_writer = RowWriter::new(buf); value_lists - .write_next_to_request(buf) + .serialize_next(&mut row_writer) .ok_or_else(|| counts_mismatch_err(idx, self.statements.len()))??; + // Go back and put the length + let count: u16 = match row_writer.value_count().try_into() { + Ok(n) => n, + Err(_) => { + return Err(SerializationError::new(SerializeValuesError::TooManyValues).into()) + } + }; + buf[length_pos..length_pos + 2].copy_from_slice(&count.to_be_bytes()); + n_serialized_statements += 1; } // At this point, we have all statements serialized. If any values are still left, we have a mismatch. @@ -196,7 +217,7 @@ impl<'b> DeserializableRequest for Batch<'b, BatchStatement<'b>, Vec { impl SerializableRequest for Execute<'_> { const OPCODE: RequestOpcode = RequestOpcode::Execute; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { // Serializing statement id types::write_short_bytes(&self.id[..], buf)?; diff --git a/scylla-cql/src/frame/request/mod.rs b/scylla-cql/src/frame/request/mod.rs index cd41d6bce1..e0146156a2 100644 --- a/scylla-cql/src/frame/request/mod.rs +++ b/scylla-cql/src/frame/request/mod.rs @@ -7,8 +7,9 @@ pub mod query; pub mod register; pub mod startup; +use crate::types::serialize::row::SerializedValues; use crate::{frame::frame_errors::ParseError, Consistency}; -use bytes::{BufMut, Bytes}; +use bytes::Bytes; use num_enum::TryFromPrimitive; pub use auth_response::AuthResponse; @@ -22,7 +23,6 @@ pub use startup::Startup; use self::batch::BatchStatement; use super::types::SerialConsistency; -use super::value::SerializedValues; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, TryFromPrimitive)] #[repr(u8)] @@ -40,7 +40,7 @@ pub enum RequestOpcode { pub trait SerializableRequest { const OPCODE: RequestOpcode; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError>; + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError>; fn to_bytes(&self) -> Result { let mut v = Vec::new(); @@ -112,9 +112,10 @@ mod tests { query::{Query, QueryParameters}, DeserializableRequest, SerializableRequest, }, + response::result::ColumnType, types::{self, SerialConsistency}, - value::SerializedValues, }, + types::serialize::row::SerializedValues, Consistency, }; @@ -130,7 +131,7 @@ mod tests { paging_state: Some(vec![2, 1, 3, 7].into()), values: { let mut vals = SerializedValues::new(); - vals.add_value(&2137).unwrap(); + vals.add_value(&2137, &ColumnType::Int).unwrap(); Cow::Owned(vals) }, }; @@ -157,8 +158,8 @@ mod tests { paging_state: None, values: { let mut vals = SerializedValues::new(); - vals.add_named_value("the_answer", &42).unwrap(); - vals.add_named_value("really?", &2137).unwrap(); + vals.add_value(&42, &ColumnType::Int).unwrap(); + vals.add_value(&2137, &ColumnType::Int).unwrap(); Cow::Owned(vals) }, }; @@ -212,7 +213,7 @@ mod tests { timestamp: None, page_size: None, paging_state: None, - values: Cow::Owned(SerializedValues::new()), + values: Cow::Borrowed(SerializedValues::EMPTY), }; let query = Query { contents: contents.clone(), diff --git a/scylla-cql/src/frame/request/options.rs b/scylla-cql/src/frame/request/options.rs index a1a5e8d5fe..5efdada0c6 100644 --- a/scylla-cql/src/frame/request/options.rs +++ b/scylla-cql/src/frame/request/options.rs @@ -1,5 +1,4 @@ use crate::frame::frame_errors::ParseError; -use bytes::BufMut; use crate::frame::request::{RequestOpcode, SerializableRequest}; @@ -8,7 +7,7 @@ pub struct Options; impl SerializableRequest for Options { const OPCODE: RequestOpcode = RequestOpcode::Options; - fn serialize(&self, _buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, _buf: &mut Vec) -> Result<(), ParseError> { Ok(()) } } diff --git a/scylla-cql/src/frame/request/prepare.rs b/scylla-cql/src/frame/request/prepare.rs index d427389181..c30e25727a 100644 --- a/scylla-cql/src/frame/request/prepare.rs +++ b/scylla-cql/src/frame/request/prepare.rs @@ -1,5 +1,4 @@ use crate::frame::frame_errors::ParseError; -use bytes::BufMut; use crate::{ frame::request::{RequestOpcode, SerializableRequest}, @@ -13,7 +12,7 @@ pub struct Prepare<'a> { impl<'a> SerializableRequest for Prepare<'a> { const OPCODE: RequestOpcode = RequestOpcode::Prepare; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { types::write_long_string(self.query, buf)?; Ok(()) } diff --git a/scylla-cql/src/frame/request/query.rs b/scylla-cql/src/frame/request/query.rs index ff0b0cc867..164118f081 100644 --- a/scylla-cql/src/frame/request/query.rs +++ b/scylla-cql/src/frame/request/query.rs @@ -1,12 +1,14 @@ use std::borrow::Cow; -use crate::frame::{frame_errors::ParseError, types::SerialConsistency}; +use crate::{ + frame::{frame_errors::ParseError, types::SerialConsistency}, + types::serialize::row::SerializedValues, +}; use bytes::{Buf, BufMut, Bytes}; use crate::{ frame::request::{RequestOpcode, SerializableRequest}, frame::types, - frame::value::SerializedValues, }; use super::DeserializableRequest; @@ -36,7 +38,7 @@ pub struct Query<'q> { impl SerializableRequest for Query<'_> { const OPCODE: RequestOpcode = RequestOpcode::Query; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { types::write_long_string(&self.contents, buf)?; self.parameters.serialize(buf)?; Ok(()) @@ -102,10 +104,6 @@ impl QueryParameters<'_> { flags |= FLAG_WITH_DEFAULT_TIMESTAMP; } - if self.values.has_names() { - flags |= FLAG_WITH_NAMES_FOR_VALUES; - } - buf.put_u8(flags); if !self.values.is_empty() { @@ -151,8 +149,14 @@ impl<'q> QueryParameters<'q> { let default_timestamp_flag = (flags & FLAG_WITH_DEFAULT_TIMESTAMP) != 0; let values_have_names_flag = (flags & FLAG_WITH_NAMES_FOR_VALUES) != 0; + if values_have_names_flag { + return Err(ParseError::BadIncomingData( + "Named values in frame are currently unsupported".to_string(), + )); + } + let values = Cow::Owned(if values_flag { - SerializedValues::new_from_frame(buf, values_have_names_flag)? + SerializedValues::new_from_frame(buf)? } else { SerializedValues::new() }); diff --git a/scylla-cql/src/frame/request/register.rs b/scylla-cql/src/frame/request/register.rs index d8f88ea3d7..c29c821964 100644 --- a/scylla-cql/src/frame/request/register.rs +++ b/scylla-cql/src/frame/request/register.rs @@ -1,5 +1,3 @@ -use bytes::BufMut; - use crate::frame::{ frame_errors::ParseError, request::{RequestOpcode, SerializableRequest}, @@ -14,7 +12,7 @@ pub struct Register { impl SerializableRequest for Register { const OPCODE: RequestOpcode = RequestOpcode::Register; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { let event_types_list = self .event_types_to_register_for .iter() diff --git a/scylla-cql/src/frame/request/startup.rs b/scylla-cql/src/frame/request/startup.rs index 044a98830c..a1d41df5c4 100644 --- a/scylla-cql/src/frame/request/startup.rs +++ b/scylla-cql/src/frame/request/startup.rs @@ -1,5 +1,4 @@ use crate::frame::frame_errors::ParseError; -use bytes::BufMut; use std::collections::HashMap; @@ -15,7 +14,7 @@ pub struct Startup { impl SerializableRequest for Startup { const OPCODE: RequestOpcode = RequestOpcode::Startup; - fn serialize(&self, buf: &mut impl BufMut) -> Result<(), ParseError> { + fn serialize(&self, buf: &mut Vec) -> Result<(), ParseError> { types::write_string_map(&self.options, buf)?; Ok(()) } diff --git a/scylla-cql/src/frame/response/cql_to_rust.rs b/scylla-cql/src/frame/response/cql_to_rust.rs index 9a95ffc803..19fc72cbe2 100644 --- a/scylla-cql/src/frame/response/cql_to_rust.rs +++ b/scylla-cql/src/frame/response/cql_to_rust.rs @@ -1,7 +1,6 @@ use super::result::{CqlValue, Row}; -use crate::frame::value::{Counter, CqlDuration}; +use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use bigdecimal::BigDecimal; -use chrono::{DateTime, Duration, NaiveDate, TimeZone, Utc}; use num_bigint::BigInt; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{BuildHasher, Hash}; @@ -9,6 +8,9 @@ use std::net::IpAddr; use thiserror::Error; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; + #[cfg(feature = "secret")] use secrecy::{Secret, Zeroize}; @@ -125,7 +127,6 @@ impl_from_cql_value_from_method!(Counter, as_counter); // Counter::from_cql impl_from_cql_value_from_method!(BigInt, into_varint); // BigInt::from_cql impl_from_cql_value_from_method!(i8, as_tinyint); // i8::from_cql -impl_from_cql_value_from_method!(NaiveDate, as_date); // NaiveDate::from_cql impl_from_cql_value_from_method!(f32, as_float); // f32::from_cql impl_from_cql_value_from_method!(f64, as_double); // f64::from_cql impl_from_cql_value_from_method!(bool, as_boolean); // bool::from_cql @@ -134,8 +135,10 @@ impl_from_cql_value_from_method!(Vec, into_blob); // Vec::from_cql impl_from_cql_value_from_method!(Uuid, as_uuid); // Uuid::from_cql impl_from_cql_value_from_method!(BigDecimal, into_decimal); // BigDecimal::from_cql -impl_from_cql_value_from_method!(Duration, as_duration); // Duration::from_cql impl_from_cql_value_from_method!(CqlDuration, as_cql_duration); // CqlDuration::from_cql +impl_from_cql_value_from_method!(CqlDate, as_cql_date); // CqlDate::from_cql +impl_from_cql_value_from_method!(CqlTime, as_cql_time); // CqlTime::from_cql +impl_from_cql_value_from_method!(CqlTimestamp, as_cql_timestamp); // CqlTimestamp::from_cql impl FromCqlVal for [u8; N] { fn from_cql(cql_val: CqlValue) -> Result { @@ -144,43 +147,68 @@ impl FromCqlVal for [u8; N] { } } -impl FromCqlVal for crate::frame::value::Date { +#[cfg(feature = "chrono")] +impl FromCqlVal for NaiveDate { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { - CqlValue::Date(d) => Ok(crate::frame::value::Date(d)), + CqlValue::Date(cql_date) => cql_date.try_into().map_err(|_| FromCqlValError::BadVal), _ => Err(FromCqlValError::BadCqlType), } } } -impl FromCqlVal for crate::frame::value::Time { +#[cfg(feature = "time")] +impl FromCqlVal for time::Date { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { - CqlValue::Time(d) => Ok(Self(d)), + CqlValue::Date(cql_date) => cql_date.try_into().map_err(|_| FromCqlValError::BadVal), _ => Err(FromCqlValError::BadCqlType), } } } -impl FromCqlVal for crate::frame::value::Timestamp { +#[cfg(feature = "chrono")] +impl FromCqlVal for NaiveTime { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { - CqlValue::Timestamp(d) => Ok(Self(d)), + CqlValue::Time(cql_time) => cql_time.try_into().map_err(|_| FromCqlValError::BadVal), _ => Err(FromCqlValError::BadCqlType), } } } -impl FromCqlVal for DateTime { +#[cfg(feature = "time")] +impl FromCqlVal for time::Time { fn from_cql(cql_val: CqlValue) -> Result { - let timestamp = cql_val.as_bigint().ok_or(FromCqlValError::BadCqlType)?; - match Utc.timestamp_millis_opt(timestamp) { - chrono::LocalResult::Single(datetime) => Ok(datetime), - _ => Err(FromCqlValError::BadVal), + match cql_val { + CqlValue::Time(cql_time) => cql_time.try_into().map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), } } } +#[cfg(feature = "chrono")] +impl FromCqlVal for DateTime { + fn from_cql(cql_val: CqlValue) -> Result { + cql_val + .as_cql_timestamp() + .ok_or(FromCqlValError::BadCqlType)? + .try_into() + .map_err(|_| FromCqlValError::BadVal) + } +} + +#[cfg(feature = "time")] +impl FromCqlVal for time::OffsetDateTime { + fn from_cql(cql_val: CqlValue) -> Result { + cql_val + .as_cql_timestamp() + .ok_or(FromCqlValError::BadCqlType)? + .try_into() + .map_err(|_| FromCqlValError::BadVal) + } +} + #[cfg(feature = "secret")] impl + Zeroize> FromCqlVal for Secret { fn from_cql(cql_val: CqlValue) -> Result { @@ -362,10 +390,9 @@ impl_tuple_from_cql!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 mod tests { use super::{CqlValue, FromCqlVal, FromCqlValError, FromRow, FromRowError, Row}; use crate as scylla; - use crate::frame::value::Counter; + use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use crate::macros::FromRow; use bigdecimal::BigDecimal; - use chrono::{Duration, NaiveDate}; use num_bigint::{BigInt, ToBigInt}; use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr}; @@ -454,71 +481,90 @@ mod tests { assert_eq!(Ok(counter), Counter::from_cql(CqlValue::Counter(counter))); } + #[cfg(feature = "chrono")] #[test] fn naive_date_from_cql() { - let unix_epoch: CqlValue = CqlValue::Date(2_u32.pow(31)); + use chrono::NaiveDate; + + let unix_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31))); assert_eq!( Ok(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), NaiveDate::from_cql(unix_epoch) ); - let before_epoch: CqlValue = CqlValue::Date(2_u32.pow(31) - 30); + let before_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31) - 30)); assert_eq!( Ok(NaiveDate::from_ymd_opt(1969, 12, 2).unwrap()), NaiveDate::from_cql(before_epoch) ); - let after_epoch: CqlValue = CqlValue::Date(2_u32.pow(31) + 30); + let after_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31) + 30)); assert_eq!( Ok(NaiveDate::from_ymd_opt(1970, 1, 31).unwrap()), NaiveDate::from_cql(after_epoch) ); - let min_date: CqlValue = CqlValue::Date(0); + let min_date: CqlValue = CqlValue::Date(CqlDate(0)); assert!(NaiveDate::from_cql(min_date).is_err()); - let max_date: CqlValue = CqlValue::Date(u32::MAX); + let max_date: CqlValue = CqlValue::Date(CqlDate(u32::MAX)); assert!(NaiveDate::from_cql(max_date).is_err()); } #[test] - fn date_from_cql() { - use crate::frame::value::Date; + fn cql_date_from_cql() { + let unix_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31))); + assert_eq!(Ok(CqlDate(2_u32.pow(31))), CqlDate::from_cql(unix_epoch)); - let unix_epoch: CqlValue = CqlValue::Date(2_u32.pow(31)); - assert_eq!(Ok(Date(2_u32.pow(31))), Date::from_cql(unix_epoch)); + let min_date: CqlValue = CqlValue::Date(CqlDate(0)); + assert_eq!(Ok(CqlDate(0)), CqlDate::from_cql(min_date)); - let min_date: CqlValue = CqlValue::Date(0); - assert_eq!(Ok(Date(0)), Date::from_cql(min_date)); - - let max_date: CqlValue = CqlValue::Date(u32::MAX); - assert_eq!(Ok(Date(u32::MAX)), Date::from_cql(max_date)); + let max_date: CqlValue = CqlValue::Date(CqlDate(u32::MAX)); + assert_eq!(Ok(CqlDate(u32::MAX)), CqlDate::from_cql(max_date)); } + #[cfg(feature = "time")] #[test] - fn duration_from_cql() { - let time_duration = Duration::nanoseconds(86399999999999); + fn date_from_cql() { + // UNIX epoch + let unix_epoch = CqlValue::Date(CqlDate(1 << 31)); + assert_eq!( + Ok(time::Date::from_ordinal_date(1970, 1).unwrap()), + time::Date::from_cql(unix_epoch) + ); + + // 7 days after UNIX epoch + let after_epoch = CqlValue::Date(CqlDate((1 << 31) + 7)); assert_eq!( - time_duration, - Duration::from_cql(CqlValue::Time(time_duration)).unwrap(), + Ok(time::Date::from_ordinal_date(1970, 8).unwrap()), + time::Date::from_cql(after_epoch) ); - let timestamp_duration = Duration::milliseconds(i64::MIN); + // 3 days before UNIX epoch + let before_epoch = CqlValue::Date(CqlDate((1 << 31) - 3)); assert_eq!( - timestamp_duration, - Duration::from_cql(CqlValue::Timestamp(timestamp_duration)).unwrap(), + Ok(time::Date::from_ordinal_date(1969, 363).unwrap()), + time::Date::from_cql(before_epoch) ); - let timestamp_i64 = 997; + // Min possible stored date. Since value is out of `time::Date` range, it should return `BadVal` error + let min_date = CqlValue::Date(CqlDate(u32::MIN)); + assert_eq!(Err(FromCqlValError::BadVal), time::Date::from_cql(min_date)); + + // Max possible stored date. Since value is out of `time::Date` range, it should return `BadVal` error + let max_date = CqlValue::Date(CqlDate(u32::MAX)); + assert_eq!(Err(FromCqlValError::BadVal), time::Date::from_cql(max_date)); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); assert_eq!( - timestamp_i64, - i64::from_cql(CqlValue::Timestamp(Duration::milliseconds(timestamp_i64))).unwrap() - ) + Err(FromCqlValError::BadCqlType), + time::Date::from_cql(bad_type) + ); } #[test] fn cql_duration_from_cql() { - use crate::frame::value::CqlDuration; let cql_duration = CqlDuration { months: 3, days: 2, @@ -530,31 +576,114 @@ mod tests { ); } + #[test] + fn cql_time_from_cql() { + let time_ns = 86399999999999; + let cql_value = CqlValue::Time(CqlTime(time_ns)); + assert_eq!(time_ns, CqlTime::from_cql(cql_value).unwrap().0); + } + + #[cfg(feature = "chrono")] + #[test] + fn naive_time_from_cql() { + use chrono::NaiveTime; + + // Midnight + let midnight = CqlValue::Time(CqlTime(0)); + assert_eq!(Ok(NaiveTime::MIN), NaiveTime::from_cql(midnight)); + + // 7:15:21.123456789 + let morning = CqlValue::Time(CqlTime( + (7 * 3600 + 15 * 60 + 21) * 1_000_000_000 + 123_456_789, + )); + assert_eq!( + Ok(NaiveTime::from_hms_nano_opt(7, 15, 21, 123_456_789).unwrap()), + NaiveTime::from_cql(morning) + ); + + // 23:59:59.999999999 + let late_night = CqlValue::Time(CqlTime( + (23 * 3600 + 59 * 60 + 59) * 1_000_000_000 + 999_999_999, + )); + assert_eq!( + Ok(NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap()), + NaiveTime::from_cql(late_night) + ); + + // Bad values. Since value is out of `chrono::NaiveTime` range, it should return `BadVal` error + let bad_time1 = CqlValue::Time(CqlTime(-1)); + assert_eq!(Err(FromCqlValError::BadVal), NaiveTime::from_cql(bad_time1)); + let bad_time2 = CqlValue::Time(CqlTime(i64::MAX)); + assert_eq!(Err(FromCqlValError::BadVal), NaiveTime::from_cql(bad_time2)); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); + assert_eq!( + Err(FromCqlValError::BadCqlType), + NaiveTime::from_cql(bad_type) + ); + } + + #[cfg(feature = "time")] #[test] fn time_from_cql() { - use crate::frame::value::Time; - let time_duration = Duration::nanoseconds(86399999999999); + // Midnight + let midnight = CqlValue::Time(CqlTime(0)); + assert_eq!(Ok(time::Time::MIDNIGHT), time::Time::from_cql(midnight)); + + // 7:15:21.123456789 + let morning = CqlValue::Time(CqlTime( + (7 * 3600 + 15 * 60 + 21) * 1_000_000_000 + 123_456_789, + )); assert_eq!( - time_duration, - Time::from_cql(CqlValue::Time(time_duration)).unwrap().0, + Ok(time::Time::from_hms_nano(7, 15, 21, 123_456_789).unwrap()), + time::Time::from_cql(morning) + ); + + // 23:59:59.999999999 + let late_night = CqlValue::Time(CqlTime( + (23 * 3600 + 59 * 60 + 59) * 1_000_000_000 + 999_999_999, + )); + assert_eq!( + Ok(time::Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap()), + time::Time::from_cql(late_night) + ); + + // Bad values. Since value is out of `time::Time` range, it should return `BadVal` error + let bad_time1 = CqlValue::Time(CqlTime(-1)); + assert_eq!( + Err(FromCqlValError::BadVal), + time::Time::from_cql(bad_time1) + ); + let bad_time2 = CqlValue::Time(CqlTime(i64::MAX)); + assert_eq!( + Err(FromCqlValError::BadVal), + time::Time::from_cql(bad_time2) + ); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); + assert_eq!( + Err(FromCqlValError::BadCqlType), + time::Time::from_cql(bad_type) ); } #[test] - fn timestamp_from_cql() { - use crate::frame::value::Timestamp; - let timestamp_duration = Duration::milliseconds(86399999999999); + fn cql_timestamp_from_cql() { + let timestamp_ms = 86399999999999; assert_eq!( - timestamp_duration, - Timestamp::from_cql(CqlValue::Timestamp(timestamp_duration)) + timestamp_ms, + CqlTimestamp::from_cql(CqlValue::Timestamp(CqlTimestamp(timestamp_ms))) .unwrap() .0, ); } + #[cfg(feature = "chrono")] #[test] fn datetime_from_cql() { - use chrono::{DateTime, Duration, Utc}; + use chrono::{DateTime, NaiveDate, Utc}; let naivedatetime_utc = NaiveDate::from_ymd_opt(2022, 12, 31) .unwrap() .and_hms_opt(2, 0, 0) @@ -563,13 +692,65 @@ mod tests { assert_eq!( datetime_utc, - DateTime::::from_cql(CqlValue::Timestamp(Duration::milliseconds( + DateTime::::from_cql(CqlValue::Timestamp(CqlTimestamp( datetime_utc.timestamp_millis() ))) .unwrap() ); } + #[cfg(feature = "time")] + #[test] + fn offset_datetime_from_cql() { + // UNIX epoch + let unix_epoch = CqlValue::Timestamp(CqlTimestamp(0)); + assert_eq!( + Ok(time::OffsetDateTime::UNIX_EPOCH), + time::OffsetDateTime::from_cql(unix_epoch) + ); + + // 1 day 2 hours 3 minutes 4 seconds and 5 nanoseconds before UNIX epoch + let before_epoch = CqlValue::Timestamp(CqlTimestamp(-(26 * 3600 + 3 * 60 + 4) * 1000 - 5)); + assert_eq!( + Ok(time::OffsetDateTime::UNIX_EPOCH + - time::Duration::new(26 * 3600 + 3 * 60 + 4, 5 * 1_000_000)), + time::OffsetDateTime::from_cql(before_epoch) + ); + + // 6 days 7 hours 8 minutes 9 seconds and 10 nanoseconds after UNIX epoch + let after_epoch = + CqlValue::Timestamp(CqlTimestamp(((6 * 24 + 7) * 3600 + 8 * 60 + 9) * 1000 + 10)); + assert_eq!( + Ok(time::PrimitiveDateTime::new( + time::Date::from_ordinal_date(1970, 7).unwrap(), + time::Time::from_hms_milli(7, 8, 9, 10).unwrap() + ) + .assume_utc()), + time::OffsetDateTime::from_cql(after_epoch) + ); + + // Min possible stored timestamp. Since value is out of `time::OffsetDateTime` range, it should return `BadVal` error + let min_timestamp = CqlValue::Timestamp(CqlTimestamp(i64::MIN)); + assert_eq!( + Err(FromCqlValError::BadVal), + time::OffsetDateTime::from_cql(min_timestamp) + ); + + // Max possible stored timestamp. Since value is out of `time::OffsetDateTime` range, it should return `BadVal` error + let max_timestamp = CqlValue::Timestamp(CqlTimestamp(i64::MAX)); + assert_eq!( + Err(FromCqlValError::BadVal), + time::OffsetDateTime::from_cql(max_timestamp) + ); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); + assert_eq!( + Err(FromCqlValError::BadCqlType), + time::OffsetDateTime::from_cql(bad_type) + ); + } + #[test] fn uuid_from_cql() { let test_uuid: Uuid = Uuid::parse_str("8e14e760-7fa8-11eb-bc66-000000000001").unwrap(); diff --git a/scylla-cql/src/frame/response/result.rs b/scylla-cql/src/frame/response/result.rs index 5ade677343..f516d6e510 100644 --- a/scylla-cql/src/frame/response/result.rs +++ b/scylla-cql/src/frame/response/result.rs @@ -1,13 +1,11 @@ use crate::cql_to_rust::{FromRow, FromRowError}; use crate::frame::response::event::SchemaChangeEvent; use crate::frame::types::vint_decode; -use crate::frame::value::{Counter, CqlDuration}; +use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use crate::frame::{frame_errors::ParseError, types}; use bigdecimal::BigDecimal; use byteorder::{BigEndian, ReadBytesExt}; use bytes::{Buf, Bytes}; -use chrono; -use chrono::prelude::*; use num_bigint::BigInt; use std::{ convert::{TryFrom, TryInto}, @@ -17,6 +15,9 @@ use std::{ }; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, Utc}; + #[derive(Debug)] pub struct SetKeyspace { pub keyspace_name: String, @@ -83,7 +84,7 @@ pub enum CqlValue { Decimal(BigDecimal), /// Days since -5877641-06-23 i.e. 2^31 days before unix epoch /// Can be converted to chrono::NaiveDate (-262145-1-1 to 262143-12-31) using as_date - Date(u32), + Date(CqlDate), Double(f64), Duration(CqlDuration), Empty, @@ -92,7 +93,7 @@ pub enum CqlValue { BigInt(i64), Text(String), /// Milliseconds since unix epoch - Timestamp(chrono::Duration), + Timestamp(CqlTimestamp), Inet(IpAddr), List(Vec), Map(Vec<(CqlValue, CqlValue)>), @@ -108,13 +109,38 @@ pub enum CqlValue { SmallInt(i16), TinyInt(i8), /// Nanoseconds since midnight - Time(chrono::Duration), + Time(CqlTime), Timeuuid(Uuid), Tuple(Vec>), Uuid(Uuid), Varint(BigInt), } +impl ColumnType { + // Returns true if the type allows a special, empty value in addition to its + // natural representation. For example, bigint represents a 32-bit integer, + // but it can also hold a 0-bit empty value. + // + // It looks like Cassandra 4.1.3 rejects empty values for some more types than + // Scylla: date, time, smallint and tinyint. We will only check against + // Scylla's set of types supported for empty values as it's smaller; + // with Cassandra, some rejects will just have to be rejected on the db side. + pub(crate) fn supports_special_empty_value(&self) -> bool { + #[allow(clippy::match_like_matches_macro)] + match self { + ColumnType::Counter + | ColumnType::Duration + | ColumnType::List(_) + | ColumnType::Map(_, _) + | ColumnType::Set(_) + | ColumnType::UserDefinedType { .. } + | ColumnType::Custom(_) => false, + + _ => true, + } + } +} + impl CqlValue { pub fn as_ascii(&self) -> Option<&String> { match self { @@ -123,31 +149,57 @@ impl CqlValue { } } - pub fn as_date(&self) -> Option { - // Days since -5877641-06-23 i.e. 2^31 days before unix epoch - let date_days: u32 = match self { - CqlValue::Date(days) => *days, - _ => return None, - }; + pub fn as_cql_date(&self) -> Option { + match self { + Self::Date(d) => Some(*d), + _ => None, + } + } - // date_days is u32 then converted to i64 - // then we substract 2^31 - this can't panic - let days_since_epoch = - chrono::Duration::days(date_days.into()) - chrono::Duration::days(1 << 31); + #[cfg(feature = "chrono")] + pub fn as_naive_date(&self) -> Option { + self.as_cql_date().and_then(|date| date.try_into().ok()) + } - NaiveDate::from_ymd_opt(1970, 1, 1) - .unwrap() - .checked_add_signed(days_since_epoch) + #[cfg(feature = "time")] + pub fn as_date(&self) -> Option { + self.as_cql_date().and_then(|date| date.try_into().ok()) } - pub fn as_duration(&self) -> Option { + pub fn as_cql_timestamp(&self) -> Option { match self { Self::Timestamp(i) => Some(*i), + _ => None, + } + } + + #[cfg(feature = "chrono")] + pub fn as_datetime(&self) -> Option> { + self.as_cql_timestamp().and_then(|ts| ts.try_into().ok()) + } + + #[cfg(feature = "time")] + pub fn as_offset_date_time(&self) -> Option { + self.as_cql_timestamp().and_then(|ts| ts.try_into().ok()) + } + + pub fn as_cql_time(&self) -> Option { + match self { Self::Time(i) => Some(*i), _ => None, } } + #[cfg(feature = "chrono")] + pub fn as_naive_time(&self) -> Option { + self.as_cql_time().and_then(|ts| ts.try_into().ok()) + } + + #[cfg(feature = "time")] + pub fn as_time(&self) -> Option { + self.as_cql_time().and_then(|ts| ts.try_into().ok()) + } + pub fn as_cql_duration(&self) -> Option { match self { Self::Duration(i) => Some(*i), @@ -201,7 +253,6 @@ impl CqlValue { pub fn as_bigint(&self) -> Option { match self { Self::BigInt(i) => Some(*i), - Self::Timestamp(d) => Some(d.num_milliseconds()), _ => None, } } @@ -608,7 +659,7 @@ pub fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult()?; - CqlValue::Date(date_value) + CqlValue::Date(CqlDate(date_value)) } Counter => { if buf.len() != 8 { @@ -691,7 +742,7 @@ pub fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult()?; - CqlValue::Timestamp(chrono::Duration::milliseconds(millis)) + CqlValue::Timestamp(CqlTimestamp(millis)) } Time => { if buf.len() != 8 { @@ -709,7 +760,7 @@ pub fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult { if buf.len() != 16 { @@ -915,10 +966,8 @@ pub fn deserialize(buf: &mut &[u8]) -> StdResult { #[cfg(test)] mod tests { use crate as scylla; - use crate::frame::value::{Counter, CqlDuration}; + use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use bigdecimal::BigDecimal; - use chrono::Duration; - use chrono::NaiveDate; use num_bigint::BigInt; use num_bigint::ToBigInt; use scylla::frame::response::result::{ColumnType, CqlValue}; @@ -1212,73 +1261,168 @@ mod tests { } #[test] - fn date_deserialize() { + fn test_deserialize_date() { // Date is correctly parsed from a 4 byte array let four_bytes: [u8; 4] = [12, 23, 34, 45]; let date: CqlValue = super::deser_cql_value(&ColumnType::Date, &mut four_bytes.as_ref()).unwrap(); - assert_eq!(date, CqlValue::Date(u32::from_be_bytes(four_bytes))); + assert_eq!( + date, + CqlValue::Date(CqlDate(u32::from_be_bytes(four_bytes))) + ); // Date is parsed as u32 not i32, u32::MAX is u32::MAX let date: CqlValue = super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) .unwrap(); - assert_eq!(date, CqlValue::Date(u32::MAX)); + assert_eq!(date, CqlValue::Date(CqlDate(u32::MAX))); // Trying to parse a 0, 3 or 5 byte array fails super::deser_cql_value(&ColumnType::Date, &mut [].as_ref()).unwrap(); super::deser_cql_value(&ColumnType::Date, &mut [1, 2, 3].as_ref()).unwrap_err(); super::deser_cql_value(&ColumnType::Date, &mut [1, 2, 3, 4, 5].as_ref()).unwrap_err(); + // Deserialize unix epoch + let unix_epoch_bytes = 2_u32.pow(31).to_be_bytes(); + + let date = + super::deser_cql_value(&ColumnType::Date, &mut unix_epoch_bytes.as_ref()).unwrap(); + assert_eq!(date.as_cql_date(), Some(CqlDate(1 << 31))); + + // 2^31 - 30 when converted to NaiveDate is 1969-12-02 + let before_epoch = CqlDate((1 << 31) - 30); + let date: CqlValue = super::deser_cql_value( + &ColumnType::Date, + &mut ((1_u32 << 31) - 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_cql_date(), Some(before_epoch)); + + // 2^31 + 30 when converted to NaiveDate is 1970-01-31 + let after_epoch = CqlDate((1 << 31) + 30); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1_u32 << 31) + 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_cql_date(), Some(after_epoch)); + + // Min date + let min_date = CqlDate(u32::MIN); + let date = super::deser_cql_value(&ColumnType::Date, &mut u32::MIN.to_be_bytes().as_ref()) + .unwrap(); + assert_eq!(date.as_cql_date(), Some(min_date)); + + // Max date + let max_date = CqlDate(u32::MAX); + let date = super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) + .unwrap(); + assert_eq!(date.as_cql_date(), Some(max_date)); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_naive_date_from_cql() { + use chrono::NaiveDate; + // 2^31 when converted to NaiveDate is 1970-01-01 - let unix_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); - let date: CqlValue = - super::deser_cql_value(&ColumnType::Date, &mut 2_u32.pow(31).to_be_bytes().as_ref()) + let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + let date = + super::deser_cql_value(&ColumnType::Date, &mut (1u32 << 31).to_be_bytes().as_ref()) .unwrap(); - assert_eq!(date.as_date().unwrap(), unix_epoch); + assert_eq!(date.as_naive_date(), Some(unix_epoch)); // 2^31 - 30 when converted to NaiveDate is 1969-12-02 - let before_epoch: NaiveDate = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); - let date: CqlValue = super::deser_cql_value( + let before_epoch = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); + let date = super::deser_cql_value( &ColumnType::Date, - &mut (2_u32.pow(31) - 30).to_be_bytes().as_ref(), + &mut ((1u32 << 31) - 30).to_be_bytes().as_ref(), ) .unwrap(); - assert_eq!(date.as_date().unwrap(), before_epoch); + assert_eq!(date.as_naive_date(), Some(before_epoch)); // 2^31 + 30 when converted to NaiveDate is 1970-01-31 - let after_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); - let date: CqlValue = super::deser_cql_value( + let after_epoch = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); + let date = super::deser_cql_value( &ColumnType::Date, - &mut (2_u32.pow(31) + 30).to_be_bytes().as_ref(), + &mut ((1u32 << 31) + 30).to_be_bytes().as_ref(), ) .unwrap(); - assert_eq!(date.as_date().unwrap(), after_epoch); + assert_eq!(date.as_naive_date(), Some(after_epoch)); - // 0 and u32::MAX is out of NaiveDate range, fails with an error, not panics - assert!( + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( super::deser_cql_value(&ColumnType::Date, &mut 0_u32.to_be_bytes().as_ref()) .unwrap() - .as_date() - .is_none() + .as_naive_date(), + None ); - assert!( + assert_eq!( super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) .unwrap() - .as_date() - .is_none() + .as_naive_date(), + None ); + } - // It's hard to test NaiveDate more because it involves calculating days between calendar dates - // There are more tests using database queries that should cover it + #[cfg(feature = "time")] + #[test] + fn test_date_from_cql() { + use time::Date; + use time::Month::*; + + // 2^31 when converted to time::Date is 1970-01-01 + let unix_epoch = Date::from_calendar_date(1970, January, 1).unwrap(); + let date = + super::deser_cql_value(&ColumnType::Date, &mut (1u32 << 31).to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_date(), Some(unix_epoch)); + + // 2^31 - 30 when converted to time::Date is 1969-12-02 + let before_epoch = Date::from_calendar_date(1969, December, 2).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) - 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_date(), Some(before_epoch)); + + // 2^31 + 30 when converted to time::Date is 1970-01-31 + let after_epoch = Date::from_calendar_date(1970, January, 31).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) + 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_date(), Some(after_epoch)); + + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( + super::deser_cql_value(&ColumnType::Date, &mut 0_u32.to_be_bytes().as_ref()) + .unwrap() + .as_date(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_date(), + None + ); } #[test] - fn test_time_deserialize() { + fn test_deserialize_time() { // Time is an i64 - nanoseconds since midnight // in range 0..=86399999999999 @@ -1290,7 +1434,7 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); let cql_value: CqlValue = super::deser_cql_value(&ColumnType::Time, &mut &bytes[..]).unwrap(); - assert_eq!(cql_value, CqlValue::Time(Duration::nanoseconds(*test_val))); + assert_eq!(cql_value, CqlValue::Time(CqlTime(*test_val))); } // Negative values cause an error @@ -1299,19 +1443,85 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); super::deser_cql_value(&ColumnType::Time, &mut &bytes[..]).unwrap_err(); } + } - // chrono::Duration has enough precision to represent nanoseconds accurately - assert_eq!(Duration::nanoseconds(1).num_nanoseconds().unwrap(), 1); - assert_eq!( - Duration::nanoseconds(7364737473).num_nanoseconds().unwrap(), - 7364737473 - ); - assert_eq!( - Duration::nanoseconds(86399999999999) - .num_nanoseconds() - .unwrap(), - 86399999999999 - ); + #[cfg(feature = "chrono")] + #[test] + fn test_naive_time_from_cql() { + use chrono::NaiveTime; + + // 0 when converted to NaiveTime is 0:0:0.0 + let midnight = NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(); + let time = + super::deser_cql_value(&ColumnType::Time, &mut (0i64).to_be_bytes().as_ref()).unwrap(); + + assert_eq!(time.as_naive_time(), Some(midnight)); + + // 10:10:30.500,000,001 + let (h, m, s, n) = (10, 10, 30, 500_000_001); + let midnight = NaiveTime::from_hms_nano_opt(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_naive_time(), Some(midnight)); + + // 23:59:59.999,999,999 + let (h, m, s, n) = (23, 59, 59, 999_999_999); + let midnight = NaiveTime::from_hms_nano_opt(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_naive_time(), Some(midnight)); + } + + #[cfg(feature = "time")] + #[test] + fn test_primitive_time_from_cql() { + use time::Time; + + // 0 when converted to NaiveTime is 0:0:0.0 + let midnight = Time::from_hms_nano(0, 0, 0, 0).unwrap(); + let time = + super::deser_cql_value(&ColumnType::Time, &mut (0i64).to_be_bytes().as_ref()).unwrap(); + + dbg!(&time); + assert_eq!(time.as_time(), Some(midnight)); + + // 10:10:30.500,000,001 + let (h, m, s, n) = (10, 10, 30, 500_000_001); + let midnight = Time::from_hms_nano(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_time(), Some(midnight)); + + // 23:59:59.999,999,999 + let (h, m, s, n) = (23, 59, 59, 999_999_999); + let midnight = Time::from_hms_nano(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_time(), Some(midnight)); } #[test] @@ -1323,19 +1533,126 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); let cql_value: CqlValue = super::deser_cql_value(&ColumnType::Timestamp, &mut &bytes[..]).unwrap(); - assert_eq!( - cql_value, - CqlValue::Timestamp(Duration::milliseconds(*test_val)) - ); - - // Check that Duration converted back to i64 is correct - assert_eq!( - Duration::milliseconds(*test_val).num_milliseconds(), - *test_val - ); + assert_eq!(cql_value, CqlValue::Timestamp(CqlTimestamp(*test_val))); } } + #[cfg(feature = "chrono")] + #[test] + fn test_datetime_from_cql() { + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + // 0 when converted to DateTime is 1970-01-01 0:00:00.00 + let unix_epoch = NaiveDateTime::from_timestamp_opt(0, 0).unwrap().and_utc(); + let date = super::deser_cql_value(&ColumnType::Timestamp, &mut 0i64.to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_datetime(), Some(unix_epoch)); + + // When converted to NaiveDateTime, this is 1969-12-01 11:29:29.5 + let timestamp: i64 = -((((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500); + let before_epoch = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1969, 12, 1).unwrap(), + NaiveTime::from_hms_milli_opt(11, 29, 29, 500).unwrap(), + ) + .and_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime(), Some(before_epoch)); + + // when converted to NaiveDateTime, this is is 1970-01-31 12:30:30.5 + let timestamp: i64 = (((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500; + let after_epoch = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(), + NaiveTime::from_hms_milli_opt(12, 30, 30, 500).unwrap(), + ) + .and_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime(), Some(after_epoch)); + + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MIN.to_be_bytes().as_ref()) + .unwrap() + .as_datetime(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_datetime(), + None + ); + } + + #[cfg(feature = "time")] + #[test] + fn test_offset_datetime_from_cql() { + use time::{Date, Month::*, OffsetDateTime, PrimitiveDateTime, Time}; + + // 0 when converted to OffsetDateTime is 1970-01-01 0:00:00.00 + let unix_epoch = OffsetDateTime::from_unix_timestamp(0).unwrap(); + let date = super::deser_cql_value(&ColumnType::Timestamp, &mut 0i64.to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), Some(unix_epoch)); + + // When converted to NaiveDateTime, this is 1969-12-01 11:29:29.5 + let timestamp: i64 = -((((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500); + let before_epoch = PrimitiveDateTime::new( + Date::from_calendar_date(1969, December, 1).unwrap(), + Time::from_hms_milli(11, 29, 29, 500).unwrap(), + ) + .assume_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), Some(before_epoch)); + + // when converted to NaiveDateTime, this is is 1970-01-31 12:30:30.5 + let timestamp: i64 = (((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500; + let after_epoch = PrimitiveDateTime::new( + Date::from_calendar_date(1970, January, 31).unwrap(), + Time::from_hms_milli(12, 30, 30, 500).unwrap(), + ) + .assume_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), Some(after_epoch)); + + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MIN.to_be_bytes().as_ref()) + .unwrap() + .as_offset_date_time(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_offset_date_time(), + None + ); + } + #[test] fn test_serialize_empty() { use crate::frame::value::Value; diff --git a/scylla-cql/src/frame/types.rs b/scylla-cql/src/frame/types.rs index 672fe2f97e..5de8124111 100644 --- a/scylla-cql/src/frame/types.rs +++ b/scylla-cql/src/frame/types.rs @@ -104,6 +104,23 @@ impl From for ParseError { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RawValue<'a> { + Null, + Unset, + Value(&'a [u8]), +} + +impl<'a> RawValue<'a> { + #[inline] + pub fn as_value(&self) -> Option<&'a [u8]> { + match self { + RawValue::Value(v) => Some(v), + RawValue::Null | RawValue::Unset => None, + } + } +} + fn read_raw_bytes<'a>(count: usize, buf: &mut &'a [u8]) -> Result<&'a [u8], ParseError> { if buf.len() < count { return Err(ParseError::BadIncomingData(format!( @@ -218,6 +235,22 @@ pub fn read_bytes<'a>(buf: &mut &'a [u8]) -> Result<&'a [u8], ParseError> { Ok(v) } +pub fn read_value<'a>(buf: &mut &'a [u8]) -> Result, ParseError> { + let len = read_int(buf)?; + match len { + -2 => Ok(RawValue::Unset), + -1 => Ok(RawValue::Null), + len if len >= 0 => { + let v = read_raw_bytes(len as usize, buf)?; + Ok(RawValue::Value(v)) + } + len => Err(ParseError::BadIncomingData(format!( + "invalid value length: {}", + len, + ))), + } +} + pub fn read_short_bytes<'a>(buf: &mut &'a [u8]) -> Result<&'a [u8], ParseError> { let len = read_short_length(buf)?; let v = read_raw_bytes(len, buf)?; diff --git a/scylla-cql/src/frame/value.rs b/scylla-cql/src/frame/value.rs index 17b75ea855..3c17c8629f 100644 --- a/scylla-cql/src/frame/value.rs +++ b/scylla-cql/src/frame/value.rs @@ -2,8 +2,6 @@ use crate::frame::frame_errors::ParseError; use crate::frame::types; use bigdecimal::BigDecimal; use bytes::BufMut; -use chrono::prelude::*; -use chrono::Duration; use num_bigint::BigInt; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; @@ -13,8 +11,12 @@ use std::net::IpAddr; use thiserror::Error; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc}; + use super::response::result::CqlValue; use super::types::vint_encode; +use super::types::RawValue; #[cfg(feature = "secret")] use secrecy::{ExposeSecret, Secret, Zeroize}; @@ -29,7 +31,12 @@ pub trait Value { #[error("Value too big to be sent in a request - max 2GiB allowed")] pub struct ValueTooBig; +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[error("Value is too large to fit in the CQL type")] +pub struct ValueOverflow; + /// Represents an unset value +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Unset; /// Represents an counter value @@ -37,31 +44,221 @@ pub struct Unset; pub struct Counter(pub i64); /// Enum providing a way to represent a value that might be unset -#[derive(Clone, Copy)] -pub enum MaybeUnset { +#[derive(Clone, Copy, Default)] +pub enum MaybeUnset { + #[default] Unset, Set(V), } -/// Wrapper that allows to send dates outside of NaiveDate range (-262145-1-1 to 262143-12-31) -/// Days since -5877641-06-23 i.e. 2^31 days before unix epoch +/// Native CQL date representation that allows for a bigger range of dates (-262145-1-1 to 262143-12-31). +/// +/// Represented as number of days since -5877641-06-23 i.e. 2^31 days before unix epoch. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Date(pub u32); +pub struct CqlDate(pub u32); -/// Wrapper used to differentiate between Time and Timestamp as sending values -/// Milliseconds since unix epoch +/// Native CQL timestamp representation that allows full supported timestamp range. +/// +/// Represented as signed milliseconds since unix epoch. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Timestamp(pub Duration); +pub struct CqlTimestamp(pub i64); -/// Wrapper used to differentiate between Time and Timestamp as sending values -/// Nanoseconds since midnight +/// Native CQL time representation. +/// +/// Represented as nanoseconds since midnight. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Time(pub Duration); +pub struct CqlTime(pub i64); + +#[cfg(feature = "chrono")] +impl From for CqlDate { + fn from(value: NaiveDate) -> Self { + let unix_epoch = NaiveDate::from_yo_opt(1970, 1).unwrap(); + + // `NaiveDate` range is -262145-01-01 to 262143-12-31 + // Both values are well within supported range + let days = ((1 << 31) + value.signed_duration_since(unix_epoch).num_days()) as u32; + + Self(days) + } +} + +#[cfg(feature = "chrono")] +impl TryInto for CqlDate { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + let days_since_unix_epoch = self.0 as i64 - (1 << 31); + + // date_days is u32 then converted to i64 then we subtract 2^31; + // Max value is 2^31, min value is -2^31. Both values can safely fit in chrono::Duration, this call won't panic + let duration_since_unix_epoch = chrono::Duration::days(days_since_unix_epoch); + + NaiveDate::from_yo_opt(1970, 1) + .unwrap() + .checked_add_signed(duration_since_unix_epoch) + .ok_or(ValueOverflow) + } +} + +#[cfg(feature = "chrono")] +impl From> for CqlTimestamp { + fn from(value: DateTime) -> Self { + Self(value.timestamp_millis()) + } +} + +#[cfg(feature = "chrono")] +impl TryInto> for CqlTimestamp { + type Error = ValueOverflow; + + fn try_into(self) -> Result, Self::Error> { + match Utc.timestamp_millis_opt(self.0) { + chrono::LocalResult::Single(datetime) => Ok(datetime), + _ => Err(ValueOverflow), + } + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for CqlTime { + type Error = ValueOverflow; + + fn try_from(value: NaiveTime) -> Result { + let nanos = value + .signed_duration_since(chrono::NaiveTime::MIN) + .num_nanoseconds() + .unwrap(); + + // Value can exceed max CQL time in case of leap second + if nanos <= 86399999999999 { + Ok(Self(nanos)) + } else { + Err(ValueOverflow) + } + } +} + +#[cfg(feature = "chrono")] +impl TryInto for CqlTime { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + let secs = (self.0 / 1_000_000_000) + .try_into() + .map_err(|_| ValueOverflow)?; + let nanos = (self.0 % 1_000_000_000) + .try_into() + .map_err(|_| ValueOverflow)?; + NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or(ValueOverflow) + } +} + +#[cfg(feature = "time")] +impl From for CqlDate { + fn from(value: time::Date) -> Self { + const JULIAN_DAY_OFFSET: i64 = + (1 << 31) - time::OffsetDateTime::UNIX_EPOCH.date().to_julian_day() as i64; + + // Statically assert that no possible value will ever overflow + const _: () = + assert!(time::Date::MAX.to_julian_day() as i64 + JULIAN_DAY_OFFSET < u32::MAX as i64); + const _: () = + assert!(time::Date::MIN.to_julian_day() as i64 + JULIAN_DAY_OFFSET > u32::MIN as i64); + + let days = value.to_julian_day() as i64 + JULIAN_DAY_OFFSET; + + Self(days as u32) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlDate { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + const JULIAN_DAY_OFFSET: i64 = + (1 << 31) - time::OffsetDateTime::UNIX_EPOCH.date().to_julian_day() as i64; + + let julian_days = (self.0 as i64 - JULIAN_DAY_OFFSET) + .try_into() + .map_err(|_| ValueOverflow)?; + + time::Date::from_julian_day(julian_days).map_err(|_| ValueOverflow) + } +} + +#[cfg(feature = "time")] +impl From for CqlTimestamp { + fn from(value: time::OffsetDateTime) -> Self { + // Statically assert that no possible value will ever overflow. OffsetDateTime doesn't allow offset to overflow + // the UTC PrimitiveDateTime value value + const _: () = assert!( + time::PrimitiveDateTime::MAX + .assume_utc() + .unix_timestamp_nanos() + // Nanos to millis + / 1_000_000 + < i64::MAX as i128 + ); + const _: () = assert!( + time::PrimitiveDateTime::MIN + .assume_utc() + .unix_timestamp_nanos() + / 1_000_000 + > i64::MIN as i128 + ); + + // Edge cases were statically asserted above, checked math is not required + Self(value.unix_timestamp() * 1000 + value.millisecond() as i64) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlTimestamp { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + time::OffsetDateTime::from_unix_timestamp_nanos(self.0 as i128 * 1_000_000) + .map_err(|_| ValueOverflow) + } +} + +#[cfg(feature = "time")] +impl From for CqlTime { + fn from(value: time::Time) -> Self { + let (h, m, s, n) = value.as_hms_nano(); + + // no need for checked arithmetic as all these types are guaranteed to fit in i64 without overflow + let nanos = (h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64; + + Self(nanos) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlTime { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + let h = self.0 / 3_600_000_000_000; + let m = self.0 / 60_000_000_000 % 60; + let s = self.0 / 1_000_000_000 % 60; + let n = self.0 % 1_000_000_000; + + time::Time::from_hms_nano( + h.try_into().map_err(|_| ValueOverflow)?, + m as u8, + s as u8, + n as u32, + ) + .map_err(|_| ValueOverflow) + } +} /// Keeps a buffer with serialized Values /// Allows adding new Values and iterating over serialized ones #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct SerializedValues { +pub struct LegacySerializedValues { serialized_values: Vec, values_num: u16, contains_names: bool, @@ -87,27 +284,27 @@ pub enum SerializeValuesError { ParseError, } -pub type SerializedResult<'a> = Result, SerializeValuesError>; +pub type SerializedResult<'a> = Result, SerializeValuesError>; /// Represents list of values to be sent in a query /// gets serialized and but into request pub trait ValueList { - /// Provides a view of ValueList as SerializedValues - /// returns `Cow` to make impl ValueList for SerializedValues efficient + /// Provides a view of ValueList as LegacySerializedValues + /// returns `Cow` to make impl ValueList for LegacySerializedValues efficient fn serialized(&self) -> SerializedResult<'_>; fn write_to_request(&self, buf: &mut impl BufMut) -> Result<(), SerializeValuesError> { let serialized = self.serialized()?; - SerializedValues::write_to_request(&serialized, buf); + LegacySerializedValues::write_to_request(&serialized, buf); Ok(()) } } -impl SerializedValues { +impl LegacySerializedValues { /// Creates empty value list pub const fn new() -> Self { - SerializedValues { + LegacySerializedValues { serialized_values: Vec::new(), values_num: 0, contains_names: false, @@ -115,7 +312,7 @@ impl SerializedValues { } pub fn with_capacity(capacity: usize) -> Self { - SerializedValues { + LegacySerializedValues { serialized_values: Vec::with_capacity(capacity), values_num: 0, contains_names: false, @@ -127,7 +324,7 @@ impl SerializedValues { } /// A const empty instance, useful for taking references - pub const EMPTY: &'static SerializedValues = &SerializedValues::new(); + pub const EMPTY: &'static LegacySerializedValues = &LegacySerializedValues::new(); /// Serializes value and appends it to the list pub fn add_value(&mut self, val: &impl Value) -> Result<(), SerializeValuesError> { @@ -176,8 +373,8 @@ impl SerializedValues { Ok(()) } - pub fn iter(&self) -> impl Iterator> { - SerializedValuesIterator { + pub fn iter(&self) -> impl Iterator { + LegacySerializedValuesIterator { serialized_values: &self.serialized_values, contains_names: self.contains_names, } @@ -213,35 +410,35 @@ impl SerializedValues { let values_len_in_buf = values_beg.len() - buf.len(); let values_in_frame = &values_beg[0..values_len_in_buf]; - Ok(SerializedValues { + Ok(LegacySerializedValues { serialized_values: values_in_frame.to_vec(), values_num, contains_names, }) } - pub fn iter_name_value_pairs(&self) -> impl Iterator, &[u8])> { + pub fn iter_name_value_pairs(&self) -> impl Iterator, RawValue)> { let mut buf = &self.serialized_values[..]; (0..self.values_num).map(move |_| { - // `unwrap()`s here are safe, as we assume type-safety: if `SerializedValues` exits, + // `unwrap()`s here are safe, as we assume type-safety: if `LegacySerializedValues` exits, // we have a guarantee that the layout of the serialized values is valid. let name = self .contains_names .then(|| types::read_string(&mut buf).unwrap()); - let serialized = types::read_bytes(&mut buf).unwrap(); + let serialized = types::read_value(&mut buf).unwrap(); (name, serialized) }) } } #[derive(Clone, Copy)] -pub struct SerializedValuesIterator<'a> { +pub struct LegacySerializedValuesIterator<'a> { serialized_values: &'a [u8], contains_names: bool, } -impl<'a> Iterator for SerializedValuesIterator<'a> { - type Item = Option<&'a [u8]>; +impl<'a> Iterator for LegacySerializedValuesIterator<'a> { + type Item = RawValue<'a>; fn next(&mut self) -> Option { if self.serialized_values.is_empty() { @@ -253,20 +450,20 @@ impl<'a> Iterator for SerializedValuesIterator<'a> { types::read_short_bytes(&mut self.serialized_values).expect("badly encoded value name"); } - Some(types::read_bytes_opt(&mut self.serialized_values).expect("badly encoded value")) + Some(types::read_value(&mut self.serialized_values).expect("badly encoded value")) } } /// Represents List of ValueList for Batch statement -pub trait BatchValues { +pub trait LegacyBatchValues { /// For some unknown reason, this type, when not resolved to a concrete type for a given async function, /// cannot live across await boundaries while maintaining the corresponding future `Send`, unless `'r: 'static` /// /// See for more details - type BatchValuesIter<'r>: BatchValuesIterator<'r> + type LegacyBatchValuesIter<'r>: LegacyBatchValuesIterator<'r> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_>; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_>; } /// An iterator-like for `ValueList` @@ -277,7 +474,7 @@ pub trait BatchValues { /// It's just essentially making methods from `ValueList` accessible instead of being an actual iterator because of /// compiler limitations that would otherwise be very complex to overcome. /// (specifically, types being different would require yielding enums for tuple impls) -pub trait BatchValuesIterator<'a> { +pub trait LegacyBatchValuesIterator<'a> { fn next_serialized(&mut self) -> Option>; fn write_next_to_request( &mut self, @@ -298,13 +495,13 @@ pub trait BatchValuesIterator<'a> { /// Implements `BatchValuesIterator` from an `Iterator` over references to things that implement `ValueList` /// -/// Essentially used internally by this lib to provide implementors of `BatchValuesIterator` for cases +/// Essentially used internally by this lib to provide implementers of `BatchValuesIterator` for cases /// that always serialize the same concrete `ValueList` type -pub struct BatchValuesIteratorFromIterator { +pub struct LegacyBatchValuesIteratorFromIterator { it: IT, } -impl<'r, 'a: 'r, IT, VL> BatchValuesIterator<'r> for BatchValuesIteratorFromIterator +impl<'r, 'a: 'r, IT, VL> LegacyBatchValuesIterator<'r> for LegacyBatchValuesIteratorFromIterator where IT: Iterator, VL: ValueList + 'a, @@ -323,13 +520,13 @@ where } } -impl From for BatchValuesIteratorFromIterator +impl From for LegacyBatchValuesIteratorFromIterator where IT: Iterator, IT::Item: ValueList, { fn from(it: IT) -> Self { - BatchValuesIteratorFromIterator { it } + LegacyBatchValuesIteratorFromIterator { it } } } @@ -385,24 +582,14 @@ impl Value for BigDecimal { } } +#[cfg(feature = "chrono")] impl Value for NaiveDate { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { - buf.put_i32(4); - let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); - - let days: u32 = self - .signed_duration_since(unix_epoch) - .num_days() - .checked_add(1 << 31) - .and_then(|days| days.try_into().ok()) // convert to u32 - .ok_or(ValueTooBig)?; - - buf.put_u32(days); - Ok(()) + CqlDate::from(*self).serialize(buf) } } -impl Value for Date { +impl Value for CqlDate { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(4); buf.put_u32(self.0); @@ -410,27 +597,56 @@ impl Value for Date { } } -impl Value for Timestamp { +#[cfg(feature = "time")] +impl Value for time::Date { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlDate::from(*self).serialize(buf) + } +} + +impl Value for CqlTimestamp { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(8); - buf.put_i64(self.0.num_milliseconds()); + buf.put_i64(self.0); Ok(()) } } -impl Value for Time { +impl Value for CqlTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(8); - buf.put_i64(self.0.num_nanoseconds().ok_or(ValueTooBig)?); + buf.put_i64(self.0); Ok(()) } } +#[cfg(feature = "chrono")] impl Value for DateTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { - buf.put_i32(8); - buf.put_i64(self.timestamp_millis()); - Ok(()) + CqlTimestamp::from(*self).serialize(buf) + } +} + +#[cfg(feature = "time")] +impl Value for time::OffsetDateTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTimestamp::from(*self).serialize(buf) + } +} + +#[cfg(feature = "chrono")] +impl Value for NaiveTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTime::try_from(*self) + .map_err(|_| ValueTooBig)? + .serialize(buf) + } +} + +#[cfg(feature = "time")] +impl Value for time::Time { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTime::from(*self).serialize(buf) } } @@ -725,7 +941,7 @@ fn serialize_empty(buf: &mut Vec) -> Result<(), ValueTooBig> { impl Value for CqlValue { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { match self { - CqlValue::Map(m) => serialize_map(m.iter().map(|(k, v)| (k, v)), m.len(), buf), + CqlValue::Map(m) => serialize_map(m.iter().map(|p| (&p.0, &p.1)), m.len(), buf), CqlValue::Tuple(t) => serialize_tuple(t.iter(), buf), // A UDT value is composed of successive [bytes] values, one for each field of the UDT @@ -734,10 +950,10 @@ impl Value for CqlValue { serialize_tuple(fields.iter().map(|(_, value)| value), buf) } - CqlValue::Date(d) => Date(*d).serialize(buf), + CqlValue::Date(d) => d.serialize(buf), CqlValue::Duration(d) => d.serialize(buf), - CqlValue::Timestamp(t) => Timestamp(*t).serialize(buf), - CqlValue::Time(t) => Time(*t).serialize(buf), + CqlValue::Timestamp(t) => t.serialize(buf), + CqlValue::Time(t) => t.serialize(buf), CqlValue::Ascii(s) | CqlValue::Text(s) => s.serialize(buf), CqlValue::List(v) | CqlValue::Set(v) => v.serialize(buf), @@ -816,14 +1032,14 @@ impl_value_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13 // Implement ValueList for the unit type impl ValueList for () { fn serialized(&self) -> SerializedResult<'_> { - Ok(Cow::Owned(SerializedValues::new())) + Ok(Cow::Owned(LegacySerializedValues::new())) } } // Implement ValueList for &[] - u8 because otherwise rust can't infer type impl ValueList for [u8; 0] { fn serialized(&self) -> SerializedResult<'_> { - Ok(Cow::Owned(SerializedValues::new())) + Ok(Cow::Owned(LegacySerializedValues::new())) } } @@ -831,7 +1047,7 @@ impl ValueList for [u8; 0] { impl ValueList for &[T] { fn serialized(&self) -> SerializedResult<'_> { let size = std::mem::size_of_val(*self); - let mut result = SerializedValues::with_capacity(size); + let mut result = LegacySerializedValues::with_capacity(size); for val in *self { result.add_value(val)?; } @@ -845,7 +1061,7 @@ impl ValueList for Vec { fn serialized(&self) -> SerializedResult<'_> { let slice = self.as_slice(); let size = std::mem::size_of_val(slice); - let mut result = SerializedValues::with_capacity(size); + let mut result = LegacySerializedValues::with_capacity(size); for val in self { result.add_value(val)?; } @@ -859,7 +1075,7 @@ macro_rules! impl_value_list_for_btree_map { ($key_type:ty) => { impl ValueList for BTreeMap<$key_type, T> { fn serialized(&self) -> SerializedResult<'_> { - let mut result = SerializedValues::with_capacity(self.len()); + let mut result = LegacySerializedValues::with_capacity(self.len()); for (key, val) in self { result.add_named_value(key, val)?; } @@ -875,7 +1091,7 @@ macro_rules! impl_value_list_for_hash_map { ($key_type:ty) => { impl ValueList for HashMap<$key_type, T, S> { fn serialized(&self) -> SerializedResult<'_> { - let mut result = SerializedValues::with_capacity(self.len()); + let mut result = LegacySerializedValues::with_capacity(self.len()); for (key, val) in self { result.add_named_value(key, val)?; } @@ -898,7 +1114,7 @@ impl_value_list_for_btree_map!(&str); impl ValueList for (T0,) { fn serialized(&self) -> SerializedResult<'_> { let size = std::mem::size_of_val(self); - let mut result = SerializedValues::with_capacity(size); + let mut result = LegacySerializedValues::with_capacity(size); result.add_value(&self.0)?; Ok(Cow::Owned(result)) } @@ -912,7 +1128,7 @@ macro_rules! impl_value_list_for_tuple { { fn serialized(&self) -> SerializedResult<'_> { let size = std::mem::size_of_val(self); - let mut result = SerializedValues::with_capacity(size); + let mut result = LegacySerializedValues::with_capacity(size); $( result.add_value(&self.$FieldI) ?; )* @@ -951,13 +1167,13 @@ impl ValueList for &T { } } -impl ValueList for SerializedValues { +impl ValueList for LegacySerializedValues { fn serialized(&self) -> SerializedResult<'_> { Ok(Cow::Borrowed(self)) } } -impl<'b> ValueList for Cow<'b, SerializedValues> { +impl<'b> ValueList for Cow<'b, LegacySerializedValues> { fn serialized(&self) -> SerializedResult<'_> { Ok(Cow::Borrowed(self.as_ref())) } @@ -977,12 +1193,12 @@ impl<'b> ValueList for Cow<'b, SerializedValues> { /// The underlying iterator will always be cloned at least once, once to compute the length if it can't be known /// in advance, and be re-cloned at every retry. /// It is consequently expected that the provided iterator is cheap to clone (e.g. `slice.iter().map(...)`). -pub struct BatchValuesFromIter<'a, IT> { +pub struct LegacyBatchValuesFromIter<'a, IT> { it: IT, _spooky: std::marker::PhantomData<&'a ()>, } -impl<'a, IT, VL> BatchValuesFromIter<'a, IT> +impl<'a, IT, VL> LegacyBatchValuesFromIter<'a, IT> where IT: Iterator + Clone, VL: ValueList + 'a, @@ -995,7 +1211,7 @@ where } } -impl<'a, IT, VL> From for BatchValuesFromIter<'a, IT> +impl<'a, IT, VL> From for LegacyBatchValuesFromIter<'a, IT> where IT: Iterator + Clone, VL: ValueList + 'a, @@ -1005,38 +1221,38 @@ where } } -impl<'a, IT, VL> BatchValues for BatchValuesFromIter<'a, IT> +impl<'a, IT, VL> LegacyBatchValues for LegacyBatchValuesFromIter<'a, IT> where IT: Iterator + Clone, VL: ValueList + 'a, { - type BatchValuesIter<'r> = BatchValuesIteratorFromIterator where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + type LegacyBatchValuesIter<'r> = LegacyBatchValuesIteratorFromIterator where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { self.it.clone().into() } } // Implement BatchValues for slices of ValueList types -impl BatchValues for [T] { - type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { +impl LegacyBatchValues for [T] { + type LegacyBatchValuesIter<'r> = LegacyBatchValuesIteratorFromIterator> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { self.iter().into() } } // Implement BatchValues for Vec -impl BatchValues for Vec { - type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { - BatchValues::batch_values_iter(self.as_slice()) +impl LegacyBatchValues for Vec { + type LegacyBatchValuesIter<'r> = LegacyBatchValuesIteratorFromIterator> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { + LegacyBatchValues::batch_values_iter(self.as_slice()) } } // Here is an example implementation for (T0, ) // Further variants are done using a macro -impl BatchValues for (T0,) { - type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { +impl LegacyBatchValues for (T0,) { + type LegacyBatchValuesIter<'r> = LegacyBatchValuesIteratorFromIterator> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { std::iter::once(&self.0).into() } } @@ -1048,19 +1264,19 @@ pub struct TupleValuesIter<'a, T> { macro_rules! impl_batch_values_for_tuple { ( $($Ti:ident),* ; $($FieldI:tt),* ; $TupleSize:tt) => { - impl<$($Ti),+> BatchValues for ($($Ti,)+) + impl<$($Ti),+> LegacyBatchValues for ($($Ti,)+) where $($Ti: ValueList),+ { - type BatchValuesIter<'r> = TupleValuesIter<'r, ($($Ti,)+)> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + type LegacyBatchValuesIter<'r> = TupleValuesIter<'r, ($($Ti,)+)> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { TupleValuesIter { tuple: self, idx: 0, } } } - impl<'r, $($Ti),+> BatchValuesIterator<'r> for TupleValuesIter<'r, ($($Ti,)+)> + impl<'r, $($Ti),+> LegacyBatchValuesIterator<'r> for TupleValuesIter<'r, ($($Ti,)+)> where $($Ti: ValueList),+ { @@ -1123,26 +1339,29 @@ impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15; 16); // Every &impl BatchValues should also implement BatchValues -impl<'a, T: BatchValues + ?Sized> BatchValues for &'a T { - type BatchValuesIter<'r> = ::BatchValuesIter<'r> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { - ::batch_values_iter(*self) +impl<'a, T: LegacyBatchValues + ?Sized> LegacyBatchValues for &'a T { + type LegacyBatchValuesIter<'r> = ::LegacyBatchValuesIter<'r> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { + ::batch_values_iter(*self) } } /// Allows reusing already-serialized first value /// -/// We'll need to build a `SerializedValues` for the first ~`ValueList` of a batch to figure out the shard (#448). +/// We'll need to build a `LegacySerializedValues` for the first ~`ValueList` of a batch to figure out the shard (#448). /// Once that is done, we can use that instead of re-serializing. /// /// This struct implements both `BatchValues` and `BatchValuesIterator` for that purpose -pub struct BatchValuesFirstSerialized<'f, T> { - first: Option<&'f SerializedValues>, +pub struct LegacyBatchValuesFirstSerialized<'f, T> { + first: Option<&'f LegacySerializedValues>, rest: T, } -impl<'f, T: BatchValues> BatchValuesFirstSerialized<'f, T> { - pub fn new(batch_values: T, already_serialized_first: Option<&'f SerializedValues>) -> Self { +impl<'f, T: LegacyBatchValues> LegacyBatchValuesFirstSerialized<'f, T> { + pub fn new( + batch_values: T, + already_serialized_first: Option<&'f LegacySerializedValues>, + ) -> Self { Self { first: already_serialized_first, rest: batch_values, @@ -1150,19 +1369,19 @@ impl<'f, T: BatchValues> BatchValuesFirstSerialized<'f, T> { } } -impl<'f, BV: BatchValues> BatchValues for BatchValuesFirstSerialized<'f, BV> { - type BatchValuesIter<'r> = - BatchValuesFirstSerialized<'f, ::BatchValuesIter<'r>> where Self: 'r; - fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { - BatchValuesFirstSerialized { +impl<'f, BV: LegacyBatchValues> LegacyBatchValues for LegacyBatchValuesFirstSerialized<'f, BV> { + type LegacyBatchValuesIter<'r> = + LegacyBatchValuesFirstSerialized<'f, ::LegacyBatchValuesIter<'r>> where Self: 'r; + fn batch_values_iter(&self) -> Self::LegacyBatchValuesIter<'_> { + LegacyBatchValuesFirstSerialized { first: self.first, rest: self.rest.batch_values_iter(), } } } -impl<'a, 'f: 'a, IT: BatchValuesIterator<'a>> BatchValuesIterator<'a> - for BatchValuesFirstSerialized<'f, IT> +impl<'a, 'f: 'a, IT: LegacyBatchValuesIterator<'a>> LegacyBatchValuesIterator<'a> + for LegacyBatchValuesFirstSerialized<'f, IT> { fn next_serialized(&mut self) -> Option> { match self.first.take() { diff --git a/scylla-cql/src/frame/value_tests.rs b/scylla-cql/src/frame/value_tests.rs index bd411f45a8..cb6a94ee49 100644 --- a/scylla-cql/src/frame/value_tests.rs +++ b/scylla-cql/src/frame/value_tests.rs @@ -1,79 +1,303 @@ -use crate::frame::value::BatchValuesIterator; +use crate::frame::{response::result::CqlValue, types::RawValue, value::LegacyBatchValuesIterator}; +use crate::types::serialize::batch::{BatchValues, BatchValuesIterator, LegacyBatchValuesAdapter}; +use crate::types::serialize::row::{RowSerializationContext, SerializeRow}; +use crate::types::serialize::value::SerializeCql; +use crate::types::serialize::{CellWriter, RowWriter}; +use super::response::result::{ColumnSpec, ColumnType, TableSpec}; use super::value::{ - BatchValues, Date, MaybeUnset, SerializeValuesError, SerializedValues, Time, Timestamp, Unset, - Value, ValueList, ValueTooBig, + CqlDate, CqlDuration, CqlTime, CqlTimestamp, LegacyBatchValues, LegacySerializedValues, + MaybeUnset, SerializeValuesError, Unset, Value, ValueList, ValueTooBig, }; +use bigdecimal::BigDecimal; use bytes::BufMut; -use chrono::{Duration, NaiveDate}; +use num_bigint::BigInt; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::{BuildHasherDefault, Hasher}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::{borrow::Cow, convert::TryInto}; use uuid::Uuid; -fn serialized(val: impl Value) -> Vec { +fn serialized(val: T, typ: ColumnType) -> Vec +where + T: Value + SerializeCql, +{ let mut result: Vec = Vec::new(); - val.serialize(&mut result).unwrap(); + Value::serialize(&val, &mut result).unwrap(); + + let mut new_result: Vec = Vec::new(); + let writer = CellWriter::new(&mut new_result); + SerializeCql::serialize(&val, &typ, writer).unwrap(); + + assert_eq!(result, new_result); + + result +} + +fn serialized_only_new(val: T, typ: ColumnType) -> Vec { + let mut result: Vec = Vec::new(); + let writer = CellWriter::new(&mut result); + SerializeCql::serialize(&val, &typ, writer).unwrap(); result } #[test] -fn basic_serialization() { - assert_eq!(serialized(8_i8), vec![0, 0, 0, 1, 8]); - assert_eq!(serialized(16_i16), vec![0, 0, 0, 2, 0, 16]); - assert_eq!(serialized(32_i32), vec![0, 0, 0, 4, 0, 0, 0, 32]); +fn boolean_serialization() { + assert_eq!(serialized(true, ColumnType::Boolean), vec![0, 0, 0, 1, 1]); + assert_eq!(serialized(false, ColumnType::Boolean), vec![0, 0, 0, 1, 0]); +} + +#[test] +fn fixed_integral_serialization() { + assert_eq!(serialized(8_i8, ColumnType::TinyInt), vec![0, 0, 0, 1, 8]); assert_eq!( - serialized(64_i64), + serialized(16_i16, ColumnType::SmallInt), + vec![0, 0, 0, 2, 0, 16] + ); + assert_eq!( + serialized(32_i32, ColumnType::Int), + vec![0, 0, 0, 4, 0, 0, 0, 32] + ); + assert_eq!( + serialized(64_i64, ColumnType::BigInt), vec![0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 64] ); +} + +#[test] +fn counter_serialization() { + assert_eq!( + serialized(0x0123456789abcdef_i64, ColumnType::BigInt), + vec![0, 0, 0, 8, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef] + ); +} + +#[test] +fn bigint_serialization() { + let cases_from_the_spec: &[(i64, Vec)] = &[ + (0, vec![0x00]), + (1, vec![0x01]), + (127, vec![0x7F]), + (128, vec![0x00, 0x80]), + (129, vec![0x00, 0x81]), + (-1, vec![0xFF]), + (-128, vec![0x80]), + (-129, vec![0xFF, 0x7F]), + ]; + + for (i, b) in cases_from_the_spec { + let x = BigInt::from(*i); + let b_with_len = (b.len() as i32) + .to_be_bytes() + .iter() + .chain(b) + .cloned() + .collect::>(); + assert_eq!(serialized(x, ColumnType::Varint), b_with_len); + } +} + +#[test] +fn bigdecimal_serialization() { + // Bigint cases + let cases_from_the_spec: &[(i64, Vec)] = &[ + (0, vec![0x00]), + (1, vec![0x01]), + (127, vec![0x7F]), + (128, vec![0x00, 0x80]), + (129, vec![0x00, 0x81]), + (-1, vec![0xFF]), + (-128, vec![0x80]), + (-129, vec![0xFF, 0x7F]), + ]; + + for exponent in -10_i32..10_i32 { + for (digits, serialized_digits) in cases_from_the_spec { + let repr = ((serialized_digits.len() + 4) as i32) + .to_be_bytes() + .iter() + .chain(&exponent.to_be_bytes()) + .chain(serialized_digits) + .cloned() + .collect::>(); + let digits = BigInt::from(*digits); + let x = BigDecimal::new(digits, exponent as i64); + assert_eq!(serialized(x, ColumnType::Decimal), repr); + } + } +} + +#[test] +fn floating_point_serialization() { + assert_eq!( + serialized(123.456f32, ColumnType::Float), + [0, 0, 0, 4] + .into_iter() + .chain((123.456f32).to_be_bytes()) + .collect::>() + ); + assert_eq!( + serialized(123.456f64, ColumnType::Double), + [0, 0, 0, 8] + .into_iter() + .chain((123.456f64).to_be_bytes()) + .collect::>() + ); +} - assert_eq!(serialized("abc"), vec![0, 0, 0, 3, 97, 98, 99]); - assert_eq!(serialized("abc".to_string()), vec![0, 0, 0, 3, 97, 98, 99]); +#[test] +fn text_serialization() { + assert_eq!( + serialized("abc", ColumnType::Text), + vec![0, 0, 0, 3, 97, 98, 99] + ); + assert_eq!( + serialized("abc".to_string(), ColumnType::Ascii), + vec![0, 0, 0, 3, 97, 98, 99] + ); } #[test] fn u8_array_serialization() { let val = [1u8; 4]; - assert_eq!(serialized(val), vec![0, 0, 0, 4, 1, 1, 1, 1]); + assert_eq!( + serialized(val, ColumnType::Blob), + vec![0, 0, 0, 4, 1, 1, 1, 1] + ); } #[test] fn u8_slice_serialization() { let val = vec![1u8, 1, 1, 1]; - assert_eq!(serialized(val.as_slice()), vec![0, 0, 0, 4, 1, 1, 1, 1]); + assert_eq!( + serialized(val.as_slice(), ColumnType::Blob), + vec![0, 0, 0, 4, 1, 1, 1, 1] + ); +} + +#[test] +fn cql_date_serialization() { + assert_eq!( + serialized(CqlDate(0), ColumnType::Date), + vec![0, 0, 0, 4, 0, 0, 0, 0] + ); + assert_eq!( + serialized(CqlDate(u32::MAX), ColumnType::Date), + vec![0, 0, 0, 4, 255, 255, 255, 255] + ); +} + +#[test] +fn vec_u8_slice_serialization() { + let val = vec![1u8, 1, 1, 1]; + assert_eq!( + serialized(val, ColumnType::Blob), + vec![0, 0, 0, 4, 1, 1, 1, 1] + ); +} + +#[test] +fn ipaddr_serialization() { + let ipv4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); + assert_eq!( + serialized(ipv4, ColumnType::Inet), + vec![0, 0, 0, 4, 1, 2, 3, 4] + ); + + let ipv6 = IpAddr::V6(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8)); + assert_eq!( + serialized(ipv6, ColumnType::Inet), + vec![ + 0, 0, 0, 16, // serialized size + 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, // contents + ] + ); } +#[cfg(feature = "chrono")] #[test] fn naive_date_serialization() { + use chrono::NaiveDate; // 1970-01-31 is 2^31 let unix_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); - assert_eq!(serialized(unix_epoch), vec![0, 0, 0, 4, 128, 0, 0, 0]); + assert_eq!( + serialized(unix_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 0] + ); assert_eq!(2_u32.pow(31).to_be_bytes(), [128, 0, 0, 0]); // 1969-12-02 is 2^31 - 30 let before_epoch: NaiveDate = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); assert_eq!( - serialized(before_epoch), + serialized(before_epoch, ColumnType::Date), vec![0, 0, 0, 4, 127, 255, 255, 226] ); assert_eq!((2_u32.pow(31) - 30).to_be_bytes(), [127, 255, 255, 226]); // 1970-01-31 is 2^31 + 30 let after_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); - assert_eq!(serialized(after_epoch), vec![0, 0, 0, 4, 128, 0, 0, 30]); + assert_eq!( + serialized(after_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 30] + ); assert_eq!((2_u32.pow(31) + 30).to_be_bytes(), [128, 0, 0, 30]); } +#[cfg(feature = "time")] #[test] fn date_serialization() { - assert_eq!(serialized(Date(0)), vec![0, 0, 0, 4, 0, 0, 0, 0]); + // 1970-01-31 is 2^31 + let unix_epoch = time::Date::from_ordinal_date(1970, 1).unwrap(); assert_eq!( - serialized(Date(u32::MAX)), - vec![0, 0, 0, 4, 255, 255, 255, 255] + serialized(unix_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 0] + ); + assert_eq!(2_u32.pow(31).to_be_bytes(), [128, 0, 0, 0]); + + // 1969-12-02 is 2^31 - 30 + let before_epoch = time::Date::from_calendar_date(1969, time::Month::December, 2).unwrap(); + assert_eq!( + serialized(before_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 127, 255, 255, 226] + ); + assert_eq!((2_u32.pow(31) - 30).to_be_bytes(), [127, 255, 255, 226]); + + // 1970-01-31 is 2^31 + 30 + let after_epoch = time::Date::from_calendar_date(1970, time::Month::January, 31).unwrap(); + assert_eq!( + serialized(after_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 30] + ); + assert_eq!((2_u32.pow(31) + 30).to_be_bytes(), [128, 0, 0, 30]); + + // Min date represented by time::Date (without large-dates feature) + let long_before_epoch = time::Date::from_calendar_date(-9999, time::Month::January, 1).unwrap(); + let days_till_epoch = (unix_epoch - long_before_epoch).whole_days(); + assert_eq!( + (2_u32.pow(31) - days_till_epoch as u32).to_be_bytes(), + [127, 189, 75, 125] + ); + assert_eq!( + serialized(long_before_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 127, 189, 75, 125] + ); + + // Max date represented by time::Date (without large-dates feature) + let long_after_epoch = time::Date::from_calendar_date(9999, time::Month::December, 31).unwrap(); + let days_since_epoch = (long_after_epoch - unix_epoch).whole_days(); + assert_eq!( + (2_u32.pow(31) + days_since_epoch as u32).to_be_bytes(), + [128, 44, 192, 160] + ); + assert_eq!( + serialized(long_after_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 44, 192, 160] ); } #[test] -fn time_serialization() { - // Time is an i64 - nanoseconds since midnight +fn cql_time_serialization() { + // CqlTime is an i64 - nanoseconds since midnight // in range 0..=86399999999999 let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; @@ -81,9 +305,9 @@ fn time_serialization() { // Check that basic values are serialized correctly // Invalid values are also serialized correctly - database will respond with an error - for test_val in [0, 1, 15, 18463, max_time, -1, -324234, max_time + 16].iter() { - let test_time: Time = Time(Duration::nanoseconds(*test_val)); - let bytes: Vec = serialized(test_time); + for test_val in [0, 1, 15, 18463, max_time, -1, -324234, max_time + 16].into_iter() { + let test_time: CqlTime = CqlTime(test_val); + let bytes: Vec = serialized(test_time, ColumnType::Time); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; expected_bytes.extend_from_slice(&test_val.to_be_bytes()); @@ -91,19 +315,79 @@ fn time_serialization() { assert_eq!(bytes, expected_bytes); assert_eq!(expected_bytes.len(), 12); } +} + +#[cfg(feature = "chrono")] +#[test] +fn naive_time_serialization() { + use chrono::NaiveTime; + + let midnight_time: i64 = 0; + let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; + let any_time: i64 = (3600 + 2 * 60 + 3) * 1_000_000_000 + 4; + let test_cases = [ + (NaiveTime::MIN, midnight_time.to_be_bytes()), + ( + NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(), + max_time.to_be_bytes(), + ), + ( + NaiveTime::from_hms_nano_opt(1, 2, 3, 4).unwrap(), + any_time.to_be_bytes(), + ), + ]; + for (time, expected) in test_cases { + let bytes = serialized(time, ColumnType::Time); + + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); + + assert_eq!(bytes, expected_bytes) + } + + // Leap second must return error on serialize + let leap_second = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_500_000_000).unwrap(); + let mut buffer = Vec::new(); + assert_eq!( + <_ as Value>::serialize(&leap_second, &mut buffer), + Err(ValueTooBig) + ) +} + +#[cfg(feature = "time")] +#[test] +fn time_serialization() { + let midnight_time: i64 = 0; + let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; + let any_time: i64 = (3600 + 2 * 60 + 3) * 1_000_000_000 + 4; + let test_cases = [ + (time::Time::MIDNIGHT, midnight_time.to_be_bytes()), + ( + time::Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap(), + max_time.to_be_bytes(), + ), + ( + time::Time::from_hms_nano(1, 2, 3, 4).unwrap(), + any_time.to_be_bytes(), + ), + ]; + for (time, expected) in test_cases { + let bytes = serialized(time, ColumnType::Time); - // Durations so long that nanoseconds don't fit in i64 cause an error - let long_time = Time(Duration::milliseconds(i64::MAX)); - assert_eq!(long_time.serialize(&mut Vec::new()), Err(ValueTooBig)); + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); + + assert_eq!(bytes, expected_bytes) + } } #[test] -fn timestamp_serialization() { - // Timestamp is milliseconds since unix epoch represented as i64 +fn cql_timestamp_serialization() { + // CqlTimestamp is milliseconds since unix epoch represented as i64 for test_val in &[0, -1, 1, -45345346, 453451, i64::MIN, i64::MAX] { - let test_timestamp: Timestamp = Timestamp(Duration::milliseconds(*test_val)); - let bytes: Vec = serialized(test_timestamp); + let test_timestamp: CqlTimestamp = CqlTimestamp(*test_val); + let bytes: Vec = serialized(test_timestamp, ColumnType::Timestamp); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; expected_bytes.extend_from_slice(&test_val.to_be_bytes()); @@ -113,23 +397,115 @@ fn timestamp_serialization() { } } +#[cfg(feature = "chrono")] #[test] -fn datetime_serialization() { - use chrono::{DateTime, NaiveDateTime, Utc}; - // Datetime is milliseconds since unix epoch represented as i64 - let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; +fn naive_date_time_serialization() { + use chrono::NaiveDateTime; + let test_cases = [ + ( + // Max time serialized without error + NaiveDateTime::MAX, + NaiveDateTime::MAX.timestamp_millis().to_be_bytes(), + ), + ( + // Min time serialized without error + NaiveDateTime::MIN, + NaiveDateTime::MIN.timestamp_millis().to_be_bytes(), + ), + ( + // UNIX epoch baseline + NaiveDateTime::from_timestamp_opt(0, 0).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // One second since UNIX epoch + NaiveDateTime::from_timestamp_opt(1, 0).unwrap(), + 1000i64.to_be_bytes(), + ), + ( + // 1 nanosecond since UNIX epoch, lost during serialization + NaiveDateTime::from_timestamp_opt(0, 1).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // 1 millisecond since UNIX epoch + NaiveDateTime::from_timestamp_opt(0, 1_000_000).unwrap(), + 1i64.to_be_bytes(), + ), + ( + // 2 days before UNIX epoch + NaiveDateTime::from_timestamp_opt(-2 * 24 * 60 * 60, 0).unwrap(), + (-2 * 24i64 * 60 * 60 * 1000).to_be_bytes(), + ), + ]; + for (datetime, expected) in test_cases { + let test_datetime = datetime.and_utc(); + let bytes: Vec = serialized(test_datetime, ColumnType::Timestamp); + + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); - for test_val in &[0, 1, 15, 18463, max_time, max_time + 16] { - let native_datetime = NaiveDateTime::from_timestamp_opt( - *test_val / 1000, - ((*test_val % 1000) as i32 * 1_000_000) as u32, - ) - .expect("invalid or out-of-range datetime"); - let test_datetime = DateTime::::from_naive_utc_and_offset(native_datetime, Utc); - let bytes: Vec = serialized(test_datetime); + assert_eq!(bytes, expected_bytes); + assert_eq!(expected_bytes.len(), 12); + } +} + +#[cfg(feature = "time")] +#[test] +fn offset_date_time_serialization() { + use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; + let offset_max = + PrimitiveDateTime::MAX.assume_offset(time::UtcOffset::from_hms(-23, -59, -59).unwrap()); + let offset_min = + PrimitiveDateTime::MIN.assume_offset(time::UtcOffset::from_hms(23, 59, 59).unwrap()); + let test_cases = [ + ( + // Max time serialized without error + offset_max, + (offset_max.unix_timestamp() * 1000 + offset_max.nanosecond() as i64 / 1_000_000) + .to_be_bytes(), + ), + ( + // Min time serialized without error + offset_min, + (offset_min.unix_timestamp() * 1000 + offset_min.nanosecond() as i64 / 1_000_000) + .to_be_bytes(), + ), + ( + // UNIX epoch baseline + OffsetDateTime::from_unix_timestamp(0).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // One second since UNIX epoch + OffsetDateTime::from_unix_timestamp(1).unwrap(), + 1000i64.to_be_bytes(), + ), + ( + // 1 nanosecond since UNIX epoch, lost during serialization + OffsetDateTime::from_unix_timestamp_nanos(1).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // 1 millisecond since UNIX epoch + OffsetDateTime::from_unix_timestamp_nanos(1_000_000).unwrap(), + 1i64.to_be_bytes(), + ), + ( + // 2 days before UNIX epoch + PrimitiveDateTime::new( + Date::from_calendar_date(1969, Month::December, 30).unwrap(), + Time::MIDNIGHT, + ) + .assume_utc(), + (-2 * 24i64 * 60 * 60 * 1000).to_be_bytes(), + ), + ]; + for (datetime, expected) in test_cases { + let bytes: Vec = serialized(datetime, ColumnType::Timestamp); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; - expected_bytes.extend_from_slice(&test_val.to_be_bytes()); + expected_bytes.extend_from_slice(&expected); assert_eq!(bytes, expected_bytes); assert_eq!(expected_bytes.len(), 12); @@ -153,7 +529,7 @@ fn timeuuid_serialization() { for uuid_bytes in &tests { let uuid = Uuid::from_slice(uuid_bytes.as_ref()).unwrap(); - let uuid_serialized: Vec = serialized(uuid); + let uuid_serialized: Vec = serialized(uuid, ColumnType::Uuid); let mut expected_serialized: Vec = vec![0, 0, 0, 16]; expected_serialized.extend_from_slice(uuid_bytes.as_ref()); @@ -162,22 +538,279 @@ fn timeuuid_serialization() { } } +#[test] +fn cqlduration_serialization() { + let duration = CqlDuration { + months: 1, + days: 2, + nanoseconds: 3, + }; + assert_eq!( + serialized(duration, ColumnType::Duration), + vec![0, 0, 0, 3, 2, 4, 6] + ); +} + +#[test] +fn box_serialization() { + let x: Box = Box::new(123); + assert_eq!( + serialized(x, ColumnType::Int), + vec![0, 0, 0, 4, 0, 0, 0, 123] + ); +} + +#[test] +fn vec_set_serialization() { + let m = vec!["ala", "ma", "kota"]; + assert_eq!( + serialized(m, ColumnType::Set(Box::new(ColumnType::Text))), + vec![ + 0, 0, 0, 25, // 25 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 3, 97, 108, 97, // ala + 0, 0, 0, 2, 109, 97, // ma + 0, 0, 0, 4, 107, 111, 116, 97, // kota + ] + ) +} + +#[test] +fn slice_set_serialization() { + let m = ["ala", "ma", "kota"]; + assert_eq!( + serialized(m.as_ref(), ColumnType::Set(Box::new(ColumnType::Text))), + vec![ + 0, 0, 0, 25, // 25 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 3, 97, 108, 97, // ala + 0, 0, 0, 2, 109, 97, // ma + 0, 0, 0, 4, 107, 111, 116, 97, // kota + ] + ) +} + +// A deterministic hasher just for the tests. +#[derive(Default)] +struct DumbHasher { + state: u8, +} + +impl Hasher for DumbHasher { + fn finish(&self) -> u64 { + self.state as u64 + } + + fn write(&mut self, bytes: &[u8]) { + for b in bytes { + self.state ^= b; + } + } +} + +type DumbBuildHasher = BuildHasherDefault; + +#[test] +fn hashset_serialization() { + let m: HashSet<&'static str, DumbBuildHasher> = ["ala", "ma", "kota"].into_iter().collect(); + assert_eq!( + serialized(m, ColumnType::Set(Box::new(ColumnType::Text))), + vec![ + 0, 0, 0, 25, // 25 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 2, 109, 97, // ma + 0, 0, 0, 4, 107, 111, 116, 97, // kota + 0, 0, 0, 3, 97, 108, 97, // ala + ] + ) +} + +#[test] +fn hashmap_serialization() { + let m: HashMap<&'static str, i32, DumbBuildHasher> = + [("ala", 1), ("ma", 2), ("kota", 3)].into_iter().collect(); + assert_eq!( + serialized( + m, + ColumnType::Map(Box::new(ColumnType::Text), Box::new(ColumnType::Int)) + ), + vec![ + 0, 0, 0, 49, // 49 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 2, 109, 97, // ma + 0, 0, 0, 4, 0, 0, 0, 2, // 2 + 0, 0, 0, 4, 107, 111, 116, 97, // kota + 0, 0, 0, 4, 0, 0, 0, 3, // 3 + 0, 0, 0, 3, 97, 108, 97, // ala + 0, 0, 0, 4, 0, 0, 0, 1, // 1 + ] + ) +} + +#[test] +fn btreeset_serialization() { + let m: BTreeSet<&'static str> = ["ala", "ma", "kota"].into_iter().collect(); + assert_eq!( + serialized(m, ColumnType::Set(Box::new(ColumnType::Text))), + vec![ + 0, 0, 0, 25, // 25 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 3, 97, 108, 97, // ala + 0, 0, 0, 4, 107, 111, 116, 97, // kota + 0, 0, 0, 2, 109, 97, // ma + ] + ) +} + +#[test] +fn btreemap_serialization() { + let m: BTreeMap<&'static str, i32> = [("ala", 1), ("ma", 2), ("kota", 3)].into_iter().collect(); + assert_eq!( + serialized( + m, + ColumnType::Map(Box::new(ColumnType::Text), Box::new(ColumnType::Int)) + ), + vec![ + 0, 0, 0, 49, // 49 bytes + 0, 0, 0, 3, // 3 items + 0, 0, 0, 3, 97, 108, 97, // ala + 0, 0, 0, 4, 0, 0, 0, 1, // 1 + 0, 0, 0, 4, 107, 111, 116, 97, // kota + 0, 0, 0, 4, 0, 0, 0, 3, // 3 + 0, 0, 0, 2, 109, 97, // ma + 0, 0, 0, 4, 0, 0, 0, 2, // 2 + ] + ) +} + +#[test] +fn cqlvalue_serialization() { + // We only check those variants here which have some custom logic, + // e.g. UDTs or tuples. + + // Empty + assert_eq!( + serialized(CqlValue::Empty, ColumnType::Int), + vec![0, 0, 0, 0], + ); + + // UDTs + let udt = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "t".to_string(), + fields: vec![ + ("foo".to_string(), Some(CqlValue::Int(123))), + ("bar".to_string(), None), + ], + }; + let typ = ColumnType::UserDefinedType { + type_name: "t".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("foo".to_string(), ColumnType::Int), + ("bar".to_string(), ColumnType::Text), + ], + }; + + assert_eq!( + serialized(udt, typ.clone()), + vec![ + 0, 0, 0, 12, // size of the whole thing + 0, 0, 0, 4, 0, 0, 0, 123, // foo: 123_i32 + 255, 255, 255, 255, // bar: null + ] + ); + + // Unlike the legacy Value trait, SerializeCql takes case of reordering + // the fields + let udt = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "t".to_string(), + fields: vec![ + ("bar".to_string(), None), + ("foo".to_string(), Some(CqlValue::Int(123))), + ], + }; + + assert_eq!( + serialized_only_new(udt, typ.clone()), + vec![ + 0, 0, 0, 12, // size of the whole thing + 0, 0, 0, 4, 0, 0, 0, 123, // foo: 123_i32 + 255, 255, 255, 255, // bar: null + ] + ); + + // Tuples + let tup = CqlValue::Tuple(vec![Some(CqlValue::Int(123)), None]); + let typ = ColumnType::Tuple(vec![ColumnType::Int, ColumnType::Text]); + assert_eq!( + serialized(tup, typ), + vec![ + 0, 0, 0, 12, // size of the whole thing + 0, 0, 0, 4, 0, 0, 0, 123, // 123_i32 + 255, 255, 255, 255, // null + ] + ); + + // It's not required to specify all the values for the tuple, + // only some prefix is sufficient. The rest will be treated by the DB + // as nulls. + // TODO: Need a database test for that + let tup = CqlValue::Tuple(vec![Some(CqlValue::Int(123)), None]); + let typ = ColumnType::Tuple(vec![ColumnType::Int, ColumnType::Text, ColumnType::Counter]); + assert_eq!( + serialized(tup, typ), + vec![ + 0, 0, 0, 12, // size of the whole thing + 0, 0, 0, 4, 0, 0, 0, 123, // 123_i32 + 255, 255, 255, 255, // null + ] + ); +} + +#[cfg(feature = "secret")] +#[test] +fn secret_serialization() { + use secrecy::Secret; + let secret = Secret::new(987654i32); + assert_eq!( + serialized(secret, ColumnType::Int), + vec![0, 0, 0, 4, 0x00, 0x0f, 0x12, 0x06] + ); +} + #[test] fn option_value() { - assert_eq!(serialized(Some(32_i32)), vec![0, 0, 0, 4, 0, 0, 0, 32]); + assert_eq!( + serialized(Some(32_i32), ColumnType::Int), + vec![0, 0, 0, 4, 0, 0, 0, 32] + ); let null_i32: Option = None; - assert_eq!(serialized(null_i32), &(-1_i32).to_be_bytes()[..]); + assert_eq!( + serialized(null_i32, ColumnType::Int), + &(-1_i32).to_be_bytes()[..] + ); } #[test] fn unset_value() { - assert_eq!(serialized(Unset), &(-2_i32).to_be_bytes()[..]); + assert_eq!( + serialized(Unset, ColumnType::Int), + &(-2_i32).to_be_bytes()[..] + ); let unset_i32: MaybeUnset = MaybeUnset::Unset; - assert_eq!(serialized(unset_i32), &(-2_i32).to_be_bytes()[..]); + assert_eq!( + serialized(unset_i32, ColumnType::Int), + &(-2_i32).to_be_bytes()[..] + ); let set_i32: MaybeUnset = MaybeUnset::Set(32); - assert_eq!(serialized(set_i32), vec![0, 0, 0, 4, 0, 0, 0, 32]); + assert_eq!( + serialized(set_i32, ColumnType::Int), + vec![0, 0, 0, 4, 0, 0, 0, 32] + ); } #[test] @@ -200,7 +833,7 @@ fn ref_value() { #[test] fn empty_serialized_values() { - const EMPTY: SerializedValues = SerializedValues::new(); + const EMPTY: LegacySerializedValues = LegacySerializedValues::new(); assert_eq!(EMPTY.len(), 0); assert!(EMPTY.is_empty()); assert_eq!(EMPTY.iter().next(), None); @@ -212,7 +845,7 @@ fn empty_serialized_values() { #[test] fn serialized_values() { - let mut values = SerializedValues::new(); + let mut values = LegacySerializedValues::new(); assert!(values.is_empty()); // Add first value @@ -225,7 +858,10 @@ fn serialized_values() { values.write_to_request(&mut request); assert_eq!(request, vec![0, 1, 0, 0, 0, 1, 8]); - assert_eq!(values.iter().collect::>(), vec![Some([8].as_ref())]); + assert_eq!( + values.iter().collect::>(), + vec![RawValue::Value([8].as_ref())] + ); } // Add second value @@ -240,7 +876,10 @@ fn serialized_values() { assert_eq!( values.iter().collect::>(), - vec![Some([8].as_ref()), Some([0, 16].as_ref())] + vec![ + RawValue::Value([8].as_ref()), + RawValue::Value([0, 16].as_ref()) + ] ); } @@ -272,21 +911,24 @@ fn serialized_values() { assert_eq!( values.iter().collect::>(), - vec![Some([8].as_ref()), Some([0, 16].as_ref())] + vec![ + RawValue::Value([8].as_ref()), + RawValue::Value([0, 16].as_ref()) + ] ); } } #[test] fn unit_value_list() { - let serialized_unit: SerializedValues = + let serialized_unit: LegacySerializedValues = <() as ValueList>::serialized(&()).unwrap().into_owned(); assert!(serialized_unit.is_empty()); } #[test] fn empty_array_value_list() { - let serialized_arr: SerializedValues = <[u8; 0] as ValueList>::serialized(&[]) + let serialized_arr: LegacySerializedValues = <[u8; 0] as ValueList>::serialized(&[]) .unwrap() .into_owned(); assert!(serialized_arr.is_empty()); @@ -295,16 +937,19 @@ fn empty_array_value_list() { #[test] fn slice_value_list() { let values: &[i32] = &[1, 2, 3]; - let serialized: SerializedValues = <&[i32] as ValueList>::serialized(&values) - .unwrap() - .into_owned(); + let cols = &[ + col_spec("ala", ColumnType::Int), + col_spec("ma", ColumnType::Int), + col_spec("kota", ColumnType::Int), + ]; + let serialized = serialize_values(values, cols); assert_eq!( serialized.iter().collect::>(), vec![ - Some([0, 0, 0, 1].as_ref()), - Some([0, 0, 0, 2].as_ref()), - Some([0, 0, 0, 3].as_ref()) + RawValue::Value([0, 0, 0, 1].as_ref()), + RawValue::Value([0, 0, 0, 2].as_ref()), + RawValue::Value([0, 0, 0, 3].as_ref()) ] ); } @@ -312,29 +957,89 @@ fn slice_value_list() { #[test] fn vec_value_list() { let values: Vec = vec![1, 2, 3]; - let serialized: SerializedValues = as ValueList>::serialized(&values) - .unwrap() - .into_owned(); + let cols = &[ + col_spec("ala", ColumnType::Int), + col_spec("ma", ColumnType::Int), + col_spec("kota", ColumnType::Int), + ]; + let serialized = serialize_values(values, cols); assert_eq!( serialized.iter().collect::>(), vec![ - Some([0, 0, 0, 1].as_ref()), - Some([0, 0, 0, 2].as_ref()), - Some([0, 0, 0, 3].as_ref()) + RawValue::Value([0, 0, 0, 1].as_ref()), + RawValue::Value([0, 0, 0, 2].as_ref()), + RawValue::Value([0, 0, 0, 3].as_ref()) ] ); } +fn col_spec(name: &str, typ: ColumnType) -> ColumnSpec { + ColumnSpec { + table_spec: TableSpec { + ks_name: "ks".to_string(), + table_name: "tbl".to_string(), + }, + name: name.to_string(), + typ, + } +} + +fn serialize_values( + vl: T, + columns: &[ColumnSpec], +) -> LegacySerializedValues { + let serialized = ::serialized(&vl).unwrap().into_owned(); + let mut old_serialized = Vec::new(); + serialized.write_to_request(&mut old_serialized); + + let ctx = RowSerializationContext { columns }; + let mut new_serialized = vec![0, 0]; + let mut writer = RowWriter::new(&mut new_serialized); + ::serialize(&vl, &ctx, &mut writer).unwrap(); + let value_count: u16 = writer.value_count().try_into().unwrap(); + let is_empty = writer.value_count() == 0; + + // Prepend with value count, like `ValueList` does + new_serialized[0..2].copy_from_slice(&value_count.to_be_bytes()); + + assert_eq!(old_serialized, new_serialized); + assert_eq!(::is_empty(&vl), is_empty); + assert_eq!(serialized.is_empty(), is_empty); + + serialized +} + +fn serialize_values_only_new(vl: T, columns: &[ColumnSpec]) -> Vec { + let ctx = RowSerializationContext { columns }; + let mut serialized = vec![0, 0]; + let mut writer = RowWriter::new(&mut serialized); + ::serialize(&vl, &ctx, &mut writer).unwrap(); + let value_count: u16 = writer.value_count().try_into().unwrap(); + let is_empty = writer.value_count() == 0; + + // Prepend with value count, like `ValueList` does + serialized[0..2].copy_from_slice(&value_count.to_be_bytes()); + + assert_eq!(::is_empty(&vl), is_empty); + + serialized +} + #[test] fn tuple_value_list() { - fn check_i8_tuple(tuple: impl ValueList, expected: core::ops::Range) { - let serialized: SerializedValues = tuple.serialized().unwrap().into_owned(); + fn check_i8_tuple(tuple: impl ValueList + SerializeRow, expected: core::ops::Range) { + let typs = expected + .clone() + .enumerate() + .map(|(i, _)| col_spec(&format!("col_{i}"), ColumnType::TinyInt)) + .collect::>(); + let serialized = serialize_values(tuple, &typs); assert_eq!(serialized.len() as usize, expected.len()); let serialized_vals: Vec = serialized .iter() - .map(|o: Option<&[u8]>| o.unwrap()[0]) + .map(|o: RawValue| o.as_value().unwrap()[0]) .collect(); let expected: Vec = expected.collect(); @@ -342,6 +1047,7 @@ fn tuple_value_list() { assert_eq!(serialized_vals, expected); } + check_i8_tuple((), 1..1); check_i8_tuple((1_i8,), 1..2); check_i8_tuple((1_i8, 2_i8), 1..3); check_i8_tuple((1_i8, 2_i8, 3_i8), 1..4); @@ -398,30 +1104,66 @@ fn tuple_value_list() { ); } +#[test] +fn map_value_list() { + // The legacy ValueList would serialize this as a list of named values, + // whereas the new SerializeRow will order the values by their names. + + // Note that the alphabetical order of the keys is "ala", "kota", "ma", + // but the impl sorts properly. + let row = BTreeMap::from_iter([("ala", 1), ("ma", 2), ("kota", 3)]); + let cols = &[ + col_spec("ala", ColumnType::Int), + col_spec("ma", ColumnType::Int), + col_spec("kota", ColumnType::Int), + ]; + let new_values = serialize_values_only_new(row.clone(), cols); + assert_eq!( + new_values, + vec![ + 0, 3, // value count: 3 + 0, 0, 0, 4, 0, 0, 0, 1, // ala: 1 + 0, 0, 0, 4, 0, 0, 0, 2, // ma: 2 + 0, 0, 0, 4, 0, 0, 0, 3, // kota: 3 + ] + ); + + // While ValueList will serialize differently, the fallback SerializeRow impl + // should convert it to how serialized BTreeMap would look like if serialized + // directly through SerializeRow. + let ser = <_ as ValueList>::serialized(&row).unwrap(); + let fallbacked = serialize_values_only_new(ser, cols); + + assert_eq!(new_values, fallbacked); +} + #[test] fn ref_value_list() { let values: &[i32] = &[1, 2, 3]; - let serialized: SerializedValues = <&&[i32] as ValueList>::serialized(&&values) - .unwrap() - .into_owned(); + let typs = &[ + col_spec("col_1", ColumnType::Int), + col_spec("col_2", ColumnType::Int), + col_spec("col_3", ColumnType::Int), + ]; + let serialized = serialize_values::<&&[i32]>(&values, typs); assert_eq!( serialized.iter().collect::>(), vec![ - Some([0, 0, 0, 1].as_ref()), - Some([0, 0, 0, 2].as_ref()), - Some([0, 0, 0, 3].as_ref()) + RawValue::Value([0, 0, 0, 1].as_ref()), + RawValue::Value([0, 0, 0, 2].as_ref()), + RawValue::Value([0, 0, 0, 3].as_ref()) ] ); } #[test] fn serialized_values_value_list() { - let mut ser_values = SerializedValues::new(); + let mut ser_values = LegacySerializedValues::new(); ser_values.add_value(&1_i32).unwrap(); ser_values.add_value(&"qwertyuiop").unwrap(); - let ser_ser_values: Cow = ser_values.serialized().unwrap(); + let ser_ser_values: Cow = ser_values.serialized().unwrap(); assert!(matches!(ser_ser_values, Cow::Borrowed(_))); assert_eq!(&ser_values, ser_ser_values.as_ref()); @@ -429,27 +1171,87 @@ fn serialized_values_value_list() { #[test] fn cow_serialized_values_value_list() { - let cow_ser_values: Cow = Cow::Owned(SerializedValues::new()); + let cow_ser_values: Cow = Cow::Owned(LegacySerializedValues::new()); - let serialized: Cow = cow_ser_values.serialized().unwrap(); + let serialized: Cow = cow_ser_values.serialized().unwrap(); assert!(matches!(serialized, Cow::Borrowed(_))); assert_eq!(cow_ser_values.as_ref(), serialized.as_ref()); } +fn make_batch_value_iters<'bv, BV: BatchValues + LegacyBatchValues>( + bv: &'bv BV, + adapter_bv: &'bv LegacyBatchValuesAdapter<&'bv BV>, +) -> ( + BV::LegacyBatchValuesIter<'bv>, + BV::BatchValuesIter<'bv>, + as BatchValues>::BatchValuesIter<'bv>, +) { + ( + ::batch_values_iter(bv), + ::batch_values_iter(bv), + <_ as BatchValues>::batch_values_iter(adapter_bv), + ) +} + +fn serialize_batch_value_iterators<'a>( + (legacy_bvi, bvi, bvi_adapted): &mut ( + impl LegacyBatchValuesIterator<'a>, + impl BatchValuesIterator<'a>, + impl BatchValuesIterator<'a>, + ), + columns: &[ColumnSpec], +) -> Vec { + let mut legacy_data = Vec::new(); + legacy_bvi + .write_next_to_request(&mut legacy_data) + .unwrap() + .unwrap(); + + fn serialize_bvi<'bv>( + bvi: &mut impl BatchValuesIterator<'bv>, + ctx: &RowSerializationContext, + ) -> Vec { + let mut data = vec![0, 0]; + let mut writer = RowWriter::new(&mut data); + bvi.serialize_next(ctx, &mut writer).unwrap().unwrap(); + let value_count: u16 = writer.value_count().try_into().unwrap(); + data[0..2].copy_from_slice(&value_count.to_be_bytes()); + data + } + + let ctx = RowSerializationContext { columns }; + let data = serialize_bvi(bvi, &ctx); + let adapted_data = serialize_bvi(bvi_adapted, &ctx); + + assert_eq!(legacy_data, data); + assert_eq!(adapted_data, data); + data +} + #[test] fn slice_batch_values() { let batch_values: &[&[i8]] = &[&[1, 2], &[2, 3, 4, 5], &[6]]; - let mut it = batch_values.batch_values_iter(); + let legacy_batch_values = LegacyBatchValuesAdapter(&batch_values); + + let mut iters = make_batch_value_iters(&batch_values, &legacy_batch_values); { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[ + col_spec("a", ColumnType::TinyInt), + col_spec("b", ColumnType::TinyInt), + ]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!(request, vec![0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 1, 2]); } { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[ + col_spec("a", ColumnType::TinyInt), + col_spec("b", ColumnType::TinyInt), + col_spec("c", ColumnType::TinyInt), + col_spec("d", ColumnType::TinyInt), + ]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!( request, vec![0, 4, 0, 0, 0, 1, 2, 0, 0, 0, 1, 3, 0, 0, 0, 1, 4, 0, 0, 0, 1, 5] @@ -457,28 +1259,42 @@ fn slice_batch_values() { } { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[col_spec("a", ColumnType::TinyInt)]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!(request, vec![0, 1, 0, 0, 0, 1, 6]); } - assert_eq!(it.write_next_to_request(&mut Vec::new()), None); + assert_eq!(iters.0.write_next_to_request(&mut Vec::new()), None); + + let ctx = RowSerializationContext { columns: &[] }; + let mut data = Vec::new(); + let mut writer = RowWriter::new(&mut data); + assert!(iters.1.serialize_next(&ctx, &mut writer).is_none()); } #[test] fn vec_batch_values() { let batch_values: Vec> = vec![vec![1, 2], vec![2, 3, 4, 5], vec![6]]; + let legacy_batch_values = LegacyBatchValuesAdapter(&batch_values); - let mut it = batch_values.batch_values_iter(); + let mut iters = make_batch_value_iters(&batch_values, &legacy_batch_values); { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[ + col_spec("a", ColumnType::TinyInt), + col_spec("b", ColumnType::TinyInt), + ]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!(request, vec![0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 1, 2]); } { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[ + col_spec("a", ColumnType::TinyInt), + col_spec("b", ColumnType::TinyInt), + col_spec("c", ColumnType::TinyInt), + col_spec("d", ColumnType::TinyInt), + ]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!( request, vec![0, 4, 0, 0, 0, 1, 2, 0, 0, 0, 1, 3, 0, 0, 0, 1, 4, 0, 0, 0, 1, 5] @@ -486,19 +1302,24 @@ fn vec_batch_values() { } { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[col_spec("a", ColumnType::TinyInt)]; + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!(request, vec![0, 1, 0, 0, 0, 1, 6]); } } #[test] fn tuple_batch_values() { - fn check_twoi32_tuple(tuple: impl BatchValues, size: usize) { - let mut it = tuple.batch_values_iter(); + fn check_twoi32_tuple(tuple: impl BatchValues + LegacyBatchValues, size: usize) { + let legacy_tuple = LegacyBatchValuesAdapter(&tuple); + let mut iters = make_batch_value_iters(&tuple, &legacy_tuple); for i in 0..size { - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let cols = &[ + col_spec("a", ColumnType::Int), + col_spec("b", ColumnType::Int), + ]; + + let request = serialize_batch_value_iterators(&mut iters, cols); let mut expected: Vec = Vec::new(); let i: i32 = i.try_into().unwrap(); @@ -685,13 +1506,17 @@ fn tuple_batch_values() { #[allow(clippy::needless_borrow)] fn ref_batch_values() { let batch_values: &[&[i8]] = &[&[1, 2], &[2, 3, 4, 5], &[6]]; + let cols = &[ + col_spec("a", ColumnType::TinyInt), + col_spec("b", ColumnType::TinyInt), + ]; - return check_ref_bv::<&&&&&[&[i8]]>(&&&&batch_values); - fn check_ref_bv(batch_values: B) { - let mut it = ::batch_values_iter(&batch_values); + return check_ref_bv::<&&&&&[&[i8]]>(&&&&batch_values, cols); + fn check_ref_bv(batch_values: B, cols: &[ColumnSpec]) { + let legacy_batch_values = LegacyBatchValuesAdapter(&batch_values); + let mut iters = make_batch_value_iters(&batch_values, &legacy_batch_values); - let mut request: Vec = Vec::new(); - it.write_next_to_request(&mut request).unwrap().unwrap(); + let request = serialize_batch_value_iterators(&mut iters, cols); assert_eq!(request, vec![0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 1, 2]); } } @@ -699,24 +1524,38 @@ fn ref_batch_values() { #[test] #[allow(clippy::needless_borrow)] fn check_ref_tuple() { - fn assert_has_batch_values(bv: BV) { - let mut it = bv.batch_values_iter(); - let mut request: Vec = Vec::new(); - while let Some(res) = it.write_next_to_request(&mut request) { - res.unwrap() + fn assert_has_batch_values( + bv: BV, + cols: &[&[ColumnSpec]], + ) { + let legacy_bv = LegacyBatchValuesAdapter(&bv); + let mut iters = make_batch_value_iters(&bv, &legacy_bv); + for cols in cols { + serialize_batch_value_iterators(&mut iters, cols); } } let s = String::from("hello"); let tuple: ((&str,),) = ((&s,),); - assert_has_batch_values(&tuple); + let cols: &[&[ColumnSpec]] = &[&[col_spec("a", ColumnType::Text)]]; + assert_has_batch_values::<&_>(&tuple, cols); let tuple2: ((&str, &str), (&str, &str)) = ((&s, &s), (&s, &s)); - assert_has_batch_values(&tuple2); + let cols: &[&[ColumnSpec]] = &[ + &[ + col_spec("a", ColumnType::Text), + col_spec("b", ColumnType::Text), + ], + &[ + col_spec("a", ColumnType::Text), + col_spec("b", ColumnType::Text), + ], + ]; + assert_has_batch_values::<&_>(&tuple2, cols); } #[test] fn check_batch_values_iterator_is_not_lending() { // This is an interesting property if we want to improve the batch shard selection heuristic - fn f(bv: impl BatchValues) { + fn f(bv: impl LegacyBatchValues) { let mut it = bv.batch_values_iter(); let mut it2 = bv.batch_values_iter(); // Make sure we can hold all these at the same time @@ -728,5 +1567,24 @@ fn check_batch_values_iterator_is_not_lending() { ]; let _ = v; } - f(((10,), (11,))) + fn g(bv: impl BatchValues) { + let mut it = bv.batch_values_iter(); + let mut it2 = bv.batch_values_iter(); + + let columns = &[col_spec("a", ColumnType::Int)]; + let ctx = RowSerializationContext { columns }; + let mut data = Vec::new(); + let mut writer = RowWriter::new(&mut data); + + // Make sure we can hold all these at the same time + let v = vec![ + it.serialize_next(&ctx, &mut writer).unwrap().unwrap(), + it2.serialize_next(&ctx, &mut writer).unwrap().unwrap(), + it.serialize_next(&ctx, &mut writer).unwrap().unwrap(), + it2.serialize_next(&ctx, &mut writer).unwrap().unwrap(), + ]; + let _ = v; + } + f(((10,), (11,))); + g(((10,), (11,))); } diff --git a/scylla-cql/src/lib.rs b/scylla-cql/src/lib.rs index b8d7d28671..95794184db 100644 --- a/scylla-cql/src/lib.rs +++ b/scylla-cql/src/lib.rs @@ -1,7 +1,19 @@ pub mod errors; pub mod frame; #[macro_use] -pub mod macros; +pub mod macros { + pub use scylla_macros::FromRow; + pub use scylla_macros::FromUserType; + pub use scylla_macros::IntoUserType; + pub use scylla_macros::SerializeCql; + pub use scylla_macros::SerializeRow; + pub use scylla_macros::ValueList; + + // Reexports for derive(IntoUserType) + pub use bytes::{BufMut, Bytes, BytesMut}; + + pub use crate::impl_from_cql_value_from_method; +} pub mod types; @@ -17,7 +29,28 @@ pub mod _macro_internal { }; pub use crate::frame::response::result::{CqlValue, Row}; pub use crate::frame::value::{ - SerializedResult, SerializedValues, Value, ValueList, ValueTooBig, + LegacySerializedValues, SerializedResult, Value, ValueList, ValueTooBig, }; pub use crate::macros::*; + + pub use crate::types::serialize::row::{ + BuiltinSerializationError as BuiltinRowSerializationError, + BuiltinSerializationErrorKind as BuiltinRowSerializationErrorKind, + BuiltinTypeCheckError as BuiltinRowTypeCheckError, + BuiltinTypeCheckErrorKind as BuiltinRowTypeCheckErrorKind, RowSerializationContext, + SerializeRow, + }; + pub use crate::types::serialize::value::{ + BuiltinSerializationError as BuiltinTypeSerializationError, + BuiltinSerializationErrorKind as BuiltinTypeSerializationErrorKind, + BuiltinTypeCheckError as BuiltinTypeTypeCheckError, + BuiltinTypeCheckErrorKind as BuiltinTypeTypeCheckErrorKind, SerializeCql, + UdtSerializationErrorKind, UdtTypeCheckErrorKind, + }; + pub use crate::types::serialize::writers::WrittenCellProof; + pub use crate::types::serialize::{ + CellValueBuilder, CellWriter, RowWriter, SerializationError, + }; + + pub use crate::frame::response::result::ColumnType; } diff --git a/scylla-cql/src/macros.rs b/scylla-cql/src/macros.rs deleted file mode 100644 index 8d60312145..0000000000 --- a/scylla-cql/src/macros.rs +++ /dev/null @@ -1,19 +0,0 @@ -/// #[derive(FromRow)] derives FromRow for struct -/// Works only on simple structs without generics etc -pub use scylla_macros::FromRow; - -/// #[derive(FromUserType)] allows to parse struct as a User Defined Type -/// Works only on simple structs without generics etc -pub use scylla_macros::FromUserType; - -/// #[derive(IntoUserType)] allows to pass struct a User Defined Type Value in queries -/// Works only on simple structs without generics etc -pub use scylla_macros::IntoUserType; - -/// #[derive(ValueList)] allows to pass struct as a list of values for a query -pub use scylla_macros::ValueList; - -// Reexports for derive(IntoUserType) -pub use bytes::{BufMut, Bytes, BytesMut}; - -pub use crate::impl_from_cql_value_from_method; diff --git a/scylla-cql/src/types/serialize/batch.rs b/scylla-cql/src/types/serialize/batch.rs new file mode 100644 index 0000000000..4fed3524a3 --- /dev/null +++ b/scylla-cql/src/types/serialize/batch.rs @@ -0,0 +1,364 @@ +//! Contains the [`BatchValues`] and [`BatchValuesIterator`] trait and their +//! implementations. + +use crate::frame::value::{LegacyBatchValues, LegacyBatchValuesIterator}; + +use super::row::{RowSerializationContext, SerializeRow}; +use super::{RowWriter, SerializationError}; + +/// Represents a list of sets of values for a batch statement. +/// +/// The data in the object can be consumed with an iterator-like object returned +/// by the [`BatchValues::batch_values_iter`] method. +pub trait BatchValues { + /// An `Iterator`-like object over the values from the parent `BatchValues` object. + // For some unknown reason, this type, when not resolved to a concrete type for a given async function, + // cannot live across await boundaries while maintaining the corresponding future `Send`, unless `'r: 'static` + // + // See for more details + type BatchValuesIter<'r>: BatchValuesIterator<'r> + where + Self: 'r; + + /// Returns an iterator over the data contained in this object. + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_>; +} + +/// An `Iterator`-like object over the values from the parent [`BatchValues`] object. +/// +/// It's not a true [`Iterator`] because it does not provide direct access to the +/// items being iterated over, instead it allows calling methods of the underlying +/// [`SerializeRow`] trait while advancing the iterator. +pub trait BatchValuesIterator<'bv> { + /// Serializes the next set of values in the sequence and advances the iterator. + fn serialize_next( + &mut self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Option>; + + /// Returns whether the next set of values is empty or not and advances the iterator. + fn is_empty_next(&mut self) -> Option; + + /// Skips the next set of values. + fn skip_next(&mut self) -> Option<()>; + + /// Return the number of sets of values, consuming the iterator in the process. + #[inline] + fn count(mut self) -> usize + where + Self: Sized, + { + let mut count = 0; + while self.skip_next().is_some() { + count += 1; + } + count + } +} + +/// Implements `BatchValuesIterator` from an `Iterator` over references to things that implement `SerializeRow` +/// +/// Essentially used internally by this lib to provide implementers of `BatchValuesIterator` for cases +/// that always serialize the same concrete `SerializeRow` type +pub struct BatchValuesIteratorFromIterator { + it: IT, +} + +impl<'bv, 'sr: 'bv, IT, SR> BatchValuesIterator<'bv> for BatchValuesIteratorFromIterator +where + IT: Iterator, + SR: SerializeRow + 'sr, +{ + #[inline] + fn serialize_next( + &mut self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Option> { + self.it.next().map(|sr| sr.serialize(ctx, writer)) + } + + #[inline] + fn is_empty_next(&mut self) -> Option { + self.it.next().map(|sr| sr.is_empty()) + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + self.it.next().map(|_| ()) + } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.it.count() + } +} + +impl From for BatchValuesIteratorFromIterator +where + IT: Iterator, + IT::Item: SerializeRow, +{ + #[inline] + fn from(it: IT) -> Self { + BatchValuesIteratorFromIterator { it } + } +} + +// +// BatchValues impls +// + +/// Implements `BatchValues` from an `Iterator` over references to things that implement `SerializeRow` +/// +/// This is to avoid requiring allocating a new `Vec` containing all the `SerializeRow`s directly: +/// with this, one can write: +/// `session.batch(&batch, BatchValuesFromIter::from(lines_to_insert.iter().map(|l| &l.value_list)))` +/// where `lines_to_insert` may also contain e.g. data to pick the statement... +/// +/// The underlying iterator will always be cloned at least once, once to compute the length if it can't be known +/// in advance, and be re-cloned at every retry. +/// It is consequently expected that the provided iterator is cheap to clone (e.g. `slice.iter().map(...)`). +pub struct BatchValuesFromIterator<'sr, IT> { + it: IT, + + // Without artificially introducing a lifetime to the struct, I couldn't get + // impl BatchValues for BatchValuesFromIterator to work. I wish I understood + // why it's needed. + _phantom: std::marker::PhantomData<&'sr ()>, +} + +impl<'sr, IT, SR> BatchValuesFromIterator<'sr, IT> +where + IT: Iterator + Clone, + SR: SerializeRow + 'sr, +{ + /// Creates a new `BatchValuesFromIter`` object. + #[inline] + pub fn new(into_iter: impl IntoIterator) -> Self { + Self { + it: into_iter.into_iter(), + _phantom: std::marker::PhantomData, + } + } +} + +impl<'sr, IT, SR> From for BatchValuesFromIterator<'sr, IT> +where + IT: Iterator + Clone, + SR: SerializeRow + 'sr, +{ + #[inline] + fn from(it: IT) -> Self { + Self::new(it) + } +} + +impl<'sr, IT, SR> BatchValues for BatchValuesFromIterator<'sr, IT> +where + IT: Iterator + Clone, + SR: SerializeRow + 'sr, +{ + type BatchValuesIter<'r> = BatchValuesIteratorFromIterator where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + self.it.clone().into() + } +} + +// Implement BatchValues for slices of SerializeRow types +impl BatchValues for [T] { + type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + self.iter().into() + } +} + +// Implement BatchValues for Vec +impl BatchValues for Vec { + type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + BatchValues::batch_values_iter(self.as_slice()) + } +} + +// Here is an example implementation for (T0, ) +// Further variants are done using a macro +impl BatchValues for (T0,) { + type BatchValuesIter<'r> = BatchValuesIteratorFromIterator> where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + std::iter::once(&self.0).into() + } +} + +/// A [`BatchValuesIterator`] over a tuple. +pub struct TupleValuesIter<'sr, T> { + tuple: &'sr T, + idx: usize, +} + +macro_rules! impl_batch_values_for_tuple { + ( $($Ti:ident),* ; $($FieldI:tt),* ; $TupleSize:tt) => { + impl<$($Ti),+> BatchValues for ($($Ti,)+) + where + $($Ti: SerializeRow),+ + { + type BatchValuesIter<'r> = TupleValuesIter<'r, ($($Ti,)+)> where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + TupleValuesIter { + tuple: self, + idx: 0, + } + } + } + + impl<'bv, $($Ti),+> BatchValuesIterator<'bv> for TupleValuesIter<'bv, ($($Ti,)+)> + where + $($Ti: SerializeRow),+ + { + #[inline] + fn serialize_next( + &mut self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Option> { + let ret = match self.idx { + $( + $FieldI => self.tuple.$FieldI.serialize(ctx, writer), + )* + _ => return None, + }; + self.idx += 1; + Some(ret) + } + + #[inline] + fn is_empty_next(&mut self) -> Option { + let ret = match self.idx { + $( + $FieldI => self.tuple.$FieldI.is_empty(), + )* + _ => return None, + }; + self.idx += 1; + Some(ret) + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + if self.idx < $TupleSize { + self.idx += 1; + Some(()) + } else { + None + } + } + + #[inline] + fn count(self) -> usize { + $TupleSize - self.idx + } + } + } +} + +impl_batch_values_for_tuple!(T0, T1; 0, 1; 2); +impl_batch_values_for_tuple!(T0, T1, T2; 0, 1, 2; 3); +impl_batch_values_for_tuple!(T0, T1, T2, T3; 0, 1, 2, 3; 4); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4; 0, 1, 2, 3, 4; 5); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5; 0, 1, 2, 3, 4, 5; 6); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6; 0, 1, 2, 3, 4, 5, 6; 7); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7; 0, 1, 2, 3, 4, 5, 6, 7; 8); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8; 0, 1, 2, 3, 4, 5, 6, 7, 8; 9); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9; 10); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; 11); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11; 12); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12; 13); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13; 14); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14; 15); +impl_batch_values_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15; 16); + +// Every &impl BatchValues should also implement BatchValues +impl<'a, T: BatchValues + ?Sized> BatchValues for &'a T { + type BatchValuesIter<'r> = ::BatchValuesIter<'r> where Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + ::batch_values_iter(*self) + } +} + +/// A newtype wrapper which adjusts an existing types that implement +/// [`LegacyBatchValues`] to the current [`BatchValues`] API. +/// +/// Note that the [`LegacyBatchValues`] trait is deprecated and will be +/// removed in the future, and you should prefer using [`BatchValues`] as it is +/// more type-safe. +pub struct LegacyBatchValuesAdapter(pub T); + +impl BatchValues for LegacyBatchValuesAdapter +where + T: LegacyBatchValues, +{ + type BatchValuesIter<'r> = LegacyBatchValuesIteratorAdapter> + where + Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + LegacyBatchValuesIteratorAdapter(self.0.batch_values_iter()) + } +} + +/// A newtype wrapper which adjusts an existing types that implement +/// [`LegacyBatchValuesIterator`] to the current [`BatchValuesIterator`] API. +pub struct LegacyBatchValuesIteratorAdapter(pub T); + +impl<'r, T> BatchValuesIterator<'r> for LegacyBatchValuesIteratorAdapter +where + T: LegacyBatchValuesIterator<'r>, +{ + #[inline] + fn serialize_next( + &mut self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Option> { + self.0.next_serialized().map(|sv| { + sv.map_err(SerializationError::new) + .and_then(|sv| sv.serialize(ctx, writer)) + }) + } + + #[inline] + fn is_empty_next(&mut self) -> Option { + self.0 + .next_serialized() + .map(|sv| sv.map_or(false, |sv| sv.len() == 0)) + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + self.0.skip_next() + } +} diff --git a/scylla-cql/src/types/serialize/mod.rs b/scylla-cql/src/types/serialize/mod.rs index 0cda84e252..7c6c62c7a7 100644 --- a/scylla-cql/src/types/serialize/mod.rs +++ b/scylla-cql/src/types/serialize/mod.rs @@ -1,6 +1,50 @@ -use std::{any::Any, sync::Arc}; +#![warn(missing_docs)] +//! Types and traits related to serialization of values to the CQL format. + +use std::{error::Error, fmt::Display, sync::Arc}; + +use thiserror::Error; + +pub mod batch; +pub mod raw_batch; pub mod row; pub mod value; +pub mod writers; + +pub use writers::{CellValueBuilder, CellWriter, RowWriter}; + +/// An error indicating that a failure happened during serialization. +/// +/// The error is type-erased so that the crate users can define their own +/// serialization impls and their errors. As for the impls defined or generated +/// by the driver itself, the following errors can be returned: +/// +/// - [`row::BuiltinSerializationError`] is returned when serialization of +/// one of types with an impl built into the driver fails. It is also returned +/// from impls generated by the `SerializeRow` macro. +/// - [`value::BuiltinSerializationError`] is analogous to the above but is +/// returned from [`SerializeCql::serialize`](value::SerializeCql::serialize) +/// instead both in the case of builtin impls and impls generated by the +/// `SerializeCql` macro. It won't be returned by the `Session` directly, +/// but it might be nested in the [`row::BuiltinSerializationError`]. +/// - [`row::ValueListToSerializeRowAdapterError`] is returned in case when +/// a list of named values encoded with the legacy `ValueList` trait is passed +/// as an argument to the statement, and rewriting it using the new +/// `SerializeRow` interface fails. +#[derive(Debug, Clone, Error)] +pub struct SerializationError(Arc); + +impl SerializationError { + /// Constructs a new `SerializationError`. + #[inline] + pub fn new(err: impl Error + Send + Sync + 'static) -> SerializationError { + SerializationError(Arc::new(err)) + } +} -type SerializationError = Arc; +impl Display for SerializationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SerializationError: {}", self.0) + } +} diff --git a/scylla-cql/src/types/serialize/raw_batch.rs b/scylla-cql/src/types/serialize/raw_batch.rs new file mode 100644 index 0000000000..e378f42dcb --- /dev/null +++ b/scylla-cql/src/types/serialize/raw_batch.rs @@ -0,0 +1,162 @@ +//! Contains the [`RawBatchValues`] and [`RawBatchValuesIterator`] trait and their +//! implementations. + +use super::batch::{BatchValues, BatchValuesIterator}; +use super::row::{RowSerializationContext, SerializedValues}; +use super::{RowWriter, SerializationError}; + +/// Represents a list of sets of values for a batch statement. +/// +/// Unlike [`BatchValues`]), it doesn't require type +/// information from the statements of the batch in order to be serialized. +/// +/// This is a lower level trait than [`BatchValues`]) +/// and is only used for interaction between the code in `scylla` and +/// `scylla-cql` crates. If you are a regular user of the driver, you shouldn't +/// care about this trait at all. +pub trait RawBatchValues { + /// An `Iterator`-like object over the values from the parent `BatchValues` object. + // For some unknown reason, this type, when not resolved to a concrete type for a given async function, + // cannot live across await boundaries while maintaining the corresponding future `Send`, unless `'r: 'static` + // + // See for more details + type RawBatchValuesIter<'r>: RawBatchValuesIterator<'r> + where + Self: 'r; + + /// Returns an iterator over the data contained in this object. + fn batch_values_iter(&self) -> Self::RawBatchValuesIter<'_>; +} + +/// An `Iterator`-like object over the values from the parent [`RawBatchValues`] object. +/// +/// It's not a true [`Iterator`] because it does not provide direct access to the +/// items being iterated over, instead it allows calling methods of the underlying +/// [`SerializeRow`](super::row::SerializeRow) trait while advancing the iterator. +/// +/// Unlike [`BatchValuesIterator`], it doesn't +/// need type information for serialization. +pub trait RawBatchValuesIterator<'a> { + /// Serializes the next set of values in the sequence and advances the iterator. + fn serialize_next(&mut self, writer: &mut RowWriter) -> Option>; + + /// Returns whether the next set of values is empty or not and advances the iterator. + fn is_empty_next(&mut self) -> Option; + + /// Skips the next set of values. + fn skip_next(&mut self) -> Option<()>; + + /// Return the number of sets of values, consuming the iterator in the process. + #[inline] + fn count(mut self) -> usize + where + Self: Sized, + { + let mut count = 0; + while self.skip_next().is_some() { + count += 1; + } + count + } +} + +// An implementation used by `scylla-proxy` +impl RawBatchValues for Vec { + type RawBatchValuesIter<'r> = std::slice::Iter<'r, SerializedValues> + where + Self: 'r; + + fn batch_values_iter(&self) -> Self::RawBatchValuesIter<'_> { + self.iter() + } +} + +impl<'r> RawBatchValuesIterator<'r> for std::slice::Iter<'r, SerializedValues> { + #[inline] + fn serialize_next(&mut self, writer: &mut RowWriter) -> Option> { + self.next().map(|sv| { + writer.append_serialize_row(sv); + Ok(()) + }) + } + + fn is_empty_next(&mut self) -> Option { + self.next().map(|sv| sv.is_empty()) + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + self.next().map(|_| ()) + } + + #[inline] + fn count(self) -> usize { + <_ as Iterator>::count(self) + } +} + +/// Takes `BatchValues` and an iterator over contexts, and turns them into a `RawBatchValues`. +pub struct RawBatchValuesAdapter { + batch_values: BV, + contexts: CTX, +} + +impl RawBatchValuesAdapter { + /// Creates a new `RawBatchValuesAdapter` object. + #[inline] + pub fn new(batch_values: BV, contexts: CTX) -> Self { + Self { + batch_values, + contexts, + } + } +} + +impl<'ctx, BV, CTX> RawBatchValues for RawBatchValuesAdapter +where + BV: BatchValues, + CTX: Iterator> + Clone, +{ + type RawBatchValuesIter<'r> = RawBatchValuesIteratorAdapter, CTX> + where + Self: 'r; + + #[inline] + fn batch_values_iter(&self) -> Self::RawBatchValuesIter<'_> { + RawBatchValuesIteratorAdapter { + batch_values_iterator: self.batch_values.batch_values_iter(), + contexts: self.contexts.clone(), + } + } +} + +/// Takes `BatchValuesIterator` and an iterator over contexts, and turns them into a `RawBatchValuesIterator`. +pub struct RawBatchValuesIteratorAdapter { + batch_values_iterator: BVI, + contexts: CTX, +} + +impl<'bvi, 'ctx, BVI, CTX> RawBatchValuesIterator<'bvi> for RawBatchValuesIteratorAdapter +where + BVI: BatchValuesIterator<'bvi>, + CTX: Iterator>, +{ + #[inline] + fn serialize_next(&mut self, writer: &mut RowWriter) -> Option> { + let ctx = self.contexts.next()?; + self.batch_values_iterator.serialize_next(&ctx, writer) + } + + fn is_empty_next(&mut self) -> Option { + self.contexts.next()?; + let ret = self.batch_values_iterator.is_empty_next()?; + Some(ret) + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + self.contexts.next()?; + self.batch_values_iterator.skip_next()?; + Some(()) + } +} diff --git a/scylla-cql/src/types/serialize/row.rs b/scylla-cql/src/types/serialize/row.rs index 2e9832412d..6c485ed147 100644 --- a/scylla-cql/src/types/serialize/row.rs +++ b/scylla-cql/src/types/serialize/row.rs @@ -1,20 +1,53 @@ -use std::sync::Arc; +//! Contains the [`SerializeRow`] trait and its implementations. -use crate::frame::response::result::ColumnSpec; -use crate::frame::value::ValueList; +use std::borrow::Cow; +use std::collections::{BTreeMap, HashSet}; +use std::fmt::Display; +use std::hash::BuildHasher; +use std::{collections::HashMap, sync::Arc}; -use super::SerializationError; +use bytes::BufMut; +use thiserror::Error; +use crate::frame::frame_errors::ParseError; +use crate::frame::response::result::ColumnType; +use crate::frame::response::result::PreparedMetadata; +use crate::frame::types; +use crate::frame::value::SerializeValuesError; +use crate::frame::value::{LegacySerializedValues, ValueList}; +use crate::frame::{response::result::ColumnSpec, types::RawValue}; + +use super::value::SerializeCql; +use super::{CellWriter, RowWriter, SerializationError}; + +/// Contains information needed to serialize a row. pub struct RowSerializationContext<'a> { - columns: &'a [ColumnSpec], + pub(crate) columns: &'a [ColumnSpec], } impl<'a> RowSerializationContext<'a> { + /// Creates the serialization context from prepared statement metadata. + #[inline] + pub fn from_prepared(prepared: &'a PreparedMetadata) -> Self { + Self { + columns: prepared.col_specs.as_slice(), + } + } + + /// Constructs an empty `RowSerializationContext`, as if for a statement + /// with no bind markers. + #[inline] + pub const fn empty() -> Self { + Self { columns: &[] } + } + + /// Returns column/bind marker specifications for given query. #[inline] pub fn columns(&self) -> &'a [ColumnSpec] { self.columns } + /// Looks up and returns a column/bind marker by name. // TODO: change RowSerializationContext to make this faster #[inline] pub fn column_by_name(&self, target: &str) -> Option<&ColumnSpec> { @@ -22,28 +55,1508 @@ impl<'a> RowSerializationContext<'a> { } } +/// Represents a set of values that can be sent along a CQL statement. +/// +/// This is a low-level trait that is exposed to the specifics to the CQL +/// protocol and usually does not have to be implemented directly. See the +/// chapter on "Query Values" in the driver docs for information about how +/// this trait is supposed to be used. pub trait SerializeRow { - fn preliminary_type_check(ctx: &RowSerializationContext<'_>) -> Result<(), SerializationError>; + /// Serializes the row according to the information in the given context. + /// + /// It's the trait's responsibility to produce values in the order as + /// specified in given serialization context. fn serialize( &self, ctx: &RowSerializationContext<'_>, - out: &mut Vec, + writer: &mut RowWriter, ) -> Result<(), SerializationError>; + + /// Returns whether this row contains any values or not. + /// + /// This method is used before executing a simple statement in order to check + /// whether there are any values provided to it. If there are some, then + /// the query will be prepared first in order to obtain information about + /// the bind marker types and names so that the values can be properly + /// type checked and serialized. + fn is_empty(&self) -> bool; +} + +macro_rules! fallback_impl_contents { + () => { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + serialize_legacy_row(self, ctx, writer) + } + #[inline] + fn is_empty(&self) -> bool { + LegacySerializedValues::is_empty(self) + } + }; +} + +macro_rules! impl_serialize_row_for_unit { + () => { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + _writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + if !ctx.columns().is_empty() { + return Err(mk_typck_err::( + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: 0, + asked_for: ctx.columns().len(), + }, + )); + } + // Row is empty - do nothing + Ok(()) + } + + #[inline] + fn is_empty(&self) -> bool { + true + } + }; +} + +impl SerializeRow for () { + impl_serialize_row_for_unit!(); +} + +impl SerializeRow for [u8; 0] { + impl_serialize_row_for_unit!(); +} + +macro_rules! impl_serialize_row_for_slice { + () => { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + if ctx.columns().len() != self.len() { + return Err(mk_typck_err::( + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: self.len(), + asked_for: ctx.columns().len(), + }, + )); + } + for (col, val) in ctx.columns().iter().zip(self.iter()) { + ::serialize(val, &col.typ, writer.make_cell_writer()).map_err( + |err| { + mk_ser_err::( + BuiltinSerializationErrorKind::ColumnSerializationFailed { + name: col.name.clone(), + err, + }, + ) + }, + )?; + } + Ok(()) + } + + #[inline] + fn is_empty(&self) -> bool { + <[T]>::is_empty(self.as_ref()) + } + }; +} + +impl<'a, T: SerializeCql + 'a> SerializeRow for &'a [T] { + impl_serialize_row_for_slice!(); +} + +impl SerializeRow for Vec { + impl_serialize_row_for_slice!(); +} + +macro_rules! impl_serialize_row_for_map { + () => { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + // Unfortunately, column names aren't guaranteed to be unique. + // We need to track not-yet-used columns in order to see + // whether some values were not used at the end, and report an error. + let mut unused_columns: HashSet<&str> = self.keys().map(|k| k.as_ref()).collect(); + + for col in ctx.columns.iter() { + match self.get(col.name.as_str()) { + None => { + return Err(mk_typck_err::( + BuiltinTypeCheckErrorKind::ValueMissingForColumn { + name: col.name.clone(), + }, + )) + } + Some(v) => { + ::serialize(v, &col.typ, writer.make_cell_writer()) + .map_err(|err| { + mk_ser_err::( + BuiltinSerializationErrorKind::ColumnSerializationFailed { + name: col.name.clone(), + err, + }, + ) + })?; + let _ = unused_columns.remove(col.name.as_str()); + } + } + } + + if !unused_columns.is_empty() { + // Report the lexicographically first value for deterministic error messages + let name = unused_columns.iter().min().unwrap(); + return Err(mk_typck_err::( + BuiltinTypeCheckErrorKind::NoColumnWithName { + name: name.to_string(), + }, + )); + } + + Ok(()) + } + + #[inline] + fn is_empty(&self) -> bool { + Self::is_empty(self) + } + }; +} + +impl SerializeRow for BTreeMap { + impl_serialize_row_for_map!(); +} + +impl SerializeRow for BTreeMap<&str, T> { + impl_serialize_row_for_map!(); +} + +impl SerializeRow for HashMap { + impl_serialize_row_for_map!(); +} + +impl SerializeRow for HashMap<&str, T, S> { + impl_serialize_row_for_map!(); } -impl SerializeRow for T { - fn preliminary_type_check( - _ctx: &RowSerializationContext<'_>, +impl SerializeRow for &T { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, ) -> Result<(), SerializationError> { - Ok(()) + ::serialize(self, ctx, writer) } + #[inline] + fn is_empty(&self) -> bool { + ::is_empty(self) + } +} + +impl SerializeRow for LegacySerializedValues { + fallback_impl_contents!(); +} + +impl<'b> SerializeRow for Cow<'b, LegacySerializedValues> { + fallback_impl_contents!(); +} + +macro_rules! impl_tuple { + ( + $($typs:ident),*; + $($fidents:ident),*; + $($tidents:ident),*; + $length:expr + ) => { + impl<$($typs: SerializeCql),*> SerializeRow for ($($typs,)*) { + fn serialize( + &self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + let ($($tidents,)*) = match ctx.columns() { + [$($tidents),*] => ($($tidents,)*), + _ => return Err(mk_typck_err::( + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: $length, + asked_for: ctx.columns().len(), + }, + )), + }; + let ($($fidents,)*) = self; + $( + <$typs as SerializeCql>::serialize($fidents, &$tidents.typ, writer.make_cell_writer()).map_err(|err| { + mk_ser_err::(BuiltinSerializationErrorKind::ColumnSerializationFailed { + name: $tidents.name.clone(), + err, + }) + })?; + )* + Ok(()) + } + + #[inline] + fn is_empty(&self) -> bool { + $length == 0 + } + } + }; +} + +macro_rules! impl_tuples { + (;;;$length:expr) => {}; + ( + $typ:ident$(, $($typs:ident),*)?; + $fident:ident$(, $($fidents:ident),*)?; + $tident:ident$(, $($tidents:ident),*)?; + $length:expr + ) => { + impl_tuples!( + $($($typs),*)?; + $($($fidents),*)?; + $($($tidents),*)?; + $length - 1 + ); + impl_tuple!( + $typ$(, $($typs),*)?; + $fident$(, $($fidents),*)?; + $tident$(, $($tidents),*)?; + $length + ); + }; +} + +impl_tuples!( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15; + f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15; + t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15; + 16 +); + +/// Implements the [`SerializeRow`] trait for a type, provided that the type +/// already implements the legacy +/// [`ValueList`](crate::frame::value::ValueList) trait. +/// +/// # Note +/// +/// The translation from one trait to another encounters a performance penalty +/// and does not utilize the stronger guarantees of `SerializeRow`. Before +/// resorting to this macro, you should consider other options instead: +/// +/// - If the impl was generated using the `ValueList` procedural macro, you +/// should switch to the `SerializeRow` procedural macro. *The new macro +/// behaves differently by default, so please read its documentation first!* +/// - If the impl was written by hand, it is still preferable to rewrite it +/// manually. You have an opportunity to make your serialization logic +/// type-safe and potentially improve performance. +/// +/// Basically, you should consider using the macro if you have a hand-written +/// impl and the moment it is not easy/not desirable to rewrite it. +/// +/// # Example +/// +/// ```rust +/// # use std::borrow::Cow; +/// # use scylla_cql::frame::value::{Value, ValueList, SerializedResult, LegacySerializedValues}; +/// # use scylla_cql::impl_serialize_row_via_value_list; +/// struct NoGenerics {} +/// impl ValueList for NoGenerics { +/// fn serialized(&self) -> SerializedResult<'_> { +/// ().serialized() +/// } +/// } +/// impl_serialize_row_via_value_list!(NoGenerics); +/// +/// // Generic types are also supported. You must specify the bounds if the +/// // struct/enum contains any. +/// struct WithGenerics(T, U); +/// impl ValueList for WithGenerics { +/// fn serialized(&self) -> SerializedResult<'_> { +/// let mut values = LegacySerializedValues::new(); +/// values.add_value(&self.0); +/// values.add_value(&self.1.clone()); +/// Ok(Cow::Owned(values)) +/// } +/// } +/// impl_serialize_row_via_value_list!(WithGenerics); +/// ``` +#[macro_export] +macro_rules! impl_serialize_row_via_value_list { + ($t:ident$(<$($targ:tt $(: $tbound:tt)?),*>)?) => { + impl $(<$($targ $(: $tbound)?),*>)? $crate::types::serialize::row::SerializeRow + for $t$(<$($targ),*>)? + where + Self: $crate::frame::value::ValueList, + { + fn serialize( + &self, + ctx: &$crate::types::serialize::row::RowSerializationContext<'_>, + writer: &mut $crate::types::serialize::writers::RowWriter, + ) -> ::std::result::Result<(), $crate::types::serialize::SerializationError> { + $crate::types::serialize::row::serialize_legacy_row(self, ctx, writer) + } + + #[inline] + fn is_empty(&self) -> bool { + match $crate::frame::value::ValueList::serialized(self) { + Ok(s) => s.is_empty(), + Err(e) => false + } + } + } + }; +} + +/// Implements [`SerializeRow`] if the type wrapped over implements [`ValueList`]. +/// +/// See the [`impl_serialize_row_via_value_list`] macro on information about +/// the properties of the [`SerializeRow`] implementation. +pub struct ValueListAdapter(pub T); + +impl SerializeRow for ValueListAdapter +where + T: ValueList, +{ + #[inline] fn serialize( &self, - _ctx: &RowSerializationContext<'_>, - out: &mut Vec, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Result<(), SerializationError> { + serialize_legacy_row(&self.0, ctx, writer) + } + + #[inline] + fn is_empty(&self) -> bool { + match self.0.serialized() { + Ok(s) => s.is_empty(), + Err(_) => false, + } + } +} + +/// Serializes an object implementing [`ValueList`] by using the [`RowWriter`] +/// interface. +/// +/// The function first serializes the value with [`ValueList::serialized`], then +/// parses the result and serializes it again with given `RowWriter`. In case +/// or serialized values with names, they are converted to serialized values +/// without names, based on the information about the bind markers provided +/// in the [`RowSerializationContext`]. +/// +/// It is a lazy and inefficient way to implement `RowWriter` via an existing +/// `ValueList` impl. +/// +/// Returns an error if `ValueList::serialized` call failed or, in case of +/// named serialized values, some bind markers couldn't be matched to a +/// named value. +/// +/// See [`impl_serialize_row_via_value_list`] which generates a boilerplate +/// [`SerializeRow`] implementation that uses this function. +pub fn serialize_legacy_row( + r: &T, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, +) -> Result<(), SerializationError> { + let serialized = + ::serialized(r).map_err(|err| SerializationError(Arc::new(err)))?; + + let mut append_value = |value: RawValue| { + let cell_writer = writer.make_cell_writer(); + let _proof = match value { + RawValue::Null => cell_writer.set_null(), + RawValue::Unset => cell_writer.set_unset(), + // The unwrap below will succeed because the value was successfully + // deserialized from the CQL format, so it must have had correct + // size. + RawValue::Value(v) => cell_writer.set_value(v).unwrap(), + }; + }; + + if !serialized.has_names() { + serialized.iter().for_each(append_value); + } else { + let mut values_by_name = serialized + .iter_name_value_pairs() + .map(|(k, v)| (k.unwrap(), (v, false))) + .collect::>(); + let mut unused_count = values_by_name.len(); + + for col in ctx.columns() { + let (val, visited) = values_by_name.get_mut(col.name.as_str()).ok_or_else(|| { + SerializationError(Arc::new( + ValueListToSerializeRowAdapterError::ValueMissingForBindMarker { + name: col.name.clone(), + }, + )) + })?; + if !*visited { + *visited = true; + unused_count -= 1; + } + append_value(*val); + } + + if unused_count != 0 { + // Choose the lexicographically earliest name for the sake + // of deterministic errors + let name = values_by_name + .iter() + .filter_map(|(k, (_, visited))| (!visited).then_some(k)) + .min() + .unwrap() + .to_string(); + return Err(SerializationError::new( + ValueListToSerializeRowAdapterError::NoBindMarkerWithName { name }, + )); + } + } + + Ok(()) +} + +/// Failed to type check values for a statement, represented by one of the types +/// built into the driver. +#[derive(Debug, Error, Clone)] +#[error("Failed to type check query arguments {rust_name}: {kind}")] +pub struct BuiltinTypeCheckError { + /// Name of the Rust type used to represent the values. + pub rust_name: &'static str, + + /// Detailed information about the failure. + pub kind: BuiltinTypeCheckErrorKind, +} + +fn mk_typck_err(kind: impl Into) -> SerializationError { + mk_typck_err_named(std::any::type_name::(), kind) +} + +fn mk_typck_err_named( + name: &'static str, + kind: impl Into, +) -> SerializationError { + SerializationError::new(BuiltinTypeCheckError { + rust_name: name, + kind: kind.into(), + }) +} + +/// Failed to serialize values for a statement, represented by one of the types +/// built into the driver. +#[derive(Debug, Error, Clone)] +#[error("Failed to serialize query arguments {rust_name}: {kind}")] +pub struct BuiltinSerializationError { + /// Name of the Rust type used to represent the values. + pub rust_name: &'static str, + + /// Detailed information about the failure. + pub kind: BuiltinSerializationErrorKind, +} + +fn mk_ser_err(kind: impl Into) -> SerializationError { + mk_ser_err_named(std::any::type_name::(), kind) +} + +fn mk_ser_err_named( + name: &'static str, + kind: impl Into, +) -> SerializationError { + SerializationError::new(BuiltinSerializationError { + rust_name: name, + kind: kind.into(), + }) +} + +/// Describes why type checking values for a statement failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum BuiltinTypeCheckErrorKind { + /// The Rust type expects `actual` column, but the statement requires `asked_for`. + WrongColumnCount { + /// The number of values that the Rust type provides. + actual: usize, + + /// The number of columns that the statement requires. + asked_for: usize, + }, + + /// The Rust type provides a value for some column, but that column is not + /// present in the statement. + NoColumnWithName { + /// Name of the column that is missing in the statement. + name: String, + }, + + /// A value required by the statement is not provided by the Rust type. + ValueMissingForColumn { + /// Name of the column for which the Rust type doesn't + /// provide a value. + name: String, + }, + + /// A different column name was expected at given position. + ColumnNameMismatch { + /// Name of the column, as expected by the Rust type. + rust_column_name: String, + + /// Name of the column for which the DB requested a value. + db_column_name: String, + }, +} + +impl Display for BuiltinTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuiltinTypeCheckErrorKind::WrongColumnCount { actual, asked_for } => { + write!(f, "wrong column count: the query requires {asked_for} columns, but {actual} were provided") + } + BuiltinTypeCheckErrorKind::NoColumnWithName { name } => { + write!( + f, + "value for column {name} was provided, but there is no bind marker for this column in the query" + ) + } + BuiltinTypeCheckErrorKind::ValueMissingForColumn { name } => { + write!( + f, + "value for column {name} was not provided, but the query requires it" + ) + } + BuiltinTypeCheckErrorKind::ColumnNameMismatch { rust_column_name, db_column_name } => write!( + f, + "expected column with name {db_column_name} at given position, but the Rust field name is {rust_column_name}" + ), + } + } +} + +/// Describes why serializing values for a statement failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum BuiltinSerializationErrorKind { + /// One of the columns failed to serialize. + ColumnSerializationFailed { + /// Name of the column that failed to serialize. + name: String, + + /// The error that caused the column serialization to fail. + err: SerializationError, + }, +} + +impl Display for BuiltinSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuiltinSerializationErrorKind::ColumnSerializationFailed { name, err } => { + write!(f, "failed to serialize column {name}: {err}") + } + } + } +} + +/// Describes a failure to translate the output of the [`ValueList`] legacy trait +/// into an output of the [`SerializeRow`] trait. +#[derive(Error, Debug)] +pub enum ValueListToSerializeRowAdapterError { + /// The values generated by the [`ValueList`] trait were provided in + /// name-value pairs, and there is a column in the statement for which + /// there is no corresponding named value. + #[error("Missing named value for column {name}")] + ValueMissingForBindMarker { + /// Name of the bind marker for which there is no value. + name: String, + }, + + /// The values generated by the [`ValueList`] trait were provided in + /// name-value pairs, and there is a named value which does not match + /// to any of the columns. + #[error("There is no bind marker with name {name}, but a value for it was provided")] + NoBindMarkerWithName { + /// Name of the value that does not match to any of the bind markers. + name: String, + }, +} + +/// A buffer containing already serialized values. +/// +/// It is not aware of the types of contained values, +/// it is basically a byte buffer in the format expected by the CQL protocol. +/// Usually there is no need for a user of a driver to use this struct, it is mostly internal. +/// The exception are APIs like `ClusterData::compute_token` / `ClusterData::get_endpoints`. +/// Allows adding new values to the buffer and iterating over the content. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct SerializedValues { + serialized_values: Vec, + element_count: u16, +} + +impl SerializedValues { + /// Constructs a new, empty `SerializedValues`. + pub const fn new() -> Self { + SerializedValues { + serialized_values: Vec::new(), + element_count: 0, + } + } + + /// A const empty instance, useful for taking references + pub const EMPTY: &'static SerializedValues = &SerializedValues::new(); + + /// Constructs `SerializedValues` from given [`SerializeRow`] object. + pub fn from_serializable( + ctx: &RowSerializationContext, + row: &T, + ) -> Result { + Self::from_closure(|writer| row.serialize(ctx, writer)).map(|(sr, _)| sr) + } + + /// Constructs `SerializedValues` via given closure. + pub fn from_closure(f: F) -> Result<(Self, R), SerializationError> + where + F: FnOnce(&mut RowWriter) -> Result, + { + let mut data = Vec::new(); + let mut writer = RowWriter::new(&mut data); + let ret = f(&mut writer)?; + let element_count = match writer.value_count().try_into() { + Ok(n) => n, + Err(_) => { + return Err(SerializationError(Arc::new( + SerializeValuesError::TooManyValues, + ))) + } + }; + + Ok(( + SerializedValues { + serialized_values: data, + element_count, + }, + ret, + )) + } + + /// Returns `true` if the row contains no elements. + #[inline] + pub fn is_empty(&self) -> bool { + self.element_count() == 0 + } + + /// Returns an iterator over the values serialized into the object so far. + #[inline] + pub fn iter(&self) -> impl Iterator { + SerializedValuesIterator { + serialized_values: &self.serialized_values, + } + } + + /// Returns the number of values written so far. + #[inline] + pub fn element_count(&self) -> u16 { + self.element_count + } + + /// Returns the total serialized size of the values written so far. + #[inline] + pub fn buffer_size(&self) -> usize { + self.serialized_values.len() + } + + pub(crate) fn write_to_request(&self, buf: &mut impl BufMut) { + buf.put_u16(self.element_count); + buf.put(self.serialized_values.as_slice()) + } + + // Gets the serialized values as raw bytes, without the preceding u16 length. + pub(crate) fn get_contents(&self) -> &[u8] { + &self.serialized_values + } + + /// Serializes value and appends it to the list + pub fn add_value( + &mut self, + val: &T, + typ: &ColumnType, ) -> Result<(), SerializationError> { - self.write_to_request(out) - .map_err(|err| Arc::new(err) as SerializationError) + if self.element_count() == u16::MAX { + return Err(SerializationError(Arc::new( + SerializeValuesError::TooManyValues, + ))); + } + + let len_before_serialize: usize = self.serialized_values.len(); + + let writer = CellWriter::new(&mut self.serialized_values); + if let Err(e) = val.serialize(typ, writer) { + self.serialized_values.resize(len_before_serialize, 0); + Err(e) + } else { + self.element_count += 1; + Ok(()) + } + } + + /// Creates value list from the request frame + pub(crate) fn new_from_frame(buf: &mut &[u8]) -> Result { + let values_num = types::read_short(buf)?; + let values_beg = *buf; + for _ in 0..values_num { + let _serialized = types::read_value(buf)?; + } + + let values_len_in_buf = values_beg.len() - buf.len(); + let values_in_frame = &values_beg[0..values_len_in_buf]; + Ok(SerializedValues { + serialized_values: values_in_frame.to_vec(), + element_count: values_num, + }) + } +} + +impl Default for SerializedValues { + fn default() -> Self { + Self::new() + } +} + +/// An iterator over raw values in some [`SerializedValues`]. +#[derive(Clone, Copy)] +pub struct SerializedValuesIterator<'a> { + serialized_values: &'a [u8], +} + +impl<'a> Iterator for SerializedValuesIterator<'a> { + type Item = RawValue<'a>; + + fn next(&mut self) -> Option { + if self.serialized_values.is_empty() { + return None; + } + + Some(types::read_value(&mut self.serialized_values).expect("badly encoded value")) + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::collections::BTreeMap; + + use crate::frame::response::result::{ColumnSpec, ColumnType, TableSpec}; + use crate::frame::types::RawValue; + use crate::frame::value::{LegacySerializedValues, MaybeUnset, SerializedResult, ValueList}; + use crate::types::serialize::row::ValueListAdapter; + use crate::types::serialize::{RowWriter, SerializationError}; + + use super::{ + BuiltinSerializationError, BuiltinSerializationErrorKind, BuiltinTypeCheckError, + BuiltinTypeCheckErrorKind, RowSerializationContext, SerializeCql, SerializeRow, + }; + + use super::SerializedValues; + use scylla_macros::SerializeRow; + + fn col_spec(name: &str, typ: ColumnType) -> ColumnSpec { + ColumnSpec { + table_spec: TableSpec { + ks_name: "ks".to_string(), + table_name: "tbl".to_string(), + }, + name: name.to_string(), + typ, + } + } + + #[test] + fn test_legacy_fallback() { + let row = ( + 1i32, + "Ala ma kota", + None::, + MaybeUnset::Unset::, + ); + + let mut legacy_data = Vec::new(); + <_ as ValueList>::write_to_request(&row, &mut legacy_data).unwrap(); + + let mut new_data = Vec::new(); + let mut new_data_writer = RowWriter::new(&mut new_data); + let ctx = RowSerializationContext { + columns: &[ + col_spec("a", ColumnType::Int), + col_spec("b", ColumnType::Text), + col_spec("c", ColumnType::BigInt), + col_spec("b", ColumnType::Ascii), + ], + }; + <_ as SerializeRow>::serialize(&row, &ctx, &mut new_data_writer).unwrap(); + assert_eq!(new_data_writer.value_count(), 4); + + // Skip the value count + assert_eq!(&legacy_data[2..], new_data); + } + + #[test] + fn test_legacy_fallback_with_names() { + let sorted_row = ( + 1i32, + "Ala ma kota", + None::, + MaybeUnset::Unset::, + ); + + let mut sorted_row_data = Vec::new(); + <_ as ValueList>::write_to_request(&sorted_row, &mut sorted_row_data).unwrap(); + + let mut unsorted_row = LegacySerializedValues::new(); + unsorted_row.add_named_value("a", &1i32).unwrap(); + unsorted_row.add_named_value("b", &"Ala ma kota").unwrap(); + unsorted_row + .add_named_value("d", &MaybeUnset::Unset::) + .unwrap(); + unsorted_row.add_named_value("c", &None::).unwrap(); + + let mut unsorted_row_data = Vec::new(); + let mut unsorted_row_data_writer = RowWriter::new(&mut unsorted_row_data); + let ctx = RowSerializationContext { + columns: &[ + col_spec("a", ColumnType::Int), + col_spec("b", ColumnType::Text), + col_spec("c", ColumnType::BigInt), + col_spec("d", ColumnType::Ascii), + ], + }; + <_ as SerializeRow>::serialize(&unsorted_row, &ctx, &mut unsorted_row_data_writer).unwrap(); + assert_eq!(unsorted_row_data_writer.value_count(), 4); + + // Skip the value count + assert_eq!(&sorted_row_data[2..], unsorted_row_data); + } + + #[test] + fn test_dyn_serialize_row() { + let row = ( + 1i32, + "Ala ma kota", + None::, + MaybeUnset::Unset::, + ); + let ctx = RowSerializationContext { + columns: &[ + col_spec("a", ColumnType::Int), + col_spec("b", ColumnType::Text), + col_spec("c", ColumnType::BigInt), + col_spec("d", ColumnType::Ascii), + ], + }; + + let mut typed_data = Vec::new(); + let mut typed_data_writer = RowWriter::new(&mut typed_data); + <_ as SerializeRow>::serialize(&row, &ctx, &mut typed_data_writer).unwrap(); + + let row = &row as &dyn SerializeRow; + let mut erased_data = Vec::new(); + let mut erased_data_writer = RowWriter::new(&mut erased_data); + <_ as SerializeRow>::serialize(&row, &ctx, &mut erased_data_writer).unwrap(); + + assert_eq!( + typed_data_writer.value_count(), + erased_data_writer.value_count(), + ); + assert_eq!(typed_data, erased_data); + } + + fn do_serialize(t: T, columns: &[ColumnSpec]) -> Vec { + let ctx = RowSerializationContext { columns }; + let mut ret = Vec::new(); + let mut builder = RowWriter::new(&mut ret); + t.serialize(&ctx, &mut builder).unwrap(); + ret + } + + fn do_serialize_err(t: T, columns: &[ColumnSpec]) -> SerializationError { + let ctx = RowSerializationContext { columns }; + let mut ret = Vec::new(); + let mut builder = RowWriter::new(&mut ret); + t.serialize(&ctx, &mut builder).unwrap_err() + } + + fn col(name: &str, typ: ColumnType) -> ColumnSpec { + ColumnSpec { + table_spec: TableSpec { + ks_name: "ks".to_string(), + table_name: "tbl".to_string(), + }, + name: name.to_string(), + typ, + } + } + + #[test] + fn test_legacy_wrapper() { + struct Foo; + impl ValueList for Foo { + fn serialized(&self) -> SerializedResult<'_> { + let mut values = LegacySerializedValues::new(); + values.add_value(&123i32)?; + values.add_value(&321i32)?; + Ok(Cow::Owned(values)) + } + } + + let columns = &[ + col_spec("a", ColumnType::Int), + col_spec("b", ColumnType::Int), + ]; + let buf = do_serialize(ValueListAdapter(Foo), columns); + let expected = vec![ + 0, 0, 0, 4, 0, 0, 0, 123, // First value + 0, 0, 0, 4, 0, 0, 1, 65, // Second value + ]; + assert_eq!(buf, expected); + } + + fn get_typeck_err(err: &SerializationError) -> &BuiltinTypeCheckError { + match err.0.downcast_ref() { + Some(err) => err, + None => panic!("not a BuiltinTypeCheckError: {}", err), + } + } + + fn get_ser_err(err: &SerializationError) -> &BuiltinSerializationError { + match err.0.downcast_ref() { + Some(err) => err, + None => panic!("not a BuiltinSerializationError: {}", err), + } + } + + #[test] + fn test_tuple_errors() { + // Unit + #[allow(clippy::let_unit_value)] // The let binding below is intentional + let v = (); + let spec = [col("a", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<()>()); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: 0, + asked_for: 1, + } + )); + + // Non-unit tuple + // Count mismatch + let v = ("Ala ma kota",); + let spec = [col("a", ColumnType::Text), col("b", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<(&str,)>()); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: 1, + asked_for: 2, + } + )); + + // Serialization of one of the element fails + let v = ("Ala ma kota", 123_i32); + let spec = [col("a", ColumnType::Text), col("b", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<(&str, i32)>()); + let BuiltinSerializationErrorKind::ColumnSerializationFailed { name, err: _ } = &err.kind; + assert_eq!(name, "b"); + } + + #[test] + fn test_slice_errors() { + // Non-unit tuple + // Count mismatch + let v = vec!["Ala ma kota"]; + let spec = [col("a", ColumnType::Text), col("b", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::WrongColumnCount { + actual: 1, + asked_for: 2, + } + )); + + // Serialization of one of the element fails + let v = vec!["Ala ma kota", "Kot ma pchły"]; + let spec = [col("a", ColumnType::Text), col("b", ColumnType::Int)]; + let err = do_serialize_err(v, &spec); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + let BuiltinSerializationErrorKind::ColumnSerializationFailed { name, err: _ } = &err.kind; + assert_eq!(name, "b"); + } + + #[test] + fn test_map_errors() { + // Missing value for a bind marker + let v: BTreeMap<_, _> = vec![("a", 123_i32)].into_iter().collect(); + let spec = [col("a", ColumnType::Int), col("b", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + let BuiltinTypeCheckErrorKind::ValueMissingForColumn { name } = &err.kind else { + panic!("unexpected error kind: {}", err.kind) + }; + assert_eq!(name, "b"); + + // Additional value, not present in the query + let v: BTreeMap<_, _> = vec![("a", 123_i32), ("b", 456_i32)].into_iter().collect(); + let spec = [col("a", ColumnType::Int)]; + let err = do_serialize_err(v, &spec); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + let BuiltinTypeCheckErrorKind::NoColumnWithName { name } = &err.kind else { + panic!("unexpected error kind: {}", err.kind) + }; + assert_eq!(name, "b"); + + // Serialization of one of the element fails + let v: BTreeMap<_, _> = vec![("a", 123_i32), ("b", 456_i32)].into_iter().collect(); + let spec = [col("a", ColumnType::Int), col("b", ColumnType::Text)]; + let err = do_serialize_err(v, &spec); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + let BuiltinSerializationErrorKind::ColumnSerializationFailed { name, err: _ } = &err.kind; + assert_eq!(name, "b"); + } + + // Do not remove. It's not used in tests but we keep it here to check that + // we properly ignore warnings about unused variables, unnecessary `mut`s + // etc. that usually pop up when generating code for empty structs. + #[derive(SerializeRow)] + #[scylla(crate = crate)] + struct TestRowWithNoColumns {} + + #[derive(SerializeRow, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate)] + struct TestRowWithColumnSorting { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_row_serialization_with_column_sorting_correct_order() { + let spec = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + ]; + + let reference = do_serialize(("Ala ma kota", 42i32, vec![1i64, 2i64, 3i64]), &spec); + let row = do_serialize( + TestRowWithColumnSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[test] + fn test_row_serialization_with_column_sorting_incorrect_order() { + // The order of two last columns is swapped + let spec = [ + col("a", ColumnType::Text), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + col("b", ColumnType::Int), + ]; + + let reference = do_serialize(("Ala ma kota", vec![1i64, 2i64, 3i64], 42i32), &spec); + let row = do_serialize( + TestRowWithColumnSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[test] + fn test_row_serialization_failing_type_check() { + let row = TestRowWithColumnSorting::default(); + let mut data = Vec::new(); + let mut row_writer = RowWriter::new(&mut data); + + let spec_without_c = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + // Missing column c + ]; + + let ctx = RowSerializationContext { + columns: &spec_without_c, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::ValueMissingForColumn { .. } + )); + + let spec_duplicate_column = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + // Unexpected last column + col("d", ColumnType::Counter), + ]; + + let ctx = RowSerializationContext { + columns: &spec_duplicate_column, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::NoColumnWithName { .. } + )); + + let spec_wrong_type = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::TinyInt), // Wrong type + ]; + + let ctx = RowSerializationContext { + columns: &spec_wrong_type, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::ColumnSerializationFailed { .. } + )); + } + + #[derive(SerializeRow)] + #[scylla(crate = crate)] + struct TestRowWithGenerics<'a, T: SerializeCql> { + a: &'a str, + b: T, + } + + #[test] + fn test_row_serialization_with_generics() { + // A minimal smoke test just to test that it works. + fn check_with_type(typ: ColumnType, t: T) { + let spec = [col("a", ColumnType::Text), col("b", typ)]; + let reference = do_serialize(("Ala ma kota", t), &spec); + let row = do_serialize( + TestRowWithGenerics { + a: "Ala ma kota", + b: t, + }, + &spec, + ); + assert_eq!(reference, row); + } + + check_with_type(ColumnType::Int, 123_i32); + check_with_type(ColumnType::Double, 123_f64); + } + + #[derive(SerializeRow, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate, flavor = "enforce_order")] + struct TestRowWithEnforcedOrder { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_row_serialization_with_enforced_order_correct_order() { + let spec = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + ]; + + let reference = do_serialize(("Ala ma kota", 42i32, vec![1i64, 2i64, 3i64]), &spec); + let row = do_serialize( + TestRowWithEnforcedOrder { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[test] + fn test_row_serialization_with_enforced_order_failing_type_check() { + let row = TestRowWithEnforcedOrder::default(); + let mut data = Vec::new(); + let mut writer = RowWriter::new(&mut data); + + // The order of two last columns is swapped + let spec = [ + col("a", ColumnType::Text), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + col("b", ColumnType::Int), + ]; + let ctx = RowSerializationContext { columns: &spec }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::ColumnNameMismatch { .. } + )); + + let spec_without_c = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + // Missing column c + ]; + + let ctx = RowSerializationContext { + columns: &spec_without_c, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::ValueMissingForColumn { .. } + )); + + let spec_duplicate_column = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + // Unexpected last column + col("d", ColumnType::Counter), + ]; + + let ctx = RowSerializationContext { + columns: &spec_duplicate_column, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::NoColumnWithName { .. } + )); + + let spec_wrong_type = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::TinyInt), // Wrong type + ]; + + let ctx = RowSerializationContext { + columns: &spec_wrong_type, + }; + let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut writer).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::ColumnSerializationFailed { .. } + )); + } + + #[test] + fn test_empty_serialized_values() { + let values = SerializedValues::new(); + assert!(values.is_empty()); + assert_eq!(values.element_count(), 0); + assert_eq!(values.buffer_size(), 0); + assert_eq!(values.iter().count(), 0); + } + + #[test] + fn test_serialized_values_content() { + let mut values = SerializedValues::new(); + values.add_value(&1234i32, &ColumnType::Int).unwrap(); + values.add_value(&"abcdefg", &ColumnType::Ascii).unwrap(); + let mut buf = Vec::new(); + values.write_to_request(&mut buf); + assert_eq!( + buf, + [ + 0, 2, // element count + 0, 0, 0, 4, // size of int + 0, 0, 4, 210, // content of int (1234) + 0, 0, 0, 7, // size of string + 97, 98, 99, 100, 101, 102, 103, // content of string ('abcdefg') + ] + ) + } + + #[test] + fn test_serialized_values_iter() { + let mut values = SerializedValues::new(); + values.add_value(&1234i32, &ColumnType::Int).unwrap(); + values.add_value(&"abcdefg", &ColumnType::Ascii).unwrap(); + + let mut iter = values.iter(); + assert_eq!(iter.next(), Some(RawValue::Value(&[0, 0, 4, 210]))); + assert_eq!( + iter.next(), + Some(RawValue::Value(&[97, 98, 99, 100, 101, 102, 103])) + ); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_serialized_values_max_capacity() { + let mut values = SerializedValues::new(); + for _ in 0..65535 { + values + .add_value(&123456789i64, &ColumnType::BigInt) + .unwrap(); + } + + // Adding this value should fail, we reached max capacity + values + .add_value(&123456789i64, &ColumnType::BigInt) + .unwrap_err(); + + assert_eq!(values.iter().count(), 65535); + assert!(values + .iter() + .all(|v| v == RawValue::Value(&[0, 0, 0, 0, 0x07, 0x5b, 0xcd, 0x15]))) + } + + #[derive(SerializeRow, Debug)] + #[scylla(crate = crate)] + struct TestRowWithColumnRename { + a: String, + #[scylla(rename = "x")] + b: i32, + } + + #[derive(SerializeRow, Debug)] + #[scylla(crate = crate, flavor = "enforce_order")] + struct TestRowWithColumnRenameAndEnforceOrder { + a: String, + #[scylla(rename = "x")] + b: i32, + } + + #[test] + fn test_row_serialization_with_column_rename() { + let spec = [col("x", ColumnType::Int), col("a", ColumnType::Text)]; + + let reference = do_serialize((42i32, "Ala ma kota"), &spec); + let row = do_serialize( + TestRowWithColumnRename { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[test] + fn test_row_serialization_with_column_rename_and_enforce_order() { + let spec = [col("a", ColumnType::Text), col("x", ColumnType::Int)]; + + let reference = do_serialize(("Ala ma kota", 42i32), &spec); + let row = do_serialize( + TestRowWithColumnRenameAndEnforceOrder { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[derive(SerializeRow, Debug)] + #[scylla(crate = crate, flavor = "enforce_order", skip_name_checks)] + struct TestRowWithSkippedNameChecks { + a: String, + b: i32, + } + + #[test] + fn test_row_serialization_with_skipped_name_checks() { + let spec = [col("a", ColumnType::Text), col("x", ColumnType::Int)]; + + let reference = do_serialize(("Ala ma kota", 42i32), &spec); + let row = do_serialize( + TestRowWithSkippedNameChecks { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &spec, + ); + + assert_eq!(reference, row); + } + + #[derive(SerializeRow, Debug)] + #[scylla(crate = crate)] + struct TestRowWithSkippedFields { + a: String, + b: i32, + #[scylla(skip)] + #[allow(dead_code)] + skipped: Vec, + c: Vec, + } + + #[test] + fn test_row_serialization_with_skipped_field() { + let spec = [ + col("a", ColumnType::Text), + col("b", ColumnType::Int), + col("c", ColumnType::List(Box::new(ColumnType::BigInt))), + ]; + + let reference = do_serialize( + TestRowWithColumnSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &spec, + ); + let row = do_serialize( + TestRowWithSkippedFields { + a: "Ala ma kota".to_owned(), + b: 42, + skipped: vec!["abcd".to_owned(), "efgh".to_owned()], + c: vec![1, 2, 3], + }, + &spec, + ); + + assert_eq!(reference, row); } } diff --git a/scylla-cql/src/types/serialize/value.rs b/scylla-cql/src/types/serialize/value.rs index 43eb9ef738..9e5a691be6 100644 --- a/scylla-cql/src/types/serialize/value.rs +++ b/scylla-cql/src/types/serialize/value.rs @@ -1,22 +1,2742 @@ +//! Contains the [`SerializeCql`] trait and its implementations. + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt::Display; +use std::hash::BuildHasher; +use std::net::IpAddr; use std::sync::Arc; -use crate::frame::response::result::ColumnType; -use crate::frame::value::Value; +use bigdecimal::BigDecimal; +use num_bigint::BigInt; +use thiserror::Error; +use uuid::Uuid; + +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; + +#[cfg(feature = "secret")] +use secrecy::{ExposeSecret, Secret, Zeroize}; + +use crate::frame::response::result::{ColumnType, CqlValue}; +use crate::frame::types::vint_encode; +use crate::frame::value::{ + Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, MaybeUnset, Unset, Value, +}; -use super::SerializationError; +#[cfg(feature = "chrono")] +use crate::frame::value::ValueOverflow; +use super::writers::WrittenCellProof; +use super::{CellWriter, SerializationError}; + +/// A type that can be serialized and sent along with a CQL statement. +/// +/// This is a low-level trait that is exposed to the specifics to the CQL +/// protocol and usually does not have to be implemented directly. See the +/// chapter on "Query Values" in the driver docs for information about how +/// this trait is supposed to be used. pub trait SerializeCql { - fn preliminary_type_check(typ: &ColumnType) -> Result<(), SerializationError>; - fn serialize(&self, typ: &ColumnType, buf: &mut Vec) -> Result<(), SerializationError>; + /// Serializes the value to given CQL type. + /// + /// The value should produce a `[value]`, according to the [CQL protocol + /// specification](https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec), + /// containing the serialized value. See section 6 of the document on how + /// the contents of the `[value]` should look like. + /// + /// The value produced should match the type provided by `typ`. If the + /// value cannot be serialized to that type, an error should be returned. + /// + /// The [`CellWriter`] provided to the method ensures that the value produced + /// will be properly framed (i.e. incorrectly written value should not + /// cause the rest of the request to be misinterpreted), but otherwise + /// the implementor of the trait is responsible for producing the a value + /// in a correct format. + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError>; +} + +macro_rules! exact_type_check { + ($typ:ident, $($cql:tt),*) => { + match $typ { + $(ColumnType::$cql)|* => {}, + _ => return Err(mk_typck_err::( + $typ, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[$(ColumnType::$cql),*], + } + )) + } + }; +} + +macro_rules! impl_serialize_via_writer { + (|$me:ident, $writer:ident| $e:expr) => { + impl_serialize_via_writer!(|$me, _typ, $writer| $e); + }; + (|$me:ident, $typ:ident, $writer:ident| $e:expr) => { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + let $writer = writer; + let $typ = typ; + let $me = self; + let proof = $e; + Ok(proof) + } + }; +} + +impl SerializeCql for i8 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, TinyInt); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for i16 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, SmallInt); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for i32 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Int); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for i64 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, BigInt); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for BigDecimal { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Decimal); + let mut builder = writer.into_value_builder(); + let (value, scale) = me.as_bigint_and_exponent(); + let scale: i32 = scale + .try_into() + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::ValueOverflow))?; + builder.append_bytes(&scale.to_be_bytes()); + builder.append_bytes(&value.to_signed_bytes_be()); + builder + .finish() + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for CqlDate { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Date); + writer.set_value(me.0.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for CqlTimestamp { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Timestamp); + writer.set_value(me.0.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for CqlTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Time); + writer.set_value(me.0.to_be_bytes().as_slice()).unwrap() + }); +} +#[cfg(feature = "chrono")] +impl SerializeCql for NaiveDate { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Date); + ::serialize(&(*me).into(), typ, writer)? + }); +} +#[cfg(feature = "chrono")] +impl SerializeCql for DateTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Timestamp); + ::serialize(&(*me).into(), typ, writer)? + }); +} +#[cfg(feature = "chrono")] +impl SerializeCql for NaiveTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Time); + let cql_time = CqlTime::try_from(*me).map_err(|_: ValueOverflow| { + mk_ser_err::(typ, BuiltinSerializationErrorKind::ValueOverflow) + })?; + ::serialize(&cql_time, typ, writer)? + }); +} +#[cfg(feature = "time")] +impl SerializeCql for time::Date { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Date); + ::serialize(&(*me).into(), typ, writer)? + }); +} +#[cfg(feature = "time")] +impl SerializeCql for time::OffsetDateTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Timestamp); + ::serialize(&(*me).into(), typ, writer)? + }); +} +#[cfg(feature = "time")] +impl SerializeCql for time::Time { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Time); + ::serialize(&(*me).into(), typ, writer)? + }); +} +#[cfg(feature = "secret")] +impl SerializeCql for Secret { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + V::serialize(self.expose_secret(), typ, writer) + } +} +impl SerializeCql for bool { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Boolean); + writer.set_value(&[*me as u8]).unwrap() + }); +} +impl SerializeCql for f32 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Float); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for f64 { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Double); + writer.set_value(me.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for Uuid { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Uuid, Timeuuid); + writer.set_value(me.as_bytes().as_ref()).unwrap() + }); +} +impl SerializeCql for BigInt { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Varint); + // TODO: The allocation here can be avoided and we can reimplement + // `to_signed_bytes_be` by using `to_u64_digits` and a bit of custom + // logic. Need better tests in order to do this. + writer + .set_value(me.to_signed_bytes_be().as_slice()) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for &str { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Ascii, Text); + writer + .set_value(me.as_bytes()) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for Vec { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Blob); + writer + .set_value(me.as_ref()) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for &[u8] { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Blob); + writer + .set_value(me) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for [u8; N] { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Blob); + writer + .set_value(me.as_ref()) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for IpAddr { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Inet); + match me { + IpAddr::V4(ip) => writer.set_value(&ip.octets()).unwrap(), + IpAddr::V6(ip) => writer.set_value(&ip.octets()).unwrap(), + } + }); +} +impl SerializeCql for String { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Ascii, Text); + writer + .set_value(me.as_bytes()) + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow))? + }); +} +impl SerializeCql for Option { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + match self { + Some(v) => v.serialize(typ, writer), + None => Ok(writer.set_null()), + } + } +} +impl SerializeCql for Unset { + impl_serialize_via_writer!(|_me, writer| writer.set_unset()); +} +impl SerializeCql for Counter { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Counter); + writer.set_value(me.0.to_be_bytes().as_slice()).unwrap() + }); +} +impl SerializeCql for CqlDuration { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Duration); + // TODO: adjust vint_encode to use CellValueBuilder or something like that + let mut buf = Vec::with_capacity(27); // worst case size is 27 + vint_encode(me.months as i64, &mut buf); + vint_encode(me.days as i64, &mut buf); + vint_encode(me.nanoseconds, &mut buf); + writer.set_value(buf.as_slice()).unwrap() + }); +} +impl SerializeCql for MaybeUnset { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + match self { + MaybeUnset::Set(v) => v.serialize(typ, writer), + MaybeUnset::Unset => Ok(writer.set_unset()), + } + } +} +impl SerializeCql for &T { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + T::serialize(*self, typ, writer) + } +} +impl SerializeCql for Box { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + T::serialize(&**self, typ, writer) + } +} +impl SerializeCql for HashSet { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_sequence( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl SerializeCql for HashMap { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_mapping( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl SerializeCql for BTreeSet { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_sequence( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl SerializeCql for BTreeMap { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_mapping( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl SerializeCql for Vec { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_sequence( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl<'a, T: SerializeCql + 'a> SerializeCql for &'a [T] { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_sequence( + std::any::type_name::(), + self.len(), + self.iter(), + typ, + writer, + ) + } +} +impl SerializeCql for CqlValue { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_cql_value(self, typ, writer).map_err(fix_cql_value_name_in_err) + } +} + +fn serialize_cql_value<'b>( + value: &CqlValue, + typ: &ColumnType, + writer: CellWriter<'b>, +) -> Result, SerializationError> { + if let ColumnType::Custom(_) = typ { + return Err(mk_typck_err::( + typ, + BuiltinTypeCheckErrorKind::CustomTypeUnsupported, + )); + } + match value { + CqlValue::Ascii(a) => <_ as SerializeCql>::serialize(&a, typ, writer), + CqlValue::Boolean(b) => <_ as SerializeCql>::serialize(&b, typ, writer), + CqlValue::Blob(b) => <_ as SerializeCql>::serialize(&b, typ, writer), + CqlValue::Counter(c) => <_ as SerializeCql>::serialize(&c, typ, writer), + CqlValue::Decimal(d) => <_ as SerializeCql>::serialize(&d, typ, writer), + CqlValue::Date(d) => <_ as SerializeCql>::serialize(&d, typ, writer), + CqlValue::Double(d) => <_ as SerializeCql>::serialize(&d, typ, writer), + CqlValue::Duration(d) => <_ as SerializeCql>::serialize(&d, typ, writer), + CqlValue::Empty => { + if !typ.supports_special_empty_value() { + return Err(mk_typck_err::( + typ, + BuiltinTypeCheckErrorKind::NotEmptyable, + )); + } + Ok(writer.set_value(&[]).unwrap()) + } + CqlValue::Float(f) => <_ as SerializeCql>::serialize(&f, typ, writer), + CqlValue::Int(i) => <_ as SerializeCql>::serialize(&i, typ, writer), + CqlValue::BigInt(b) => <_ as SerializeCql>::serialize(&b, typ, writer), + CqlValue::Text(t) => <_ as SerializeCql>::serialize(&t, typ, writer), + CqlValue::Timestamp(t) => <_ as SerializeCql>::serialize(&t, typ, writer), + CqlValue::Inet(i) => <_ as SerializeCql>::serialize(&i, typ, writer), + CqlValue::List(l) => <_ as SerializeCql>::serialize(&l, typ, writer), + CqlValue::Map(m) => serialize_mapping( + std::any::type_name::(), + m.len(), + m.iter().map(|p| (&p.0, &p.1)), + typ, + writer, + ), + CqlValue::Set(s) => <_ as SerializeCql>::serialize(&s, typ, writer), + CqlValue::UserDefinedType { + keyspace, + type_name, + fields, + } => serialize_udt(typ, keyspace, type_name, fields, writer), + CqlValue::SmallInt(s) => <_ as SerializeCql>::serialize(&s, typ, writer), + CqlValue::TinyInt(t) => <_ as SerializeCql>::serialize(&t, typ, writer), + CqlValue::Time(t) => <_ as SerializeCql>::serialize(&t, typ, writer), + CqlValue::Timeuuid(t) => <_ as SerializeCql>::serialize(&t, typ, writer), + CqlValue::Tuple(t) => { + // We allow serializing tuples that have less fields + // than the database tuple, but not the other way around. + let fields = match typ { + ColumnType::Tuple(fields) => { + if fields.len() < t.len() { + return Err(mk_typck_err::( + typ, + TupleTypeCheckErrorKind::WrongElementCount { + actual: t.len(), + asked_for: fields.len(), + }, + )); + } + fields + } + _ => { + return Err(mk_typck_err::( + typ, + TupleTypeCheckErrorKind::NotTuple, + )) + } + }; + serialize_tuple_like(typ, fields.iter(), t.iter(), writer) + } + CqlValue::Uuid(u) => <_ as SerializeCql>::serialize(&u, typ, writer), + CqlValue::Varint(v) => <_ as SerializeCql>::serialize(&v, typ, writer), + } +} + +fn fix_cql_value_name_in_err(mut err: SerializationError) -> SerializationError { + // The purpose of this function is to change the `rust_name` field + // in the error to CqlValue. Most of the time, the `err` given to the + // function here will be the sole owner of the data, so theoretically + // we could fix this in place. + + let rust_name = std::any::type_name::(); + + match Arc::get_mut(&mut err.0) { + Some(err_mut) => { + if let Some(err) = err_mut.downcast_mut::() { + err.rust_name = rust_name; + } else if let Some(err) = err_mut.downcast_mut::() { + err.rust_name = rust_name; + } + } + None => { + // The `None` case shouldn't happen consisdering how we are using + // the function in the code now, but let's provide it here anyway + // for correctness. + if let Some(err) = err.0.downcast_ref::() { + if err.rust_name != rust_name { + return SerializationError::new(BuiltinTypeCheckError { + rust_name, + ..err.clone() + }); + } + } + if let Some(err) = err.0.downcast_ref::() { + if err.rust_name != rust_name { + return SerializationError::new(BuiltinSerializationError { + rust_name, + ..err.clone() + }); + } + } + } + }; + + err +} + +fn serialize_udt<'b>( + typ: &ColumnType, + keyspace: &str, + type_name: &str, + values: &[(String, Option)], + writer: CellWriter<'b>, +) -> Result, SerializationError> { + let (dst_type_name, dst_keyspace, field_types) = match typ { + ColumnType::UserDefinedType { + type_name, + keyspace, + field_types, + } => (type_name, keyspace, field_types), + _ => return Err(mk_typck_err::(typ, UdtTypeCheckErrorKind::NotUdt)), + }; + + if keyspace != dst_keyspace || type_name != dst_type_name { + return Err(mk_typck_err::( + typ, + UdtTypeCheckErrorKind::NameMismatch { + keyspace: dst_keyspace.clone(), + type_name: dst_type_name.clone(), + }, + )); + } + + // Allow columns present in the CQL type which are not present in CqlValue, + // but not the other way around + let mut indexed_fields: HashMap<_, _> = values.iter().map(|(k, v)| (k.as_str(), v)).collect(); + + let mut builder = writer.into_value_builder(); + for (fname, ftyp) in field_types { + // Take a value from the original list. + // If a field is missing, write null instead. + let fvalue = indexed_fields + .remove(fname.as_str()) + .and_then(|x| x.as_ref()); + + let writer = builder.make_sub_writer(); + match fvalue { + None => writer.set_null(), + Some(v) => serialize_cql_value(v, ftyp, writer).map_err(|err| { + let err = fix_cql_value_name_in_err(err); + mk_ser_err::( + typ, + UdtSerializationErrorKind::FieldSerializationFailed { + field_name: fname.clone(), + err, + }, + ) + })?, + }; + } + + // If there are some leftover fields, it's an error. + if !indexed_fields.is_empty() { + // In order to have deterministic errors, return an error about + // the lexicographically smallest field. + let fname = indexed_fields.keys().min().unwrap(); + return Err(mk_typck_err::( + typ, + UdtTypeCheckErrorKind::NoSuchFieldInUdt { + field_name: fname.to_string(), + }, + )); + } + + builder + .finish() + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow)) +} + +fn serialize_tuple_like<'t, 'b>( + typ: &ColumnType, + field_types: impl Iterator, + field_values: impl Iterator>, + writer: CellWriter<'b>, +) -> Result, SerializationError> { + let mut builder = writer.into_value_builder(); + + for (index, (el, el_typ)) in field_values.zip(field_types).enumerate() { + let sub = builder.make_sub_writer(); + match el { + None => sub.set_null(), + Some(el) => serialize_cql_value(el, el_typ, sub).map_err(|err| { + let err = fix_cql_value_name_in_err(err); + mk_ser_err::( + typ, + TupleSerializationErrorKind::ElementSerializationFailed { index, err }, + ) + })?, + }; + } + + builder + .finish() + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow)) +} + +macro_rules! impl_tuple { + ( + $($typs:ident),*; + $($fidents:ident),*; + $($tidents:ident),*; + $length:expr + ) => { + impl<$($typs: SerializeCql),*> SerializeCql for ($($typs,)*) { + fn serialize<'b>( + &self, + typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + let ($($tidents,)*) = match typ { + ColumnType::Tuple(typs) => match typs.as_slice() { + [$($tidents),*] => ($($tidents,)*), + _ => return Err(mk_typck_err::( + typ, + TupleTypeCheckErrorKind::WrongElementCount { + actual: $length, + asked_for: typs.len(), + } + )) + } + _ => return Err(mk_typck_err::( + typ, + TupleTypeCheckErrorKind::NotTuple, + )) + }; + let ($($fidents,)*) = self; + let mut builder = writer.into_value_builder(); + let index = 0; + $( + <$typs as SerializeCql>::serialize($fidents, $tidents, builder.make_sub_writer()) + .map_err(|err| mk_ser_err::( + typ, + TupleSerializationErrorKind::ElementSerializationFailed { + index, + err, + } + ))?; + let index = index + 1; + )* + let _ = index; + builder + .finish() + .map_err(|_| mk_ser_err::(typ, BuiltinSerializationErrorKind::SizeOverflow)) + } + } + }; +} + +macro_rules! impl_tuples { + (;;;$length:expr) => {}; + ( + $typ:ident$(, $($typs:ident),*)?; + $fident:ident$(, $($fidents:ident),*)?; + $tident:ident$(, $($tidents:ident),*)?; + $length:expr + ) => { + impl_tuples!( + $($($typs),*)?; + $($($fidents),*)?; + $($($tidents),*)?; + $length - 1 + ); + impl_tuple!( + $typ$(, $($typs),*)?; + $fident$(, $($fidents),*)?; + $tident$(, $($tidents),*)?; + $length + ); + }; +} + +impl_tuples!( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15; + f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15; + t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15; + 16 +); + +fn serialize_sequence<'t, 'b, T: SerializeCql + 't>( + rust_name: &'static str, + len: usize, + iter: impl Iterator, + typ: &ColumnType, + writer: CellWriter<'b>, +) -> Result, SerializationError> { + let elt = match typ { + ColumnType::List(elt) | ColumnType::Set(elt) => elt, + _ => { + return Err(mk_typck_err_named( + rust_name, + typ, + SetOrListTypeCheckErrorKind::NotSetOrList, + )); + } + }; + + let mut builder = writer.into_value_builder(); + + let element_count: i32 = len.try_into().map_err(|_| { + mk_ser_err_named( + rust_name, + typ, + SetOrListSerializationErrorKind::TooManyElements, + ) + })?; + builder.append_bytes(&element_count.to_be_bytes()); + + for el in iter { + T::serialize(el, elt, builder.make_sub_writer()).map_err(|err| { + mk_ser_err_named( + rust_name, + typ, + SetOrListSerializationErrorKind::ElementSerializationFailed(err), + ) + })?; + } + + builder + .finish() + .map_err(|_| mk_ser_err_named(rust_name, typ, BuiltinSerializationErrorKind::SizeOverflow)) +} + +fn serialize_mapping<'t, 'b, K: SerializeCql + 't, V: SerializeCql + 't>( + rust_name: &'static str, + len: usize, + iter: impl Iterator, + typ: &ColumnType, + writer: CellWriter<'b>, +) -> Result, SerializationError> { + let (ktyp, vtyp) = match typ { + ColumnType::Map(k, v) => (k, v), + _ => { + return Err(mk_typck_err_named( + rust_name, + typ, + MapTypeCheckErrorKind::NotMap, + )); + } + }; + + let mut builder = writer.into_value_builder(); + + let element_count: i32 = len.try_into().map_err(|_| { + mk_ser_err_named(rust_name, typ, MapSerializationErrorKind::TooManyElements) + })?; + builder.append_bytes(&element_count.to_be_bytes()); + + for (k, v) in iter { + K::serialize(k, ktyp, builder.make_sub_writer()).map_err(|err| { + mk_ser_err_named( + rust_name, + typ, + MapSerializationErrorKind::KeySerializationFailed(err), + ) + })?; + V::serialize(v, vtyp, builder.make_sub_writer()).map_err(|err| { + mk_ser_err_named( + rust_name, + typ, + MapSerializationErrorKind::ValueSerializationFailed(err), + ) + })?; + } + + builder + .finish() + .map_err(|_| mk_ser_err_named(rust_name, typ, BuiltinSerializationErrorKind::SizeOverflow)) +} + +/// Implements the [`SerializeCql`] trait for a type, provided that the type +/// already implements the legacy [`Value`](crate::frame::value::Value) trait. +/// +/// # Note +/// +/// The translation from one trait to another encounters a performance penalty +/// and does not utilize the stronger guarantees of `SerializeCql`. Before +/// resorting to this macro, you should consider other options instead: +/// +/// - If the impl was generated using the `Value` procedural macro, you should +/// switch to the `SerializeCql` procedural macro. *The new macro behaves +/// differently by default, so please read its documentation first!* +/// - If the impl was written by hand, it is still preferable to rewrite it +/// manually. You have an opportunity to make your serialization logic +/// type-safe and potentially improve performance. +/// +/// Basically, you should consider using the macro if you have a hand-written +/// impl and the moment it is not easy/not desirable to rewrite it. +/// +/// # Example +/// +/// ```rust +/// # use scylla_cql::frame::value::{Value, ValueTooBig}; +/// # use scylla_cql::impl_serialize_cql_via_value; +/// struct NoGenerics {} +/// impl Value for NoGenerics { +/// fn serialize<'b>(&self, _buf: &mut Vec) -> Result<(), ValueTooBig> { +/// Ok(()) +/// } +/// } +/// impl_serialize_cql_via_value!(NoGenerics); +/// +/// // Generic types are also supported. You must specify the bounds if the +/// // struct/enum contains any. +/// struct WithGenerics(T, U); +/// impl Value for WithGenerics { +/// fn serialize<'b>(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { +/// self.0.serialize(buf)?; +/// self.1.clone().serialize(buf)?; +/// Ok(()) +/// } +/// } +/// impl_serialize_cql_via_value!(WithGenerics); +/// ``` +#[macro_export] +macro_rules! impl_serialize_cql_via_value { + ($t:ident$(<$($targ:tt $(: $tbound:tt)?),*>)?) => { + impl $(<$($targ $(: $tbound)?),*>)? $crate::types::serialize::value::SerializeCql + for $t$(<$($targ),*>)? + where + Self: $crate::frame::value::Value, + { + fn serialize<'b>( + &self, + _typ: &$crate::frame::response::result::ColumnType, + writer: $crate::types::serialize::writers::CellWriter<'b>, + ) -> ::std::result::Result< + $crate::types::serialize::writers::WrittenCellProof<'b>, + $crate::types::serialize::SerializationError, + > { + $crate::types::serialize::value::serialize_legacy_value(self, writer) + } + } + }; +} + +/// Implements [`SerializeCql`] if the type wrapped over implements [`Value`]. +/// +/// See the [`impl_serialize_cql_via_value`] macro on information about +/// the properties of the [`SerializeCql`] implementation. +pub struct ValueAdapter(pub T); + +impl SerializeCql for ValueAdapter +where + T: Value, +{ + #[inline] + fn serialize<'b>( + &self, + _typ: &ColumnType, + writer: CellWriter<'b>, + ) -> Result, SerializationError> { + serialize_legacy_value(&self.0, writer) + } +} + +/// Serializes a value implementing [`Value`] by using the [`CellWriter`] +/// interface. +/// +/// The function first serializes the value with [`Value::serialize`], then +/// parses the result and serializes it again with given `CellWriter`. It is +/// a lazy and inefficient way to implement `CellWriter` via an existing `Value` +/// impl. +/// +/// Returns an error if the result of the `Value::serialize` call was not +/// a properly encoded `[value]` as defined in the CQL protocol spec. +/// +/// See [`impl_serialize_cql_via_value`] which generates a boilerplate +/// [`SerializeCql`] implementation that uses this function. +pub fn serialize_legacy_value<'b, T: Value>( + v: &T, + writer: CellWriter<'b>, +) -> Result, SerializationError> { + // It's an inefficient and slightly tricky but correct implementation. + let mut buf = Vec::new(); + ::serialize(v, &mut buf) + .map_err(|_| SerializationError::new(ValueToSerializeCqlAdapterError::TooBig))?; + + // Analyze the output. + // All this dance shows how unsafe our previous interface was... + if buf.len() < 4 { + return Err(SerializationError(Arc::new( + ValueToSerializeCqlAdapterError::TooShort { size: buf.len() }, + ))); + } + + let (len_bytes, contents) = buf.split_at(4); + let len = i32::from_be_bytes(len_bytes.try_into().unwrap()); + match len { + -2 => Ok(writer.set_unset()), + -1 => Ok(writer.set_null()), + len if len >= 0 => { + if contents.len() != len as usize { + Err(SerializationError(Arc::new( + ValueToSerializeCqlAdapterError::DeclaredVsActualSizeMismatch { + declared: len as usize, + actual: contents.len(), + }, + ))) + } else { + Ok(writer.set_value(contents).unwrap()) // len <= i32::MAX, so unwrap will succeed + } + } + _ => Err(SerializationError(Arc::new( + ValueToSerializeCqlAdapterError::InvalidDeclaredSize { size: len }, + ))), + } +} + +/// Type checking of one of the built-in types failed. +#[derive(Debug, Error, Clone)] +#[error("Failed to type check Rust type {rust_name} against CQL type {got:?}: {kind}")] +pub struct BuiltinTypeCheckError { + /// Name of the Rust type being serialized. + pub rust_name: &'static str, + + /// The CQL type that the Rust type was being serialized to. + pub got: ColumnType, + + /// Detailed information about the failure. + pub kind: BuiltinTypeCheckErrorKind, +} + +fn mk_typck_err( + got: &ColumnType, + kind: impl Into, +) -> SerializationError { + mk_typck_err_named(std::any::type_name::(), got, kind) +} + +fn mk_typck_err_named( + name: &'static str, + got: &ColumnType, + kind: impl Into, +) -> SerializationError { + SerializationError::new(BuiltinTypeCheckError { + rust_name: name, + got: got.clone(), + kind: kind.into(), + }) +} + +/// Serialization of one of the built-in types failed. +#[derive(Debug, Error, Clone)] +#[error("Failed to serialize Rust type {rust_name} into CQL type {got:?}: {kind}")] +pub struct BuiltinSerializationError { + /// Name of the Rust type being serialized. + pub rust_name: &'static str, + + /// The CQL type that the Rust type was being serialized to. + pub got: ColumnType, + + /// Detailed information about the failure. + pub kind: BuiltinSerializationErrorKind, +} + +fn mk_ser_err( + got: &ColumnType, + kind: impl Into, +) -> SerializationError { + mk_ser_err_named(std::any::type_name::(), got, kind) +} + +fn mk_ser_err_named( + name: &'static str, + got: &ColumnType, + kind: impl Into, +) -> SerializationError { + SerializationError::new(BuiltinSerializationError { + rust_name: name, + got: got.clone(), + kind: kind.into(), + }) +} + +/// Describes why type checking some of the built-in types has failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum BuiltinTypeCheckErrorKind { + /// Expected one from a list of particular types. + MismatchedType { + /// The list of types that the Rust type can serialize as. + expected: &'static [ColumnType], + }, + + /// Expected a type that can be empty. + NotEmptyable, + + /// A type check failure specific to a CQL set or list. + SetOrListError(SetOrListTypeCheckErrorKind), + + /// A type check failure specific to a CQL map. + MapError(MapTypeCheckErrorKind), + + /// A type check failure specific to a CQL tuple. + TupleError(TupleTypeCheckErrorKind), + + /// A type check failure specific to a CQL UDT. + UdtError(UdtTypeCheckErrorKind), + + /// Custom CQL type - unsupported + // TODO: Should we actually support it? Counters used to be implemented like that. + CustomTypeUnsupported, +} + +impl From for BuiltinTypeCheckErrorKind { + fn from(value: SetOrListTypeCheckErrorKind) -> Self { + BuiltinTypeCheckErrorKind::SetOrListError(value) + } +} + +impl From for BuiltinTypeCheckErrorKind { + fn from(value: MapTypeCheckErrorKind) -> Self { + BuiltinTypeCheckErrorKind::MapError(value) + } +} + +impl From for BuiltinTypeCheckErrorKind { + fn from(value: TupleTypeCheckErrorKind) -> Self { + BuiltinTypeCheckErrorKind::TupleError(value) + } +} + +impl From for BuiltinTypeCheckErrorKind { + fn from(value: UdtTypeCheckErrorKind) -> Self { + BuiltinTypeCheckErrorKind::UdtError(value) + } } -impl SerializeCql for T { - fn preliminary_type_check(_typ: &ColumnType) -> Result<(), SerializationError> { - Ok(()) +impl Display for BuiltinTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuiltinTypeCheckErrorKind::MismatchedType { expected } => { + write!(f, "expected one of the CQL types: {expected:?}") + } + BuiltinTypeCheckErrorKind::NotEmptyable => { + write!( + f, + "the separate empty representation is not valid for this type" + ) + } + BuiltinTypeCheckErrorKind::SetOrListError(err) => err.fmt(f), + BuiltinTypeCheckErrorKind::MapError(err) => err.fmt(f), + BuiltinTypeCheckErrorKind::TupleError(err) => err.fmt(f), + BuiltinTypeCheckErrorKind::UdtError(err) => err.fmt(f), + BuiltinTypeCheckErrorKind::CustomTypeUnsupported => { + write!(f, "custom CQL types are unsupported") + } + } } +} + +/// Describes why serialization of some of the built-in types has failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum BuiltinSerializationErrorKind { + /// The size of the Rust value is too large to fit in the CQL serialization + /// format (over i32::MAX bytes). + SizeOverflow, + + /// The Rust value is out of range supported by the CQL type. + ValueOverflow, + + /// A serialization failure specific to a CQL set or list. + SetOrListError(SetOrListSerializationErrorKind), + + /// A serialization failure specific to a CQL map. + MapError(MapSerializationErrorKind), + + /// A serialization failure specific to a CQL tuple. + TupleError(TupleSerializationErrorKind), + + /// A serialization failure specific to a CQL UDT. + UdtError(UdtSerializationErrorKind), +} + +impl From for BuiltinSerializationErrorKind { + fn from(value: SetOrListSerializationErrorKind) -> Self { + BuiltinSerializationErrorKind::SetOrListError(value) + } +} + +impl From for BuiltinSerializationErrorKind { + fn from(value: MapSerializationErrorKind) -> Self { + BuiltinSerializationErrorKind::MapError(value) + } +} + +impl From for BuiltinSerializationErrorKind { + fn from(value: TupleSerializationErrorKind) -> Self { + BuiltinSerializationErrorKind::TupleError(value) + } +} + +impl From for BuiltinSerializationErrorKind { + fn from(value: UdtSerializationErrorKind) -> Self { + BuiltinSerializationErrorKind::UdtError(value) + } +} + +impl Display for BuiltinSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuiltinSerializationErrorKind::SizeOverflow => { + write!( + f, + "the Rust value is too big to be serialized in the CQL protocol format" + ) + } + BuiltinSerializationErrorKind::ValueOverflow => { + write!( + f, + "the Rust value is out of range supported by the CQL type" + ) + } + BuiltinSerializationErrorKind::SetOrListError(err) => err.fmt(f), + BuiltinSerializationErrorKind::MapError(err) => err.fmt(f), + BuiltinSerializationErrorKind::TupleError(err) => err.fmt(f), + BuiltinSerializationErrorKind::UdtError(err) => err.fmt(f), + } + } +} + +/// Describes why type checking of a map type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum MapTypeCheckErrorKind { + /// The CQL type is not a map. + NotMap, +} + +impl Display for MapTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MapTypeCheckErrorKind::NotMap => { + write!( + f, + "the CQL type the map was attempted to be serialized to was not map" + ) + } + } + } +} + +/// Describes why serialization of a map type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum MapSerializationErrorKind { + /// The many contains too many items, exceeding the protocol limit (i32::MAX). + TooManyElements, + + /// One of the keys in the map failed to serialize. + KeySerializationFailed(SerializationError), + + /// One of the values in the map failed to serialize. + ValueSerializationFailed(SerializationError), +} + +impl Display for MapSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MapSerializationErrorKind::TooManyElements => { + write!( + f, + "the map contains too many elements to fit in CQL representation" + ) + } + MapSerializationErrorKind::KeySerializationFailed(err) => { + write!(f, "failed to serialize one of the keys: {}", err) + } + MapSerializationErrorKind::ValueSerializationFailed(err) => { + write!(f, "failed to serialize one of the values: {}", err) + } + } + } +} + +/// Describes why type checking of a set or list type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum SetOrListTypeCheckErrorKind { + /// The CQL type is neither a set not a list. + NotSetOrList, +} + +impl Display for SetOrListTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SetOrListTypeCheckErrorKind::NotSetOrList => { + write!( + f, + "the CQL type the tuple was attempted to was neither a set or a list" + ) + } + } + } +} + +/// Describes why serialization of a set or list type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum SetOrListSerializationErrorKind { + /// The set/list contains too many items, exceeding the protocol limit (i32::MAX). + TooManyElements, + + /// One of the elements of the set/list failed to serialize. + ElementSerializationFailed(SerializationError), +} + +impl Display for SetOrListSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SetOrListSerializationErrorKind::TooManyElements => { + write!( + f, + "the collection contains too many elements to fit in CQL representation" + ) + } + SetOrListSerializationErrorKind::ElementSerializationFailed(err) => { + write!(f, "failed to serialize one of the elements: {err}") + } + } + } +} + +/// Describes why type checking of a tuple failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum TupleTypeCheckErrorKind { + /// The CQL type is not a tuple. + NotTuple, + + /// The tuple has the wrong element count. + /// + /// Note that it is allowed to write a Rust tuple with less elements + /// than the corresponding CQL type, but not more. The additional, unknown + /// elements will be set to null. + WrongElementCount { + /// The number of elements that the Rust tuple has. + actual: usize, + + /// The number of elements that the CQL tuple type has. + asked_for: usize, + }, +} + +impl Display for TupleTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TupleTypeCheckErrorKind::NotTuple => write!( + f, + "the CQL type the tuple was attempted to be serialized to is not a tuple" + ), + TupleTypeCheckErrorKind::WrongElementCount { actual, asked_for } => write!( + f, + "wrong tuple element count: CQL type has {asked_for}, the Rust tuple has {actual}" + ), + } + } +} + +/// Describes why serialize of a tuple failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum TupleSerializationErrorKind { + /// One of the tuple elements failed to serialize. + ElementSerializationFailed { + /// Index of the tuple element that failed to serialize. + index: usize, + + /// The error that caused the tuple field serialization to fail. + err: SerializationError, + }, +} + +impl Display for TupleSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TupleSerializationErrorKind::ElementSerializationFailed { index, err } => { + write!(f, "element no. {index} failed to serialize: {err}") + } + } + } +} + +/// Describes why type checking of a user defined type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum UdtTypeCheckErrorKind { + /// The CQL type is not a user defined type. + NotUdt, + + /// The name of the UDT being serialized to does not match. + NameMismatch { + /// Keyspace in which the UDT was defined. + keyspace: String, + + /// Name of the UDT. + type_name: String, + }, + + /// The Rust data does not have a field that is required in the CQL UDT type. + ValueMissingForUdtField { + /// Name of field that the CQL UDT requires but is missing in the Rust struct. + field_name: String, + }, + + /// The Rust data contains a field that is not present in the UDT. + NoSuchFieldInUdt { + /// Name of the Rust struct field that is missing in the UDT. + field_name: String, + }, + + /// A different field name was expected at given position. + FieldNameMismatch { + /// The name of the Rust field. + rust_field_name: String, + + /// The name of the CQL UDT field. + db_field_name: String, + }, +} + +impl Display for UdtTypeCheckErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UdtTypeCheckErrorKind::NotUdt => write!( + f, + "the CQL type the tuple was attempted to be type checked against is not a UDT" + ), + UdtTypeCheckErrorKind::NameMismatch { + keyspace, + type_name, + } => write!( + f, + "the Rust UDT name does not match the actual CQL UDT name ({keyspace}.{type_name})" + ), + UdtTypeCheckErrorKind::ValueMissingForUdtField { field_name } => { + write!(f, "the field {field_name} is missing in the Rust data but is required by the CQL UDT type") + } + UdtTypeCheckErrorKind::NoSuchFieldInUdt { field_name } => write!( + f, + "the field {field_name} that is present in the Rust data is not present in the CQL type" + ), + UdtTypeCheckErrorKind::FieldNameMismatch { rust_field_name, db_field_name } => write!( + f, + "expected field with name {db_field_name} at given position, but the Rust field name is {rust_field_name}" + ), + } + } +} + +/// Describes why serialization of a user defined type failed. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum UdtSerializationErrorKind { + /// One of the fields failed to serialize. + FieldSerializationFailed { + /// Name of the field which failed to serialize. + field_name: String, + + /// The error that caused the UDT field serialization to fail. + err: SerializationError, + }, +} + +impl Display for UdtSerializationErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UdtSerializationErrorKind::FieldSerializationFailed { field_name, err } => { + write!(f, "field {field_name} failed to serialize: {err}") + } + } + } +} + +/// Describes a failure to translate the output of the [`Value`] legacy trait +/// into an output of the [`SerializeCql`] trait. +#[derive(Error, Debug)] +pub enum ValueToSerializeCqlAdapterError { + /// The value is too bit to be serialized as it exceeds the maximum 2GB size limit. + #[error("The value is too big to be serialized as it exceeds the maximum 2GB size limit")] + TooBig, + + /// Output produced by the Value trait is less than 4 bytes in size and cannot be considered to be a proper CQL-encoded value. + #[error("Output produced by the Value trait is too short to be considered a value: {size} < 4 minimum bytes")] + TooShort { + /// Size of the produced data. + size: usize, + }, + + /// Mismatch between the value size written at the beginning and the actual size of the data appended to the Vec. + #[error("Mismatch between the declared value size vs. actual size: {declared} != {actual}")] + DeclaredVsActualSizeMismatch { + /// The declared size of the output. + declared: usize, + + /// The actual size of the output. + actual: usize, + }, + + /// The value size written at the beginning is invalid (it is negative and less than -2). + #[error("Invalid declared value size: {size}")] + InvalidDeclaredSize { + /// Declared size of the output. + size: i32, + }, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use crate::frame::response::result::{ColumnType, CqlValue}; + use crate::frame::value::{Counter, MaybeUnset, Unset, Value, ValueTooBig}; + use crate::types::serialize::value::{ + BuiltinSerializationError, BuiltinSerializationErrorKind, BuiltinTypeCheckError, + BuiltinTypeCheckErrorKind, MapSerializationErrorKind, MapTypeCheckErrorKind, + SetOrListSerializationErrorKind, SetOrListTypeCheckErrorKind, TupleSerializationErrorKind, + TupleTypeCheckErrorKind, ValueAdapter, + }; + use crate::types::serialize::{CellWriter, SerializationError}; + + use bigdecimal::BigDecimal; + use num_bigint::BigInt; + use scylla_macros::SerializeCql; + + use super::{SerializeCql, UdtSerializationErrorKind, UdtTypeCheckErrorKind}; + + fn check_compat(v: V) { + let mut legacy_data = Vec::new(); + ::serialize(&v, &mut legacy_data).unwrap(); + + let mut new_data = Vec::new(); + let new_data_writer = CellWriter::new(&mut new_data); + ::serialize(&v, &ColumnType::Int, new_data_writer).unwrap(); + + assert_eq!(legacy_data, new_data); + } + + #[test] + fn test_legacy_fallback() { + check_compat(123i32); + check_compat(None::); + check_compat(MaybeUnset::Unset::); + } + + #[test] + fn test_dyn_serialize_cql() { + let v: i32 = 123; + let mut typed_data = Vec::new(); + let typed_data_writer = CellWriter::new(&mut typed_data); + <_ as SerializeCql>::serialize(&v, &ColumnType::Int, typed_data_writer).unwrap(); + + let v = &v as &dyn SerializeCql; + let mut erased_data = Vec::new(); + let erased_data_writer = CellWriter::new(&mut erased_data); + <_ as SerializeCql>::serialize(&v, &ColumnType::Int, erased_data_writer).unwrap(); + + assert_eq!(typed_data, erased_data); + } + + fn do_serialize(t: T, typ: &ColumnType) -> Vec { + let mut ret = Vec::new(); + let writer = CellWriter::new(&mut ret); + t.serialize(typ, writer).unwrap(); + ret + } + + fn do_serialize_err(t: T, typ: &ColumnType) -> SerializationError { + let mut ret = Vec::new(); + let writer = CellWriter::new(&mut ret); + t.serialize(typ, writer).unwrap_err() + } + + #[test] + fn test_legacy_wrapper() { + struct Foo; + impl Value for Foo { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + let s = "Ala ma kota"; + buf.extend_from_slice(&(s.len() as i32).to_be_bytes()); + buf.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + let buf = do_serialize(ValueAdapter(Foo), &ColumnType::Text); + let expected = vec![ + 0, 0, 0, 11, // Length of the value + 65, 108, 97, 32, 109, 97, 32, 107, 111, 116, 97, // The string + ]; + assert_eq!(buf, expected); + } + + fn get_typeck_err(err: &SerializationError) -> &BuiltinTypeCheckError { + match err.0.downcast_ref() { + Some(err) => err, + None => panic!("not a BuiltinTypeCheckError: {}", err), + } + } + + fn get_ser_err(err: &SerializationError) -> &BuiltinSerializationError { + match err.0.downcast_ref() { + Some(err) => err, + None => panic!("not a BuiltinSerializationError: {}", err), + } + } + + #[test] + fn test_native_errors() { + // Simple type mismatch + let v = 123_i32; + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Int], + }, + )); + + // str (and also Uuid) are interesting because they accept two types, + // also check str here + let v = "Ala ma kota"; + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<&str>()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Ascii, ColumnType::Text], + }, + )); + + // We'll skip testing for SizeOverflow as this would require producing + // a value which is at least 2GB in size. + + // Value overflow (type out of representable range) + let v = BigDecimal::new(BigInt::from(123), 1i64 << 40); + let err = do_serialize_err(v, &ColumnType::Decimal); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, ColumnType::Decimal); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::ValueOverflow, + )); + } + + #[test] + fn test_set_or_list_errors() { + // Not a set or list + let v = vec![123_i32]; + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::SetOrListError(SetOrListTypeCheckErrorKind::NotSetOrList), + )); + + // Trick: Unset is a ZST, so [Unset; 1 << 33] is a ZST, too. + // While it's probably incorrect to use Unset in a collection, this + // allows us to trigger the right error without going out of memory. + // Such an array is also created instantaneously. + let v = &[Unset; 1 << 33] as &[Unset]; + let typ = ColumnType::List(Box::new(ColumnType::Int)); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<&[Unset]>()); + assert_eq!(err.got, typ); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::SetOrListError( + SetOrListSerializationErrorKind::TooManyElements + ), + )); + + // Error during serialization of an element + let v = vec![123_i32]; + let typ = ColumnType::List(Box::new(ColumnType::Double)); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::SetOrListError( + SetOrListSerializationErrorKind::ElementSerializationFailed(err), + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Int], + } + )); + } + + #[test] + fn test_map_errors() { + // Not a map + let v = BTreeMap::from([("foo", "bar")]); + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MapError(MapTypeCheckErrorKind::NotMap), + )); + + // It's not practical to check the TooManyElements error as it would + // require allocating a huge amount of memory. + + // Error during serialization of a key + let v = BTreeMap::from([(123_i32, 456_i32)]); + let typ = ColumnType::Map(Box::new(ColumnType::Double), Box::new(ColumnType::Int)); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::MapError( + MapSerializationErrorKind::KeySerializationFailed(err), + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Int], + } + )); + + // Error during serialization of a value + let v = BTreeMap::from([(123_i32, 456_i32)]); + let typ = ColumnType::Map(Box::new(ColumnType::Int), Box::new(ColumnType::Double)); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::>()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::MapError( + MapSerializationErrorKind::ValueSerializationFailed(err), + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Int], + } + )); + } + + #[test] + fn test_tuple_errors() { + // Not a tuple + let v = (123_i32, 456_i32, 789_i32); + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<(i32, i32, i32)>()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::TupleError(TupleTypeCheckErrorKind::NotTuple), + )); + + // The Rust tuple has more elements than the CQL type + let v = (123_i32, 456_i32, 789_i32); + let typ = ColumnType::Tuple(vec![ColumnType::Int; 2]); + let err = do_serialize_err(v, &typ); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<(i32, i32, i32)>()); + assert_eq!(err.got, typ); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::TupleError(TupleTypeCheckErrorKind::WrongElementCount { + actual: 3, + asked_for: 2, + }), + )); + + // Error during serialization of one of the elements + let v = (123_i32, "Ala ma kota", 789.0_f64); + let typ = ColumnType::Tuple(vec![ColumnType::Int, ColumnType::Text, ColumnType::Uuid]); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::<(i32, &str, f64)>()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::TupleError( + TupleSerializationErrorKind::ElementSerializationFailed { index: 2, err }, + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Double], + } + )); + } + + #[test] + fn test_cql_value_errors() { + // Tried to encode Empty value into a non-emptyable type + let v = CqlValue::Empty; + let err = do_serialize_err(v, &ColumnType::Counter); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, ColumnType::Counter); + assert!(matches!(err.kind, BuiltinTypeCheckErrorKind::NotEmptyable)); + + // Handle tuples and UDTs in separate tests, as they have some + // custom logic + } + + #[test] + fn test_cql_value_tuple_errors() { + // Not a tuple + let v = CqlValue::Tuple(vec![ + Some(CqlValue::Int(123_i32)), + Some(CqlValue::Int(456_i32)), + Some(CqlValue::Int(789_i32)), + ]); + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::TupleError(TupleTypeCheckErrorKind::NotTuple), + )); + + // The Rust tuple has more elements than the CQL type + let v = CqlValue::Tuple(vec![ + Some(CqlValue::Int(123_i32)), + Some(CqlValue::Int(456_i32)), + Some(CqlValue::Int(789_i32)), + ]); + let typ = ColumnType::Tuple(vec![ColumnType::Int; 2]); + let err = do_serialize_err(v, &typ); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, typ); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::TupleError(TupleTypeCheckErrorKind::WrongElementCount { + actual: 3, + asked_for: 2, + }), + )); + + // Error during serialization of one of the elements + let v = CqlValue::Tuple(vec![ + Some(CqlValue::Int(123_i32)), + Some(CqlValue::Text("Ala ma kota".to_string())), + Some(CqlValue::Double(789_f64)), + ]); + let typ = ColumnType::Tuple(vec![ColumnType::Int, ColumnType::Text, ColumnType::Uuid]); + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::TupleError( + TupleSerializationErrorKind::ElementSerializationFailed { index: 2, err }, + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Double], + } + )); + } + + #[test] + fn test_cql_value_udt_errors() { + // Not a UDT + let v = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "udt".to_string(), + fields: vec![ + ("a".to_string(), Some(CqlValue::Int(123_i32))), + ("b".to_string(), Some(CqlValue::Int(456_i32))), + ("c".to_string(), Some(CqlValue::Int(789_i32))), + ], + }; + let err = do_serialize_err(v, &ColumnType::Double); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, ColumnType::Double); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NotUdt), + )); + + // Wrong type name + let v = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "udt".to_string(), + fields: vec![ + ("a".to_string(), Some(CqlValue::Int(123_i32))), + ("b".to_string(), Some(CqlValue::Int(456_i32))), + ("c".to_string(), Some(CqlValue::Int(789_i32))), + ], + }; + let typ = ColumnType::UserDefinedType { + type_name: "udt2".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Int), + ("b".to_string(), ColumnType::Int), + ("c".to_string(), ColumnType::Int), + ], + }; + let err = do_serialize_err(v, &typ); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, typ); + let BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NameMismatch { + keyspace, + type_name, + }) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + assert_eq!(keyspace, "ks"); + assert_eq!(type_name, "udt2"); + + // Some fields are missing from the CQL type + let v = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "udt".to_string(), + fields: vec![ + ("a".to_string(), Some(CqlValue::Int(123_i32))), + ("b".to_string(), Some(CqlValue::Int(456_i32))), + ("c".to_string(), Some(CqlValue::Int(789_i32))), + ], + }; + let typ = ColumnType::UserDefinedType { + type_name: "udt".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Int), + ("b".to_string(), ColumnType::Int), + // c is missing + ], + }; + let err = do_serialize_err(v, &typ); + let err = get_typeck_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, typ); + let BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NoSuchFieldInUdt { + field_name, + }) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + assert_eq!(field_name, "c"); + + // It is allowed for a Rust UDT to have less fields than the CQL UDT, + // so skip UnexpectedFieldInDestination. + + // Error during serialization of one of the fields + let v = CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "udt".to_string(), + fields: vec![ + ("a".to_string(), Some(CqlValue::Int(123_i32))), + ("b".to_string(), Some(CqlValue::Int(456_i32))), + ("c".to_string(), Some(CqlValue::Int(789_i32))), + ], + }; + let typ = ColumnType::UserDefinedType { + type_name: "udt".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Int), + ("b".to_string(), ColumnType::Int), + ("c".to_string(), ColumnType::Double), + ], + }; + let err = do_serialize_err(v, &typ); + let err = get_ser_err(&err); + assert_eq!(err.rust_name, std::any::type_name::()); + assert_eq!(err.got, typ); + let BuiltinSerializationErrorKind::UdtError( + UdtSerializationErrorKind::FieldSerializationFailed { field_name, err }, + ) = &err.kind + else { + panic!("unexpected error kind: {}", err.kind) + }; + assert_eq!(field_name, "c"); + let err = get_typeck_err(err); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::MismatchedType { + expected: &[ColumnType::Int], + } + )); + } + + // Do not remove. It's not used in tests but we keep it here to check that + // we properly ignore warnings about unused variables, unnecessary `mut`s + // etc. that usually pop up when generating code for empty structs. + #[derive(SerializeCql)] + #[scylla(crate = crate)] + struct TestUdtWithNoFields {} + + #[derive(SerializeCql, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate)] + struct TestUdtWithFieldSorting { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_udt_serialization_with_field_sorting_correct_order() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let reference = do_serialize( + CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "typ".to_string(), + fields: vec![ + ( + "a".to_string(), + Some(CqlValue::Text(String::from("Ala ma kota"))), + ), + ("b".to_string(), Some(CqlValue::Int(42))), + ( + "c".to_string(), + Some(CqlValue::List(vec![ + CqlValue::BigInt(1), + CqlValue::BigInt(2), + CqlValue::BigInt(3), + ])), + ), + ], + }, + &typ, + ); + let udt = do_serialize( + TestUdtWithFieldSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[test] + fn test_udt_serialization_with_field_sorting_incorrect_order() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + // Two first columns are swapped + ("b".to_string(), ColumnType::Int), + ("a".to_string(), ColumnType::Text), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let reference = do_serialize( + CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "typ".to_string(), + fields: vec![ + // FIXME: UDTs in CqlValue should also honor the order + // For now, it's swapped here as well + ("b".to_string(), Some(CqlValue::Int(42))), + ( + "a".to_string(), + Some(CqlValue::Text(String::from("Ala ma kota"))), + ), + ( + "c".to_string(), + Some(CqlValue::List(vec![ + CqlValue::BigInt(1), + CqlValue::BigInt(2), + CqlValue::BigInt(3), + ])), + ), + ], + }, + &typ, + ); + let udt = do_serialize( + TestUdtWithFieldSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[test] + fn test_udt_serialization_with_missing_rust_fields_at_end() { + let udt = TestUdtWithFieldSorting::default(); + + let typ_normal = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let typ_unexpected_field = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + // Unexpected fields + ("d".to_string(), ColumnType::Counter), + ("e".to_string(), ColumnType::Counter), + ], + }; + + let result_normal = do_serialize(&udt, &typ_normal); + let result_additional_field = do_serialize(&udt, &typ_unexpected_field); + + assert_eq!(result_normal, result_additional_field); + } + + #[derive(SerializeCql, Debug, PartialEq, Default)] + #[scylla(crate = crate)] + struct TestUdtWithFieldSorting2 { + a: String, + b: i32, + d: Option, + c: Vec, + } + + #[derive(SerializeCql, Debug, PartialEq, Default)] + #[scylla(crate = crate)] + struct TestUdtWithFieldSorting3 { + a: String, + b: i32, + d: Option, + e: Option, + c: Vec, + } + + #[test] + fn test_udt_serialization_with_missing_rust_field_in_middle() { + let udt = TestUdtWithFieldSorting::default(); + let udt2 = TestUdtWithFieldSorting2::default(); + let udt3 = TestUdtWithFieldSorting3::default(); + + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + // Unexpected fields + ("d".to_string(), ColumnType::Counter), + ("e".to_string(), ColumnType::Float), + // Remaining normal field + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let result_1 = do_serialize(udt, &typ); + let result_2 = do_serialize(udt2, &typ); + let result_3 = do_serialize(udt3, &typ); + + assert_eq!(result_1, result_2); + assert_eq!(result_2, result_3); + } + + #[test] + fn test_udt_serialization_failing_type_check() { + let typ_not_udt = ColumnType::Ascii; + let udt = TestUdtWithFieldSorting::default(); + let mut data = Vec::new(); + + let err = udt + .serialize(&typ_not_udt, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NotUdt) + )); + + let typ_without_c = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + // Last field is missing + ], + }; + + let err = udt + .serialize(&typ_without_c, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError( + UdtTypeCheckErrorKind::ValueMissingForUdtField { .. } + ) + )); + + let typ_wrong_type = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ("c".to_string(), ColumnType::TinyInt), // Wrong column type + ], + }; + + let err = udt + .serialize(&typ_wrong_type, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::UdtError( + UdtSerializationErrorKind::FieldSerializationFailed { .. } + ) + )); + } + + #[derive(SerializeCql)] + #[scylla(crate = crate)] + struct TestUdtWithGenerics<'a, T: SerializeCql> { + a: &'a str, + b: T, + } + + #[test] + fn test_udt_serialization_with_generics() { + // A minimal smoke test just to test that it works. + fn check_with_type(typ: ColumnType, t: T, cql_t: CqlValue) { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![("a".to_string(), ColumnType::Text), ("b".to_string(), typ)], + }; + let reference = do_serialize( + CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "typ".to_string(), + fields: vec![ + ( + "a".to_string(), + Some(CqlValue::Text(String::from("Ala ma kota"))), + ), + ("b".to_string(), Some(cql_t)), + ], + }, + &typ, + ); + let udt = do_serialize( + TestUdtWithGenerics { + a: "Ala ma kota", + b: t, + }, + &typ, + ); + assert_eq!(reference, udt); + } + + check_with_type(ColumnType::Int, 123_i32, CqlValue::Int(123_i32)); + check_with_type(ColumnType::Double, 123_f64, CqlValue::Double(123_f64)); + } + + #[derive(SerializeCql, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate, flavor = "enforce_order")] + struct TestUdtWithEnforcedOrder { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_udt_serialization_with_enforced_order_correct_order() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let reference = do_serialize( + CqlValue::UserDefinedType { + keyspace: "ks".to_string(), + type_name: "typ".to_string(), + fields: vec![ + ( + "a".to_string(), + Some(CqlValue::Text(String::from("Ala ma kota"))), + ), + ("b".to_string(), Some(CqlValue::Int(42))), + ( + "c".to_string(), + Some(CqlValue::List(vec![ + CqlValue::BigInt(1), + CqlValue::BigInt(2), + CqlValue::BigInt(3), + ])), + ), + ], + }, + &typ, + ); + let udt = do_serialize( + TestUdtWithEnforcedOrder { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[test] + fn test_udt_serialization_with_enforced_order_additional_field() { + let udt = TestUdtWithEnforcedOrder::default(); + + let typ_normal = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let typ_unexpected_field = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + // Unexpected field + ("d".to_string(), ColumnType::Counter), + ], + }; + + let result_normal = do_serialize(&udt, &typ_normal); + let result_additional_field = do_serialize(&udt, &typ_unexpected_field); + + assert_eq!(result_normal, result_additional_field); + } + + #[test] + fn test_udt_serialization_with_enforced_order_failing_type_check() { + let typ_not_udt = ColumnType::Ascii; + let udt = TestUdtWithEnforcedOrder::default(); + + let mut data = Vec::new(); + + let err = <_ as SerializeCql>::serialize(&udt, &typ_not_udt, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NotUdt) + )); + + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + // Two first columns are swapped + ("b".to_string(), ColumnType::Int), + ("a".to_string(), ColumnType::Text), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let err = + <_ as SerializeCql>::serialize(&udt, &typ, CellWriter::new(&mut data)).unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::FieldNameMismatch { .. }) + )); + + let typ_without_c = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + // Last field is missing + ], + }; + + let err = <_ as SerializeCql>::serialize(&udt, &typ_without_c, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError( + UdtTypeCheckErrorKind::ValueMissingForUdtField { .. } + ) + )); + + let typ_unexpected_field = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ("c".to_string(), ColumnType::TinyInt), // Wrong column type + ], + }; + + let err = + <_ as SerializeCql>::serialize(&udt, &typ_unexpected_field, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinSerializationErrorKind::UdtError( + UdtSerializationErrorKind::FieldSerializationFailed { .. } + ) + )); + } + + #[derive(SerializeCql, Debug)] + #[scylla(crate = crate)] + struct TestUdtWithFieldRename { + a: String, + #[scylla(rename = "x")] + b: i32, + } + + #[derive(SerializeCql, Debug)] + #[scylla(crate = crate, flavor = "enforce_order")] + struct TestUdtWithFieldRenameAndEnforceOrder { + a: String, + #[scylla(rename = "x")] + b: i32, + } + + #[test] + fn test_udt_serialization_with_field_rename() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("x".to_string(), ColumnType::Int), + ("a".to_string(), ColumnType::Text), + ], + }; + + let mut reference = Vec::new(); + // Total length of the struct is 23 + reference.extend_from_slice(&23i32.to_be_bytes()); + // Field 'x' + reference.extend_from_slice(&4i32.to_be_bytes()); + reference.extend_from_slice(&42i32.to_be_bytes()); + // Field 'a' + reference.extend_from_slice(&("Ala ma kota".len() as i32).to_be_bytes()); + reference.extend_from_slice("Ala ma kota".as_bytes()); + + let udt = do_serialize( + TestUdtWithFieldRename { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[test] + fn test_udt_serialization_with_field_rename_and_enforce_order() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("x".to_string(), ColumnType::Int), + ], + }; + + let mut reference = Vec::new(); + // Total length of the struct is 23 + reference.extend_from_slice(&23i32.to_be_bytes()); + // Field 'a' + reference.extend_from_slice(&("Ala ma kota".len() as i32).to_be_bytes()); + reference.extend_from_slice("Ala ma kota".as_bytes()); + // Field 'x' + reference.extend_from_slice(&4i32.to_be_bytes()); + reference.extend_from_slice(&42i32.to_be_bytes()); + + let udt = do_serialize( + TestUdtWithFieldRenameAndEnforceOrder { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[derive(SerializeCql, Debug)] + #[scylla(crate = crate, flavor = "enforce_order", skip_name_checks)] + struct TestUdtWithSkippedNameChecks { + a: String, + b: i32, + } + + #[test] + fn test_udt_serialization_with_skipped_name_checks() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("x".to_string(), ColumnType::Int), + ], + }; + + let mut reference = Vec::new(); + // Total length of the struct is 23 + reference.extend_from_slice(&23i32.to_be_bytes()); + // Field 'a' + reference.extend_from_slice(&("Ala ma kota".len() as i32).to_be_bytes()); + reference.extend_from_slice("Ala ma kota".as_bytes()); + // Field 'x' + reference.extend_from_slice(&4i32.to_be_bytes()); + reference.extend_from_slice(&42i32.to_be_bytes()); + + let udt = do_serialize( + TestUdtWithFieldRenameAndEnforceOrder { + a: "Ala ma kota".to_owned(), + b: 42, + }, + &typ, + ); + + assert_eq!(reference, udt); + } + + #[derive(SerializeCql, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate, force_exact_match)] + struct TestStrictUdtWithFieldSorting { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_strict_udt_with_field_sorting_rejects_additional_field() { + let udt = TestStrictUdtWithFieldSorting::default(); + let mut data = Vec::new(); + + let typ_unexpected_field = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + // Unexpected field + ("d".to_string(), ColumnType::Counter), + ], + }; + + let err = udt + .serialize(&typ_unexpected_field, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NoSuchFieldInUdt { .. }) + )); + + let typ_unexpected_field_middle = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + // Unexpected field + ("b_c".to_string(), ColumnType::Counter), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let err = udt + .serialize(&typ_unexpected_field_middle, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NoSuchFieldInUdt { .. }) + )); + } + + #[derive(SerializeCql, Debug, PartialEq, Eq, Default)] + #[scylla(crate = crate, flavor = "enforce_order", force_exact_match)] + struct TestStrictUdtWithEnforcedOrder { + a: String, + b: i32, + c: Vec, + } + + #[test] + fn test_strict_udt_with_enforced_order_rejects_additional_field() { + let udt = TestStrictUdtWithEnforcedOrder::default(); + let mut data = Vec::new(); + + let typ_unexpected_field = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + // Unexpected field + ("d".to_string(), ColumnType::Counter), + ], + }; + + let err = + <_ as SerializeCql>::serialize(&udt, &typ_unexpected_field, CellWriter::new(&mut data)) + .unwrap_err(); + let err = err.0.downcast_ref::().unwrap(); + assert!(matches!( + err.kind, + BuiltinTypeCheckErrorKind::UdtError(UdtTypeCheckErrorKind::NoSuchFieldInUdt { .. }) + )); + } + + #[derive(SerializeCql, Debug)] + #[scylla(crate = crate, flavor = "enforce_order", skip_name_checks)] + struct TestUdtWithSkippedFields { + a: String, + b: i32, + #[scylla(skip)] + #[allow(dead_code)] + skipped: Vec, + c: Vec, + } + + #[test] + fn test_row_serialization_with_skipped_field() { + let typ = ColumnType::UserDefinedType { + type_name: "typ".to_string(), + keyspace: "ks".to_string(), + field_types: vec![ + ("a".to_string(), ColumnType::Text), + ("b".to_string(), ColumnType::Int), + ( + "c".to_string(), + ColumnType::List(Box::new(ColumnType::BigInt)), + ), + ], + }; + + let reference = do_serialize( + TestUdtWithFieldSorting { + a: "Ala ma kota".to_owned(), + b: 42, + c: vec![1, 2, 3], + }, + &typ, + ); + let row = do_serialize( + TestUdtWithSkippedFields { + a: "Ala ma kota".to_owned(), + b: 42, + skipped: vec!["abcd".to_owned(), "efgh".to_owned()], + c: vec![1, 2, 3], + }, + &typ, + ); - fn serialize(&self, _typ: &ColumnType, buf: &mut Vec) -> Result<(), SerializationError> { - self.serialize(buf) - .map_err(|err| Arc::new(err) as SerializationError) + assert_eq!(reference, row); } } diff --git a/scylla-cql/src/types/serialize/writers.rs b/scylla-cql/src/types/serialize/writers.rs new file mode 100644 index 0000000000..6587634a47 --- /dev/null +++ b/scylla-cql/src/types/serialize/writers.rs @@ -0,0 +1,281 @@ +//! Contains types and traits used for safe serialization of values for a CQL statement. + +use thiserror::Error; + +use super::row::SerializedValues; + +/// An interface that facilitates writing values for a CQL query. +pub struct RowWriter<'buf> { + // Buffer that this value should be serialized to. + buf: &'buf mut Vec, + + // Number of values written so far. + value_count: usize, +} + +impl<'buf> RowWriter<'buf> { + /// Creates a new row writer based on an existing Vec. + /// + /// The newly created row writer will append data to the end of the vec. + #[inline] + pub fn new(buf: &'buf mut Vec) -> Self { + Self { + buf, + value_count: 0, + } + } + + /// Returns the number of values that were written so far. + /// + /// Note that the protocol allows at most u16::MAX to be written into a query, + /// but the writer's interface allows more to be written. + #[inline] + pub fn value_count(&self) -> usize { + self.value_count + } + + /// Appends a new value to the sequence and returns an object that allows + /// to fill it in. + #[inline] + pub fn make_cell_writer(&mut self) -> CellWriter<'_> { + self.value_count += 1; + CellWriter::new(self.buf) + } + + /// Appends the values from an existing [`SerializedValues`] object to the + /// current `RowWriter`. + #[inline] + pub fn append_serialize_row(&mut self, sv: &SerializedValues) { + self.value_count += sv.element_count() as usize; + self.buf.extend_from_slice(sv.get_contents()) + } +} + +/// Represents a handle to a CQL value that needs to be written into. +/// +/// The writer can either be transformed into a ready value right away +/// (via [`set_null`](CellWriter::set_null), +/// [`set_unset`](CellWriter::set_unset) +/// or [`set_value`](CellWriter::set_value) or transformed into +/// the [`CellValueBuilder`] in order to gradually initialize +/// the value when the contents are not available straight away. +/// +/// After the value is fully initialized, the handle is consumed and +/// a [`WrittenCellProof`] object is returned +/// in its stead. This is a type-level proof that the value was fully initialized +/// and is used in [`SerializeCql::serialize`](`super::value::SerializeCql::serialize`) +/// in order to enforce the implementer to fully initialize the provided handle +/// to CQL value. +/// +/// Dropping this type without calling any of its methods will result +/// in nothing being written. +pub struct CellWriter<'buf> { + buf: &'buf mut Vec, +} + +impl<'buf> CellWriter<'buf> { + /// Creates a new cell writer based on an existing Vec. + /// + /// The newly created row writer will append data to the end of the vec. + #[inline] + pub fn new(buf: &'buf mut Vec) -> Self { + Self { buf } + } + + /// Sets this value to be null, consuming this object. + #[inline] + pub fn set_null(self) -> WrittenCellProof<'buf> { + self.buf.extend_from_slice(&(-1i32).to_be_bytes()); + WrittenCellProof::new() + } + + /// Sets this value to represent an unset value, consuming this object. + #[inline] + pub fn set_unset(self) -> WrittenCellProof<'buf> { + self.buf.extend_from_slice(&(-2i32).to_be_bytes()); + WrittenCellProof::new() + } + + /// Sets this value to a non-zero, non-unset value with given contents. + /// + /// Prefer this to [`into_value_builder`](CellWriter::into_value_builder) + /// if you have all of the contents of the value ready up front (e.g. for + /// fixed size types). + /// + /// Fails if the contents size overflows the maximum allowed CQL cell size + /// (which is i32::MAX). + #[inline] + pub fn set_value(self, contents: &[u8]) -> Result, CellOverflowError> { + let value_len: i32 = contents.len().try_into().map_err(|_| CellOverflowError)?; + self.buf.extend_from_slice(&value_len.to_be_bytes()); + self.buf.extend_from_slice(contents); + Ok(WrittenCellProof::new()) + } + + /// Turns this writter into a [`CellValueBuilder`] which can be used + /// to gradually initialize the CQL value. + /// + /// This method should be used if you don't have all of the data + /// up front, e.g. when serializing compound types such as collections + /// or UDTs. + #[inline] + pub fn into_value_builder(self) -> CellValueBuilder<'buf> { + CellValueBuilder::new(self.buf) + } +} + +/// Allows appending bytes to a non-null, non-unset cell. +/// +/// This object needs to be dropped in order for the value to be correctly +/// serialized. Failing to drop this value will result in a payload that will +/// not be parsed by the database correctly, but otherwise should not cause +/// data to be misinterpreted. +pub struct CellValueBuilder<'buf> { + // Buffer that this value should be serialized to. + buf: &'buf mut Vec, + + // Starting position of the value in the buffer. + starting_pos: usize, +} + +impl<'buf> CellValueBuilder<'buf> { + #[inline] + fn new(buf: &'buf mut Vec) -> Self { + // "Length" of a [bytes] frame can either be a non-negative i32, + // -1 (null) or -1 (not set). Push an invalid value here. It will be + // overwritten eventually either by set_null, set_unset or Drop. + // If the CellSerializer is not dropped as it should, this will trigger + // an error on the DB side and the serialized data + // won't be misinterpreted. + let starting_pos = buf.len(); + buf.extend_from_slice(&(-3i32).to_be_bytes()); + Self { buf, starting_pos } + } + + /// Appends raw bytes to this cell. + #[inline] + pub fn append_bytes(&mut self, bytes: &[u8]) { + self.buf.extend_from_slice(bytes); + } + + /// Appends a sub-value to the end of the current contents of the cell + /// and returns an object that allows to fill it in. + #[inline] + pub fn make_sub_writer(&mut self) -> CellWriter<'_> { + CellWriter::new(self.buf) + } + + /// Finishes serializing the value. + /// + /// Fails if the constructed cell size overflows the maximum allowed + /// CQL cell size (which is i32::MAX). + #[inline] + pub fn finish(self) -> Result, CellOverflowError> { + let value_len: i32 = (self.buf.len() - self.starting_pos - 4) + .try_into() + .map_err(|_| CellOverflowError)?; + self.buf[self.starting_pos..self.starting_pos + 4] + .copy_from_slice(&value_len.to_be_bytes()); + Ok(WrittenCellProof::new()) + } +} + +/// An object that indicates a type-level proof that something was written +/// by a [`CellWriter`] or [`CellValueBuilder`] with lifetime parameter `'buf`. +/// +/// This type is returned by [`set_null`](CellWriter::set_null), +/// [`set_unset`](CellWriter::set_unset), +/// [`set_value`](CellWriter::set_value) +/// and also [`CellValueBuilder::finish`] - generally speaking, after +/// the value is fully initialized and the `CellWriter` is destroyed. +/// +/// The purpose of this type is to enforce the contract of +/// [`SerializeCql::serialize`](super::value::SerializeCql::serialize): either +/// the method succeeds and returns a proof that it serialized itself +/// into the given value, or it fails and returns an error or panics. +#[derive(Debug)] +pub struct WrittenCellProof<'buf> { + /// Using *mut &'buf () is deliberate and makes WrittenCellProof invariant + /// on the 'buf lifetime parameter. + /// Ref: + _phantom: std::marker::PhantomData<*mut &'buf ()>, +} + +impl<'buf> WrittenCellProof<'buf> { + /// A shorthand for creating the proof. + /// + /// Do not make it public! It's important that only the row writer defined + /// in this module is able to create a proof. + #[inline] + fn new() -> Self { + WrittenCellProof { + _phantom: std::marker::PhantomData, + } + } +} + +/// There was an attempt to produce a CQL value over the maximum size limit (i32::MAX) +#[derive(Debug, Clone, Copy, Error)] +#[error("CQL cell overflowed the maximum allowed size of 2^31 - 1")] +pub struct CellOverflowError; + +#[cfg(test)] +mod tests { + use super::{CellWriter, RowWriter}; + + #[test] + fn test_cell_writer() { + let mut data = Vec::new(); + let writer = CellWriter::new(&mut data); + let mut sub_writer = writer.into_value_builder(); + sub_writer.make_sub_writer().set_null(); + sub_writer + .make_sub_writer() + .set_value(&[1, 2, 3, 4]) + .unwrap(); + sub_writer.make_sub_writer().set_unset(); + sub_writer.finish().unwrap(); + + assert_eq!( + data, + [ + 0, 0, 0, 16, // Length of inner data is 16 + 255, 255, 255, 255, // Null (encoded as -1) + 0, 0, 0, 4, 1, 2, 3, 4, // Four byte value + 255, 255, 255, 254, // Unset (encoded as -2) + ] + ); + } + + #[test] + fn test_poisoned_appender() { + let mut data = Vec::new(); + let writer = CellWriter::new(&mut data); + let _ = writer.into_value_builder(); + + assert_eq!( + data, + [ + 255, 255, 255, 253, // Invalid value + ] + ); + } + + #[test] + fn test_row_writer() { + let mut data = Vec::new(); + let mut writer = RowWriter::new(&mut data); + writer.make_cell_writer().set_null(); + writer.make_cell_writer().set_value(&[1, 2, 3, 4]).unwrap(); + writer.make_cell_writer().set_unset(); + + assert_eq!( + data, + [ + 255, 255, 255, 255, // Null (encoded as -1) + 0, 0, 0, 4, 1, 2, 3, 4, // Four byte value + 255, 255, 255, 254, // Unset (encoded as -2) + ] + ) + } +} diff --git a/scylla-macros/Cargo.toml b/scylla-macros/Cargo.toml index d39bd58116..bff8eb7f18 100644 --- a/scylla-macros/Cargo.toml +++ b/scylla-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scylla-macros" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "proc macros for scylla async CQL driver" repository = "https://github.com/scylladb/scylla-rust-driver" @@ -12,6 +12,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] +darling = "0.20.0" syn = "2.0" quote = "1.0" proc-macro2 = "1.0" \ No newline at end of file diff --git a/scylla-macros/src/lib.rs b/scylla-macros/src/lib.rs index 59300a0020..cb8b967c84 100644 --- a/scylla-macros/src/lib.rs +++ b/scylla-macros/src/lib.rs @@ -1,4 +1,5 @@ use proc_macro::TokenStream; +use quote::ToTokens; mod from_row; mod from_user_type; @@ -6,32 +7,60 @@ mod into_user_type; mod parser; mod value_list; -/// #[derive(FromRow)] derives FromRow for struct -/// Works only on simple structs without generics etc +mod serialize; + +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. +#[proc_macro_derive(SerializeCql, attributes(scylla))] +pub fn serialize_cql_derive(tokens_input: TokenStream) -> TokenStream { + match serialize::cql::derive_serialize_cql(tokens_input) { + Ok(t) => t.into_token_stream().into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. +#[proc_macro_derive(SerializeRow, attributes(scylla))] +pub fn serialize_row_derive(tokens_input: TokenStream) -> TokenStream { + match serialize::row::derive_serialize_row(tokens_input) { + Ok(t) => t.into_token_stream().into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. #[proc_macro_derive(FromRow, attributes(scylla_crate))] pub fn from_row_derive(tokens_input: TokenStream) -> TokenStream { let res = from_row::from_row_derive(tokens_input); res.unwrap_or_else(|e| e.into_compile_error().into()) } -/// #[derive(FromUserType)] allows to parse a struct as User Defined Type -/// Works only on simple structs without generics etc +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. #[proc_macro_derive(FromUserType, attributes(scylla_crate))] pub fn from_user_type_derive(tokens_input: TokenStream) -> TokenStream { let res = from_user_type::from_user_type_derive(tokens_input); res.unwrap_or_else(|e| e.into_compile_error().into()) } -/// #[derive(IntoUserType)] allows to parse a struct as User Defined Type -/// Works only on simple structs without generics etc +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. #[proc_macro_derive(IntoUserType, attributes(scylla_crate))] pub fn into_user_type_derive(tokens_input: TokenStream) -> TokenStream { let res = into_user_type::into_user_type_derive(tokens_input); res.unwrap_or_else(|e| e.into_compile_error().into()) } -/// #[derive(ValueList)] derives ValueList for struct -/// Works only on simple structs without generics etc +/// Documentation for this macro can only be found +/// in `scylla` crate - not in scylla-macros nor in scylla-cql. +/// This is because of rustdocs limitations that are hard to explain here. #[proc_macro_derive(ValueList, attributes(scylla_crate))] pub fn value_list_derive(tokens_input: TokenStream) -> TokenStream { let res = value_list::value_list_derive(tokens_input); diff --git a/scylla-macros/src/serialize/cql.rs b/scylla-macros/src/serialize/cql.rs new file mode 100644 index 0000000000..feb1819b4e --- /dev/null +++ b/scylla-macros/src/serialize/cql.rs @@ -0,0 +1,478 @@ +use std::collections::HashMap; + +use darling::FromAttributes; +use proc_macro::TokenStream; +use proc_macro2::Span; +use syn::parse_quote; + +use super::Flavor; + +#[derive(FromAttributes)] +#[darling(attributes(scylla))] +struct Attributes { + #[darling(rename = "crate")] + crate_path: Option, + + #[darling(default)] + flavor: Flavor, + + #[darling(default)] + skip_name_checks: bool, + + #[darling(default)] + force_exact_match: bool, +} + +impl Attributes { + fn crate_path(&self) -> syn::Path { + self.crate_path + .as_ref() + .map(|p| parse_quote!(#p::_macro_internal)) + .unwrap_or_else(|| parse_quote!(::scylla::_macro_internal)) + } +} + +struct Field { + ident: syn::Ident, + ty: syn::Type, + attrs: FieldAttributes, +} + +impl Field { + fn field_name(&self) -> String { + match &self.attrs.rename { + Some(name) => name.clone(), + None => self.ident.to_string(), + } + } +} + +#[derive(FromAttributes)] +#[darling(attributes(scylla))] +struct FieldAttributes { + rename: Option, + + #[darling(default)] + skip: bool, +} + +struct Context { + attributes: Attributes, + fields: Vec, +} + +pub fn derive_serialize_cql(tokens_input: TokenStream) -> Result { + let input: syn::DeriveInput = syn::parse(tokens_input)?; + let struct_name = input.ident.clone(); + let named_fields = crate::parser::parse_named_fields(&input, "SerializeCql")?; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let attributes = Attributes::from_attributes(&input.attrs)?; + + let crate_path = attributes.crate_path(); + let implemented_trait: syn::Path = parse_quote!(#crate_path::SerializeCql); + + let fields = named_fields + .named + .iter() + .map(|f| { + FieldAttributes::from_attributes(&f.attrs).map(|attrs| Field { + ident: f.ident.clone().unwrap(), + ty: f.ty.clone(), + attrs, + }) + }) + // Filter the fields now instead of at the places that use them later + // as it's less error prone - we just filter in one place instead of N places. + .filter(|f| f.as_ref().map(|f| !f.attrs.skip).unwrap_or(true)) + .collect::>()?; + let ctx = Context { attributes, fields }; + ctx.validate(&input.ident)?; + + let gen: Box = match ctx.attributes.flavor { + Flavor::MatchByName => Box::new(FieldSortingGenerator { ctx: &ctx }), + Flavor::EnforceOrder => Box::new(FieldOrderedGenerator { ctx: &ctx }), + }; + + let serialize_item = gen.generate_serialize(); + + let res = parse_quote! { + impl #impl_generics #implemented_trait for #struct_name #ty_generics #where_clause { + #serialize_item + } + }; + Ok(res) +} + +impl Context { + fn validate(&self, struct_ident: &syn::Ident) -> Result<(), syn::Error> { + let mut errors = darling::Error::accumulator(); + + if self.attributes.skip_name_checks { + // Skipping name checks is only available in enforce_order mode + if self.attributes.flavor != Flavor::EnforceOrder { + let err = darling::Error::custom( + "the `skip_name_checks` attribute is only allowed with the `enforce_order` flavor", + ) + .with_span(struct_ident); + errors.push(err); + } + + // `rename` annotations don't make sense with skipped name checks + for field in self.fields.iter() { + if field.attrs.rename.is_some() { + let err = darling::Error::custom( + "the `rename` annotations don't make sense with `skip_name_checks` attribute", + ) + .with_span(&field.ident); + errors.push(err); + } + } + } + + // Check for name collisions + let mut used_names = HashMap::::new(); + for field in self.fields.iter() { + let field_name = field.field_name(); + if let Some(other_field) = used_names.get(&field_name) { + let other_field_ident = &other_field.ident; + let msg = format!("the UDT field name `{field_name}` used by this struct field is already used by field `{other_field_ident}`"); + let err = darling::Error::custom(msg).with_span(&field.ident); + errors.push(err); + } else { + used_names.insert(field_name, field); + } + } + + errors.finish()?; + Ok(()) + } + + fn generate_udt_type_match(&self, err: syn::Expr) -> syn::Stmt { + let crate_path = self.attributes.crate_path(); + + parse_quote! { + let (type_name, keyspace, field_types) = match typ { + #crate_path::ColumnType::UserDefinedType { type_name, keyspace, field_types, .. } => { + (type_name, keyspace, field_types) + } + _ => return ::std::result::Result::Err(mk_typck_err(#err)), + }; + } + } + + fn generate_mk_typck_err(&self) -> syn::Stmt { + let crate_path = self.attributes.crate_path(); + parse_quote! { + let mk_typck_err = |kind: #crate_path::UdtTypeCheckErrorKind| -> #crate_path::SerializationError { + #crate_path::SerializationError::new( + #crate_path::BuiltinTypeTypeCheckError { + rust_name: ::std::any::type_name::(), + got: <_ as ::std::clone::Clone>::clone(typ), + kind: #crate_path::BuiltinTypeTypeCheckErrorKind::UdtError(kind), + } + ) + }; + } + } + + fn generate_mk_ser_err(&self) -> syn::Stmt { + let crate_path = self.attributes.crate_path(); + parse_quote! { + let mk_ser_err = |kind: #crate_path::UdtSerializationErrorKind| -> #crate_path::SerializationError { + #crate_path::SerializationError::new( + #crate_path::BuiltinTypeSerializationError { + rust_name: ::std::any::type_name::(), + got: <_ as ::std::clone::Clone>::clone(typ), + kind: #crate_path::BuiltinTypeSerializationErrorKind::UdtError(kind), + } + ) + }; + } + } +} + +trait Generator { + fn generate_serialize(&self) -> syn::TraitItemFn; +} + +// Generates an implementation of the trait which sorts the fields according +// to how it is defined in the database. +struct FieldSortingGenerator<'a> { + ctx: &'a Context, +} + +impl<'a> Generator for FieldSortingGenerator<'a> { + fn generate_serialize(&self) -> syn::TraitItemFn { + // Need to: + // - Check that all required fields are there and no more + // - Check that the field types match + let mut statements: Vec = Vec::new(); + + let crate_path = self.ctx.attributes.crate_path(); + + let rust_field_idents = self + .ctx + .fields + .iter() + .map(|f| f.ident.clone()) + .collect::>(); + let rust_field_names = self + .ctx + .fields + .iter() + .map(|f| f.field_name()) + .collect::>(); + let udt_field_names = rust_field_names.clone(); // For now, it's the same + let field_types = self.ctx.fields.iter().map(|f| &f.ty).collect::>(); + + let missing_rust_field_expression: syn::Expr = if self.ctx.attributes.force_exact_match { + parse_quote! { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::UdtTypeCheckErrorKind::NoSuchFieldInUdt { + field_name: <_ as ::std::clone::Clone>::clone(field_name), + } + )) + } + } else { + parse_quote! { + skipped_fields += 1 + } + }; + + let serialize_missing_nulls_statement: syn::Stmt = if self.ctx.attributes.force_exact_match + { + // Not sure if there is better way to create no-op statement + // parse_quote!{} / parse_quote!{ ; } doesn't work + parse_quote! { + (); + } + } else { + parse_quote! { + while skipped_fields > 0 { + let sub_builder = #crate_path::CellValueBuilder::make_sub_writer(&mut builder); + sub_builder.set_null(); + skipped_fields -= 1; + } + } + }; + + // Declare helper lambdas for creating errors + statements.push(self.ctx.generate_mk_typck_err()); + statements.push(self.ctx.generate_mk_ser_err()); + + // Check that the type we want to serialize to is a UDT + statements.push( + self.ctx + .generate_udt_type_match(parse_quote!(#crate_path::UdtTypeCheckErrorKind::NotUdt)), + ); + + // Generate a "visited" flag for each field + let visited_flag_names = rust_field_names + .iter() + .map(|s| syn::Ident::new(&format!("visited_flag_{}", s), Span::call_site())) + .collect::>(); + statements.extend::>(parse_quote! { + #(let mut #visited_flag_names = false;)* + }); + + // Generate a variable that counts down visited fields. + let field_count = self.ctx.fields.len(); + statements.push(parse_quote! { + let mut remaining_count = #field_count; + }); + + // We want to send nulls for missing rust fields in the middle, but send + // nothing for those fields at the end of UDT. While executing the loop + // we don't know if there will be any more present fields. The solution is + // to count how many fields we missed and send them when we find any present field. + if !self.ctx.attributes.force_exact_match { + statements.push(parse_quote! { + let mut skipped_fields = 0; + }); + } + + // Turn the cell writer into a value builder + statements.push(parse_quote! { + let mut builder = #crate_path::CellWriter::into_value_builder(writer); + }); + + // Generate a loop over the fields and a `match` block to match on + // the field name. + statements.push(parse_quote! { + for (field_name, field_type) in field_types { + match ::std::string::String::as_str(field_name) { + #( + #udt_field_names => { + #serialize_missing_nulls_statement + let sub_builder = #crate_path::CellValueBuilder::make_sub_writer(&mut builder); + match <#field_types as #crate_path::SerializeCql>::serialize(&self.#rust_field_idents, field_type, sub_builder) { + ::std::result::Result::Ok(_proof) => {} + ::std::result::Result::Err(err) => { + return ::std::result::Result::Err(mk_ser_err( + #crate_path::UdtSerializationErrorKind::FieldSerializationFailed { + field_name: <_ as ::std::clone::Clone>::clone(field_name), + err, + } + )); + } + } + if !#visited_flag_names { + #visited_flag_names = true; + remaining_count -= 1; + } + } + )* + _ => #missing_rust_field_expression, + } + } + }); + + // Finally, check that all fields were consumed. + // If there are some missing fields, return an error + statements.push(parse_quote! { + if remaining_count > 0 { + #( + if !#visited_flag_names { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::UdtTypeCheckErrorKind::ValueMissingForUdtField { + field_name: <_ as ::std::string::ToString>::to_string(#rust_field_names), + } + )); + } + )* + ::std::unreachable!() + } + }); + + parse_quote! { + fn serialize<'b>( + &self, + typ: &#crate_path::ColumnType, + writer: #crate_path::CellWriter<'b>, + ) -> ::std::result::Result<#crate_path::WrittenCellProof<'b>, #crate_path::SerializationError> { + #(#statements)* + let proof = #crate_path::CellValueBuilder::finish(builder) + .map_err(|_| #crate_path::SerializationError::new( + #crate_path::BuiltinTypeSerializationError { + rust_name: ::std::any::type_name::(), + got: <_ as ::std::clone::Clone>::clone(typ), + kind: #crate_path::BuiltinTypeSerializationErrorKind::SizeOverflow, + } + ) as #crate_path::SerializationError)?; + ::std::result::Result::Ok(proof) + } + } + } +} + +// Generates an implementation of the trait which requires the fields +// to be placed in the same order as they are defined in the struct. +struct FieldOrderedGenerator<'a> { + ctx: &'a Context, +} + +impl<'a> Generator for FieldOrderedGenerator<'a> { + fn generate_serialize(&self) -> syn::TraitItemFn { + let mut statements: Vec = Vec::new(); + + let crate_path = self.ctx.attributes.crate_path(); + + // Declare a helper lambda for creating errors + statements.push(self.ctx.generate_mk_typck_err()); + statements.push(self.ctx.generate_mk_ser_err()); + + // Check that the type we want to serialize to is a UDT + statements.push( + self.ctx + .generate_udt_type_match(parse_quote!(#crate_path::UdtTypeCheckErrorKind::NotUdt)), + ); + + // Turn the cell writer into a value builder + statements.push(parse_quote! { + let mut builder = #crate_path::CellWriter::into_value_builder(writer); + }); + + // Create an iterator over fields + statements.push(parse_quote! { + let mut field_iter = field_types.iter(); + }); + + // Serialize each field + for field in self.ctx.fields.iter() { + let rust_field_ident = &field.ident; + let rust_field_name = field.field_name(); + let typ = &field.ty; + let name_check_expression: syn::Expr = if !self.ctx.attributes.skip_name_checks { + parse_quote! { field_name == #rust_field_name } + } else { + parse_quote! { true } + }; + statements.push(parse_quote! { + match field_iter.next() { + Some((field_name, typ)) => { + if #name_check_expression { + let sub_builder = #crate_path::CellValueBuilder::make_sub_writer(&mut builder); + match <#typ as #crate_path::SerializeCql>::serialize(&self.#rust_field_ident, typ, sub_builder) { + Ok(_proof) => {}, + Err(err) => { + return ::std::result::Result::Err(mk_ser_err( + #crate_path::UdtSerializationErrorKind::FieldSerializationFailed { + field_name: <_ as ::std::clone::Clone>::clone(field_name), + err, + } + )); + } + } + } else { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::UdtTypeCheckErrorKind::FieldNameMismatch { + rust_field_name: <_ as ::std::string::ToString>::to_string(#rust_field_name), + db_field_name: <_ as ::std::clone::Clone>::clone(field_name), + } + )); + } + } + None => { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::UdtTypeCheckErrorKind::ValueMissingForUdtField { + field_name: <_ as ::std::string::ToString>::to_string(#rust_field_name), + } + )); + } + } + }); + } + + if self.ctx.attributes.force_exact_match { + // Check whether there are some fields remaining + statements.push(parse_quote! { + if let Some((field_name, typ)) = field_iter.next() { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::UdtTypeCheckErrorKind::NoSuchFieldInUdt { + field_name: <_ as ::std::clone::Clone>::clone(field_name), + } + )); + } + }); + } + + parse_quote! { + fn serialize<'b>( + &self, + typ: &#crate_path::ColumnType, + writer: #crate_path::CellWriter<'b>, + ) -> ::std::result::Result<#crate_path::WrittenCellProof<'b>, #crate_path::SerializationError> { + #(#statements)* + let proof = #crate_path::CellValueBuilder::finish(builder) + .map_err(|_| #crate_path::SerializationError::new( + #crate_path::BuiltinTypeSerializationError { + rust_name: ::std::any::type_name::(), + got: <_ as ::std::clone::Clone>::clone(typ), + kind: #crate_path::BuiltinTypeSerializationErrorKind::SizeOverflow, + } + ) as #crate_path::SerializationError)?; + ::std::result::Result::Ok(proof) + } + } + } +} diff --git a/scylla-macros/src/serialize/mod.rs b/scylla-macros/src/serialize/mod.rs new file mode 100644 index 0000000000..28c8b91097 --- /dev/null +++ b/scylla-macros/src/serialize/mod.rs @@ -0,0 +1,21 @@ +use darling::FromMeta; + +pub(crate) mod cql; +pub(crate) mod row; + +#[derive(Copy, Clone, PartialEq, Eq, Default)] +enum Flavor { + #[default] + MatchByName, + EnforceOrder, +} + +impl FromMeta for Flavor { + fn from_string(value: &str) -> darling::Result { + match value { + "match_by_name" => Ok(Self::MatchByName), + "enforce_order" => Ok(Self::EnforceOrder), + _ => Err(darling::Error::unknown_value(value)), + } + } +} diff --git a/scylla-macros/src/serialize/row.rs b/scylla-macros/src/serialize/row.rs new file mode 100644 index 0000000000..34bdce92c1 --- /dev/null +++ b/scylla-macros/src/serialize/row.rs @@ -0,0 +1,405 @@ +use std::collections::HashMap; + +use darling::FromAttributes; +use proc_macro::TokenStream; +use proc_macro2::Span; +use syn::parse_quote; + +use super::Flavor; + +#[derive(FromAttributes)] +#[darling(attributes(scylla))] +struct Attributes { + #[darling(rename = "crate")] + crate_path: Option, + + #[darling(default)] + flavor: Flavor, + + #[darling(default)] + skip_name_checks: bool, +} + +impl Attributes { + fn crate_path(&self) -> syn::Path { + self.crate_path + .as_ref() + .map(|p| parse_quote!(#p::_macro_internal)) + .unwrap_or_else(|| parse_quote!(::scylla::_macro_internal)) + } +} + +struct Field { + ident: syn::Ident, + ty: syn::Type, + attrs: FieldAttributes, +} + +impl Field { + fn column_name(&self) -> String { + match &self.attrs.rename { + Some(name) => name.clone(), + None => self.ident.to_string(), + } + } +} + +#[derive(FromAttributes)] +#[darling(attributes(scylla))] +struct FieldAttributes { + rename: Option, + + #[darling(default)] + skip: bool, +} + +struct Context { + attributes: Attributes, + fields: Vec, +} + +pub fn derive_serialize_row(tokens_input: TokenStream) -> Result { + let input: syn::DeriveInput = syn::parse(tokens_input)?; + let struct_name = input.ident.clone(); + let named_fields = crate::parser::parse_named_fields(&input, "SerializeRow")?; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let attributes = Attributes::from_attributes(&input.attrs)?; + + let crate_path = attributes.crate_path(); + let implemented_trait: syn::Path = parse_quote!(#crate_path::SerializeRow); + + let fields = named_fields + .named + .iter() + .map(|f| { + FieldAttributes::from_attributes(&f.attrs).map(|attrs| Field { + ident: f.ident.clone().unwrap(), + ty: f.ty.clone(), + attrs, + }) + }) + // Filter the fields now instead of at the places that use them later + // as it's less error prone - we just filter in one place instead of N places. + .filter(|f| f.as_ref().map(|f| !f.attrs.skip).unwrap_or(true)) + .collect::>()?; + let ctx = Context { attributes, fields }; + ctx.validate(&input.ident)?; + + let gen: Box = match ctx.attributes.flavor { + Flavor::MatchByName => Box::new(ColumnSortingGenerator { ctx: &ctx }), + Flavor::EnforceOrder => Box::new(ColumnOrderedGenerator { ctx: &ctx }), + }; + + let serialize_item = gen.generate_serialize(); + let is_empty_item = gen.generate_is_empty(); + + let res = parse_quote! { + impl #impl_generics #implemented_trait for #struct_name #ty_generics #where_clause { + #serialize_item + #is_empty_item + } + }; + Ok(res) +} + +impl Context { + fn validate(&self, struct_ident: &syn::Ident) -> Result<(), syn::Error> { + let mut errors = darling::Error::accumulator(); + + if self.attributes.skip_name_checks { + // Skipping name checks is only available in enforce_order mode + if self.attributes.flavor != Flavor::EnforceOrder { + let err = darling::Error::custom( + "the `skip_name_checks` attribute is only allowed with the `enforce_order` flavor", + ) + .with_span(struct_ident); + errors.push(err); + } + + // `rename` annotations don't make sense with skipped name checks + for field in self.fields.iter() { + if field.attrs.rename.is_some() { + let err = darling::Error::custom( + "the `rename` annotations don't make sense with `skip_name_checks` attribute", + ) + .with_span(&field.ident); + errors.push(err); + } + } + } + + // Check for name collisions + let mut used_names = HashMap::::new(); + for field in self.fields.iter() { + let column_name = field.column_name(); + if let Some(other_field) = used_names.get(&column_name) { + let other_field_ident = &other_field.ident; + let msg = format!("the column / bind marker name `{column_name}` used by this struct field is already used by field `{other_field_ident}`"); + let err = darling::Error::custom(msg).with_span(&field.ident); + errors.push(err); + } else { + used_names.insert(column_name, field); + } + } + + errors.finish()?; + Ok(()) + } + + fn generate_mk_typck_err(&self) -> syn::Stmt { + let crate_path = self.attributes.crate_path(); + parse_quote! { + let mk_typck_err = |kind: #crate_path::BuiltinRowTypeCheckErrorKind| -> #crate_path::SerializationError { + #crate_path::SerializationError::new( + #crate_path::BuiltinRowTypeCheckError { + rust_name: ::std::any::type_name::(), + kind, + } + ) + }; + } + } + + fn generate_mk_ser_err(&self) -> syn::Stmt { + let crate_path = self.attributes.crate_path(); + parse_quote! { + let mk_ser_err = |kind: #crate_path::BuiltinRowSerializationErrorKind| -> #crate_path::SerializationError { + #crate_path::SerializationError::new( + #crate_path::BuiltinRowSerializationError { + rust_name: ::std::any::type_name::(), + kind, + } + ) + }; + } + } +} + +trait Generator { + fn generate_serialize(&self) -> syn::TraitItemFn; + fn generate_is_empty(&self) -> syn::TraitItemFn; +} + +// Generates an implementation of the trait which sorts the columns according +// to how they are defined in prepared statement metadata. +struct ColumnSortingGenerator<'a> { + ctx: &'a Context, +} + +impl<'a> Generator for ColumnSortingGenerator<'a> { + fn generate_serialize(&self) -> syn::TraitItemFn { + // Need to: + // - Check that all required columns are there and no more + // - Check that the column types match + let mut statements: Vec = Vec::new(); + + let crate_path = self.ctx.attributes.crate_path(); + + let rust_field_idents = self + .ctx + .fields + .iter() + .map(|f| f.ident.clone()) + .collect::>(); + let rust_field_names = self + .ctx + .fields + .iter() + .map(|f| f.column_name()) + .collect::>(); + let udt_field_names = rust_field_names.clone(); // For now, it's the same + let field_types = self.ctx.fields.iter().map(|f| &f.ty).collect::>(); + + // Declare a helper lambda for creating errors + statements.push(self.ctx.generate_mk_typck_err()); + statements.push(self.ctx.generate_mk_ser_err()); + + // Generate a "visited" flag for each field + let visited_flag_names = rust_field_names + .iter() + .map(|s| syn::Ident::new(&format!("visited_flag_{}", s), Span::call_site())) + .collect::>(); + statements.extend::>(parse_quote! { + #(let mut #visited_flag_names = false;)* + }); + + // Generate a variable that counts down visited fields. + let field_count = self.ctx.fields.len(); + statements.push(parse_quote! { + let mut remaining_count = #field_count; + }); + + // Generate a loop over the fields and a `match` block to match on + // the field name. + statements.push(parse_quote! { + for spec in ctx.columns() { + match ::std::string::String::as_str(&spec.name) { + #( + #udt_field_names => { + let sub_writer = #crate_path::RowWriter::make_cell_writer(writer); + match <#field_types as #crate_path::SerializeCql>::serialize(&self.#rust_field_idents, &spec.typ, sub_writer) { + ::std::result::Result::Ok(_proof) => {} + ::std::result::Result::Err(err) => { + return ::std::result::Result::Err(mk_ser_err( + #crate_path::BuiltinRowSerializationErrorKind::ColumnSerializationFailed { + name: <_ as ::std::clone::Clone>::clone(&spec.name), + err, + } + )); + } + } + if !#visited_flag_names { + #visited_flag_names = true; + remaining_count -= 1; + } + } + )* + _ => return ::std::result::Result::Err(mk_typck_err( + #crate_path::BuiltinRowTypeCheckErrorKind::NoColumnWithName { + name: <_ as ::std::clone::Clone>::clone(&&spec.name), + } + )), + } + } + }); + + // Finally, check that all fields were consumed. + // If there are some missing fields, return an error + statements.push(parse_quote! { + if remaining_count > 0 { + #( + if !#visited_flag_names { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::BuiltinRowTypeCheckErrorKind::ValueMissingForColumn { + name: <_ as ::std::string::ToString>::to_string(#rust_field_names), + } + )); + } + )* + ::std::unreachable!() + } + }); + + parse_quote! { + fn serialize<'b>( + &self, + ctx: &#crate_path::RowSerializationContext, + writer: &mut #crate_path::RowWriter<'b>, + ) -> ::std::result::Result<(), #crate_path::SerializationError> { + #(#statements)* + ::std::result::Result::Ok(()) + } + } + } + + fn generate_is_empty(&self) -> syn::TraitItemFn { + let is_empty = self.ctx.fields.is_empty(); + parse_quote! { + #[inline] + fn is_empty(&self) -> bool { + #is_empty + } + } + } +} + +// Generates an implementation of the trait which requires the columns +// to be placed in the same order as they are defined in the struct. +struct ColumnOrderedGenerator<'a> { + ctx: &'a Context, +} + +impl<'a> Generator for ColumnOrderedGenerator<'a> { + fn generate_serialize(&self) -> syn::TraitItemFn { + let mut statements: Vec = Vec::new(); + + let crate_path = self.ctx.attributes.crate_path(); + + // Declare a helper lambda for creating errors + statements.push(self.ctx.generate_mk_typck_err()); + statements.push(self.ctx.generate_mk_ser_err()); + + // Create an iterator over fields + statements.push(parse_quote! { + let mut column_iter = ctx.columns().iter(); + }); + + // Serialize each field + for field in self.ctx.fields.iter() { + let rust_field_ident = &field.ident; + let rust_field_name = field.column_name(); + let typ = &field.ty; + let name_check_expression: syn::Expr = if !self.ctx.attributes.skip_name_checks { + parse_quote! { spec.name == #rust_field_name } + } else { + parse_quote! { true } + }; + statements.push(parse_quote! { + match column_iter.next() { + Some(spec) => { + if #name_check_expression { + let cell_writer = #crate_path::RowWriter::make_cell_writer(writer); + match <#typ as #crate_path::SerializeCql>::serialize(&self.#rust_field_ident, &spec.typ, cell_writer) { + Ok(_proof) => {}, + Err(err) => { + return ::std::result::Result::Err(mk_ser_err( + #crate_path::BuiltinRowSerializationErrorKind::ColumnSerializationFailed { + name: <_ as ::std::clone::Clone>::clone(&spec.name), + err, + } + )); + } + } + } else { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::BuiltinRowTypeCheckErrorKind::ColumnNameMismatch { + rust_column_name: <_ as ::std::string::ToString>::to_string(#rust_field_name), + db_column_name: <_ as ::std::clone::Clone>::clone(&spec.name), + } + )); + } + } + None => { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::BuiltinRowTypeCheckErrorKind::ValueMissingForColumn { + name: <_ as ::std::string::ToString>::to_string(#rust_field_name), + } + )); + } + } + }); + } + + // Check whether there are some columns remaining + statements.push(parse_quote! { + if let Some(spec) = column_iter.next() { + return ::std::result::Result::Err(mk_typck_err( + #crate_path::BuiltinRowTypeCheckErrorKind::NoColumnWithName { + name: <_ as ::std::clone::Clone>::clone(&spec.name), + } + )); + } + }); + + parse_quote! { + fn serialize<'b>( + &self, + ctx: &#crate_path::RowSerializationContext, + writer: &mut #crate_path::RowWriter<'b>, + ) -> ::std::result::Result<(), #crate_path::SerializationError> { + #(#statements)* + ::std::result::Result::Ok(()) + } + } + } + + fn generate_is_empty(&self) -> syn::TraitItemFn { + let is_empty = self.ctx.fields.is_empty(); + parse_quote! { + #[inline] + fn is_empty(&self) -> bool { + #is_empty + } + } + } +} diff --git a/scylla-macros/src/value_list.rs b/scylla-macros/src/value_list.rs index bf6fc38e9d..bc9de23c8a 100644 --- a/scylla-macros/src/value_list.rs +++ b/scylla-macros/src/value_list.rs @@ -17,7 +17,7 @@ pub fn value_list_derive(tokens_input: TokenStream) -> Result #path::SerializedResult { - let mut result = #path::SerializedValues::with_capacity(#values_len); + let mut result = #path::LegacySerializedValues::with_capacity(#values_len); #( result.add_value(&self.#field_name)?; )* diff --git a/scylla-proxy/Cargo.toml b/scylla-proxy/Cargo.toml index de14d461ff..9d630b1bdd 100644 --- a/scylla-proxy/Cargo.toml +++ b/scylla-proxy/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT OR Apache-2.0" defaults = [] [dependencies] -scylla-cql = { version = "0.0.8", path = "../scylla-cql" } +scylla-cql = { version = "0.0.11", path = "../scylla-cql" } byteorder = "1.3.4" bytes = "1.2.0" futures = "0.3.6" @@ -31,3 +31,5 @@ rand = "0.8.5" assert_matches = "1.5.0" ntest = "0.9.0" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } +tokio = { version = "1.12", features = ["signal"] } + diff --git a/scylla-proxy/src/actions.rs b/scylla-proxy/src/actions.rs index d571afd48d..216c0165fc 100644 --- a/scylla-proxy/src/actions.rs +++ b/scylla-proxy/src/actions.rs @@ -48,11 +48,15 @@ pub enum Condition { /// True for predefined number of evaluations, then always false. TrueForLimitedTimes(usize), + + // True if any REGISTER was sent on this connection. Useful to filter out control connection messages. + ConnectionRegisteredAnyEvent, } /// The context in which [`Conditions`](Condition) are evaluated. pub(crate) struct EvaluationContext { pub(crate) connection_seq_no: usize, + pub(crate) connection_has_events: bool, pub(crate) opcode: FrameOpcode, pub(crate) frame_body: Bytes, } @@ -110,7 +114,9 @@ impl Condition { *times -= 1; } val - } + }, + + Condition::ConnectionRegisteredAnyEvent => ctx.connection_has_events } } @@ -725,6 +731,7 @@ fn condition_case_insensitive_matching() { connection_seq_no: 42, opcode: FrameOpcode::Request(RequestOpcode::Options), frame_body: Bytes::from_static(b"\0\0x{0x223}Cassandra'sINEFFICIENCY\x12\x31"), + connection_has_events: false, }; assert!(condition_matching.eval(&ctx)); diff --git a/scylla-proxy/src/proxy.rs b/scylla-proxy/src/proxy.rs index ae3eea3c4a..11acf465d0 100644 --- a/scylla-proxy/src/proxy.rs +++ b/scylla-proxy/src/proxy.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::{TcpListener, TcpSocket, TcpStream}; @@ -431,8 +431,8 @@ impl RunningProxy { } } - /// Attempts to fetch the first error that has occured in proxy since last check. - /// If no errors occured, returns Ok(()). + /// Attempts to fetch the first error that has occurred in proxy since last check. + /// If no errors occurred, returns Ok(()). pub fn sanity_check(&mut self) -> Result<(), ProxyError> { match self.error_sink.try_recv() { Ok(err) => Err(err), @@ -444,13 +444,13 @@ impl RunningProxy { } } - /// Waits until an error occurs in proxy. If proxy finishes with no errors occured, returns Err(()). + /// Waits until an error occurs in proxy. If proxy finishes with no errors occurred, returns Err(()). pub async fn wait_for_error(&mut self) -> Option { self.error_sink.recv().await } /// Requests termination of all proxy workers and awaits its completion. - /// Returns the first error that occured in proxy. + /// Returns the first error that occurred in proxy. pub async fn finish(mut self) -> Result<(), ProxyError> { self.terminate_signaler.send(()).map_err(|err| { ProxyError::AwaitFinishFailure(format!( @@ -631,6 +631,7 @@ impl Doorkeeper { let (tx_response, rx_response) = mpsc::unbounded_channel::(); let (tx_cluster, rx_cluster) = mpsc::unbounded_channel::(); let (tx_driver, rx_driver) = mpsc::unbounded_channel::(); + let event_register_flag = Arc::new(AtomicBool::new(false)); tokio::task::spawn(new_worker().receiver_from_driver(driver_read, tx_request)); tokio::task::spawn(new_worker().sender_to_driver( @@ -646,6 +647,7 @@ impl Doorkeeper { connection_no, self.node.request_rules().clone(), connection_close_tx.clone(), + event_register_flag.clone(), )); if let InternalNode::Real { ref response_rules, .. @@ -666,6 +668,7 @@ impl Doorkeeper { connection_no, response_rules.clone(), connection_close_tx.clone(), + event_register_flag.clone(), )); } debug!( @@ -765,7 +768,7 @@ impl Doorkeeper { // If ShardAwareness is aware (QueryNode or FixedNum variants) and the // proxy succeeded to know the shards count (in FixedNum we get it for - // free, in QueryNode the initial Options query succceeded and Supported + // free, in QueryNode the initial Options query succeeded and Supported // contained SCYLLA_SHARDS_NUM), then upon opening each connection to the // node, the proxy issues another Options requests and acknowledges the // shard it got connected to. @@ -1052,6 +1055,7 @@ impl ProxyWorker { .await; } + #[allow(clippy::too_many_arguments)] async fn request_processor( self, mut requests_rx: mpsc::UnboundedReceiver, @@ -1060,16 +1064,21 @@ impl ProxyWorker { connection_no: usize, request_rules: Arc>>, connection_close_signaler: ConnectionCloseSignaler, + event_registered_flag: Arc, ) { let shard = self.shard; self.run_until_interrupted("request_processor", |driver_addr, _, real_addr| async move { 'mainloop: loop { match requests_rx.recv().await { Some(request) => { + if request.opcode == RequestOpcode::Register { + event_registered_flag.store(true, Ordering::Relaxed); + } let ctx = EvaluationContext { connection_seq_no: connection_no, opcode: FrameOpcode::Request(request.opcode), frame_body: request.body.clone(), + connection_has_events: event_registered_flag.load(Ordering::Relaxed), }; let mut guard = request_rules.lock().unwrap(); '_ruleloop: for (i, request_rule) in guard.iter_mut().enumerate() { @@ -1155,6 +1164,7 @@ impl ProxyWorker { .await; } + #[allow(clippy::too_many_arguments)] async fn response_processor( self, mut responses_rx: mpsc::UnboundedReceiver, @@ -1163,6 +1173,7 @@ impl ProxyWorker { connection_no: usize, response_rules: Arc>>, connection_close_signaler: ConnectionCloseSignaler, + event_registered_flag: Arc, ) { let shard = self.shard; self.run_until_interrupted("request_processor", |driver_addr, _, real_addr| async move { @@ -1173,6 +1184,7 @@ impl ProxyWorker { connection_seq_no: connection_no, opcode: FrameOpcode::Response(response.opcode), frame_body: response.body.clone(), + connection_has_events: event_registered_flag.load(Ordering::Relaxed), }; let mut guard = response_rules.lock().unwrap(); '_ruleloop: for (i, response_rule) in guard.iter_mut().enumerate() { @@ -1281,7 +1293,7 @@ pub fn get_exclusive_local_address() -> IpAddr { #[cfg(test)] mod tests { use super::*; - use crate::frame::read_request_frame; + use crate::frame::{read_frame, read_request_frame, FrameType}; use crate::{Condition, Reaction as _, RequestReaction, ResponseOpcode, ResponseReaction}; use assert_matches::assert_matches; use bytes::{BufMut, BytesMut}; @@ -2339,4 +2351,124 @@ mod tests { running_proxy.finish().await.unwrap(); } } + + #[tokio::test] + #[ntest::timeout(1000)] + async fn proxy_ignores_control_connection_messages() { + let node1_real_addr = next_local_address_with_port(9876); + let node1_proxy_addr = next_local_address_with_port(9876); + + let (request_feedback_tx, mut request_feedback_rx) = mpsc::unbounded_channel(); + let (response_feedback_tx, mut response_feedback_rx) = mpsc::unbounded_channel(); + let proxy = Proxy::new([Node::new( + node1_real_addr, + node1_proxy_addr, + ShardAwareness::Unaware, + Some(vec![RequestRule( + Condition::not(Condition::ConnectionRegisteredAnyEvent), + RequestReaction::noop().with_feedback_when_performed(request_feedback_tx), + )]), + Some(vec![ResponseRule( + Condition::not(Condition::ConnectionRegisteredAnyEvent), + ResponseReaction::noop().with_feedback_when_performed(response_feedback_tx), + )]), + )]); + let running_proxy = proxy.run().await.unwrap(); + + let mock_node_listener = TcpListener::bind(node1_real_addr).await.unwrap(); + + let (mut client_socket, mut server_socket) = join( + async { TcpStream::connect(node1_proxy_addr).await.unwrap() }, + async { mock_node_listener.accept().await.unwrap().0 }, + ) + .await; + + async fn perform_reqest_response<'a>( + req_opcode: RequestOpcode, + resp_opcode: ResponseOpcode, + client_socket_ref: &'a mut TcpStream, + server_socket_ref: &'a mut TcpStream, + body_base: &'a str, + ) { + let params = FrameParams { + flags: 0, + version: 0x04, + stream: 0, + }; + + write_frame( + params, + FrameOpcode::Request(req_opcode), + &(body_base.to_string() + "|request|").into(), + client_socket_ref, + ) + .await + .unwrap(); + + let received_request = read_frame(server_socket_ref, FrameType::Request) + .await + .unwrap(); + assert_eq!(received_request.1, FrameOpcode::Request(req_opcode)); + + write_frame( + params.for_response(), + FrameOpcode::Response(resp_opcode), + &(body_base.to_string() + "|response|").into(), + server_socket_ref, + ) + .await + .unwrap(); + + let received_response = read_frame(client_socket_ref, FrameType::Response) + .await + .unwrap(); + assert_eq!(received_response.1, FrameOpcode::Response(resp_opcode)); + } + + // Messages before REGISTER should be fed back to channels + for i in 0..5 { + perform_reqest_response( + RequestOpcode::Query, + ResponseOpcode::Result, + &mut client_socket, + &mut server_socket, + &format!("message_before_{i}"), + ) + .await + } + + perform_reqest_response( + RequestOpcode::Register, + ResponseOpcode::Result, + &mut client_socket, + &mut server_socket, + "message_register", + ) + .await; + + // Messages after REGISTER should be passed through without feedback + for i in 0..5 { + perform_reqest_response( + RequestOpcode::Query, + ResponseOpcode::Result, + &mut client_socket, + &mut server_socket, + &format!("message_after_{i}"), + ) + .await + } + + running_proxy.finish().await.unwrap(); + + for _ in 0..5 { + let (feedback_request, _shard) = request_feedback_rx.recv().await.unwrap(); + assert_eq!(feedback_request.opcode, RequestOpcode::Query); + let (feedback_response, _shard) = response_feedback_rx.recv().await.unwrap(); + assert_eq!(feedback_response.opcode, ResponseOpcode::Result); + } + + // Response to REGISTER and further requests / responses should be ignored + let _ = request_feedback_rx.try_recv().unwrap_err(); + let _ = response_feedback_rx.try_recv().unwrap_err(); + } } diff --git a/scylla/Cargo.toml b/scylla/Cargo.toml index 2506968fa1..adbb51f04a 100644 --- a/scylla/Cargo.toml +++ b/scylla/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scylla" -version = "0.9.0" +version = "0.11.1" edition = "2021" description = "Async CQL driver for Rust, optimized for Scylla, fully compatible with Apache Cassandra™" repository = "https://github.com/scylladb/scylla-rust-driver" @@ -18,10 +18,13 @@ default = [] ssl = ["dep:tokio-openssl", "dep:openssl"] cloud = ["ssl", "scylla-cql/serde", "dep:serde_yaml", "dep:serde", "dep:url", "dep:base64"] secret = ["scylla-cql/secret"] +chrono = ["scylla-cql/chrono"] +time = ["scylla-cql/time"] +full-serialization = ["chrono", "time", "secret"] [dependencies] -scylla-macros = { version = "0.2.0", path = "../scylla-macros" } -scylla-cql = { version = "0.0.8", path = "../scylla-cql" } +scylla-macros = { version = "0.3.0", path = "../scylla-macros" } +scylla-cql = { version = "0.0.11", path = "../scylla-cql" } byteorder = "1.3.4" bytes = "1.0.1" futures = "0.3.6" @@ -61,6 +64,7 @@ tokio = { version = "1.27", features = ["test-util"] } tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } assert_matches = "1.5.0" rand_chacha = "0.3.1" +time = "0.3" [[bench]] name = "benchmark" diff --git a/scylla/benches/benchmark.rs b/scylla/benches/benchmark.rs index c6058923b6..20440ea0b7 100644 --- a/scylla/benches/benchmark.rs +++ b/scylla/benches/benchmark.rs @@ -3,16 +3,16 @@ use criterion::{criterion_group, criterion_main, Criterion}; use bytes::BytesMut; use scylla::{ frame::types, - frame::value::ValueList, transport::partitioner::{calculate_token_for_partition_key, Murmur3Partitioner}, }; +use scylla_cql::{frame::response::result::ColumnType, types::serialize::row::SerializedValues}; fn types_benchmark(c: &mut Criterion) { let mut buf = BytesMut::with_capacity(64); c.bench_function("short", |b| { b.iter(|| { buf.clear(); - types::write_short(-1, &mut buf); + types::write_short(u16::MAX, &mut buf); types::read_short(&mut &buf[..]).unwrap(); }) }); @@ -40,23 +40,49 @@ fn types_benchmark(c: &mut Criterion) { } fn calculate_token_bench(c: &mut Criterion) { - let simple_pk = ("I'm prepared!!!",); - let serialized_simple_pk = simple_pk.serialized().unwrap().into_owned(); - let simple_pk_long_column = ( - 17_i32, - 16_i32, - String::from_iter(std::iter::repeat('.').take(2000)), - ); - let serialized_simple_pk_long_column = simple_pk_long_column.serialized().unwrap().into_owned(); + let mut serialized_simple_pk = SerializedValues::new(); + serialized_simple_pk + .add_value(&"I'm prepared!!!", &ColumnType::Text) + .unwrap(); - let complex_pk = (17_i32, 16_i32, "I'm prepared!!!"); - let serialized_complex_pk = complex_pk.serialized().unwrap().into_owned(); - let complex_pk_long_column = ( - 17_i32, - 16_i32, - String::from_iter(std::iter::repeat('.').take(2000)), - ); - let serialized_values_long_column = complex_pk_long_column.serialized().unwrap().into_owned(); + let mut serialized_simple_pk_long_column = SerializedValues::new(); + serialized_simple_pk_long_column + .add_value(&17_i32, &ColumnType::Int) + .unwrap(); + serialized_simple_pk_long_column + .add_value(&16_i32, &ColumnType::Int) + .unwrap(); + serialized_simple_pk_long_column + .add_value( + &String::from_iter(std::iter::repeat('.').take(2000)), + &ColumnType::Text, + ) + .unwrap(); + + let mut serialized_complex_pk = SerializedValues::new(); + serialized_complex_pk + .add_value(&17_i32, &ColumnType::Int) + .unwrap(); + serialized_complex_pk + .add_value(&16_i32, &ColumnType::Int) + .unwrap(); + serialized_complex_pk + .add_value(&"I'm prepared!!!", &ColumnType::Text) + .unwrap(); + + let mut serialized_values_long_column = SerializedValues::new(); + serialized_values_long_column + .add_value(&17_i32, &ColumnType::Int) + .unwrap(); + serialized_values_long_column + .add_value(&16_i32, &ColumnType::Int) + .unwrap(); + serialized_values_long_column + .add_value( + &String::from_iter(std::iter::repeat('.').take(2000)), + &ColumnType::Text, + ) + .unwrap(); c.bench_function("calculate_token_from_partition_key simple pk", |b| { b.iter(|| calculate_token_for_partition_key(&serialized_simple_pk, &Murmur3Partitioner)) diff --git a/scylla/src/cloud/config.rs b/scylla/src/cloud/config.rs index 74a088726a..9984cba34e 100644 --- a/scylla/src/cloud/config.rs +++ b/scylla/src/cloud/config.rs @@ -184,13 +184,13 @@ mod deserialize { // +optional apiVersion: Option, - // Datacenters is a map of referencable names to datacenter configs. + // Datacenters is a map of referenceable names to datacenter configs. datacenters: HashMap, - // AuthInfos is a map of referencable names to authentication configs. + // AuthInfos is a map of referenceable names to authentication configs. authInfos: HashMap, - // Contexts is a map of referencable names to context configs. + // Contexts is a map of referenceable names to context configs. contexts: HashMap, // CurrentContext is the name of the context that you would like to use by default. diff --git a/scylla/src/history.rs b/scylla/src/history.rs index 34c2c19244..9109601c23 100644 --- a/scylla/src/history.rs +++ b/scylla/src/history.rs @@ -56,7 +56,7 @@ pub trait HistoryListener: Debug + Send + Sync { node_addr: SocketAddr, ) -> AttemptId; - /// Log that an attempt succeded. + /// Log that an attempt succeeded. fn log_attempt_success(&self, attempt_id: AttemptId); /// Log that an attempt ended with an error. The error and decision whether to retry the attempt are also included in the log. diff --git a/scylla/src/lib.rs b/scylla/src/lib.rs index 27a3c57471..381fad34d3 100644 --- a/scylla/src/lib.rs +++ b/scylla/src/lib.rs @@ -98,8 +98,12 @@ pub mod _macro_internal { pub use scylla_cql::_macro_internal::*; } +pub mod macros; +#[doc(inline)] +pub use macros::*; + pub use scylla_cql::frame; -pub use scylla_cql::macros::{self, *}; +pub use scylla_cql::types::serialize; pub mod authentication; #[cfg(feature = "cloud")] diff --git a/scylla/src/macros.rs b/scylla/src/macros.rs new file mode 100644 index 0000000000..67a7461bf7 --- /dev/null +++ b/scylla/src/macros.rs @@ -0,0 +1,246 @@ +/// #[derive(FromRow)] derives FromRow for struct +/// +/// Works only on simple structs without generics etc +/// +/// --- +/// +pub use scylla_cql::macros::FromRow; + +/// #[derive(FromUserType)] allows to parse struct as a User Defined Type +/// +/// Works only on simple structs without generics etc +/// +/// --- +/// +pub use scylla_cql::macros::FromUserType; + +/// #[derive(IntoUserType)] allows to pass struct a User Defined Type Value in queries +/// +/// Works only on simple structs without generics etc +/// +/// --- +/// +pub use scylla_cql::macros::IntoUserType; + +/// Derive macro for the [`SerializeCql`](crate::serialize::value::SerializeCql) trait +/// which serializes given Rust structure as a User Defined Type (UDT). +/// +/// At the moment, only structs with named fields are supported. +/// +/// Serialization will fail if there are some fields in the Rust struct that don't match +/// to any of the UDT fields. +/// +/// If there are fields in UDT that are not present in Rust definition: +/// - serialization will succeed in "match_by_name" flavor (default). Missing +/// fields in the middle of UDT will be sent as NULLs, missing fields at the end will not be sent +/// at all. +/// - serialization will succed if suffix of UDT fields is missing. If there are missing fields in the +/// middle it will fail. Note that if "skip_name_checks" is enabled, and the types happen to match, +/// it is possible for serialization to succeed with unexpected result. +/// This behavior is the default to support ALTERing UDTs by adding new fields. +/// You can require exact match of fields using `force_exact_match` attribute. +/// +/// In case of failure, either [`BuiltinTypeCheckError`](crate::serialize::value::BuiltinTypeCheckError) +/// or [`BuiltinSerializationError`](crate::serialize::value::BuiltinSerializationError) +/// will be returned. +/// +/// # Example +/// +/// A UDT defined like this: +/// +/// ```notrust +/// CREATE TYPE ks.my_udt (a int, b text, c blob); +/// ``` +/// +/// ...can be serialized using the following struct: +/// +/// ```rust +/// # use scylla::SerializeCql; +/// #[derive(SerializeCql)] +/// struct MyUdt { +/// a: i32, +/// b: Option, +/// // No "c" field - it is not mandatory by default for all fields to be present +/// } +/// ``` +/// +/// # Struct attributes +/// +/// `#[scylla(flavor = "flavor_name")]` +/// +/// Allows to choose one of the possible "flavors", i.e. the way how the +/// generated code will approach serialization. Possible flavors are: +/// +/// - `"match_by_name"` (default) - the generated implementation _does not +/// require_ the fields in the Rust struct to be in the same order as the +/// fields in the UDT. During serialization, the implementation will take +/// care to serialize the fields in the order which the database expects. +/// - `"enforce_order"` - the generated implementation _requires_ the fields +/// in the Rust struct to be in the same order as the fields in the UDT. +/// If the order is incorrect, type checking/serialization will fail. +/// This is a less robust flavor than `"match_by_name"`, but should be +/// slightly more performant as it doesn't need to perform lookups by name. +/// +/// `#[scylla(crate = crate_name)]` +/// +/// By default, the code generated by the derive macro will refer to the items +/// defined by the driver (types, traits, etc.) via the `::scylla` path. +/// For example, it will refer to the [`SerializeCql`](crate::serialize::value::SerializeCql) trait +/// using the following path: +/// +/// ```rust,ignore +/// use ::scylla::_macro_internal::SerializeCql; +/// ``` +/// +/// Most users will simply add `scylla` to their dependencies, then use +/// the derive macro and the path above will work. However, there are some +/// niche cases where this path will _not_ work: +/// +/// - The `scylla` crate is imported under a different name, +/// - The `scylla` crate is _not imported at all_ - the macro actually +/// is defined in the `scylla-macros` crate and the generated code depends +/// on items defined in `scylla-cql`. +/// +/// It's not possible to automatically resolve those issues in the procedural +/// macro itself, so in those cases the user must provide an alternative path +/// to either the `scylla` or `scylla-cql` crate. +/// +/// `#[scylla(skip_name_checks)]` +/// +/// _Specific only to the `enforce_order` flavor._ +/// +/// Skips checking Rust field names against names of the UDT fields. With this +/// annotation, the generated implementation will allow mismatch between Rust +/// struct field names and UDT field names, i.e. it's OK if i-th field has a +/// different name in Rust and in the UDT. Fields are still being type-checked. +/// +/// `#[scylla(force_exact_match)]` +/// +/// Forces Rust struct to have all the fields present in UDT, otherwise +/// serialization fails. +/// +/// # Field attributes +/// +/// `#[scylla(rename = "name_in_the_udt")]` +/// +/// Serializes the field to the UDT struct field with given name instead of +/// its Rust name. +/// +/// `#[scylla(skip)]` +/// +/// Don't use the field during serialization. +/// +/// --- +/// +pub use scylla_cql::macros::SerializeCql; + +/// Derive macro for the [`SerializeRow`](crate::serialize::row::SerializeRow) trait +/// which serializes given Rust structure into bind markers for a CQL statement. +/// +/// At the moment, only structs with named fields are supported. +/// +/// Serialization will fail if there are some bind markers/columns in the statement +/// that don't match to any of the Rust struct fields, _or vice versa_. +/// +/// In case of failure, either [`BuiltinTypeCheckError`](crate::serialize::row::BuiltinTypeCheckError) +/// or [`BuiltinSerializationError`](crate::serialize::row::BuiltinSerializationError) +/// will be returned. +/// +/// # Example +/// +/// A UDT defined like this: +/// Given a table and a query: +/// +/// ```notrust +/// CREATE TABLE ks.my_t (a int PRIMARY KEY, b text, c blob); +/// INSERT INTO ks.my_t (a, b, c) VALUES (?, ?, ?); +/// ``` +/// +/// ...the values for the query can be serialized using the following struct: +/// +/// ```rust +/// # use scylla::SerializeRow; +/// #[derive(SerializeRow)] +/// struct MyValues { +/// a: i32, +/// b: Option, +/// c: Vec, +/// } +/// ``` +/// +/// # Struct attributes +/// +/// `#[scylla(flavor = "flavor_name")]` +/// +/// Allows to choose one of the possible "flavors", i.e. the way how the +/// generated code will approach serialization. Possible flavors are: +/// +/// - `"match_by_name"` (default) - the generated implementation _does not +/// require_ the fields in the Rust struct to be in the same order as the +/// columns/bind markers. During serialization, the implementation will take +/// care to serialize the fields in the order which the database expects. +/// - `"enforce_order"` - the generated implementation _requires_ the fields +/// in the Rust struct to be in the same order as the columns/bind markers. +/// If the order is incorrect, type checking/serialization will fail. +/// This is a less robust flavor than `"match_by_name"`, but should be +/// slightly more performant as it doesn't need to perform lookups by name. +/// +/// `#[scylla(crate = crate_name)]` +/// +/// By default, the code generated by the derive macro will refer to the items +/// defined by the driver (types, traits, etc.) via the `::scylla` path. +/// For example, it will refer to the [`SerializeRow`](crate::serialize::row::SerializeRow) trait +/// using the following path: +/// +/// ```rust,ignore +/// use ::scylla::_macro_internal::SerializeRow; +/// ``` +/// +/// Most users will simply add `scylla` to their dependencies, then use +/// the derive macro and the path above will work. However, there are some +/// niche cases where this path will _not_ work: +/// +/// - The `scylla` crate is imported under a different name, +/// - The `scylla` crate is _not imported at all_ - the macro actually +/// is defined in the `scylla-macros` crate and the generated code depends +/// on items defined in `scylla-cql`. +/// +/// It's not possible to automatically resolve those issues in the procedural +/// macro itself, so in those cases the user must provide an alternative path +/// to either the `scylla` or `scylla-cql` crate. +/// +/// `#[scylla(skip_name_checks)] +/// +/// _Specific only to the `enforce_order` flavor._ +/// +/// Skips checking Rust field names against names of the columns / bind markers. +/// With this annotation, the generated implementation will allow mismatch +/// between Rust struct field names and the column / bind markers, i.e. it's +/// OK if i-th Rust struct field has a different name than the column / bind +/// marker. The values are still being type-checked. +/// +/// # Field attributes +/// +/// `#[scylla(rename = "column_or_bind_marker_name")]` +/// +/// Serializes the field to the column / bind marker with given name instead of +/// its Rust name. +/// +/// `#[scylla(skip)]` +/// +/// Don't use the field during serialization. +/// +/// --- +/// +pub use scylla_cql::macros::SerializeRow; + +/// #[derive(ValueList)] allows to pass struct as a list of values for a query +/// +/// --- +/// +pub use scylla_cql::macros::ValueList; + +pub use scylla_cql::macros::impl_from_cql_value_from_method; + +// Reexports for derive(IntoUserType) +pub use bytes::{BufMut, Bytes, BytesMut}; diff --git a/scylla/src/routing.rs b/scylla/src/routing.rs index d6fff76466..e54dfcaec9 100644 --- a/scylla/src/routing.rs +++ b/scylla/src/routing.rs @@ -102,7 +102,7 @@ impl Sharder { pub enum ShardingError { #[error("ShardInfo parameters missing")] MissingShardInfoParameter, - #[error("ShardInfo parameters missing after unwraping")] + #[error("ShardInfo parameters missing after unwrapping")] MissingUnwrapedShardInfoParameter, #[error("ShardInfo contains an invalid number of shards (0)")] ZeroShards, diff --git a/scylla/src/statement/batch.rs b/scylla/src/statement/batch.rs index 6805dcb275..efe95031e2 100644 --- a/scylla/src/statement/batch.rs +++ b/scylla/src/statement/batch.rs @@ -197,3 +197,141 @@ impl<'a: 'b, 'b> From<&'a BatchStatement> } } } + +pub(crate) mod batch_values { + use scylla_cql::errors::QueryError; + use scylla_cql::types::serialize::batch::BatchValues; + use scylla_cql::types::serialize::batch::BatchValuesIterator; + use scylla_cql::types::serialize::row::RowSerializationContext; + use scylla_cql::types::serialize::row::SerializedValues; + use scylla_cql::types::serialize::{RowWriter, SerializationError}; + + use crate::routing::Token; + + use super::BatchStatement; + + // Takes an optional reference to the first statement in the batch and + // the batch values, and tries to compute the token for the statement. + // Returns the (optional) token and batch values. If the function needed + // to serialize values for the first statement, the returned batch values + // will cache the results of the serialization. + // + // NOTE: Batch values returned by this function might not type check + // the first statement when it is serialized! However, if they don't, + // then the first row was already checked by the function. It is assumed + // that `statement` holds the first prepared statement of the batch (if + // there is one), and that it will be used later to serialize the values. + pub(crate) fn peek_first_token<'bv>( + values: impl BatchValues + 'bv, + statement: Option<&BatchStatement>, + ) -> Result<(Option, impl BatchValues + 'bv), QueryError> { + let mut values_iter = values.batch_values_iter(); + let (token, first_values) = match statement { + Some(BatchStatement::PreparedStatement(ps)) => { + let ctx = RowSerializationContext::from_prepared(ps.get_prepared_metadata()); + let (first_values, did_write) = SerializedValues::from_closure(|writer| { + values_iter + .serialize_next(&ctx, writer) + .transpose() + .map(|o| o.is_some()) + })?; + if did_write { + let token = ps.calculate_token_untyped(&first_values)?; + (token, Some(first_values)) + } else { + (None, None) + } + } + _ => (None, None), + }; + + // Need to do it explicitly, otherwise the next line will complain + // that `values_iter` still borrows `values`. + std::mem::drop(values_iter); + + // Reuse the already serialized first value via `BatchValuesFirstSerialized`. + let values = BatchValuesFirstSerialized::new(values, first_values); + + Ok((token, values)) + } + + struct BatchValuesFirstSerialized { + // Contains the first value of BV in a serialized form. + // The first value in the iterator returned from `rest` should be skipped! + first: Option, + rest: BV, + } + + impl BatchValuesFirstSerialized { + fn new(rest: BV, first: Option) -> Self { + Self { first, rest } + } + } + + impl BatchValues for BatchValuesFirstSerialized + where + BV: BatchValues, + { + type BatchValuesIter<'r> = BatchValuesFirstSerializedIterator<'r, BV::BatchValuesIter<'r>> + where + Self: 'r; + + fn batch_values_iter(&self) -> Self::BatchValuesIter<'_> { + BatchValuesFirstSerializedIterator { + first: self.first.as_ref(), + rest: self.rest.batch_values_iter(), + } + } + } + + struct BatchValuesFirstSerializedIterator<'f, BVI> { + first: Option<&'f SerializedValues>, + rest: BVI, + } + + impl<'f, BVI> BatchValuesIterator<'f> for BatchValuesFirstSerializedIterator<'f, BVI> + where + BVI: BatchValuesIterator<'f>, + { + #[inline] + fn serialize_next( + &mut self, + ctx: &RowSerializationContext<'_>, + writer: &mut RowWriter, + ) -> Option> { + match self.first.take() { + Some(sr) => { + writer.append_serialize_row(sr); + self.rest.skip_next(); + Some(Ok(())) + } + None => self.rest.serialize_next(ctx, writer), + } + } + + #[inline] + fn is_empty_next(&mut self) -> Option { + match self.first.take() { + Some(s) => { + self.rest.skip_next(); + Some(s.is_empty()) + } + None => self.rest.is_empty_next(), + } + } + + #[inline] + fn skip_next(&mut self) -> Option<()> { + self.first = None; + self.rest.skip_next() + } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.rest.count() + } + } +} diff --git a/scylla/src/statement/prepared_statement.rs b/scylla/src/statement/prepared_statement.rs index 22f34e60a2..a3cd155e7c 100644 --- a/scylla/src/statement/prepared_statement.rs +++ b/scylla/src/statement/prepared_statement.rs @@ -1,5 +1,8 @@ use bytes::{Bytes, BytesMut}; use scylla_cql::errors::{BadQuery, QueryError}; +use scylla_cql::frame::types::RawValue; +use scylla_cql::types::serialize::row::{RowSerializationContext, SerializeRow, SerializedValues}; +use scylla_cql::types::serialize::SerializationError; use smallvec::{smallvec, SmallVec}; use std::convert::TryInto; use std::sync::Arc; @@ -12,7 +15,6 @@ use scylla_cql::frame::response::result::ColumnSpec; use super::StatementConfig; use crate::frame::response::result::PreparedMetadata; use crate::frame::types::{Consistency, SerialConsistency}; -use crate::frame::value::SerializedValues; use crate::history::HistoryListener; use crate::retry_policy::RetryPolicy; use crate::routing::Token; @@ -133,9 +135,10 @@ impl PreparedStatement { /// [Self::calculate_token()]. pub fn compute_partition_key( &self, - bound_values: &SerializedValues, + bound_values: &impl SerializeRow, ) -> Result { - let partition_key = self.extract_partition_key(bound_values)?; + let serialized = self.serialize_values(bound_values)?; + let partition_key = self.extract_partition_key(&serialized)?; let mut buf = BytesMut::new(); let mut writer = |chunk: &[u8]| buf.extend_from_slice(chunk); @@ -144,7 +147,7 @@ impl PreparedStatement { Ok(buf.freeze()) } - /// Determines which values consistute the partition key and puts them in order. + /// Determines which values constitute the partition key and puts them in order. /// /// This is a preparation step necessary for calculating token based on a prepared statement. pub(crate) fn extract_partition_key<'ps>( @@ -181,18 +184,24 @@ impl PreparedStatement { Ok(Some((partition_key, token))) } - /// Calculates the token for given prepared statement and serialized values. + /// Calculates the token for given prepared statement and values. /// /// Returns the token that would be computed for executing the provided /// prepared statement with the provided values. // As this function creates a `PartitionKey`, it is intended rather for external usage (by users). // For internal purposes, `PartitionKey::calculate_token()` is preferred, as `PartitionKey` // is either way used internally, among others for display in traces. - pub fn calculate_token( + pub fn calculate_token(&self, values: &impl SerializeRow) -> Result, QueryError> { + self.calculate_token_untyped(&self.serialize_values(values)?) + } + + // A version of calculate_token which skips serialization and uses SerializedValues directly. + // Not type-safe, so not exposed to users. + pub(crate) fn calculate_token_untyped( &self, - serialized_values: &SerializedValues, + values: &SerializedValues, ) -> Result, QueryError> { - self.extract_partition_key_and_calculate_token(&self.partitioner_name, serialized_values) + self.extract_partition_key_and_calculate_token(&self.partitioner_name, values) .map(|opt| opt.map(|(_pk, token)| token)) } @@ -334,6 +343,14 @@ impl PreparedStatement { pub fn get_execution_profile_handle(&self) -> Option<&ExecutionProfileHandle> { self.config.execution_profile_handle.as_ref() } + + pub(crate) fn serialize_values( + &self, + values: &impl SerializeRow, + ) -> Result { + let ctx = RowSerializationContext::from_prepared(self.get_prepared_metadata()); + SerializedValues::from_serializable(&ctx, values) + } } #[derive(Clone, Debug, Error, PartialEq, Eq, PartialOrd, Ord)] @@ -348,12 +365,14 @@ pub enum TokenCalculationError { ValueTooLong(usize), } -#[derive(Clone, Debug, Error, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Error)] pub enum PartitionKeyError { #[error(transparent)] PartitionKeyExtraction(PartitionKeyExtractionError), #[error(transparent)] TokenCalculation(TokenCalculationError), + #[error(transparent)] + Serialization(SerializationError), } impl From for PartitionKeyError { @@ -368,6 +387,12 @@ impl From for PartitionKeyError { } } +impl From for PartitionKeyError { + fn from(err: SerializationError) -> Self { + Self::Serialization(err) + } +} + pub(crate) type PartitionKeyValue<'ps> = (&'ps [u8], &'ps ColumnSpec); pub(crate) struct PartitionKey<'ps> { @@ -396,10 +421,13 @@ impl<'ps> PartitionKey<'ps> { let next_val = values_iter .nth((pk_index.index - values_iter_offset) as usize) .ok_or_else(|| { - PartitionKeyExtractionError::NoPkIndexValue(pk_index.index, bound_values.len()) + PartitionKeyExtractionError::NoPkIndexValue( + pk_index.index, + bound_values.element_count(), + ) })?; // Add it in sequence order to pk_values - if let Some(v) = next_val { + if let RawValue::Value(v) = next_val { let spec = &prepared_metadata.col_specs[pk_index.index as usize]; pk_values[pk_index.sequence as usize] = Some((v, spec)); } @@ -455,11 +483,11 @@ impl<'ps> PartitionKey<'ps> { #[cfg(test)] mod tests { - use scylla_cql::frame::{ - response::result::{ + use scylla_cql::{ + frame::response::result::{ ColumnSpec, ColumnType, PartitionKeyIndex, PreparedMetadata, TableSpec, }, - value::SerializedValues, + types::serialize::row::SerializedValues, }; use crate::prepared_statement::PartitionKey; @@ -511,11 +539,13 @@ mod tests { [4, 0, 3], ); let mut values = SerializedValues::new(); - values.add_value(&67i8).unwrap(); - values.add_value(&42i16).unwrap(); - values.add_value(&23i32).unwrap(); - values.add_value(&89i64).unwrap(); - values.add_value(&[1u8, 2, 3, 4, 5]).unwrap(); + values.add_value(&67i8, &ColumnType::TinyInt).unwrap(); + values.add_value(&42i16, &ColumnType::SmallInt).unwrap(); + values.add_value(&23i32, &ColumnType::Int).unwrap(); + values.add_value(&89i64, &ColumnType::BigInt).unwrap(); + values + .add_value(&[1u8, 2, 3, 4, 5], &ColumnType::Blob) + .unwrap(); let pk = PartitionKey::new(&meta, &values).unwrap(); let pk_cols = Vec::from_iter(pk.iter()); diff --git a/scylla/src/tracing.rs b/scylla/src/tracing.rs index 3d40e36431..7753e4bd91 100644 --- a/scylla/src/tracing.rs +++ b/scylla/src/tracing.rs @@ -5,6 +5,7 @@ use uuid::Uuid; use crate::cql_to_rust::{FromRow, FromRowError}; use crate::frame::response::result::Row; +use crate::frame::value::CqlTimestamp; /// Tracing info retrieved from `system_traces.sessions` /// with all events from `system_traces.events` @@ -17,7 +18,7 @@ pub struct TracingInfo { pub parameters: Option>, pub request: Option, /// started_at is a timestamp - time since unix epoch - pub started_at: Option, + pub started_at: Option, pub events: Vec, } @@ -64,7 +65,7 @@ impl FromRow for TracingInfo { Option, Option>, Option, - Option, + Option, )>::from_row(row)?; Ok(TracingInfo { diff --git a/scylla/src/transport/caching_session.rs b/scylla/src/transport/caching_session.rs index 82e12b1ab2..f3d0d4db88 100644 --- a/scylla/src/transport/caching_session.rs +++ b/scylla/src/transport/caching_session.rs @@ -1,5 +1,4 @@ use crate::batch::{Batch, BatchStatement}; -use crate::frame::value::{BatchValues, ValueList}; use crate::prepared_statement::PreparedStatement; use crate::query::Query; use crate::transport::errors::QueryError; @@ -10,6 +9,8 @@ use bytes::Bytes; use dashmap::DashMap; use futures::future::try_join_all; use scylla_cql::frame::response::result::PreparedMetadata; +use scylla_cql::types::serialize::batch::BatchValues; +use scylla_cql::types::serialize::row::SerializeRow; use std::collections::hash_map::RandomState; use std::hash::BuildHasher; @@ -70,38 +71,35 @@ where pub async fn execute( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { let query = query.into(); let prepared = self.add_prepared_statement_owned(query).await?; - let values = values.serialized()?; - self.session.execute(&prepared, values.clone()).await + self.session.execute(&prepared, values).await } /// Does the same thing as [`Session::execute_iter`] but uses the prepared statement cache pub async fn execute_iter( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { let query = query.into(); let prepared = self.add_prepared_statement_owned(query).await?; - let values = values.serialized()?; - self.session.execute_iter(prepared, values.clone()).await + self.session.execute_iter(prepared, values).await } /// Does the same thing as [`Session::execute_paged`] but uses the prepared statement cache pub async fn execute_paged( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, paging_state: Option, ) -> Result { let query = query.into(); let prepared = self.add_prepared_statement_owned(query).await?; - let values = values.serialized()?; self.session - .execute_paged(&prepared, values.clone(), paging_state.clone()) + .execute_paged(&prepared, values, paging_state.clone()) .await } @@ -381,7 +379,7 @@ mod tests { for expected_row in expected_rows.iter() { if !selected_rows.contains(expected_row) { panic!( - "Expected {:?} to contain row: {:?}, but they didnt", + "Expected {:?} to contain row: {:?}, but they didn't", selected_rows, expected_row ); } diff --git a/scylla/src/transport/cluster.rs b/scylla/src/transport/cluster.rs index 503d14519d..0098391854 100644 --- a/scylla/src/transport/cluster.rs +++ b/scylla/src/transport/cluster.rs @@ -1,6 +1,5 @@ /// Cluster manages up to date information and connections to database nodes use crate::frame::response::event::{Event, StatusChangeEvent}; -use crate::frame::value::ValueList; use crate::prepared_statement::TokenCalculationError; use crate::routing::Token; use crate::transport::host_filter::HostFilter; @@ -18,6 +17,7 @@ use futures::future::join_all; use futures::{future::RemoteHandle, FutureExt}; use itertools::Itertools; use scylla_cql::errors::{BadQuery, NewSessionError}; +use scylla_cql::types::serialize::row::SerializedValues; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; @@ -390,7 +390,7 @@ impl ClusterData { &self, keyspace: &str, table: &str, - partition_key: impl ValueList, + partition_key: &SerializedValues, ) -> Result { let partitioner = self .keyspaces @@ -400,12 +400,11 @@ impl ClusterData { .and_then(PartitionerName::from_str) .unwrap_or_default(); - calculate_token_for_partition_key(&partition_key.serialized().unwrap(), &partitioner) - .map_err(|err| match err { - TokenCalculationError::ValueTooLong(values_len) => { - BadQuery::ValuesTooLongForKey(values_len, u16::MAX.into()) - } - }) + calculate_token_for_partition_key(partition_key, &partitioner).map_err(|err| match err { + TokenCalculationError::ValueTooLong(values_len) => { + BadQuery::ValuesTooLongForKey(values_len, u16::MAX.into()) + } + }) } /// Access to replicas owning a given token @@ -436,7 +435,7 @@ impl ClusterData { &self, keyspace: &str, table: &str, - partition_key: impl ValueList, + partition_key: &SerializedValues, ) -> Result>, BadQuery> { Ok(self.get_token_endpoints( keyspace, diff --git a/scylla/src/transport/connection.rs b/scylla/src/transport/connection.rs index de79c5d130..b6b91b69db 100644 --- a/scylla/src/transport/connection.rs +++ b/scylla/src/transport/connection.rs @@ -4,6 +4,9 @@ use scylla_cql::errors::TranslationError; use scylla_cql::frame::request::options::Options; use scylla_cql::frame::response::Error; use scylla_cql::frame::types::SerialConsistency; +use scylla_cql::types::serialize::batch::{BatchValues, BatchValuesIterator}; +use scylla_cql::types::serialize::raw_batch::RawBatchValuesAdapter; +use scylla_cql::types::serialize::row::{RowSerializationContext, SerializedValues}; use socket2::{SockRef, TcpKeepalive}; use tokio::io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::net::{TcpSocket, TcpStream}; @@ -52,7 +55,6 @@ use crate::frame::{ request::{self, batch, execute, query, register, SerializableRequest}, response::{event::Event, result, NonErrorResponse, Response, ResponseOpcode}, server_event_type::EventType, - value::{BatchValues, BatchValuesIterator, ValueList}, FrameParams, SerializedRequest, }; use crate::query::Query; @@ -290,7 +292,7 @@ mod ssl_config { /// This struct encapsulates all Ssl-regarding configuration and helps pass it tidily through the code. // // There are 3 possible options for SslConfig, whose behaviour is somewhat subtle. - // Option 1: No ssl configuration. Then it is None everytime. + // Option 1: No ssl configuration. Then it is None every time. // Option 2: User-provided global SslContext. Then, a SslConfig is created upon Session creation // and henceforth stored in the ConnectionConfig. // Option 3: Serverless Cloud. The Option remains None in ConnectionConfig until it reaches @@ -596,7 +598,6 @@ impl Connection { pub(crate) async fn query_single_page( &self, query: impl Into, - values: impl ValueList, ) -> Result { let query: Query = query.into(); @@ -606,24 +607,18 @@ impl Connection { .determine_consistency(self.config.default_consistency); let serial_consistency = query.config.serial_consistency; - self.query_single_page_with_consistency( - query, - &values, - consistency, - serial_consistency.flatten(), - ) - .await + self.query_single_page_with_consistency(query, consistency, serial_consistency.flatten()) + .await } pub(crate) async fn query_single_page_with_consistency( &self, query: impl Into, - values: impl ValueList, consistency: Consistency, serial_consistency: Option, ) -> Result { let query: Query = query.into(); - self.query_with_consistency(&query, &values, consistency, serial_consistency, None) + self.query_with_consistency(&query, consistency, serial_consistency, None) .await? .into_query_result() } @@ -631,13 +626,11 @@ impl Connection { pub(crate) async fn query( &self, query: &Query, - values: impl ValueList, paging_state: Option, ) -> Result { // This method is used only for driver internal queries, so no need to consult execution profile here. self.query_with_consistency( query, - values, query .config .determine_consistency(self.config.default_consistency), @@ -650,33 +643,16 @@ impl Connection { pub(crate) async fn query_with_consistency( &self, query: &Query, - values: impl ValueList, consistency: Consistency, serial_consistency: Option, paging_state: Option, ) -> Result { - let serialized_values = values.serialized()?; - - let values_size = serialized_values.size(); - if values_size != 0 { - let prepared = self.prepare(query).await?; - return self - .execute_with_consistency( - &prepared, - values, - consistency, - serial_consistency, - paging_state, - ) - .await; - } - let query_frame = query::Query { contents: Cow::Borrowed(&query.contents), parameters: query::QueryParameters { consistency, serial_consistency, - values: serialized_values, + values: Cow::Borrowed(SerializedValues::EMPTY), page_size: query.get_page_size(), paging_state, timestamp: query.get_timestamp(), @@ -687,22 +663,40 @@ impl Connection { .await } + #[allow(dead_code)] + pub(crate) async fn execute( + &self, + prepared: PreparedStatement, + values: SerializedValues, + paging_state: Option, + ) -> Result { + // This method is used only for driver internal queries, so no need to consult execution profile here. + self.execute_with_consistency( + &prepared, + &values, + prepared + .config + .determine_consistency(self.config.default_consistency), + prepared.config.serial_consistency.flatten(), + paging_state, + ) + .await + } + pub(crate) async fn execute_with_consistency( &self, prepared_statement: &PreparedStatement, - values: impl ValueList, + values: &SerializedValues, consistency: Consistency, serial_consistency: Option, paging_state: Option, ) -> Result { - let serialized_values = values.serialized()?; - let execute_frame = execute::Execute { id: prepared_statement.get_id().to_owned(), parameters: query::QueryParameters { consistency, serial_consistency, - values: serialized_values, + values: Cow::Borrowed(values), page_size: prepared_statement.get_page_size(), timestamp: prepared_statement.get_timestamp(), paging_state, @@ -734,19 +728,32 @@ impl Connection { pub(crate) async fn query_iter( self: Arc, query: Query, - values: impl ValueList, ) -> Result { - let serialized_values = values.serialized()?.into_owned(); - let consistency = query .config .determine_consistency(self.config.default_consistency); let serial_consistency = query.config.serial_consistency.flatten(); - RowIterator::new_for_connection_query_iter( - query, + RowIterator::new_for_connection_query_iter(query, self, consistency, serial_consistency) + .await + } + + /// Executes a prepared statements and fetches its results over multiple pages, using + /// the asynchronous iterator interface. + pub(crate) async fn execute_iter( + self: Arc, + prepared_statement: PreparedStatement, + values: SerializedValues, + ) -> Result { + let consistency = prepared_statement + .config + .determine_consistency(self.config.default_consistency); + let serial_consistency = prepared_statement.config.serial_consistency.flatten(); + + RowIterator::new_for_connection_execute_iter( + prepared_statement, + values, self, - serialized_values, consistency, serial_consistency, ) @@ -779,6 +786,15 @@ impl Connection { ) -> Result { let batch = self.prepare_batch(init_batch, &values).await?; + let contexts = batch.statements.iter().map(|bs| match bs { + BatchStatement::Query(_) => RowSerializationContext::empty(), + BatchStatement::PreparedStatement(ps) => { + RowSerializationContext::from_prepared(ps.get_prepared_metadata()) + } + }); + + let values = RawBatchValuesAdapter::new(values, contexts); + let batch_frame = batch::Batch { statements: Cow::Borrowed(&batch.statements), values, @@ -833,11 +849,8 @@ impl Connection { let mut values_iter = values.batch_values_iter(); for stmt in &init_batch.statements { if let BatchStatement::Query(query) = stmt { - let value = values_iter.next_serialized().transpose()?; - if let Some(v) = value { - if v.len() > 0 { - to_prepare.insert(&query.contents); - } + if let Some(false) = values_iter.is_empty_next() { + to_prepare.insert(&query.contents); } } else { values_iter.skip_next(); @@ -885,7 +898,7 @@ impl Connection { false => format!("USE {}", keyspace_name.as_str()).into(), }; - let query_response = self.query(&query, (), None).await?; + let query_response = self.query(&query, None).await?; match query_response.response { Response::Result(result::Result::SetKeyspace(set_keyspace)) => { @@ -929,7 +942,7 @@ impl Connection { pub(crate) async fn fetch_schema_version(&self) -> Result { let (version_id,): (Uuid,) = self - .query_single_page(LOCAL_VERSION, &[]) + .query_single_page(LOCAL_VERSION) .await? .rows .ok_or(QueryError::ProtocolError("Version query returned not rows"))? @@ -1334,7 +1347,7 @@ impl Connection { // or passing the negotiated features via a channel/mutex/etc. // Fortunately, events do not need information about protocol features // to be serialized (yet), therefore I'm leaving this problem for - // future implementors. + // future implementers. let features = ProtocolFeatures::default(); // TODO: Use the right features let response = Self::parse_response(task_response, compression, &features)?.response; @@ -1833,7 +1846,6 @@ mod tests { use super::ConnectionConfig; use crate::query::Query; use crate::transport::connection::open_connection; - use crate::transport::connection::QueryResponse; use crate::transport::node::ResolvedContactPoint; use crate::transport::topology::UntranslatedEndpoint; use crate::utils::test_utils::unique_keyspace_name; @@ -1914,7 +1926,7 @@ mod tests { let select_query = Query::new("SELECT p FROM connection_query_iter_tab").with_page_size(7); let empty_res = connection .clone() - .query_iter(select_query.clone(), &[]) + .query_iter(select_query.clone()) .await .unwrap() .try_collect::>() @@ -1927,15 +1939,19 @@ mod tests { let mut insert_futures = Vec::new(); let insert_query = Query::new("INSERT INTO connection_query_iter_tab (p) VALUES (?)").with_page_size(7); + let prepared = connection.prepare(&insert_query).await.unwrap(); for v in &values { - insert_futures.push(connection.query_single_page(insert_query.clone(), (v,))); + let prepared_clone = prepared.clone(); + let values = prepared_clone.serialize_values(&(*v,)).unwrap(); + let fut = async { connection.execute(prepared_clone, values, None).await }; + insert_futures.push(fut); } futures::future::try_join_all(insert_futures).await.unwrap(); let mut results: Vec = connection .clone() - .query_iter(select_query.clone(), &[]) + .query_iter(select_query.clone()) .await .unwrap() .into_typed::<(i32,)>() @@ -1947,7 +1963,9 @@ mod tests { // 3. INSERT query_iter should work and not return any rows. let insert_res1 = connection - .query_iter(insert_query, (0,)) + .query_iter(Query::new( + "INSERT INTO connection_query_iter_tab (p) VALUES (0)", + )) .await .unwrap() .try_collect::>() @@ -2007,10 +2025,7 @@ mod tests { .await .unwrap(); - connection - .query(&"TRUNCATE t".into(), (), None) - .await - .unwrap(); + connection.query(&"TRUNCATE t".into(), None).await.unwrap(); let mut futs = Vec::new(); @@ -2025,10 +2040,12 @@ mod tests { let q = Query::new("INSERT INTO t (p, v) VALUES (?, ?)"); let conn = conn.clone(); async move { - let response: QueryResponse = conn - .query(&q, (j, vec![j as u8; j as usize]), None) - .await + let prepared = conn.prepare(&q).await.unwrap(); + let values = prepared + .serialize_values(&(j, vec![j as u8; j as usize])) .unwrap(); + let response = + conn.execute(prepared.clone(), values, None).await.unwrap(); // QueryResponse might contain an error - make sure that there were no errors let _nonerror_response = response.into_non_error_query_response().unwrap(); @@ -2045,7 +2062,7 @@ mod tests { // Check that everything was written properly let range_end = arithmetic_sequence_sum(NUM_BATCHES); let mut results = connection - .query(&"SELECT p, v FROM t".into(), (), None) + .query(&"SELECT p, v FROM t".into(), None) .await .unwrap() .into_query_result() @@ -2198,7 +2215,7 @@ mod tests { // As everything is normal, these queries should succeed. for _ in 0..3 { tokio::time::sleep(Duration::from_millis(500)).await; - conn.query_single_page("SELECT host_id FROM system.local", ()) + conn.query_single_page("SELECT host_id FROM system.local") .await .unwrap(); } @@ -2218,7 +2235,7 @@ mod tests { // As the router is invalidated, all further queries should immediately // return error. - conn.query_single_page("SELECT host_id FROM system.local", ()) + conn.query_single_page("SELECT host_id FROM system.local") .await .unwrap_err(); diff --git a/scylla/src/transport/cql_collections_test.rs b/scylla/src/transport/cql_collections_test.rs index cd89443271..a3e8c76089 100644 --- a/scylla/src/transport/cql_collections_test.rs +++ b/scylla/src/transport/cql_collections_test.rs @@ -1,8 +1,8 @@ use crate::cql_to_rust::FromCqlVal; -use crate::frame::value::Value; use crate::test_utils::create_new_session_builder; use crate::utils::test_utils::unique_keyspace_name; use crate::{frame::response::result::CqlValue, IntoTypedRows, Session}; +use scylla_cql::types::serialize::value::SerializeCql; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; async fn connect() -> Session { @@ -33,7 +33,7 @@ async fn insert_and_select( to_insert: &InsertT, expected: &SelectT, ) where - InsertT: Value, + InsertT: SerializeCql, SelectT: FromCqlVal> + PartialEq + std::fmt::Debug, { session diff --git a/scylla/src/transport/cql_types_test.rs b/scylla/src/transport/cql_types_test.rs index dbc4cf2537..1ab0997728 100644 --- a/scylla/src/transport/cql_types_test.rs +++ b/scylla/src/transport/cql_types_test.rs @@ -1,17 +1,16 @@ use crate as scylla; use crate::cql_to_rust::FromCqlVal; use crate::frame::response::result::CqlValue; -use crate::frame::value::Counter; -use crate::frame::value::Value; -use crate::frame::value::{Date, Time, Timestamp}; -use crate::macros::{FromUserType, IntoUserType}; +use crate::frame::value::{Counter, CqlDate, CqlTime, CqlTimestamp}; +use crate::macros::FromUserType; use crate::test_utils::create_new_session_builder; use crate::transport::session::IntoTypedRows; use crate::transport::session::Session; use crate::utils::test_utils::unique_keyspace_name; use bigdecimal::BigDecimal; -use chrono::{Duration, NaiveDate}; use num_bigint::BigInt; +use scylla_cql::types::serialize::value::SerializeCql; +use scylla_macros::SerializeCql; use std::cmp::PartialEq; use std::fmt::Debug; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -67,7 +66,7 @@ async fn init_test(table_name: &str, type_name: &str) -> Session { // Expected values and bound values are computed using T::from_str async fn run_tests(tests: &[&str], type_name: &str) where - T: Value + FromCqlVal + FromStr + Debug + Clone + PartialEq, + T: SerializeCql + FromCqlVal + FromStr + Debug + Clone + PartialEq, { let session: Session = init_test(type_name, type_name).await; session.await_schema_agreement().await.unwrap(); @@ -194,9 +193,12 @@ async fn test_counter() { } } +#[cfg(feature = "chrono")] #[tokio::test] async fn test_naive_date() { - let session: Session = init_test("naive_date", "date").await; + use chrono::NaiveDate; + + let session: Session = init_test("chrono_naive_date_tests", "date").await; let min_naive_date: NaiveDate = NaiveDate::MIN; assert_eq!( @@ -214,7 +216,7 @@ async fn test_naive_date() { // Basic test values ( "0000-01-01", - Some(NaiveDate::from_ymd_opt(0000, 1, 1).unwrap()), + Some(NaiveDate::from_ymd_opt(0, 1, 1).unwrap()), ), ( "1970-01-01", @@ -250,7 +252,7 @@ async fn test_naive_date() { session .query( format!( - "INSERT INTO naive_date (id, val) VALUES (0, '{}')", + "INSERT INTO chrono_naive_date_tests (id, val) VALUES (0, '{}')", date_text ), &[], @@ -259,7 +261,7 @@ async fn test_naive_date() { .unwrap(); let read_date: Option = session - .query("SELECT val from naive_date", &[]) + .query("SELECT val from chrono_naive_date_tests", &[]) .await .unwrap() .rows @@ -276,14 +278,14 @@ async fn test_naive_date() { if let Some(naive_date) = date { session .query( - "INSERT INTO naive_date (id, val) VALUES (0, ?)", + "INSERT INTO chrono_naive_date_tests (id, val) VALUES (0, ?)", (naive_date,), ) .await .unwrap(); let (read_date,): (NaiveDate,) = session - .query("SELECT val from naive_date", &[]) + .query("SELECT val from chrono_naive_date_tests", &[]) .await .unwrap() .rows @@ -295,11 +297,53 @@ async fn test_naive_date() { assert_eq!(read_date, *naive_date); } } +} + +#[tokio::test] +async fn test_cql_date() { + // Tests value::Date which allows to insert dates outside NaiveDate range + + let session: Session = init_test("cql_date_tests", "date").await; + + let tests = [ + ("1970-01-01", CqlDate(2_u32.pow(31))), + ("1969-12-02", CqlDate(2_u32.pow(31) - 30)), + ("1970-01-31", CqlDate(2_u32.pow(31) + 30)), + ("-5877641-06-23", CqlDate(0)), + // NOTICE: dropped for Cassandra 4 compatibility + //("5881580-07-11", Date(u32::MAX)), + ]; + + for (date_text, date) in &tests { + session + .query( + format!( + "INSERT INTO cql_date_tests (id, val) VALUES (0, '{}')", + date_text + ), + &[], + ) + .await + .unwrap(); + + let read_date: CqlDate = session + .query("SELECT val from cql_date_tests", &[]) + .await + .unwrap() + .rows + .unwrap()[0] + .columns[0] + .as_ref() + .map(|cql_val| cql_val.as_cql_date().unwrap()) + .unwrap(); + + assert_eq!(read_date, *date); + } // 1 less/more than min/max values allowed by the database should cause error session .query( - "INSERT INTO naive_date (id, val) VALUES (0, '-5877641-06-22')", + "INSERT INTO cql_date_tests (id, val) VALUES (0, '-5877641-06-22')", &[], ) .await @@ -307,33 +351,60 @@ async fn test_naive_date() { session .query( - "INSERT INTO naive_date (id, val) VALUES (0, '5881580-07-12')", + "INSERT INTO cql_date_tests (id, val) VALUES (0, '5881580-07-12')", &[], ) .await .unwrap_err(); } +#[cfg(feature = "time")] #[tokio::test] async fn test_date() { - // Tests value::Date which allows to insert dates outside NaiveDate range + use time::{Date, Month::*}; - let session: Session = init_test("date_tests", "date").await; + let session: Session = init_test("time_date_tests", "date").await; let tests = [ - ("1970-01-01", Date(2_u32.pow(31))), - ("1969-12-02", Date(2_u32.pow(31) - 30)), - ("1970-01-31", Date(2_u32.pow(31) + 30)), - ("-5877641-06-23", Date(0)), - // NOTICE: dropped for Cassandra 4 compatibility - //("5881580-07-11", Date(u32::MAX)), + // Basic test values + ( + "0000-01-01", + Some(Date::from_calendar_date(0, January, 1).unwrap()), + ), + ( + "1970-01-01", + Some(Date::from_calendar_date(1970, January, 1).unwrap()), + ), + ( + "2020-03-07", + Some(Date::from_calendar_date(2020, March, 7).unwrap()), + ), + ( + "1337-04-05", + Some(Date::from_calendar_date(1337, April, 5).unwrap()), + ), + ( + "-0001-12-31", + Some(Date::from_calendar_date(-1, December, 31).unwrap()), + ), + // min/max values allowed by time::Date depend on feature flags, but following values must always be allowed + ( + "9999-12-31", + Some(Date::from_calendar_date(9999, December, 31).unwrap()), + ), + ( + "-9999-01-01", + Some(Date::from_calendar_date(-9999, January, 1).unwrap()), + ), + // min value allowed by the database + ("-5877641-06-23", None), ]; - for (date_text, date) in &tests { + for (date_text, date) in tests.iter() { session .query( format!( - "INSERT INTO date_tests (id, val) VALUES (0, '{}')", + "INSERT INTO time_date_tests (id, val) VALUES (0, '{}')", date_text ), &[], @@ -341,40 +412,53 @@ async fn test_date() { .await .unwrap(); - let read_date: Date = session - .query("SELECT val from date_tests", &[]) + let read_date = session + .query("SELECT val from time_date_tests", &[]) .await .unwrap() - .rows - .unwrap()[0] - .columns[0] - .as_ref() - .map(|cql_val| match cql_val { - CqlValue::Date(days) => Date(*days), - _ => panic!(), - }) - .unwrap(); + .first_row_typed::<(Date,)>() + .ok() + .map(|val| val.0); assert_eq!(read_date, *date); + + // If date is representable by time::Date try inserting it and reading again + if let Some(date) = date { + session + .query( + "INSERT INTO time_date_tests (id, val) VALUES (0, ?)", + (date,), + ) + .await + .unwrap(); + + let (read_date,) = session + .query("SELECT val from time_date_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Date,)>() + .unwrap(); + assert_eq!(read_date, *date); + } } } #[tokio::test] -async fn test_time() { - // Time is an i64 - nanoseconds since midnight +async fn test_cql_time() { + // CqlTime is an i64 - nanoseconds since midnight // in range 0..=86399999999999 - let session: Session = init_test("time_tests", "time").await; + let session: Session = init_test("cql_time_tests", "time").await; let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; assert_eq!(max_time, 86399999999999); let tests = [ - ("00:00:00", Duration::nanoseconds(0)), - ("01:01:01", Duration::seconds(60 * 60 + 60 + 1)), - ("00:00:00.000000000", Duration::nanoseconds(0)), - ("00:00:00.000000001", Duration::nanoseconds(1)), - ("23:59:59.999999999", Duration::nanoseconds(max_time)), + ("00:00:00", CqlTime(0)), + ("01:01:01", CqlTime((60 * 60 + 60 + 1) * 1_000_000_000)), + ("00:00:00.000000000", CqlTime(0)), + ("00:00:00.000000001", CqlTime(1)), + ("23:59:59.999999999", CqlTime(max_time)), ]; for (time_str, time_duration) in &tests { @@ -382,7 +466,7 @@ async fn test_time() { session .query( format!( - "INSERT INTO time_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_time_tests (id, val) VALUES (0, '{}')", time_str ), &[], @@ -390,35 +474,35 @@ async fn test_time() { .await .unwrap(); - let (read_time,): (Duration,) = session - .query("SELECT val from time_tests", &[]) + let (read_time,) = session + .query("SELECT val from cql_time_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTime,)>() .next() .unwrap() .unwrap(); assert_eq!(read_time, *time_duration); - // Insert time as a bound Time value and verify that it matches + // Insert time as a bound CqlTime value and verify that it matches session .query( - "INSERT INTO time_tests (id, val) VALUES (0, ?)", - (Time(*time_duration),), + "INSERT INTO cql_time_tests (id, val) VALUES (0, ?)", + (*time_duration,), ) .await .unwrap(); - let (read_time,): (Duration,) = session - .query("SELECT val from time_tests", &[]) + let (read_time,) = session + .query("SELECT val from cql_time_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTime,)>() .next() .unwrap() .unwrap(); @@ -442,7 +526,7 @@ async fn test_time() { session .query( format!( - "INSERT INTO time_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_time_tests (id, val) VALUES (0, '{}')", time_str ), &[], @@ -452,9 +536,157 @@ async fn test_time() { } } +#[cfg(feature = "chrono")] #[tokio::test] -async fn test_timestamp() { - let session: Session = init_test("timestamp_tests", "timestamp").await; +async fn test_naive_time() { + use chrono::NaiveTime; + + let session = init_test("chrono_time_tests", "time").await; + + let tests = [ + ("00:00:00", NaiveTime::MIN), + ("01:01:01", NaiveTime::from_hms_opt(1, 1, 1).unwrap()), + ( + "00:00:00.000000000", + NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(), + ), + ( + "00:00:00.000000001", + NaiveTime::from_hms_nano_opt(0, 0, 0, 1).unwrap(), + ), + ( + "12:34:56.789012345", + NaiveTime::from_hms_nano_opt(12, 34, 56, 789_012_345).unwrap(), + ), + ( + "23:59:59.999999999", + NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(), + ), + ]; + + for (time_text, time) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO chrono_time_tests (id, val) VALUES (0, '{}')", + time_text + ), + &[], + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from chrono_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(NaiveTime,)>() + .unwrap(); + + assert_eq!(read_time, *time); + + // Insert as type and read it again + session + .query( + "INSERT INTO chrono_time_tests (id, val) VALUES (0, ?)", + (time,), + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from chrono_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(NaiveTime,)>() + .unwrap(); + assert_eq!(read_time, *time); + } + + // chrono can represent leap seconds, this should not panic + let leap_second = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_500_000_000); + session + .query( + "INSERT INTO cql_time_tests (id, val) VALUES (0, ?)", + (leap_second,), + ) + .await + .unwrap_err(); +} + +#[cfg(feature = "time")] +#[tokio::test] +async fn test_time() { + use time::Time; + + let session = init_test("time_time_tests", "time").await; + + let tests = [ + ("00:00:00", Time::MIDNIGHT), + ("01:01:01", Time::from_hms(1, 1, 1).unwrap()), + ( + "00:00:00.000000000", + Time::from_hms_nano(0, 0, 0, 0).unwrap(), + ), + ( + "00:00:00.000000001", + Time::from_hms_nano(0, 0, 0, 1).unwrap(), + ), + ( + "12:34:56.789012345", + Time::from_hms_nano(12, 34, 56, 789_012_345).unwrap(), + ), + ( + "23:59:59.999999999", + Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap(), + ), + ]; + + for (time_text, time) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO time_time_tests (id, val) VALUES (0, '{}')", + time_text + ), + &[], + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from time_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Time,)>() + .unwrap(); + + assert_eq!(read_time, *time); + + // Insert as type and read it again + session + .query( + "INSERT INTO time_time_tests (id, val) VALUES (0, ?)", + (time,), + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from time_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Time,)>() + .unwrap(); + assert_eq!(read_time, *time); + } +} + +#[tokio::test] +async fn test_cql_timestamp() { + let session: Session = init_test("cql_timestamp_tests", "timestamp").await; //let epoch_date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); @@ -465,9 +697,9 @@ async fn test_timestamp() { //let after_epoch_offset = after_epoch.signed_duration_since(epoch_date); let tests = [ - ("0", Duration::milliseconds(0)), - ("9223372036854775807", Duration::milliseconds(i64::MAX)), - ("-9223372036854775808", Duration::milliseconds(i64::MIN)), + ("0", CqlTimestamp(0)), + ("9223372036854775807", CqlTimestamp(i64::MAX)), + ("-9223372036854775808", CqlTimestamp(i64::MIN)), // NOTICE: dropped for Cassandra 4 compatibility //("1970-01-01", Duration::milliseconds(0)), //("2020-03-08", after_epoch_offset), @@ -486,7 +718,7 @@ async fn test_timestamp() { session .query( format!( - "INSERT INTO timestamp_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_timestamp_tests (id, val) VALUES (0, '{}')", timestamp_str ), &[], @@ -494,35 +726,35 @@ async fn test_timestamp() { .await .unwrap(); - let (read_timestamp,): (Duration,) = session - .query("SELECT val from timestamp_tests", &[]) + let (read_timestamp,) = session + .query("SELECT val from cql_timestamp_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTimestamp,)>() .next() .unwrap() .unwrap(); assert_eq!(read_timestamp, *timestamp_duration); - // Insert timestamp as a bound Timestamp value and verify that it matches + // Insert timestamp as a bound CqlTimestamp value and verify that it matches session .query( - "INSERT INTO timestamp_tests (id, val) VALUES (0, ?)", - (Timestamp(*timestamp_duration),), + "INSERT INTO cql_timestamp_tests (id, val) VALUES (0, ?)", + (*timestamp_duration,), ) .await .unwrap(); - let (read_timestamp,): (Duration,) = session - .query("SELECT val from timestamp_tests", &[]) + let (read_timestamp,) = session + .query("SELECT val from cql_timestamp_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTimestamp,)>() .next() .unwrap() .unwrap(); @@ -531,6 +763,315 @@ async fn test_timestamp() { } } +#[cfg(feature = "chrono")] +#[tokio::test] +async fn test_date_time() { + use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; + + let session = init_test("chrono_datetime_tests", "timestamp").await; + + let tests = [ + ( + "0", + NaiveDateTime::from_timestamp_opt(0, 0).unwrap().and_utc(), + ), + ( + "2001-02-03T04:05:06.789+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2001, 2, 3).unwrap(), + NaiveTime::from_hms_milli_opt(4, 5, 6, 789).unwrap(), + ) + .and_utc(), + ), + ( + "2011-02-03T04:05:00.000+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2011, 2, 3).unwrap(), + NaiveTime::from_hms_milli_opt(4, 5, 0, 0).unwrap(), + ) + .and_utc(), + ), + // New Zealand timezone, converted to GMT + ( + "2011-02-03T04:05:06.987+1245", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2011, 2, 2).unwrap(), + NaiveTime::from_hms_milli_opt(15, 20, 6, 987).unwrap(), + ) + .and_utc(), + ), + ( + "9999-12-31T23:59:59.999+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap(), + ) + .and_utc(), + ), + ( + "-377705116800000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(-9999, 1, 1).unwrap(), + NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(), + ) + .and_utc(), + ), + ]; + + for (datetime_text, datetime) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, '{}')", + datetime_text + ), + &[], + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + + assert_eq!(read_datetime, *datetime); + + // Insert as type and read it again + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (datetime,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, *datetime); + } + + // chrono datetime has higher precision, round excessive submillisecond time down + let nanosecond_precision_1st_half = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_nano_opt(23, 59, 59, 123_123_456).unwrap(), + ) + .and_utc(); + let nanosecond_precision_1st_half_rounded = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 123).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_1st_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_1st_half_rounded); + + let nanosecond_precision_2nd_half = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_nano_opt(23, 59, 59, 123_987_654).unwrap(), + ) + .and_utc(); + let nanosecond_precision_2nd_half_rounded = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 123).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_2nd_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_2nd_half_rounded); + + // chrono can represent leap seconds, this should not panic + let leap_second = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 1500).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO cql_datetime_tests (id, val) VALUES (0, ?)", + (leap_second,), + ) + .await + .unwrap_err(); +} + +#[cfg(feature = "time")] +#[tokio::test] +async fn test_offset_date_time() { + use time::{Date, Month::*, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; + + let session = init_test("time_datetime_tests", "timestamp").await; + + let tests = [ + ("0", OffsetDateTime::UNIX_EPOCH), + ( + "2001-02-03T04:05:06.789+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(2001, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 6, 789).unwrap(), + ) + .assume_utc(), + ), + ( + "2011-02-03T04:05:00.000+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(2011, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 0, 0).unwrap(), + ) + .assume_utc(), + ), + // New Zealand timezone, converted to GMT + ( + "2011-02-03T04:05:06.987+1245", + PrimitiveDateTime::new( + Date::from_calendar_date(2011, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 6, 987).unwrap(), + ) + .assume_offset(UtcOffset::from_hms(12, 45, 0).unwrap()), + ), + ( + "9999-12-31T23:59:59.999+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(9999, December, 31).unwrap(), + Time::from_hms_milli(23, 59, 59, 999).unwrap(), + ) + .assume_utc(), + ), + ( + "-377705116800000", + PrimitiveDateTime::new( + Date::from_calendar_date(-9999, January, 1).unwrap(), + Time::from_hms_milli(0, 0, 0, 0).unwrap(), + ) + .assume_utc(), + ), + ]; + + for (datetime_text, datetime) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, '{}')", + datetime_text + ), + &[], + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + + assert_eq!(read_datetime, *datetime); + + // Insert as type and read it again + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (datetime,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, *datetime); + } + + // time datetime has higher precision, round excessive submillisecond time down + let nanosecond_precision_1st_half = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_nano(23, 59, 59, 123_123_456).unwrap(), + ) + .assume_utc(); + let nanosecond_precision_1st_half_rounded = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_milli(23, 59, 59, 123).unwrap(), + ) + .assume_utc(); + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_1st_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_1st_half_rounded); + + let nanosecond_precision_2nd_half = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_nano(23, 59, 59, 123_987_654).unwrap(), + ) + .assume_utc(); + let nanosecond_precision_2nd_half_rounded = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_milli(23, 59, 59, 123).unwrap(), + ) + .assume_utc(); + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_2nd_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_2nd_half_rounded); +} + #[tokio::test] async fn test_timeuuid() { let session: Session = init_test("timeuuid_tests", "timeuuid").await; @@ -822,7 +1363,8 @@ async fn test_udt_after_schema_update() { .await .unwrap(); - #[derive(IntoUserType, FromUserType, Debug, PartialEq)] + #[derive(SerializeCql, FromUserType, Debug, PartialEq)] + #[scylla(crate = crate)] struct UdtV1 { pub first: i32, pub second: bool, @@ -949,3 +1491,207 @@ async fn test_empty() { assert_eq!(empty, CqlValue::Empty); } + +#[tokio::test] +async fn test_udt_with_missing_field() { + let table_name = "udt_tests"; + let type_name = "usertype1"; + + let session: Session = create_new_session_builder().build().await.unwrap(); + let ks = unique_keyspace_name(); + + session + .query( + format!( + "CREATE KEYSPACE IF NOT EXISTS {} WITH REPLICATION = \ + {{'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}}", + ks + ), + &[], + ) + .await + .unwrap(); + session.use_keyspace(ks, false).await.unwrap(); + + session + .query(format!("DROP TABLE IF EXISTS {}", table_name), &[]) + .await + .unwrap(); + + session + .query(format!("DROP TYPE IF EXISTS {}", type_name), &[]) + .await + .unwrap(); + + session + .query( + format!( + "CREATE TYPE IF NOT EXISTS {} (first int, second boolean, third float, fourth blob)", + type_name + ), + &[], + ) + .await + .unwrap(); + + session + .query( + format!( + "CREATE TABLE IF NOT EXISTS {} (id int PRIMARY KEY, val {})", + table_name, type_name + ), + &[], + ) + .await + .unwrap(); + + let mut id = 0; + + async fn verify_insert_select_identity( + session: &Session, + table_name: &str, + id: i32, + element: TQ, + expected: TR, + ) where + TQ: SerializeCql, + TR: FromCqlVal + PartialEq + Debug, + { + session + .query( + format!("INSERT INTO {}(id,val) VALUES (?,?)", table_name), + &(id, &element), + ) + .await + .unwrap(); + let result = session + .query( + format!("SELECT val from {} WHERE id = ?", table_name), + &(id,), + ) + .await + .unwrap() + .rows + .unwrap() + .into_typed::<(TR,)>() + .next() + .unwrap() + .unwrap() + .0; + assert_eq!(expected, result); + } + + #[derive(FromUserType, Debug, PartialEq)] + struct UdtFull { + pub first: i32, + pub second: bool, + pub third: Option, + pub fourth: Option>, + } + + #[derive(SerializeCql)] + #[scylla(crate = crate)] + struct UdtV1 { + pub first: i32, + pub second: bool, + } + + verify_insert_select_identity( + &session, + table_name, + id, + UdtV1 { + first: 3, + second: true, + }, + UdtFull { + first: 3, + second: true, + third: None, + fourth: None, + }, + ) + .await; + + id += 1; + + #[derive(SerializeCql)] + #[scylla(crate = crate)] + struct UdtV2 { + pub first: i32, + pub second: bool, + pub third: Option, + } + + verify_insert_select_identity( + &session, + table_name, + id, + UdtV2 { + first: 3, + second: true, + third: Some(123.45), + }, + UdtFull { + first: 3, + second: true, + third: Some(123.45), + fourth: None, + }, + ) + .await; + + id += 1; + + #[derive(SerializeCql)] + #[scylla(crate = crate)] + struct UdtV3 { + pub first: i32, + pub second: bool, + pub fourth: Option>, + } + + verify_insert_select_identity( + &session, + table_name, + id, + UdtV3 { + first: 3, + second: true, + fourth: Some(vec![3, 6, 9]), + }, + UdtFull { + first: 3, + second: true, + third: None, + fourth: Some(vec![3, 6, 9]), + }, + ) + .await; + + id += 1; + + #[derive(SerializeCql)] + #[scylla(crate = crate, flavor="enforce_order")] + struct UdtV4 { + pub first: i32, + pub second: bool, + } + + verify_insert_select_identity( + &session, + table_name, + id, + UdtV4 { + first: 3, + second: true, + }, + UdtFull { + first: 3, + second: true, + third: None, + fourth: None, + }, + ) + .await; +} diff --git a/scylla/src/transport/iterator.rs b/scylla/src/transport/iterator.rs index e9389992ed..366a7ccb4a 100644 --- a/scylla/src/transport/iterator.rs +++ b/scylla/src/transport/iterator.rs @@ -12,6 +12,7 @@ use bytes::Bytes; use futures::Stream; use scylla_cql::frame::response::NonErrorResponse; use scylla_cql::frame::types::SerialConsistency; +use scylla_cql::types::serialize::row::SerializedValues; use std::result::Result; use thiserror::Error; use tokio::sync::mpsc; @@ -22,12 +23,9 @@ use super::execution_profile::ExecutionProfileInner; use super::session::RequestSpan; use crate::cql_to_rust::{FromRow, FromRowError}; -use crate::frame::{ - response::{ - result, - result::{ColumnSpec, Row, Rows}, - }, - value::SerializedValues, +use crate::frame::response::{ + result, + result::{ColumnSpec, Row, Rows}, }; use crate::history::{self, HistoryListener}; use crate::statement::Consistency; @@ -128,7 +126,6 @@ impl RowIterator { pub(crate) async fn new_for_query( mut query: Query, - values: SerializedValues, execution_profile: Arc, cluster_data: Arc, metrics: Arc, @@ -162,29 +159,31 @@ impl RowIterator { let parent_span = tracing::Span::current(); let worker_task = async move { let query_ref = &query; - let values_ref = &values; let choose_connection = |node: Arc| async move { node.random_connection().await }; let page_query = |connection: Arc, consistency: Consistency, - paging_state: Option| async move { - connection - .query_with_consistency( - query_ref, - values_ref, - consistency, - serial_consistency, - paging_state, - ) - .await + paging_state: Option| { + async move { + connection + .query_with_consistency( + query_ref, + consistency, + serial_consistency, + paging_state, + ) + .await + } }; let query_ref = &query; - let serialized_values_size = values.size(); - let span_creator = - move || RequestSpan::new_query(&query_ref.contents, serialized_values_size); + let span_creator = move || { + let span = RequestSpan::new_query(&query_ref.contents); + span.record_request_size(0); + span + }; let worker = RowIteratorWorker { sender: sender.into(), @@ -281,7 +280,7 @@ impl RowIterator { .await }; - let serialized_values_size = config.values.size(); + let serialized_values_size = config.values.buffer_size(); let replicas: Option> = if let (Some(keyspace), Some(token)) = @@ -337,7 +336,6 @@ impl RowIterator { pub(crate) async fn new_for_connection_query_iter( mut query: Query, connection: Arc, - values: SerializedValues, consistency: Consistency, serial_consistency: Option, ) -> Result { @@ -352,6 +350,36 @@ impl RowIterator { fetcher: |paging_state| { connection.query_with_consistency( &query, + consistency, + serial_consistency, + paging_state, + ) + }, + }; + worker.work().await + }; + + Self::new_from_worker_future(worker_task, receiver).await + } + + pub(crate) async fn new_for_connection_execute_iter( + mut prepared: PreparedStatement, + values: SerializedValues, + connection: Arc, + consistency: Consistency, + serial_consistency: Option, + ) -> Result { + if prepared.get_page_size().is_none() { + prepared.set_page_size(DEFAULT_ITER_PAGE_SIZE); + } + let (sender, receiver) = mpsc::channel::>(1); + + let worker_task = async move { + let worker = SingleConnectionRowIteratorWorker { + sender: sender.into(), + fetcher: |paging_state| { + connection.execute_with_consistency( + &prepared, &values, consistency, serial_consistency, diff --git a/scylla/src/transport/load_balancing/default.rs b/scylla/src/transport/load_balancing/default.rs index d1554babf1..3fdeef18ef 100644 --- a/scylla/src/transport/load_balancing/default.rs +++ b/scylla/src/transport/load_balancing/default.rs @@ -987,7 +987,7 @@ mod tests { // and just `assert_eq` them let mut got = got.iter(); for (group_id, expected) in self.groups.iter().enumerate() { - // Collect the nodes that consistute the group + // Collect the nodes that constitute the group // in the actual plan let got_group: Vec<_> = (&mut got).take(expected.len()).copied().collect(); diff --git a/scylla/src/transport/locator/precomputed_replicas.rs b/scylla/src/transport/locator/precomputed_replicas.rs index de6d5e1a63..0a9256b7e8 100644 --- a/scylla/src/transport/locator/precomputed_replicas.rs +++ b/scylla/src/transport/locator/precomputed_replicas.rs @@ -10,7 +10,7 @@ //! to compute those lists for each strategy used in cluster. //! //! Notes on Network Topology Strategy precomputation: -//! The optimization mentioned above works ony if requested `replication factor` is <= `rack count`. +//! The optimization mentioned above works only if requested `replication factor` is <= `rack count`. use super::replication_info::ReplicationInfo; use super::TokenRing; diff --git a/scylla/src/transport/locator/test.rs b/scylla/src/transport/locator/test.rs index d09a22d7c1..bb74ee0469 100644 --- a/scylla/src/transport/locator/test.rs +++ b/scylla/src/transport/locator/test.rs @@ -496,7 +496,7 @@ fn test_replica_set_choose(locator: &ReplicaLocator) { || locator.replicas_for_token(Token { value: 75 }, &strategy, None); // Verify that after a certain number of random selections, the set of selected replicas - // will contain all nodes in the ring (replica set was created usin a strategy with + // will contain all nodes in the ring (replica set was created using a strategy with // replication factors higher than node count). let mut chosen_replicas = HashSet::new(); for _ in 0..32 { diff --git a/scylla/src/transport/locator/token_ring.rs b/scylla/src/transport/locator/token_ring.rs index 686d8e0a90..cd5b4de8f3 100644 --- a/scylla/src/transport/locator/token_ring.rs +++ b/scylla/src/transport/locator/token_ring.rs @@ -47,7 +47,7 @@ impl TokenRing { /// Provides an iterator over the ring's elements starting at the given token. /// The iterator traverses the whole ring in the direction of increasing tokens. /// After reaching the maximum token it wraps around and continues from the lowest one. - /// The iterator visits each member once, it doesn't have an infinte length. + /// The iterator visits each member once, it doesn't have an infinite length. /// To access the token along with the element you can use `ring_range_full`. pub fn ring_range(&self, token: Token) -> impl Iterator { self.ring_range_full(token).map(|(_t, e)| e) diff --git a/scylla/src/transport/partitioner.rs b/scylla/src/transport/partitioner.rs index 9c8a542325..7a9f4b083a 100644 --- a/scylla/src/transport/partitioner.rs +++ b/scylla/src/transport/partitioner.rs @@ -1,9 +1,8 @@ use bytes::Buf; +use scylla_cql::{frame::types::RawValue, types::serialize::row::SerializedValues}; use std::num::Wrapping; -use crate::{ - frame::value::SerializedValues, prepared_statement::TokenCalculationError, routing::Token, -}; +use crate::{prepared_statement::TokenCalculationError, routing::Token}; #[allow(clippy::upper_case_acronyms)] #[derive(Clone, PartialEq, Debug, Default)] @@ -341,13 +340,16 @@ pub fn calculate_token_for_partition_key( ) -> Result { let mut partitioner_hasher = partitioner.build_hasher(); - if serialized_partition_key_values.len() == 1 { + if serialized_partition_key_values.element_count() == 1 { let val = serialized_partition_key_values.iter().next().unwrap(); - if let Some(val) = val { + if let RawValue::Value(val) = val { partitioner_hasher.write(val); } } else { - for val in serialized_partition_key_values.iter().flatten() { + for val in serialized_partition_key_values + .iter() + .filter_map(|rv| rv.as_value()) + { let val_len_u16: u16 = val .len() .try_into() diff --git a/scylla/src/transport/session.rs b/scylla/src/transport/session.rs index 24cc481c93..f4f5ab2365 100644 --- a/scylla/src/transport/session.rs +++ b/scylla/src/transport/session.rs @@ -1,6 +1,7 @@ //! `Session` is the main object used in the driver.\ //! It manages all connections to the cluster and allows to perform queries. +use crate::batch::batch_values; #[cfg(feature = "cloud")] use crate::cloud::CloudConfig; @@ -16,6 +17,8 @@ use itertools::{Either, Itertools}; pub use scylla_cql::errors::TranslationError; use scylla_cql::frame::response::result::{deser_cql_value, ColumnSpec, Rows}; use scylla_cql::frame::response::NonErrorResponse; +use scylla_cql::types::serialize::batch::BatchValues; +use scylla_cql::types::serialize::row::SerializeRow; use std::borrow::Borrow; use std::collections::HashMap; use std::fmt::Display; @@ -46,9 +49,6 @@ use super::NodeRef; use crate::cql_to_rust::FromRow; use crate::frame::response::cql_to_rust::FromRowError; use crate::frame::response::result; -use crate::frame::value::{ - BatchValues, BatchValuesFirstSerialized, BatchValuesIterator, ValueList, -}; use crate::prepared_statement::PreparedStatement; use crate::query::Query; use crate::routing::Token; @@ -116,7 +116,7 @@ impl AddressTranslator for HashMap { } #[async_trait] -// Notice: this is unefficient, but what else can we do with such poor representation as str? +// Notice: this is inefficient, but what else can we do with such poor representation as str? // After all, the cluster size is small enough to make this irrelevant. impl AddressTranslator for HashMap<&'static str, &'static str> { async fn translate_address( @@ -444,7 +444,7 @@ pub(crate) enum RunQueryResult { /// Represents a CQL session, which can be used to communicate /// with the database impl Session { - /// Estabilishes a CQL session with the database + /// Establishes a CQL session with the database /// /// Usually it's easier to use [SessionBuilder](crate::transport::session_builder::SessionBuilder) /// instead of calling `Session::connect` directly, because it's more convenient. @@ -559,6 +559,10 @@ impl Session { /// /// This is the easiest way to make a query, but performance is worse than that of prepared queries. /// + /// It is discouraged to use this method with non-empty values argument (`is_empty()` method from `SerializeRow` + /// trait returns false). In such case, query first needs to be prepared (on a single connection), so + /// driver will perform 2 round trips instead of 1. Please use [`Session::execute()`] instead. + /// /// See [the book](https://rust-driver.docs.scylladb.com/stable/queries/simple.html) for more information /// # Arguments /// * `query` - query to perform, can be just a `&str` or the [Query] struct. @@ -603,12 +607,17 @@ impl Session { pub async fn query( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { self.query_paged(query, values, None).await } /// Queries the database with a custom paging state. + /// + /// It is discouraged to use this method with non-empty values argument (`is_empty()` method from `SerializeRow` + /// trait returns false). In such case, query first needs to be prepared (on a single connection), so + /// driver will perform 2 round trips instead of 1. Please use [`Session::execute_paged()`] instead. + /// /// # Arguments /// /// * `query` - query to be performed @@ -617,11 +626,10 @@ impl Session { pub async fn query_paged( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, paging_state: Option, ) -> Result { let query: Query = query.into(); - let serialized_values = values.serialized()?; let execution_profile = query .get_execution_profile_handle() @@ -640,7 +648,8 @@ impl Session { ..Default::default() }; - let span = RequestSpan::new_query(&query.contents, serialized_values.size()); + let span = RequestSpan::new_query(&query.contents); + let span_ref = &span; let run_query_result = self .run_query( statement_info, @@ -656,19 +665,35 @@ impl Session { .unwrap_or(execution_profile.serial_consistency); // Needed to avoid moving query and values into async move block let query_ref = &query; - let values_ref = &serialized_values; + let values_ref = &values; let paging_state_ref = &paging_state; async move { - connection - .query_with_consistency( - query_ref, - values_ref, - consistency, - serial_consistency, - paging_state_ref.clone(), - ) - .await - .and_then(QueryResponse::into_non_error_query_response) + if values_ref.is_empty() { + span_ref.record_request_size(0); + connection + .query_with_consistency( + query_ref, + consistency, + serial_consistency, + paging_state_ref.clone(), + ) + .await + .and_then(QueryResponse::into_non_error_query_response) + } else { + let prepared = connection.prepare(query_ref).await?; + let serialized = prepared.serialize_values(values_ref)?; + span_ref.record_request_size(serialized.buffer_size()); + connection + .execute_with_consistency( + &prepared, + &serialized, + consistency, + serial_consistency, + paging_state_ref.clone(), + ) + .await + .and_then(QueryResponse::into_non_error_query_response) + } } }, &span, @@ -734,6 +759,10 @@ impl Session { /// Returns an async iterator (stream) over all received rows\ /// Page size can be specified in the [Query] passed to the function /// + /// It is discouraged to use this method with non-empty values argument (`is_empty()` method from `SerializeRow` + /// trait returns false). In such case, query first needs to be prepared (on a single connection), so + /// driver will initially perform 2 round trips instead of 1. Please use [`Session::execute_iter()`] instead. + /// /// See [the book](https://rust-driver.docs.scylladb.com/stable/queries/paged.html) for more information /// /// # Arguments @@ -764,24 +793,38 @@ impl Session { pub async fn query_iter( &self, query: impl Into, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { let query: Query = query.into(); - let serialized_values = values.serialized()?; let execution_profile = query .get_execution_profile_handle() .unwrap_or_else(|| self.get_default_execution_profile_handle()) .access(); - RowIterator::new_for_query( - query, - serialized_values.into_owned(), - execution_profile, - self.cluster.get_data(), - self.metrics.clone(), - ) - .await + if values.is_empty() { + RowIterator::new_for_query( + query, + execution_profile, + self.cluster.get_data(), + self.metrics.clone(), + ) + .await + } else { + // Making RowIterator::new_for_query work with values is too hard (if even possible) + // so instead of sending one prepare to a specific connection on each iterator query, + // we fully prepare a statement beforehand. + let prepared = self.prepare(query).await?; + let values = prepared.serialize_values(&values)?; + RowIterator::new_for_prepared_statement(PreparedIteratorConfig { + prepared, + values, + execution_profile, + cluster_data: self.cluster.get_data(), + metrics: self.metrics.clone(), + }) + .await + } } /// Prepares a statement on the server side and returns a prepared statement, @@ -916,7 +959,7 @@ impl Session { pub async fn execute( &self, prepared: &PreparedStatement, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { self.execute_paged(prepared, values, None).await } @@ -930,18 +973,15 @@ impl Session { pub async fn execute_paged( &self, prepared: &PreparedStatement, - values: impl ValueList, + values: impl SerializeRow, paging_state: Option, ) -> Result { - let serialized_values = values.serialized()?; + let serialized_values = prepared.serialize_values(&values)?; let values_ref = &serialized_values; let paging_state_ref = &paging_state; let (partition_key, token) = prepared - .extract_partition_key_and_calculate_token( - prepared.get_partitioner_name(), - &serialized_values, - )? + .extract_partition_key_and_calculate_token(prepared.get_partitioner_name(), values_ref)? .unzip(); let execution_profile = prepared @@ -966,7 +1006,7 @@ impl Session { let span = RequestSpan::new_prepared( partition_key.as_ref().map(|pk| pk.iter()), token, - serialized_values.size(), + serialized_values.buffer_size(), ); if !span.span().is_disabled() { @@ -1076,10 +1116,10 @@ impl Session { pub async fn execute_iter( &self, prepared: impl Into, - values: impl ValueList, + values: impl SerializeRow, ) -> Result { let prepared = prepared.into(); - let serialized_values = values.serialized()?; + let serialized_values = prepared.serialize_values(&values)?; let execution_profile = prepared .get_execution_profile_handle() @@ -1088,7 +1128,7 @@ impl Session { RowIterator::new_for_prepared_statement(PreparedIteratorConfig { prepared, - values: serialized_values.into_owned(), + values: serialized_values, execution_profile, cluster_data: self.cluster.get_data(), metrics: self.metrics.clone(), @@ -1102,6 +1142,11 @@ impl Session { /// /// Batch values must contain values for each of the queries /// + /// Avoid using non-empty values (`SerializeRow::is_empty()` return false) for simple queries + /// inside the batch. Such queries will first need to be prepared, so the driver will need to + /// send (numer_of_unprepared_queries_with_values + 1) requests instead of 1 request, severly + /// affecting performance. + /// /// See [the book](https://rust-driver.docs.scylladb.com/stable/queries/batch.html) for more information /// /// # Arguments @@ -1151,9 +1196,6 @@ impl Session { BadQuery::TooManyQueriesInBatchStatement(batch_statements_length), )); } - // Extract first serialized_value - let first_serialized_value = values.batch_values_iter().next_serialized().transpose()?; - let first_serialized_value = first_serialized_value.as_deref(); let execution_profile = batch .get_execution_profile_handle() @@ -1170,29 +1212,23 @@ impl Session { .serial_consistency .unwrap_or(execution_profile.serial_consistency); - let statement_info = match (first_serialized_value, batch.statements.first()) { - (Some(first_serialized_value), Some(BatchStatement::PreparedStatement(ps))) => { - RoutingInfo { - consistency, - serial_consistency, - token: ps.calculate_token(first_serialized_value)?, - keyspace: ps.get_keyspace_name(), - is_confirmed_lwt: false, - } - } - _ => RoutingInfo { - consistency, - serial_consistency, - ..Default::default() - }, + let keyspace_name = match batch.statements.first() { + Some(BatchStatement::PreparedStatement(ps)) => ps.get_keyspace_name(), + _ => None, }; - let first_value_token = statement_info.token; - // Reuse first serialized value when serializing query, and delegate to `BatchValues::write_next_to_request` - // directly for others (if they weren't already serialized, possibly don't even allocate the `SerializedValues`) - let values = BatchValuesFirstSerialized::new(&values, first_serialized_value); + let (first_value_token, values) = + batch_values::peek_first_token(values, batch.statements.first())?; let values_ref = &values; + let statement_info = RoutingInfo { + consistency, + serial_consistency, + token: first_value_token, + keyspace: keyspace_name, + is_confirmed_lwt: false, + }; + let span = RequestSpan::new_batch(); let run_query_result = self @@ -1891,7 +1927,7 @@ pub(crate) struct RequestSpan { } impl RequestSpan { - pub(crate) fn new_query(contents: &str, request_size: usize) -> Self { + pub(crate) fn new_query(contents: &str) -> Self { use tracing::field::Empty; let span = trace_span!( @@ -1899,7 +1935,7 @@ impl RequestSpan { kind = "unprepared", contents = contents, // - request_size = request_size, + request_size = Empty, result_size = Empty, result_rows = Empty, replicas = Empty, @@ -2013,6 +2049,10 @@ impl RequestSpan { .record("replicas", tracing::field::display(&ReplicaIps(replicas))); } + pub(crate) fn record_request_size(&self, size: usize) { + self.span.record("request_size", size); + } + pub(crate) fn inc_speculative_executions(&self) { self.speculative_executions.fetch_add(1, Ordering::Relaxed); } diff --git a/scylla/src/transport/session_builder.rs b/scylla/src/transport/session_builder.rs index 394a348ca2..09ee03b961 100644 --- a/scylla/src/transport/session_builder.rs +++ b/scylla/src/transport/session_builder.rs @@ -708,7 +708,7 @@ impl GenericSessionBuilder { pub fn keepalive_timeout(mut self, timeout: Duration) -> Self { if timeout <= Duration::from_secs(1) { warn!( - "Setting the keepalive timeout to low values ({:?}) is not recommended as it may aggresively close connections. Consider setting it above 5 seconds.", + "Setting the keepalive timeout to low values ({:?}) is not recommended as it may aggressively close connections. Consider setting it above 5 seconds.", timeout ); } diff --git a/scylla/src/transport/session_test.rs b/scylla/src/transport/session_test.rs index 805217053d..b6c9c20ba4 100644 --- a/scylla/src/transport/session_test.rs +++ b/scylla/src/transport/session_test.rs @@ -1,7 +1,6 @@ use crate as scylla; use crate::batch::{Batch, BatchStatement}; use crate::frame::response::result::Row; -use crate::frame::value::ValueList; use crate::prepared_statement::PreparedStatement; use crate::query::Query; use crate::retry_policy::{QueryInfo, RetryDecision, RetryPolicy, RetrySession}; @@ -28,7 +27,9 @@ use assert_matches::assert_matches; use bytes::Bytes; use futures::{FutureExt, StreamExt, TryStreamExt}; use itertools::Itertools; -use scylla_cql::frame::value::Value; +use scylla_cql::frame::response::result::ColumnType; +use scylla_cql::types::serialize::row::{SerializeRow, SerializedValues}; +use scylla_cql::types::serialize::value::SerializeCql; use std::collections::BTreeSet; use std::collections::{BTreeMap, HashMap}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -208,7 +209,9 @@ async fn test_prepared_statement() { .unwrap(); let values = (17_i32, 16_i32, "I'm prepared!!!"); - let serialized_values = values.serialized().unwrap().into_owned(); + let serialized_values_complex_pk = prepared_complex_pk_statement + .serialize_values(&values) + .unwrap(); session.execute(&prepared_statement, &values).await.unwrap(); session @@ -231,15 +234,14 @@ async fn test_prepared_statement() { .as_bigint() .unwrap(), }; - let prepared_token = Murmur3Partitioner.hash_one( - &prepared_statement - .compute_partition_key(&serialized_values) - .unwrap(), - ); + let prepared_token = Murmur3Partitioner + .hash_one(&prepared_statement.compute_partition_key(&values).unwrap()); assert_eq!(token, prepared_token); + let mut pk = SerializedValues::new(); + pk.add_value(&17_i32, &ColumnType::Int).unwrap(); let cluster_data_token = session .get_cluster_data() - .compute_token(&ks, "t2", (17_i32,)) + .compute_token(&ks, "t2", &pk) .unwrap(); assert_eq!(token, cluster_data_token); } @@ -259,13 +261,13 @@ async fn test_prepared_statement() { }; let prepared_token = Murmur3Partitioner.hash_one( &prepared_complex_pk_statement - .compute_partition_key(&serialized_values) + .compute_partition_key(&values) .unwrap(), ); assert_eq!(token, prepared_token); let cluster_data_token = session .get_cluster_data() - .compute_token(&ks, "complex_pk", &serialized_values) + .compute_token(&ks, "complex_pk", &serialized_values_complex_pk) .unwrap(); assert_eq!(token, cluster_data_token); } @@ -319,9 +321,10 @@ async fn test_prepared_statement() { assert!(e.is_none()); assert_eq!((a, b, c, d), (17, 16, &String::from("I'm prepared!!!"), 7)) } - // Check that ValueList macro works + // Check that SerializeRow macro works { - #[derive(scylla::ValueList, scylla::FromRow, PartialEq, Debug, Clone)] + #[derive(scylla::SerializeRow, scylla::FromRow, PartialEq, Debug, Clone)] + #[scylla(crate = crate)] struct ComplexPk { a: i32, b: i32, @@ -510,7 +513,7 @@ async fn test_token_calculation() { s.push('a'); } let values = (&s,); - let serialized_values = values.serialized().unwrap().into_owned(); + let serialized_values = prepared_statement.serialize_values(&values).unwrap(); session.execute(&prepared_statement, &values).await.unwrap(); let rs = session @@ -529,11 +532,8 @@ async fn test_token_calculation() { .as_bigint() .unwrap(), }; - let prepared_token = Murmur3Partitioner.hash_one( - &prepared_statement - .compute_partition_key(&serialized_values) - .unwrap(), - ); + let prepared_token = Murmur3Partitioner + .hash_one(&prepared_statement.compute_partition_key(&values).unwrap()); assert_eq!(token, prepared_token); let cluster_data_token = session .get_cluster_data() @@ -724,7 +724,7 @@ async fn test_use_keyspace_case_sensitivity() { .await .unwrap(); - // Use uppercase keyspace without case sesitivity + // Use uppercase keyspace without case sensitivity // Should select the lowercase one session.use_keyspace(ks_upper.clone(), false).await.unwrap(); @@ -740,7 +740,7 @@ async fn test_use_keyspace_case_sensitivity() { assert_eq!(rows, vec!["lowercase".to_string()]); - // Use uppercase keyspace with case sesitivity + // Use uppercase keyspace with case sensitivity // Should select the uppercase one session.use_keyspace(ks_upper, true).await.unwrap(); @@ -1991,14 +1991,17 @@ async fn test_unusual_valuelists() { .await .unwrap(); - let values_dyn: Vec<&dyn Value> = - vec![&1 as &dyn Value, &2 as &dyn Value, &"&dyn" as &dyn Value]; + let values_dyn: Vec<&dyn SerializeCql> = vec![ + &1 as &dyn SerializeCql, + &2 as &dyn SerializeCql, + &"&dyn" as &dyn SerializeCql, + ]; session.execute(&insert_a_b_c, values_dyn).await.unwrap(); - let values_box_dyn: Vec> = vec![ - Box::new(1) as Box, - Box::new(3) as Box, - Box::new("Box dyn") as Box, + let values_box_dyn: Vec> = vec![ + Box::new(1) as Box, + Box::new(3) as Box, + Box::new("Box dyn") as Box, ]; session .execute(&insert_a_b_c, values_box_dyn) @@ -2221,7 +2224,7 @@ async fn assert_test_batch_table_rows_contain(sess: &Session, expected_rows: &[( for expected_row in expected_rows.iter() { if !selected_rows.contains(expected_row) { panic!( - "Expected {:?} to contain row: {:?}, but they didnt", + "Expected {:?} to contain row: {:?}, but they didn't", selected_rows, expected_row ); } @@ -2776,26 +2779,24 @@ async fn test_manual_primary_key_computation() { async fn assert_tokens_equal( session: &Session, prepared: &PreparedStatement, - pk_values_in_pk_order: impl ValueList, - all_values_in_query_order: impl ValueList, + serialized_pk_values_in_pk_order: &SerializedValues, + all_values_in_query_order: impl SerializeRow, ) { - let serialized_values_in_pk_order = - pk_values_in_pk_order.serialized().unwrap().into_owned(); - let serialized_values_in_query_order = - all_values_in_query_order.serialized().unwrap().into_owned(); + let token_by_prepared = prepared + .calculate_token(&all_values_in_query_order) + .unwrap() + .unwrap(); session - .execute(prepared, &serialized_values_in_query_order) + .execute(prepared, all_values_in_query_order) .await .unwrap(); - let token_by_prepared = prepared - .calculate_token(&serialized_values_in_query_order) - .unwrap() - .unwrap(); - let token_by_hand = - calculate_token_for_partition_key(&serialized_values_in_pk_order, &Murmur3Partitioner) - .unwrap(); + let token_by_hand = calculate_token_for_partition_key( + serialized_pk_values_in_pk_order, + &Murmur3Partitioner, + ) + .unwrap(); println!( "by_prepared: {}, by_hand: {}", token_by_prepared.value, token_by_hand.value @@ -2819,13 +2820,16 @@ async fn test_manual_primary_key_computation() { .await .unwrap(); - let pk_values_in_pk_order = (17_i32,); + let mut pk_values_in_pk_order = SerializedValues::new(); + pk_values_in_pk_order + .add_value(&17_i32, &ColumnType::Int) + .unwrap(); let all_values_in_query_order = (17_i32, 16_i32, "I'm prepared!!!"); assert_tokens_equal( &session, &prepared_simple_pk, - pk_values_in_pk_order, + &pk_values_in_pk_order, all_values_in_query_order, ) .await; @@ -2845,13 +2849,22 @@ async fn test_manual_primary_key_computation() { .await .unwrap(); - let pk_values_in_pk_order = (17_i32, 16_i32, "I'm prepared!!!"); + let mut pk_values_in_pk_order = SerializedValues::new(); + pk_values_in_pk_order + .add_value(&17_i32, &ColumnType::Int) + .unwrap(); + pk_values_in_pk_order + .add_value(&16_i32, &ColumnType::Int) + .unwrap(); + pk_values_in_pk_order + .add_value(&"I'm prepared!!!", &ColumnType::Ascii) + .unwrap(); let all_values_in_query_order = (17_i32, "I'm prepared!!!", 16_i32); assert_tokens_equal( &session, &prepared_complex_pk, - pk_values_in_pk_order, + &pk_values_in_pk_order, all_values_in_query_order, ) .await; diff --git a/scylla/src/transport/silent_prepare_batch_test.rs b/scylla/src/transport/silent_prepare_batch_test.rs index 3a2ed83baa..469c90b49f 100644 --- a/scylla/src/transport/silent_prepare_batch_test.rs +++ b/scylla/src/transport/silent_prepare_batch_test.rs @@ -102,7 +102,7 @@ async fn assert_test_batch_table_rows_contain(sess: &Session, expected_rows: &[( for expected_row in expected_rows.iter() { if !selected_rows.contains(expected_row) { panic!( - "Expected {:?} to contain row: {:?}, but they didnt", + "Expected {:?} to contain row: {:?}, but they didn't", selected_rows, expected_row ); } diff --git a/scylla/src/transport/topology.rs b/scylla/src/transport/topology.rs index 63ee14f5b2..e7a2adcff2 100644 --- a/scylla/src/transport/topology.rs +++ b/scylla/src/transport/topology.rs @@ -15,7 +15,6 @@ use rand::seq::SliceRandom; use rand::{thread_rng, Rng}; use scylla_cql::errors::NewSessionError; use scylla_cql::frame::response::result::Row; -use scylla_cql::frame::value::ValueList; use scylla_macros::FromRow; use std::borrow::BorrowMut; use std::cell::Cell; @@ -751,7 +750,7 @@ async fn query_peers(conn: &Arc, connect_port: u16) -> Result, connect_port: u16) -> Result( conn: &Arc, - query_str: &str, - keyspaces_to_fetch: &[String], -) -> impl Stream> { - let keyspaces = &[keyspaces_to_fetch] as &[&[String]]; - let (query_str, query_values) = if !keyspaces_to_fetch.is_empty() { - (format!("{query_str} where keyspace_name in ?"), keyspaces) - } else { - (query_str.into(), &[] as &[&[String]]) - }; - let query_values = query_values.serialized().map(|sv| sv.into_owned()); - let mut query = Query::new(query_str); + query_str: &'a str, + keyspaces_to_fetch: &'a [String], +) -> impl Stream> + 'a { let conn = conn.clone(); - query.set_page_size(1024); + let fut = async move { - let query_values = query_values?; - conn.query_iter(query, query_values).await + if keyspaces_to_fetch.is_empty() { + let mut query = Query::new(query_str); + query.set_page_size(1024); + + conn.query_iter(query).await + } else { + let keyspaces = &[keyspaces_to_fetch] as &[&[String]]; + let query_str = format!("{query_str} where keyspace_name in ?"); + + let mut query = Query::new(query_str); + query.set_page_size(1024); + + let prepared = conn.prepare(&query).await?; + let serialized_values = prepared.serialize_values(&keyspaces)?; + conn.execute_iter(prepared, serialized_values).await + } }; fut.into_stream().try_flatten() } @@ -1601,7 +1606,7 @@ async fn query_table_partitioners( let rows = conn .clone() - .query_iter(partitioner_query, &[]) + .query_iter(partitioner_query) .into_stream() .try_flatten(); diff --git a/scylla/src/utils/pretty.rs b/scylla/src/utils/pretty.rs index 37851e0883..c3e6fb71fa 100644 --- a/scylla/src/utils/pretty.rs +++ b/scylla/src/utils/pretty.rs @@ -1,5 +1,8 @@ use chrono::{LocalResult, TimeZone, Utc}; -use scylla_cql::frame::response::result::CqlValue; +use scylla_cql::frame::{ + response::result::CqlValue, + value::{CqlDate, CqlTime, CqlTimestamp}, +}; use std::borrow::Borrow; use std::fmt::{Display, LowerHex, UpperHex}; @@ -50,7 +53,7 @@ where CqlValue::TinyInt(ti) => write!(f, "{}", ti)?, CqlValue::Varint(vi) => write!(f, "{}", vi)?, CqlValue::Counter(c) => write!(f, "{}", c.0)?, - CqlValue::Date(d) => { + CqlValue::Date(CqlDate(d)) => { let days_since_epoch = chrono::Duration::days(*d as i64 - (1 << 31)); // TODO: chrono::NaiveDate does not handle the whole range @@ -64,18 +67,18 @@ where } } CqlValue::Duration(d) => write!(f, "{}mo{}d{}ns", d.months, d.days, d.nanoseconds)?, - CqlValue::Time(t) => { + CqlValue::Time(CqlTime(t)) => { write!( f, "'{:02}:{:02}:{:02}.{:09}'", - t.num_hours() % 24, - t.num_minutes() % 60, - t.num_seconds() % 60, - t.num_nanoseconds().unwrap_or(0) % 1_000_000_000, + t / 3_600_000_000_000, + t / 60_000_000_000 % 60, + t / 1_000_000_000 % 60, + t % 1_000_000_000, )?; } - CqlValue::Timestamp(t) => { - match Utc.timestamp_millis_opt(t.num_milliseconds()) { + CqlValue::Timestamp(CqlTimestamp(t)) => { + match Utc.timestamp_millis_opt(*t) { LocalResult::Ambiguous(_, _) => unreachable!(), // not supposed to happen with timestamp_millis_opt LocalResult::Single(d) => { write!(f, "{}", d.format("'%Y-%m-%d %H:%M:%S%.3f%z'"))? @@ -205,13 +208,12 @@ where #[cfg(test)] mod tests { use std::str::FromStr; - use std::time::Duration; use bigdecimal::BigDecimal; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use scylla_cql::frame::response::result::CqlValue; - use scylla_cql::frame::value::CqlDuration; + use scylla_cql::frame::value::{CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use crate::utils::pretty::CqlValueDisplayer; @@ -247,7 +249,10 @@ mod tests { // Time types are the most tricky assert_eq!( - format!("{}", CqlValueDisplayer(CqlValue::Date(40 + (1 << 31)))), + format!( + "{}", + CqlValueDisplayer(CqlValue::Date(CqlDate(40 + (1 << 31)))) + ), "'1970-02-10'" ); assert_eq!( @@ -261,15 +266,12 @@ mod tests { ), "1mo2d3ns" ); - let t = Duration::from_nanos(123) - + Duration::from_secs(4) - + Duration::from_secs(5 * 60) - + Duration::from_secs(6 * 60 * 60); + let t = chrono::NaiveTime::from_hms_nano_opt(6, 5, 4, 123) + .unwrap() + .signed_duration_since(chrono::NaiveTime::MIN); + let t = t.num_nanoseconds().unwrap(); assert_eq!( - format!( - "{}", - CqlValueDisplayer(CqlValue::Time(chrono::Duration::from_std(t).unwrap())) - ), + format!("{}", CqlValueDisplayer(CqlValue::Time(CqlTime(t)))), "'06:05:04.000000123'" ); @@ -279,9 +281,10 @@ mod tests { assert_eq!( format!( "{}", - CqlValueDisplayer(CqlValue::Timestamp( + CqlValueDisplayer(CqlValue::Timestamp(CqlTimestamp( t.signed_duration_since(NaiveDateTime::default()) - )) + .num_milliseconds() + ))) ), "'2005-04-02 19:37:42.000+0000'" ); diff --git a/scylla/tests/integration/consistency.rs b/scylla/tests/integration/consistency.rs index 93a0ad8f89..e265265335 100644 --- a/scylla/tests/integration/consistency.rs +++ b/scylla/tests/integration/consistency.rs @@ -260,15 +260,18 @@ async fn consistency_is_correctly_set_in_cql_requests() { |proxy_uris, translation_map, mut running_proxy| async move { let request_rule = |tx| { RequestRule( - Condition::or( - Condition::RequestOpcode(RequestOpcode::Execute), + Condition::and( + Condition::not(Condition::ConnectionRegisteredAnyEvent), Condition::or( - Condition::RequestOpcode(RequestOpcode::Batch), - Condition::and( - Condition::RequestOpcode(RequestOpcode::Query), - Condition::BodyContainsCaseSensitive(Box::new( - *b"INTO consistency_tests", - )), + Condition::RequestOpcode(RequestOpcode::Execute), + Condition::or( + Condition::RequestOpcode(RequestOpcode::Batch), + Condition::and( + Condition::RequestOpcode(RequestOpcode::Query), + Condition::BodyContainsCaseSensitive(Box::new( + *b"INTO consistency_tests", + )), + ), ), ), ), diff --git a/scylla/tests/integration/hygiene.rs b/scylla/tests/integration/hygiene.rs index 6195bb0256..cf2aaed7b3 100644 --- a/scylla/tests/integration/hygiene.rs +++ b/scylla/tests/integration/hygiene.rs @@ -63,6 +63,12 @@ macro_rules! test_crate { let sv2 = tuple_with_same_layout.serialized().unwrap().into_owned(); assert_eq!(sv, sv2); } + + #[derive(_scylla::macros::SerializeCql, _scylla::macros::SerializeRow)] + #[scylla(crate = _scylla)] + struct TestStructNew { + x: ::core::primitive::i32, + } }; } diff --git a/scylla/tests/integration/shards.rs b/scylla/tests/integration/shards.rs index 7bee6a3a35..f8aa2335b6 100644 --- a/scylla/tests/integration/shards.rs +++ b/scylla/tests/integration/shards.rs @@ -23,7 +23,7 @@ async fn test_consistent_shard_awareness() { }).unzip(); for (i, tx) in feedback_txs.iter().cloned().enumerate() { running_proxy.running_nodes[i].change_request_rules(Some(vec![ - RequestRule(Condition::RequestOpcode(RequestOpcode::Execute), RequestReaction::noop().with_feedback_when_performed(tx)) + RequestRule(Condition::RequestOpcode(RequestOpcode::Execute).and(Condition::not(Condition::ConnectionRegisteredAnyEvent)), RequestReaction::noop().with_feedback_when_performed(tx)) ])); } diff --git a/test/cluster/cassandra/docker-compose.yml b/test/cluster/cassandra/docker-compose.yml index d271285380..aa46efd1f6 100644 --- a/test/cluster/cassandra/docker-compose.yml +++ b/test/cluster/cassandra/docker-compose.yml @@ -10,7 +10,7 @@ networks: - subnet: 172.42.0.0/16 services: cassandra1: - image: cassandra:4.0.7 + image: cassandra healthcheck: test: ["CMD", "cqlsh", "-e", "describe keyspaces" ] interval: 5s @@ -24,7 +24,7 @@ services: - HEAP_NEWSIZE=512M - MAX_HEAP_SIZE=2048M cassandra2: - image: cassandra:4.0.7 + image: cassandra healthcheck: test: ["CMD", "cqlsh", "-e", "describe keyspaces" ] interval: 5s @@ -42,7 +42,7 @@ services: cassandra1: condition: service_healthy cassandra3: - image: cassandra:4.0.7 + image: cassandra healthcheck: test: ["CMD", "cqlsh", "-e", "describe keyspaces" ] interval: 5s diff --git a/test/cluster/docker-compose.yml b/test/cluster/docker-compose.yml index 0fa1e04327..210cc0b738 100644 --- a/test/cluster/docker-compose.yml +++ b/test/cluster/docker-compose.yml @@ -23,7 +23,7 @@ services: --smp 2 --memory 1G healthcheck: - test: [ "CMD", "cqlsh", "-e", "select * from system.local" ] + test: [ "CMD", "cqlsh", "scylla1", "-e", "select * from system.local" ] interval: 5s timeout: 5s retries: 60 @@ -41,7 +41,7 @@ services: --smp 2 --memory 1G healthcheck: - test: [ "CMD", "cqlsh", "-e", "select * from system.local" ] + test: [ "CMD", "cqlsh", "scylla2", "-e", "select * from system.local" ] interval: 5s timeout: 5s retries: 60 @@ -62,7 +62,7 @@ services: --smp 2 --memory 1G healthcheck: - test: [ "CMD", "cqlsh", "-e", "select * from system.local" ] + test: [ "CMD", "cqlsh", "scylla3", "-e", "select * from system.local" ] interval: 5s timeout: 5s retries: 60 diff --git a/test/dockerized/run.sh b/test/dockerized/run.sh index 54c44ea2cb..c7bb989584 100755 --- a/test/dockerized/run.sh +++ b/test/dockerized/run.sh @@ -9,7 +9,7 @@ fi IMAGE_NAME="scylla_rust_driver_testing" -# Build a new image with embeded driver source files and deletes the +# Build a new image with embedded driver source files and deletes the # previously built image docker tag "$IMAGE_NAME:latest" "$IMAGE_NAME:previous" &>/dev/null if docker build -f test/dockerized/Dockerfile -t "$IMAGE_NAME:latest" . ; then diff --git a/test/tls/scylla.yaml b/test/tls/scylla.yaml index ee4eee6a45..cd36533c4d 100644 --- a/test/tls/scylla.yaml +++ b/test/tls/scylla.yaml @@ -361,7 +361,7 @@ commitlog_total_space_in_mb: -1 # tombstones seen in memory so we can return them to the coordinator, which # will use them to make sure other replicas also know about the deleted rows. # With workloads that generate a lot of tombstones, this can cause performance -# problems and even exaust the server heap. +# problems and even exhaust the server heap. # (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) # Adjust the thresholds here if you understand the dangers and want to # scan more tombstones anyway. These thresholds may also be adjusted at runtime @@ -460,7 +460,7 @@ client_encryption_options: # not met, performance and reliability can be degraded. # # These requirements include: -# - A filesystem with good support for aysnchronous I/O (AIO). Currently, +# - A filesystem with good support for asynchronous I/O (AIO). Currently, # this means XFS. # # false: strict environment checks are in place; do not start if they are not met. @@ -493,7 +493,7 @@ client_encryption_options: # [shard0] [shard1] ... [shardN-1] [shard0] [shard1] ... [shardN-1] ... # # Scylla versions 1.6 and below used just one repetition of the pattern; -# this intefered with data placement among nodes (vnodes). +# this interfered with data placement among nodes (vnodes). # # Scylla versions 1.7 and above use 4096 repetitions of the pattern; this # provides for better data distribution.