diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a17025ec..2a8db0cb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.0.1 +current_version = 4.0.4 commit = False tag = False diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..aa4cd090 --- /dev/null +++ b/.clang-format @@ -0,0 +1,36 @@ +--- +BasedOnStyle: Microsoft +AlignAfterOpenBracket: AlwaysBreak +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllConstructorInitializersOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: 'false' +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: 'true' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'false' +BinPackParameters: 'false' +BreakBeforeTernaryOperators: 'true' +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +ColumnLimit: '80' +CompactNamespaces: 'true' +FixNamespaceComments: 'false' +IndentWidth: '4' +PointerAlignment: Left +SortIncludes: 'false' +SortUsingDeclarations: 'true' +SpaceAfterTemplateKeyword: 'true' +UseTab: Never +IndentPPDirectives: AfterHash +AccessModifierOffset: -4 +BreakBeforeBraces: Custom +BraceWrapping: + AfterExternBlock: false +... diff --git a/.gitignore b/.gitignore index 777983e4..cd332d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ target build +build_CXX20 cmake-build-debug cmake-build-release .vs .vscode .idea +.cache questdb-rs/Cargo.lock include/questdb/ingress/line_sender.gen.h cython/questdb/ingress/line_sender.pxd \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e4323d6..8de0cb5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15.0) -project(c-questdb-client VERSION 4.0.1) +project(c-questdb-client VERSION 4.0.4) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) @@ -16,7 +16,13 @@ set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 17) +set(MINIMUM_REQUIRED_CXX_STANDARD 17) + +if((NOT CMAKE_CXX_STANDARD) OR (CMAKE_CXX_STANDARD LESS MINIMUM_REQUIRED_CXX_STANDARD)) + set(CMAKE_CXX_STANDARD ${MINIMUM_REQUIRED_CXX_STANDARD}) +endif() +message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}") + set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/README.md b/README.md index bf670d79..f4f1931c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See the [flush troubleshooting](doc/CONSIDERATIONS.md) docs for more details on how to debug ILP/TCP. For an overview and code examples, see the -[InfluxDB Line Protocol page of the developer docs](https://questdb.io/docs/develop/insert-data/#influxdb-line-protocol). +[Ingestion overview page of the developer docs](https://questdb.io/docs/ingestion-overview/). To understand the protocol in more depth, consult the [protocol reference docs](https://questdb.io/docs/reference/api/ilp/overview/). @@ -70,9 +70,9 @@ To get started, read the language-specific guides. ## Community If you need help, have additional questions or want to provide feedback, you -may find us on [Slack](https://slack.questdb.io). +may find us on our [Community Forum](https://community.questdb.io/). -You can also [sign up to our mailing list](https://questdb.io/community/) +You can also [sign up to our mailing list](https://questdb.io/contributors/) to get notified of new releases. ## License diff --git a/cbindgen.toml b/cbindgen.toml index 3d94ffdb..09277c6b 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -8,7 +8,7 @@ header = """/******************************************************************* * \\__\\_\\\\__,_|\\___||___/\\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ci/compile.yaml b/ci/compile.yaml index e761d75c..aa9e1059 100644 --- a/ci/compile.yaml +++ b/ci/compile.yaml @@ -8,7 +8,15 @@ steps: env: JAVA_HOME: $(JAVA_HOME_11_X64) displayName: "Build Makefile with CMake" - - script: cmake --build build + - script: cmake --build build --config Release + env: + JAVA_HOME: $(JAVA_HOME_11_X64) + displayName: "Make" + - script: cmake -S . -B build_CXX20 -DCMAKE_BUILD_TYPE=Release -DQUESTDB_TESTS_AND_EXAMPLES=ON -DCMAKE_CXX_STANDARD=20 + env: + JAVA_HOME: $(JAVA_HOME_11_X64) + displayName: "Build Makefile with CMake" + - script: cmake --build build_CXX20 --config Release env: JAVA_HOME: $(JAVA_HOME_11_X64) displayName: "Make" diff --git a/ci/format_cpp.py b/ci/format_cpp.py new file mode 100755 index 00000000..79d1257e --- /dev/null +++ b/ci/format_cpp.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +""" +Format all the C and C++ code using `clang-format`. +If --check is passed, check for formatting issues instead of modifying files. +""" + +import sys +sys.dont_write_bytecode = True +import subprocess + +FILES = [ + 'include/questdb/ingress/line_sender.h', + 'include/questdb/ingress/line_sender.hpp', + 'cpp_test/build_env.h', + 'cpp_test/mock_server.hpp', + 'cpp_test/mock_server.cpp', + 'cpp_test/test_line_sender.cpp', +] + +if __name__ == '__main__': + check_mode = '--check' in sys.argv + command = [ + 'clang-format', + '--style=file', + '--dry-run' if check_mode else '-i' + ] + FILES + subprocess.check_call(command) diff --git a/ci/run_all_tests.py b/ci/run_all_tests.py index 3a976c10..b4e4dc4a 100644 --- a/ci/run_all_tests.py +++ b/ci/run_all_tests.py @@ -31,20 +31,30 @@ def run_cmd(*args, cwd=None): sys.stderr.write(f'Command {args!r} failed with return code {cpe.returncode}.\n') sys.exit(cpe.returncode) - def main(): build_dir = pathlib.Path('build') exe_suffix = '.exe' if platform.system() == 'Windows' else '' test_line_sender_path = next(iter( build_dir.glob(f'**/test_line_sender{exe_suffix}'))) - system_test_path = pathlib.Path('system_test') / 'test.py' - qdb_v = '7.3.10' # The version of QuestDB we'll test against. + build_cxx20_dir = pathlib.Path('build_CXX20') + test_line_sender_path_CXX20 = next(iter( + build_cxx20_dir.glob(f'**/test_line_sender{exe_suffix}'))) - run_cmd('cargo', 'test', '--', '--nocapture', cwd='questdb-rs') - run_cmd('cargo', 'test', '--all-features', '--', '--nocapture', cwd='questdb-rs') + system_test_path = pathlib.Path('system_test') / 'test.py' + qdb_v = '8.2.3' # The version of QuestDB we'll test against. + + run_cmd('cargo', 'test', + '--', '--nocapture', cwd='questdb-rs') + run_cmd('cargo', 'test', '--no-default-features', '--features=aws-lc-crypto,tls-native-certs', + '--', '--nocapture', cwd='questdb-rs') + run_cmd('cargo', 'test', '--no-default-features', '--features=ring-crypto,tls-native-certs,ilp-over-http', + '--', '--nocapture', cwd='questdb-rs') + run_cmd('cargo', 'test', '--features=almost-all-features', + '--', '--nocapture', cwd='questdb-rs') run_cmd(str(test_line_sender_path)) + run_cmd(str(test_line_sender_path_CXX20)) run_cmd('python3', str(system_test_path), 'run', '--versions', qdb_v, '-v') if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/ci/run_tests_pipeline.yaml b/ci/run_tests_pipeline.yaml index 78c25296..1fced6be 100644 --- a/ci/run_tests_pipeline.yaml +++ b/ci/run_tests_pipeline.yaml @@ -52,7 +52,7 @@ stages: - template: compile.yaml - script: | cd questdb-rs - cargo build --examples --all-features + cargo build --examples --features almost-all-features displayName: "Build Rust examples" - script: python3 ci/run_all_tests.py env: @@ -62,7 +62,7 @@ stages: inputs: pathToPublish: ./build displayName: "Publish build directory" - - job: CargoFmtAndClippy + - job: FormatAndLinting displayName: "cargo fmt and clippy" pool: vmImage: 'ubuntu-latest' @@ -70,16 +70,17 @@ stages: steps: - checkout: self - script: | + apt install clang-format rustup component add clippy rustup component add rustfmt - displayName: "Install clippy and rustfmt" + displayName: "Install clang-format, clippy and rustfmt" - script: | cd questdb-rs cargo fmt --all -- --check displayName: "questdb-rs: fmt" - script: | cd questdb-rs - cargo clippy --all-targets --all-features -- -D warnings + cargo clippy --all-targets --features almost-all-features -- -D warnings displayName: "questdb-rs: clippy" - script: | cd questdb-rs-ffi @@ -89,6 +90,9 @@ stages: cd questdb-rs-ffi cargo clippy --all-targets --all-features -- -D warnings displayName: "questdb-rs-ffi: clippy" + - script: | + python3 ci/format_cpp.py --check + displayName: "C/C++ clang-format" - script: | cd system_test cd tls_proxy diff --git a/corrosion/.github/actions/setup_test/action.yaml b/corrosion/.github/actions/setup_test/action.yaml index 4406e886..bb36278c 100644 --- a/corrosion/.github/actions/setup_test/action.yaml +++ b/corrosion/.github/actions/setup_test/action.yaml @@ -37,7 +37,7 @@ runs: steps: - name: Cache Cargo registry id: cache-registry - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry @@ -138,10 +138,10 @@ runs: arch: ${{ steps.arch_flags.outputs.msvc }} if: ${{ 'msvc' == steps.determine_abi.outputs.abi }} - name: Install CMake - uses: corrosion-rs/install-cmake@v2 + uses: lukka/get-cmake@519de0c7b4812477d74976b2523a9417f552d126 with: - cmake: ${{inputs.cmake}} - ninja: 1.10.0 + cmakeVersion: "${{ inputs.cmake }}" + ninjaVersion: "~1.10.0" - name: Install Rust id: install_rust uses: dtolnay/rust-toolchain@master diff --git a/corrosion/.github/workflows/gh-pages.yaml b/corrosion/.github/workflows/gh-pages.yaml index b9571478..e8a4d317 100644 --- a/corrosion/.github/workflows/gh-pages.yaml +++ b/corrosion/.github/workflows/gh-pages.yaml @@ -24,7 +24,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v3 - name: Install mdbook @@ -39,7 +39,7 @@ jobs: cd doc mdbook build # Override mdbooks default highlight.js with a custom version containing CMake support. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: repository: 'highlightjs/highlight.js' # mdbook currently (as of v0.4.27) does not support v11 yet. diff --git a/corrosion/.github/workflows/test.yaml b/corrosion/.github/workflows/test.yaml index f7c784d7..e93987ff 100644 --- a/corrosion/.github/workflows/test.yaml +++ b/corrosion/.github/workflows/test.yaml @@ -53,7 +53,7 @@ jobs: name: Test MSRV of the new lockfile runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust id: install_rust uses: dtolnay/rust-toolchain@1.56 @@ -164,7 +164,7 @@ jobs: abi: gnu steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Environment and Configure CMake uses: "./.github/actions/setup_test" with: @@ -204,13 +204,13 @@ jobs: cmake: 3.21.5 # VS on windows-2022 requires at least CMake 3.21 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # The initial configure for MSVC is quite slow, so we cache the build directory # (including the build directories of the tests) since reconfiguring is # significantly faster. - name: Cache MSVC build directory id: cache-msvc-builddir - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build key: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.rust }}-msvc-build @@ -243,8 +243,8 @@ jobs: # - os: windows-2019 # abi: gnu steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 id: cache_cxxbridge with: path: "~/.cargo/bin/cxxbridge*" @@ -283,15 +283,15 @@ jobs: - os: macos-12 rust: 1.54.0 # On MacOS-12 linking fails before Rust 1.54 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup MSVC Development Environment uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' - name: Install CMake - uses: corrosion-rs/install-cmake@v2 + uses: lukka/get-cmake@519de0c7b4812477d74976b2523a9417f552d126 with: - cmake: 3.18.0 - ninja: 1.10.0 + cmakeVersion: "~3.18.0" + ninjaVersion: "~1.10.0" - name: Install Rust uses: dtolnay/rust-toolchain@master with: diff --git a/corrosion/.github/workflows/test_legacy.yaml b/corrosion/.github/workflows/test_legacy.yaml index 8aa95462..20c7bf58 100644 --- a/corrosion/.github/workflows/test_legacy.yaml +++ b/corrosion/.github/workflows/test_legacy.yaml @@ -26,10 +26,10 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache Legacy Generator id: cache_generator - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{github.workspace}}/corrosion-prebuilt-generator key: ${{ runner.os }}-${{ inputs.rust }}-generator-${{ hashFiles('generator/src/**', 'generator/Cargo.toml', 'generator/Cargo.lock') }} diff --git a/corrosion/CMakeLists.txt b/corrosion/CMakeLists.txt index 678d133b..8b3d7d3d 100644 --- a/corrosion/CMakeLists.txt +++ b/corrosion/CMakeLists.txt @@ -5,7 +5,7 @@ project(Corrosion # tagged release. Users don't need to care about this, it is mainly to # clearly see in configure logs which version was used, without needing to # rely on `git`, since Corrosion may be installed or otherwise packaged. - VERSION 0.4.3 + VERSION 0.5.0 LANGUAGES NONE HOMEPAGE_URL "https://corrosion-rs.github.io/corrosion/" ) diff --git a/corrosion/README.md b/corrosion/README.md index fa14ad4e..4462d958 100644 --- a/corrosion/README.md +++ b/corrosion/README.md @@ -27,7 +27,7 @@ include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.4 # Optionally specify a commit hash, version tag or branch here + GIT_TAG v0.5 # Optionally specify a commit hash, version tag or branch here ) FetchContent_MakeAvailable(Corrosion) diff --git a/corrosion/RELEASES.md b/corrosion/RELEASES.md index 7a9097c6..b9444fcc 100644 --- a/corrosion/RELEASES.md +++ b/corrosion/RELEASES.md @@ -1,3 +1,107 @@ +# v0.5.1 (2024-12-29) + +### Fixes + +- Update FindRust to support `rustup` v1.28.0. Support for older rustup versions is retained, + so updating corrosion quickly is recommended to all rustup users. + +# v0.5.0 (2024-05-11) + +### Breaking Changes + +- Dashes (`-`) in names of imported CMake **library** targets are now replaced with underscores (`_`). + See [issue #501] for details. Users on older Corrosion versions will experience the same + change when using Rust 1.79 or newer. `bin` targets are not affected by this change. + +[issue #501]: https://github.com/corrosion-rs/corrosion/issues/501 + +# v0.4.10 (2024-05-11) + +### New features + +- `corrosion_experimental_cbindgen()` can now be called multiple times on the same Rust target, + as long as the output header name differs. This may be useful to generate separate C and C++ + bindings. [#507] +- If `corrosion_link_libraries()` is called on a Rust static library target, then + `target_link_libraries()` is called to propagate the dependencies to C/C++ consumers. + Previously a warning was emitted in this case and the arguments ignored. [#506] + +### Fixes + +- Combine `-framework` flags on macos to avoid linker deduplication errors [#455] +- `corrosion_experimental_cbindgen()` will now correctly use the package name, instead of assuming that + the package and crate name are identical. ([11e27c]) +- Set the `AR_<triple>` variable for `cc-rs` (except for msvc targets) [#456] +- Fix hostbuild when cross-compiling to windows [#477] +- Consider vworks executable suffix [#504] +- `corrosion_experimental_cbindgen()` now forwards the Rust target-triple (e.g. `aarch64-unknown-linux-gnu`) + to cbindgen via the `TARGET` environment variable. The `hostbuild` property is considered. [#507] +- Fix linking errors with Rust >= 1.79 and `-msvc` targets.` [#511] + + +[#455]: https://github.com/corrosion-rs/corrosion/pull/455 +[#456]: https://github.com/corrosion-rs/corrosion/pull/456 +[#477]: https://github.com/corrosion-rs/corrosion/pull/477 +[#504]: https://github.com/corrosion-rs/corrosion/pull/504 +[#506]: https://github.com/corrosion-rs/corrosion/pull/506 +[#507]: https://github.com/corrosion-rs/corrosion/pull/507 +[#511]: https://github.com/corrosion-rs/corrosion/pull/511 +[11e27c]: https://github.com/corrosion-rs/corrosion/pull/514/commits/11e27cde2cf32c7ed539c96eb03c2f10035de538 + +# v0.4.9 (2024-05-01) + +### New Features + +- Automatically detect Rust target for OpenHarmony ([#510]). + +### Fixes + +- Make find_package portable ([#509]). + +[#510]: https://github.com/corrosion-rs/corrosion/pull/510 +[#509]: https://github.com/corrosion-rs/corrosion/pull/509 + +# v0.4.8 (2024-04-03) + +### Fixes + +- Fix an internal error when passing both the `PROFILE` and `CRATES` option to + `corrosion_import_crate()` ([#496]). + +[#496]: https://github.com/corrosion-rs/corrosion/pull/496 + +# v0.4.7 (2024-01-19) + +### Fixes + +- The C/C++ compiler passed from corrosion to `cc-rs` can now be overriden by users setting + `CC_<target>` (e.g. `CC_x86_64-unknown-linux-gnu=/path/to/my-compiler`) environment variables ([#475]). + +[#475]: https://github.com/corrosion-rs/corrosion/pull/475 + +# v0.4.6 (2024-01-17) + +### Fixes + +- Fix hostbuild executables when cross-compiling from non-windows to windows targets. + (Only with CMake >= 3.19). + +# v0.4.5 (2023-11-30) + +### Fixes + +- Fix hostbuild executables when cross-compiling on windows to non-windows targets + (Only with CMake >= 3.19). + +# v0.4.4 (2023-10-06) + +### Fixes + +- Add `chimera` ([#445]) and `unikraft` ([#446]) to the list of known vendors + +[#445]: https://github.com/corrosion-rs/corrosion/pull/445 +[#446]: https://github.com/corrosion-rs/corrosion/pull/446 + # v0.4.3 (2023-09-09) ### Fixes diff --git a/corrosion/cmake/Corrosion.cmake b/corrosion/cmake/Corrosion.cmake index 876ae5c1..1eeaff9d 100644 --- a/corrosion/cmake/Corrosion.cmake +++ b/corrosion/cmake/Corrosion.cmake @@ -107,34 +107,24 @@ get_property( TARGET Rust::Cargo PROPERTY IMPORTED_LOCATION ) -# Note: Legacy function, used when respecting the `XYZ_OUTPUT_DIRECTORY` target properties is not -# possible. -function(_corrosion_set_imported_location_legacy target_name base_property filename) - foreach(config_type ${CMAKE_CONFIGURATION_TYPES}) - set(binary_root "${CMAKE_CURRENT_BINARY_DIR}/${config_type}") - string(TOUPPER "${config_type}" config_type_upper) - message(DEBUG "Setting ${base_property}_${config_type_upper} for target ${target_name}" - " to `${binary_root}/${filename}`.") - # For Multiconfig we want to specify the correct location for each configuration - set_property( - TARGET ${target_name} - PROPERTY "${base_property}_${config_type_upper}" - "${binary_root}/${filename}" - ) - endforeach() - if(NOT COR_IS_MULTI_CONFIG) - set(binary_root "${CMAKE_CURRENT_BINARY_DIR}") - endif() - message(DEBUG "Setting ${base_property} for target ${target_name}" - " to `${binary_root}/${filename}`.") +# Sets out_var to true if the byproduct copying and imported location is done in a deferred +# manner to respect target properties, etc. that may be set later. +function(_corrosion_determine_deferred_byproduct_copying_and_import_location_handling out_var) + set(${out_var} ${CORROSION_RESPECT_OUTPUT_DIRECTORY} PARENT_SCOPE) +endfunction() - # IMPORTED_LOCATION must be set regardless of possible overrides. In the multiconfig case, - # the last configuration "wins". - set_property( - TARGET ${target_name} - PROPERTY "${base_property}" "${binary_root}/${filename}" - ) +function(_corrosion_bin_target_suffix target_name out_var_suffix) + get_target_property(hostbuild "${target_name}" ${_CORR_PROP_HOST_BUILD}) + if((hostbuild AND CMAKE_HOST_WIN32) + OR ((NOT hostbuild) AND (Rust_CARGO_TARGET_OS STREQUAL "windows"))) + set(_suffix ".exe") + elseif(Rust_CARGO_TARGET_OS STREQUAL "vxworks") + set(_suffix ".vxe") + else() + set(_suffix "") + endif() + set(${out_var_suffix} "${_suffix}" PARENT_SCOPE) endfunction() # Do not call this function directly! @@ -151,6 +141,19 @@ function(_corrosion_set_imported_location_deferred target_name base_property out else() set(output_dir_prop_target_name "${target_name}") endif() + if(CORROSION_NATIVE_TOOLING) + set(output_directory_property "INTERFACE_${output_directory_property}") + endif() + + # Append .exe suffix for executable by-products if the target is windows or if it's a host + # build and the host is Windows. + get_target_property(target_type ${target_name} TYPE) + if(${target_type} STREQUAL "EXECUTABLE" AND (NOT "${filename}" MATCHES "\.pdb$")) + _corrosion_bin_target_suffix(${target_name} "suffix") + if(suffix) + set(filename "${filename}${suffix}") + endif() + endif() get_target_property(output_directory "${output_dir_prop_target_name}" "${output_directory_property}") message(DEBUG "Output directory property (target ${output_dir_prop_target_name}): ${output_directory_property} dir: ${output_directory}") @@ -228,42 +231,16 @@ endfunction() # artifact. # - filename of the artifact. function(_corrosion_set_imported_location target_name base_property output_directory_property filename) - if(CORROSION_RESPECT_OUTPUT_DIRECTORY) + _corrosion_determine_deferred_byproduct_copying_and_import_location_handling("defer") + if(defer) _corrosion_call_set_imported_location_deferred("${target_name}" "${base_property}" "${output_directory_property}" "${filename}") else() - _corrosion_set_imported_location_legacy("${target_name}" "${base_property}" "${filename}") + # We can't actually call the function in a deferred way, but we can still respect the output directory + # variables that were set **before** importing the crate. + _corrosion_set_imported_location_deferred("${target_name}" "${base_property}" "${output_directory_property}" "${filename}") endif() endfunction() -function(_corrosion_copy_byproduct_legacy target_name cargo_build_dir file_names) - if(ARGN) - message(FATAL_ERROR "Unexpected additional arguments") - endif() - - if(COR_IS_MULTI_CONFIG) - set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>") - else() - set(output_dir "${CMAKE_CURRENT_BINARY_DIR}") - endif() - - list(TRANSFORM file_names PREPEND "${cargo_build_dir}/" OUTPUT_VARIABLE src_file_names) - list(TRANSFORM file_names PREPEND "${output_dir}/" OUTPUT_VARIABLE dst_file_names) - message(DEBUG "Adding command to copy byproducts `${file_names}` to ${dst_file_names}") - add_custom_command(TARGET _cargo-build_${target_name} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${output_dir}" - COMMAND - ${CMAKE_COMMAND} -E copy_if_different - # tested to work with both multiple files and paths with spaces - ${src_file_names} - "${output_dir}" - BYPRODUCTS ${dst_file_names} - COMMENT "Copying byproducts `${file_names}` to ${output_dir}" - VERBATIM - COMMAND_EXPAND_LISTS - ) -endfunction() - function(_corrosion_copy_byproduct_deferred target_name output_dir_prop_name cargo_build_dir file_names) if(ARGN) message(FATAL_ERROR "Unexpected additional arguments") @@ -302,6 +279,22 @@ function(_corrosion_copy_byproduct_deferred target_name output_dir_prop_name car endif() endif() + # Append .exe suffix for executable by-products if the target is windows or if it's a host + # build and the host is Windows. + get_target_property(target_type "${target_name}" TYPE) + if (target_type STREQUAL "EXECUTABLE") + list(LENGTH file_names list_len) + if(NOT list_len EQUAL "1") + message(FATAL_ERROR + "Internal error: Exactly one filename should be passed for executable types.") + endif() + _corrosion_bin_target_suffix(${target_name} "suffix") + if(suffix AND (NOT "${file_names}" MATCHES "\.pdb$")) + # For executable targets we know / checked that only one file will be passed. + string(APPEND file_names "${suffix}") + endif() + endif() + list(TRANSFORM file_names PREPEND "${cargo_build_dir}/" OUTPUT_VARIABLE src_file_names) list(TRANSFORM file_names PREPEND "${output_dir}/" OUTPUT_VARIABLE dst_file_names) message(DEBUG "Adding command to copy byproducts `${file_names}` to ${dst_file_names}") @@ -322,16 +315,16 @@ function(_corrosion_copy_byproduct_deferred target_name output_dir_prop_name car endfunction() function(_corrosion_call_copy_byproduct_deferred target_name output_dir_prop_name cargo_build_dir file_names) - cmake_language(EVAL CODE " - cmake_language(DEFER - CALL - _corrosion_copy_byproduct_deferred - [[${target_name}]] - [[${output_dir_prop_name}]] - [[${cargo_build_dir}]] - [[${file_names}]] - ) - ") + cmake_language(EVAL CODE " + cmake_language(DEFER + CALL + _corrosion_copy_byproduct_deferred + [[${target_name}]] + [[${output_dir_prop_name}]] + [[${cargo_build_dir}]] + [[${file_names}]] + ) + ") endfunction() # Copy the artifacts generated by cargo to the appropriate destination. @@ -342,11 +335,13 @@ endfunction() # `RUNTIME_OUTPUT_DIRECTORY`) # - cargo_build_dir: the directory cargo build places it's output artifacts in. # - filenames: the file names of any output artifacts as a list. +# - is_binary: TRUE if the byproducts are program executables. function(_corrosion_copy_byproducts target_name output_dir_prop_name cargo_build_dir filenames) - if(CORROSION_RESPECT_OUTPUT_DIRECTORY) + _corrosion_determine_deferred_byproduct_copying_and_import_location_handling("defer") + if(defer) _corrosion_call_copy_byproduct_deferred("${target_name}" "${output_dir_prop_name}" "${cargo_build_dir}" "${filenames}") else() - _corrosion_copy_byproduct_legacy("${target_name}" "${cargo_build_dir}" "${filenames}") + _corrosion_copy_byproduct_deferred("${target_name}" "${output_dir_prop_name}" "${cargo_build_dir}" "${filenames}") endif() endfunction() @@ -451,8 +446,6 @@ function(_corrosion_add_library_target) endif() set("${CALT_OUT_ARCHIVE_OUTPUT_BYPRODUCTS}" "${archive_output_byproducts}" PARENT_SCOPE) - add_library(${target_name} INTERFACE) - if(has_staticlib) add_library(${target_name}-static STATIC IMPORTED GLOBAL) add_dependencies(${target_name}-static cargo-build_${target_name}) @@ -467,6 +460,10 @@ function(_corrosion_add_library_target) TARGET ${target_name}-static PROPERTY INTERFACE_LINK_LIBRARIES ${Rust_CARGO_TARGET_LINK_NATIVE_LIBS} ) + set_property( + TARGET ${target_name}-static + PROPERTY INTERFACE_LINK_OPTIONS ${Rust_CARGO_TARGET_LINK_OPTIONS} + ) if(is_macos) set_property(TARGET ${target_name}-static PROPERTY INTERFACE_LINK_DIRECTORIES "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" @@ -528,17 +525,9 @@ function(_corrosion_add_bin_target workspace_manifest_path bin_name out_bin_bypr set(${out_pdb_byproduct} "${pdb_name}" PARENT_SCOPE) endif() - if(Rust_CARGO_TARGET_OS STREQUAL "windows") - set(bin_filename "${bin_name}.exe") - else() - set(bin_filename "${bin_name}") - endif() + set(bin_filename "${bin_name}") + _corrosion_determine_deferred_byproduct_copying_and_import_location_handling("defer") set(${out_bin_byproduct} "${bin_filename}" PARENT_SCOPE) - - - # Todo: This is compatible with the way corrosion previously exposed the bin name, - # but maybe we want to prefix the exposed name with the package name? - add_executable(${bin_name} IMPORTED GLOBAL) add_dependencies(${bin_name} cargo-build_${bin_name}) if(Rust_CARGO_TARGET_OS STREQUAL "darwin") @@ -562,10 +551,10 @@ endif() # Note: `cmake_language(GET_MESSAGE_LOG_LEVEL <output_variable>)` requires CMake 3.25, # so we offer our own option to control verbosity of downstream commands (e.g. cargo build) if (CORROSION_VERBOSE_OUTPUT) - set(_CORROSION_VERBOSE_OUTPUT_FLAG --verbose) + set(_CORROSION_VERBOSE_OUTPUT_FLAG --verbose CACHE INTERNAL "") else() # We want to silence some less important commands by default. - set(_CORROSION_QUIET_OUTPUT_FLAG --quiet) + set(_CORROSION_QUIET_OUTPUT_FLAG --quiet CACHE INTERNAL "") endif() if(CORROSION_NATIVE_TOOLING) @@ -594,8 +583,8 @@ set(_CORROSION_RUSTC "${RUSTC_EXECUTABLE}" CACHE INTERNAL "Path to rustc used b set(_CORROSION_CARGO "${CARGO_EXECUTABLE}" CACHE INTERNAL "Path to cargo used by corrosion") string(REPLACE "-" "_" _CORROSION_RUST_CARGO_TARGET_UNDERSCORE "${Rust_CARGO_TARGET}") +set(_CORROSION_RUST_CARGO_TARGET_UNDERSCORE "${_CORROSION_RUST_CARGO_TARGET_UNDERSCORE}" CACHE INTERNAL "lowercase target triple with underscores") string(TOUPPER "${_CORROSION_RUST_CARGO_TARGET_UNDERSCORE}" _CORROSION_TARGET_TRIPLE_UPPER) -set(_CORROSION_RUST_CARGO_TARGET_UNDERSCORE ${Rust_CARGO_TARGET} CACHE INTERNAL "lowercase target triple with underscores") set(_CORROSION_RUST_CARGO_TARGET_UPPER "${_CORROSION_TARGET_TRIPLE_UPPER}" CACHE INTERNAL @@ -765,15 +754,21 @@ function(_add_cargo_build out_cargo_build_out_dir) set(corrosion_cc_rs_flags) - if(CMAKE_C_COMPILER AND _CORROSION_RUST_CARGO_TARGET_UNDERSCORE) + if(CMAKE_C_COMPILER) # This variable is read by cc-rs (often used in build scripts) to determine the c-compiler. # It can still be overridden if the user sets the non underscore variant via the environment variables # on the target. list(APPEND corrosion_cc_rs_flags "CC_${_CORROSION_RUST_CARGO_TARGET_UNDERSCORE}=${CMAKE_C_COMPILER}") endif() - if(CMAKE_CXX_COMPILER AND _CORROSION_RUST_CARGO_TARGET_UNDERSCORE) + if(CMAKE_CXX_COMPILER) list(APPEND corrosion_cc_rs_flags "CXX_${_CORROSION_RUST_CARGO_TARGET_UNDERSCORE}=${CMAKE_CXX_COMPILER}") endif() + # cc-rs doesn't seem to support `llvm-ar` (commandline syntax), wo we might as well just use + # the default AR. + if(CMAKE_AR AND NOT (Rust_CARGO_TARGET_ENV STREQUAL "msvc")) + list(APPEND corrosion_cc_rs_flags "AR_${_CORROSION_RUST_CARGO_TARGET_UNDERSCORE}=${CMAKE_AR}") + endif() + # Since we instruct cc-rs to use the compiler found by CMake, it is likely one that requires also # specifying the target sysroot to use. CMake's generator makes sure to pass --sysroot with # CMAKE_OSX_SYSROOT. Fortunately the compilers Apple ships also respect the SDKROOT environment @@ -966,7 +961,6 @@ function(corrosion_import_crate) _corrosion_option_passthrough_helper(FROZEN COR frozen) _corrosion_arg_passthrough_helper(CRATES COR crate_allowlist) _corrosion_arg_passthrough_helper(CRATE_TYPES COR crate_types) - _corrosion_arg_passthrough_helper(PROFILE COR cargo_profile) if(COR_PROFILE) if(Rust_VERSION VERSION_LESS 1.57.0) @@ -1035,7 +1029,6 @@ function(corrosion_import_crate) ${_CORROSION_CONFIGURATION_ROOT} ${crates_args} ${crate_types} - ${cargo_profile_native_generator} --imported-crates=imported_crates ${passthrough_to_acb} -o ${generated_cmake} @@ -1055,7 +1048,6 @@ function(corrosion_import_crate) imported_crates ${crate_allowlist} ${crate_types} - ${cargo_profile} ${no_linker_override} ) endif() @@ -1198,11 +1190,15 @@ function(corrosion_set_features target_name) endfunction() function(corrosion_link_libraries target_name) - if(TARGET "${target_name}-static" AND NOT TARGET "${target_name}-shared") - message(WARNING "The target ${target_name} builds a static library." - "The linker is never invoked for a static libraries to link has effect " - " aside from establishing a build dependency." - ) + if(TARGET "${target_name}-static") + message(DEBUG "The target ${target_name} builds a static Rust library." + "Calling `target_link_libraries()` instead." + ) + target_link_libraries("${target_name}-static" INTERFACE ${ARGN}) + if(NOT TARGET "${target_name}-shared") + # Early return, since Rust won't invoke the linker for static libraries + return() + endif() endif() add_dependencies(_cargo-build_${target_name} ${ARGN}) foreach(library ${ARGN}) @@ -1216,7 +1212,7 @@ function(corrosion_link_libraries target_name) corrosion_add_target_local_rustflags(${target_name} "-L$<TARGET_LINKER_FILE_DIR:${library}>") corrosion_add_target_local_rustflags(${target_name} "-l$<TARGET_LINKER_FILE_BASE_NAME:${library}>") endforeach() -endfunction(corrosion_link_libraries) +endfunction() function(corrosion_install) # Default install dirs @@ -1648,8 +1644,6 @@ between multiple invocations of this function. ANCHOR_END: corrosion_cbindgen #]=======================================================================] function(corrosion_experimental_cbindgen) - # Todo: - # - set the target-triple via the TARGET env variable based on the target triple for the rust crate. set(OPTIONS "") set(ONE_VALUE_KEYWORDS TARGET MANIFEST_DIRECTORY HEADER_NAME CBINDGEN_VERSION) set(MULTI_VALUE_KEYWORDS "FLAGS") @@ -1666,6 +1660,10 @@ function(corrosion_experimental_cbindgen) set(rust_target "${CCN_TARGET}") unset(package_manifest_dir) + + set(hostbuild_override "$<BOOL:$<TARGET_PROPERTY:${rust_target},${_CORR_PROP_HOST_BUILD}>>") + set(cbindgen_target_triple "$<IF:${hostbuild_override},${_CORROSION_RUST_CARGO_HOST_TARGET},${_CORROSION_RUST_CARGO_TARGET}>") + if(TARGET "${rust_target}") get_target_property(package_manifest_path "${rust_target}" INTERFACE_COR_PACKAGE_MANIFEST_PATH) if(NOT EXISTS "${package_manifest_path}") @@ -1683,6 +1681,18 @@ function(corrosion_experimental_cbindgen) endif() endif() + unset(rust_cargo_package) + if(NOT DEFINED CCN_CARGO_PACKAGE) + get_target_property(rust_cargo_package "${rust_target}" INTERFACE_COR_CARGO_PACKAGE_NAME ) + if(NOT rust_cargo_package) + message(FATAL_ERROR "Could not determine cargo package name for cbindgen!") + endif() + else() + set(rust_cargo_package "${CCN_CARGO_PACKAGE}") + endif() + message(STATUS "Using package ${rust_cargo_package} as crate for cbindgen") + + set(output_header_name "${CCN_HEADER_NAME}") find_program(installed_cbindgen cbindgen) @@ -1718,6 +1728,8 @@ function(corrosion_experimental_cbindgen) set(corrosion_generated_dir "${CMAKE_CURRENT_BINARY_DIR}/corrosion_generated") set(generated_dir "${corrosion_generated_dir}/cbindgen/${rust_target}") set(header_placement_dir "${generated_dir}/include/") + set(depfile_placement_dir "${generated_dir}/depfile") + set(generated_depfile "${depfile_placement_dir}/${output_header_name}.d") set(generated_header "${header_placement_dir}/${output_header_name}") message(STATUS "rust target is ${rust_target}") if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.23") @@ -1739,39 +1751,68 @@ function(corrosion_experimental_cbindgen) # This may be different from $header_placement_dir since the user specified HEADER_NAME may contain # relative directories. get_filename_component(generated_header_dir "${generated_header}" DIRECTORY) - file(MAKE_DIRECTORY ${generated_header_dir}) + file(MAKE_DIRECTORY "${generated_header_dir}") - # Todo: Add dependencies on the source Rust files here to get proper dependency rules (hard). - # For now we just specify a dummy file we won't generate as an additional output, so that the rule - # always runs. - # Future Options: a) upstream depfile creation into cbindgen, b) spider our way through the source-code ourselves. - # c) ask the user to specify the dependencies (in order to relax the rules). - add_custom_command( - OUTPUT - "${generated_header}" "${CMAKE_CURRENT_BINARY_DIR}/corrosion/non-existing-file.h" - COMMAND - "${cbindgen}" - --output "${generated_header}" - --crate "${rust_target}" - ${CCN_FLAGS} - COMMENT "Generate cbindgen bindings for crate ${rust_target}" - COMMAND_EXPAND_LISTS - WORKING_DIRECTORY "${package_manifest_dir}" - ) + unset(depfile_cbindgen_arg) + unset(depfile_cmake_arg) + get_filename_component(generated_depfile_dir "${generated_depfile}" DIRECTORY) + file(MAKE_DIRECTORY "${generated_depfile_dir}") + set(depfile_cbindgen_arg "--depfile=${generated_depfile}") + + # Users might want to call cbindgen multiple times, e.g. to generate separate C++ and C header files. + string(MAKE_C_IDENTIFIER "${output_header_name}" header_identifier ) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22") + add_custom_command( + OUTPUT + "${generated_header}" + COMMAND + "${CMAKE_COMMAND}" -E env + TARGET="${cbindgen_target_triple}" + "${cbindgen}" + --output "${generated_header}" + --crate "${rust_cargo_package}" + ${depfile_cbindgen_arg} + ${CCN_FLAGS} + COMMENT "Generate cbindgen bindings for package ${rust_cargo_package} and output header ${generated_header}" + DEPFILE "${generated_depfile}" + COMMAND_EXPAND_LISTS + WORKING_DIRECTORY "${package_manifest_dir}" + ) + add_custom_target("_corrosion_cbindgen_${rust_target}_bindings_${header_identifier}" + DEPENDS "${generated_header}" + COMMENT "Generate ${generated_header} for ${rust_target}" + ) + else() + add_custom_target("_corrosion_cbindgen_${rust_target}_bindings_${header_identifier}" + "${CMAKE_COMMAND}" -E env + TARGET="${cbindgen_target_triple}" + "${cbindgen}" + --output "${generated_header}" + --crate "${rust_cargo_package}" + ${depfile_cbindgen_arg} + ${CCN_FLAGS} + COMMENT "Generate ${generated_header} for ${rust_target}" + COMMAND_EXPAND_LISTS + WORKING_DIRECTORY "${package_manifest_dir}" + ) + endif() if(NOT installed_cbindgen) add_custom_command( - OUTPUT "${generated_header}" "${CMAKE_CURRENT_BINARY_DIR}/corrosion/non-existing-file.h" + OUTPUT "${generated_header}" APPEND DEPENDS _corrosion_cbindgen ) endif() - add_custom_target(_corrosion_cbindgen_${rust_target}_bindings - DEPENDS "${generated_header}" - COMMENT "Generate cbindgen bindings for crate ${rust_target}" - ) - add_dependencies(${rust_target} _corrosion_cbindgen_${rust_target}_bindings) + if(NOT TARGET "_corrosion_cbindgen_${rust_target}_bindings") + add_custom_target(_corrosion_cbindgen_${rust_target}_bindings + COMMENT "Generate cbindgen bindings for package ${rust_cargo_package}" + ) + endif() + + add_dependencies("_corrosion_cbindgen_${rust_target}_bindings" "_corrosion_cbindgen_${rust_target}_bindings_${header_identifier}") + add_dependencies(${rust_target} "_corrosion_cbindgen_${rust_target}_bindings") endfunction() # Parse the version of a Rust package from it's package manifest (Cargo.toml) @@ -1821,6 +1862,26 @@ function(corrosion_parse_package_version package_manifest_path out_package_versi endif() endfunction() +function(_corrosion_initialize_properties target_name) + set(prefix "") + if(CORROSION_NATIVE_TOOLING) + set(prefix "INTERFACE_") + endif() + # Initialize the `<XYZ>_OUTPUT_DIRECTORY` properties based on `CMAKE_<XYZ>_OUTPUT_DIRECTORY`. + foreach(output_var RUNTIME_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_DIRECTORY LIBRARY_OUTPUT_DIRECTORY PDB_OUTPUT_DIRECTORY) + if (DEFINED "CMAKE_${output_var}") + set_property(TARGET ${target_name} PROPERTY "${prefix}${output_var}" "${CMAKE_${output_var}}") + endif() + + foreach(config_type ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER "${config_type}" config_type_upper) + if (DEFINED "CMAKE_${output_var}_${config_type_upper}") + set_property(TARGET ${target_name} PROPERTY "${prefix}${output_var}_${config_type_upper}" "${CMAKE_${output_var}_${config_type_upper}}") + endif() + endforeach() + endforeach() +endfunction() + # Helper macro to pass through an optional `OPTION` argument parsed via `cmake_parse_arguments` # to another function that takes the same OPTION. # If the option was set, then the variable <var_name> will be set to the same option name again, diff --git a/corrosion/cmake/CorrosionConfig.cmake.in b/corrosion/cmake/CorrosionConfig.cmake.in index abf099c3..a43a3f6b 100644 --- a/corrosion/cmake/CorrosionConfig.cmake.in +++ b/corrosion/cmake/CorrosionConfig.cmake.in @@ -4,7 +4,7 @@ if (Corrosion_FOUND) return() endif() -list(APPEND CMAKE_MODULE_PATH "@CMAKE_INSTALL_FULL_DATADIR@/cmake") +list(APPEND CMAKE_MODULE_PATH "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_DATADIR@/cmake") set(CORROSION_NATIVE_TOOLING_INSTALLED @CORROSION_NATIVE_TOOLING@) if(CORROSION_NATIVE_TOOLING_INSTALLED AND NOT TARGET Corrosion::Generator) diff --git a/corrosion/cmake/CorrosionGenerator.cmake b/corrosion/cmake/CorrosionGenerator.cmake index 2f717a89..9acdcf61 100644 --- a/corrosion/cmake/CorrosionGenerator.cmake +++ b/corrosion/cmake/CorrosionGenerator.cmake @@ -79,7 +79,6 @@ function(_generator_add_package_targets) string(JSON target_name GET "${target}" "name") string(JSON target_kind GET "${target}" "kind") string(JSON target_kind_len LENGTH "${target_kind}") - string(JSON target_name GET "${target}" "name") math(EXPR target_kind_len-1 "${target_kind_len} - 1") set(kinds) @@ -106,10 +105,19 @@ function(_generator_add_package_targets) endif() if("staticlib" IN_LIST kinds OR "cdylib" IN_LIST kinds) + # Explicitly set library names have always been forbidden from using dashes (by cargo). + # Starting with Rust 1.79, names inherited from the package name will have dashes replaced + # by underscores too. Corrosion will thus replace dashes with underscores, to make the target + # name consistent independent of the Rust version. `bin` target names are not affected. + # See https://github.com/corrosion-rs/corrosion/issues/501 for more details. + string(REPLACE "\-" "_" target_name "${target_name}") + set(archive_byproducts "") set(shared_lib_byproduct "") set(pdb_byproduct "") + add_library(${target_name} INTERFACE) + _corrosion_initialize_properties(${target_name}) _corrosion_add_library_target( WORKSPACE_MANIFEST_PATH "${workspace_manifest_path}" TARGET_NAME "${target_name}" @@ -150,10 +158,13 @@ function(_generator_add_package_targets) ) endif() list(APPEND corrosion_targets ${target_name}) + set_property(TARGET "${target_name}" PROPERTY INTERFACE_COR_CARGO_PACKAGE_NAME "${package_name}" ) # Note: "bin" is mutually exclusive with "staticlib/cdylib", since `bin`s are seperate crates from libraries. elseif("bin" IN_LIST kinds) set(bin_byproduct "") set(pdb_byproduct "") + add_executable(${target_name} IMPORTED GLOBAL) + _corrosion_initialize_properties(${target_name}) _corrosion_add_bin_target("${workspace_manifest_path}" "${target_name}" "bin_byproduct" "pdb_byproduct" ) @@ -182,6 +193,7 @@ function(_generator_add_package_targets) ) endif() list(APPEND corrosion_targets ${target_name}) + set_property(TARGET "${target_name}" PROPERTY INTERFACE_COR_CARGO_PACKAGE_NAME "${package_name}" ) else() # ignore other kinds (like examples, tests, build scripts, ...) endif() @@ -298,21 +310,4 @@ function(_generator_add_cargo_targets) if(GGC_IMPORTED_CRATES) set(${GGC_IMPORTED_CRATES} "${created_targets}" PARENT_SCOPE) endif() - - foreach(target_name ${created_targets}) - foreach(output_var RUNTIME_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_DIRECTORY LIBRARY_OUTPUT_DIRECTORY PDB_OUTPUT_DIRECTORY) - get_target_property(output_dir ${target_name} "${output_var}") - if (NOT output_dir AND DEFINED "CMAKE_${output_var}") - set_property(TARGET ${target_name} PROPERTY ${output_var} "${CMAKE_${output_var}}") - endif() - - foreach(config_type ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER "${config_type}" config_type_upper) - get_target_property(output_dir ${target_name} "${output_var}_${config_type_upper}") - if (NOT output_dir AND DEFINED "CMAKE_${output_var}_${config_type_upper}") - set_property(TARGET ${target_name} PROPERTY "${output_var}_${config_type_upper}" "${CMAKE_${output_var}_${config_type_upper}}") - endif() - endforeach() - endforeach() - endforeach() endfunction() diff --git a/corrosion/cmake/FindRust.cmake b/corrosion/cmake/FindRust.cmake index a468b4e5..fccfac37 100644 --- a/corrosion/cmake/FindRust.cmake +++ b/corrosion/cmake/FindRust.cmake @@ -17,15 +17,14 @@ include(FindPackageHandleStandardArgs) list(APPEND CMAKE_MESSAGE_CONTEXT "FindRust") -# Print error message and return. +# Print error message and return. Should not be used from inside functions macro(_findrust_failed) if("${Rust_FIND_REQUIRED}") message(FATAL_ERROR ${ARGN}) elseif(NOT "${Rust_FIND_QUIETLY}") message(WARNING ${ARGN}) endif() - # Note: PARENT_SCOPE is the scope of the caller of the caller of this macro. - set(Rust_FOUND "" PARENT_SCOPE) + set(Rust_FOUND "") return() endmacro() @@ -93,6 +92,8 @@ function(_corrosion_parse_target_triple target_triple out_arch out_vendor out_os "nvidia" "openwrt" "alpine" + "chimera" + "unikraft" "unknown" "uwp" # aarch64-uwp-windows-msvc "wrs" # e.g. aarch64-wrs-vxworks @@ -131,7 +132,7 @@ function(_corrosion_parse_target_triple target_triple out_arch out_vendor out_os set("${out_env}" "${CMAKE_MATCH_6}" PARENT_SCOPE) endfunction() -function(_corrosion_determine_libs_new target_triple out_libs) +function(_corrosion_determine_libs_new target_triple out_libs out_flags) set(package_dir "${CMAKE_BINARY_DIR}/corrosion/required_libs") # Cleanup on reconfigure to get a cleans state (in case we change something in the future) file(REMOVE_RECURSE "${package_dir}") @@ -160,11 +161,30 @@ function(_corrosion_determine_libs_new target_triple out_libs) if(cargo_build_error_message MATCHES "native-static-libs: ([^\r\n]+)\r?\n") string(REPLACE " " ";" "libs_list" "${CMAKE_MATCH_1}") set(stripped_lib_list "") + set(flag_list "") + + set(was_last_framework OFF) foreach(lib ${libs_list}) - # Strip leading `-l` (unix) and potential .lib suffix (windows) - string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}") - string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}") - list(APPEND stripped_lib_list "${stripped_lib}") + # merge -framework;lib -> "-framework lib" as CMake does de-duplication of link libraries, and -framework prefix is required + if (lib STREQUAL "-framework") + set(was_last_framework ON) + continue() + endif() + if (was_last_framework) + list(APPEND stripped_lib_list "-framework ${lib}") + set(was_last_framework OFF) + continue() + endif() + + # Flags start with / for MSVC + if (lib MATCHES "^/" AND ${target_triple} MATCHES "msvc$") + list(APPEND flag_list "${lib}") + else() + # Strip leading `-l` (unix) and potential .lib suffix (windows) + string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}") + string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}") + list(APPEND stripped_lib_list "${stripped_lib}") + endif() endforeach() set(libs_list "${stripped_lib_list}") # Special case `msvcrt` to link with the debug version in Debug mode. @@ -176,6 +196,7 @@ function(_corrosion_determine_libs_new target_triple out_libs) endif() endif() set("${out_libs}" "${libs_list}" PARENT_SCOPE) + set("${out_flags}" "${flag_list}" PARENT_SCOPE) endfunction() if (NOT "${Rust_TOOLCHAIN}" STREQUAL "$CACHE{Rust_TOOLCHAIN}") @@ -298,18 +319,18 @@ if (Rust_RESOLVE_RUSTUP_TOOLCHAINS) set(_DISCOVERED_TOOLCHAINS_VERSION "") foreach(_TOOLCHAIN_RAW ${_TOOLCHAINS_RAW}) - if (_TOOLCHAIN_RAW MATCHES "([a-zA-Z0-9\\._\\-]+)[ \t\r\n]?(\\(default\\) \\(override\\)|\\(default\\)|\\(override\\))?[ \t\r\n]+(.+)") + if (_TOOLCHAIN_RAW MATCHES "([a-zA-Z0-9\\._\\-]+)[ \t\r\n]?(\\(active\\)|\\(active, default\\)|\\(default\\) \\(override\\)|\\(default\\)|\\(override\\))?[ \t\r\n]+(.+)") set(_TOOLCHAIN "${CMAKE_MATCH_1}") set(_TOOLCHAIN_TYPE "${CMAKE_MATCH_2}") set(_TOOLCHAIN_PATH "${CMAKE_MATCH_3}") set(_TOOLCHAIN_${_TOOLCHAIN}_PATH "${CMAKE_MATCH_3}") - if (_TOOLCHAIN_TYPE MATCHES ".*\\(default\\).*") + if (_TOOLCHAIN_TYPE MATCHES ".*\\((active, )?default\\).*") set(_TOOLCHAIN_DEFAULT "${_TOOLCHAIN}") endif() - if (_TOOLCHAIN_TYPE MATCHES ".*\\(override\\).*") + if (_TOOLCHAIN_TYPE MATCHES ".*\\((active|override)\\).*") set(_TOOLCHAIN_OVERRIDE "${_TOOLCHAIN}") endif() @@ -699,6 +720,19 @@ if (NOT Rust_CARGO_TARGET_CACHED) if (_Rust_ANDROID_TARGET) set(Rust_CARGO_TARGET_CACHED "${_Rust_ANDROID_TARGET}" CACHE STRING "Target triple") endif() + elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "OHOS") + if(CMAKE_OHOS_ARCH_ABI STREQUAL arm64-v8a) + set(_RUST_OHOS_TARGET aarch64-unknown-linux-ohos) + elseif(CMAKE_OHOS_ARCH_ABI STREQUAL armeabi-v7a) + set(_RUST_OHOS_TARGET armv7-unknown-linux-ohos) + elseif(CMAKE_OHOS_ARCH_ABI STREQUAL x86_64) + set(_RUST_OHOS_TARGET x86_64-unknown-linux-ohos) + else() + message(WARNING "unrecognized OHOS architecture: ${OHOS_ARCH}") + endif() + if(_RUST_OHOS_TARGET) + set(Rust_CARGO_TARGET_CACHED "${_RUST_OHOS_TARGET}" CACHE STRING "Target triple") + endif() endif() # Fallback to the default host target if(NOT Rust_CARGO_TARGET_CACHED) @@ -735,10 +769,13 @@ set(Rust_CARGO_HOST_ENV "${rust_host_env}" CACHE INTERNAL "Host environment") if(NOT DEFINED CACHE{Rust_CARGO_TARGET_LINK_NATIVE_LIBS}) message(STATUS "Determining required link libraries for target ${Rust_CARGO_TARGET_CACHED}") unset(required_native_libs) - _corrosion_determine_libs_new("${Rust_CARGO_TARGET_CACHED}" required_native_libs) + _corrosion_determine_libs_new("${Rust_CARGO_TARGET_CACHED}" required_native_libs required_link_flags) if(DEFINED required_native_libs) message(STATUS "Required static libs for target ${Rust_CARGO_TARGET_CACHED}: ${required_native_libs}" ) endif() + if(DEFINED required_link_flags) + message(STATUS "Required link flags for target ${Rust_CARGO_TARGET_CACHED}: ${required_link_flags}" ) + endif() # In very recent corrosion versions it is possible to override the rust compiler version # per target, so to be totally correct we would need to determine the libraries for # every installed Rust version, that the user could choose from. @@ -746,17 +783,21 @@ if(NOT DEFINED CACHE{Rust_CARGO_TARGET_LINK_NATIVE_LIBS}) # for the target and once for the host target (if cross-compiling). set(Rust_CARGO_TARGET_LINK_NATIVE_LIBS "${required_native_libs}" CACHE INTERNAL "Required native libraries when linking Rust static libraries") + set(Rust_CARGO_TARGET_LINK_OPTIONS "${required_link_flags}" CACHE INTERNAL + "Required link flags when linking Rust static libraries") endif() if(Rust_CROSSCOMPILING AND NOT DEFINED CACHE{Rust_CARGO_HOST_TARGET_LINK_NATIVE_LIBS}) message(STATUS "Determining required link libraries for target ${Rust_CARGO_HOST_TARGET_CACHED}") unset(host_libs) - _corrosion_determine_libs_new("${Rust_CARGO_HOST_TARGET_CACHED}" host_libs) + _corrosion_determine_libs_new("${Rust_CARGO_HOST_TARGET_CACHED}" host_libs host_flags) if(DEFINED host_libs) message(STATUS "Required static libs for host target ${Rust_CARGO_HOST_TARGET_CACHED}: ${host_libs}" ) endif() set(Rust_CARGO_HOST_TARGET_LINK_NATIVE_LIBS "${host_libs}" CACHE INTERNAL "Required native libraries when linking Rust static libraries for the host target") + set(Rust_CARGO_HOST_TARGET_LINK_OPTIONS "${host_flags}" CACHE INTERNAL + "Required linker flags when linking Rust static libraries for the host target") endif() # Set the input variables as non-cache variables so that the variables are available after diff --git a/corrosion/doc/book.toml b/corrosion/doc/book.toml index 7930b4d0..bb294c05 100644 --- a/corrosion/doc/book.toml +++ b/corrosion/doc/book.toml @@ -2,4 +2,4 @@ language = "en" multilingual = false src = "src" -title = "Corrosion documentation" +title = "Corrosion v0.5 documentation" diff --git a/corrosion/doc/src/advanced.md b/corrosion/doc/src/advanced.md index 52238ad8..203c48bf 100644 --- a/corrosion/doc/src/advanced.md +++ b/corrosion/doc/src/advanced.md @@ -40,17 +40,21 @@ For rust `cdylib`s and `bin`s, the linker is invoked via `rustc` and CMake just When CMake invokes the linker, everything is as usual. CMake will call the linker with the compiler as the linker driver and users can just use the regular CMake functions to -modify linking behaviour. The corrosion functions mentioned below have **no effect**. +modify linking behaviour. `corrosion_set_linker()` has **no effect**. +As a convenience, `corrosion_link_libraries()` will forward its arguments to `target_link_libraries()`. #### Rustc invokes the linker Rust `cdylib`s and `bin`s are linked via `rustc`. Corrosion provides several helper functions to influence the linker invocation for such targets. -`corrosion_link_libraries()` is essentially the equivalent to `target_link_libraries()`, -if the target is a rust `cdylib` or `bin`. +`corrosion_link_libraries()` is a limited version of `target_link_libraries()` +for rust `cdylib` or `bin` targets. Under the hood this function passes `-l` and `-L` flags to the linker invocation and ensures the linked libraries are built first. +Much of the advanced functionality available in `target_link_libraries()` is not implemented yet, +but pull-requests are welcome! In the meantime, users may want to use +`corrosion_add_target_local_rustflags()` to pass customized linking flags. `corrosion_set_linker()` can be used to specify a custom linker, in case the default one chosen by corrosion is not what you want. diff --git a/corrosion/doc/src/introduction.md b/corrosion/doc/src/introduction.md index 0e52cf3f..e44401d6 100644 --- a/corrosion/doc/src/introduction.md +++ b/corrosion/doc/src/introduction.md @@ -10,4 +10,10 @@ For rust executables and dynamic libraries corrosion provides a `corrosion_link_ helper function to conveniently add the necessary flags to link C/C++ libraries into the rust target. +You are currently viewing the documentation of the stable v0.5 release branch. + [`target_link_libraries()`]: https://cmake.org/cmake/help/latest/command/target_link_libraries.html + +## Requirements + +Corrosion v0.5 requires at least CMake 3.15 and at least Rust 1.46 or newer. \ No newline at end of file diff --git a/corrosion/doc/src/quick_start.md b/corrosion/doc/src/quick_start.md index aa486b05..3e5b8392 100644 --- a/corrosion/doc/src/quick_start.md +++ b/corrosion/doc/src/quick_start.md @@ -16,7 +16,7 @@ include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.4 # Optionally specify a commit hash, version tag or branch here + GIT_TAG v0.5 # Optionally specify a commit hash, version tag or branch here ) # Set any global configuration variables such as `Rust_TOOLCHAIN` before this line! FetchContent_MakeAvailable(Corrosion) diff --git a/corrosion/doc/src/setup_corrosion.md b/corrosion/doc/src/setup_corrosion.md index f2fac9d4..509be054 100644 --- a/corrosion/doc/src/setup_corrosion.md +++ b/corrosion/doc/src/setup_corrosion.md @@ -20,7 +20,7 @@ include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.4 # Optionally specify a commit hash, version tag or branch here + GIT_TAG v0.5 # Optionally specify a commit hash, version tag or branch here ) # Set any global configuration variables such as `Rust_TOOLCHAIN` before this line! FetchContent_MakeAvailable(Corrosion) diff --git a/corrosion/generator/src/subcommands/gen_cmake/target.rs b/corrosion/generator/src/subcommands/gen_cmake/target.rs index 0d0af9db..489540e6 100644 --- a/corrosion/generator/src/subcommands/gen_cmake/target.rs +++ b/corrosion/generator/src/subcommands/gen_cmake/target.rs @@ -82,8 +82,13 @@ impl CargoTarget { }) } - pub(crate) fn target_name(&self) -> &str { - &self.cargo_target.name + /// Cargo / Rust 1.78 and newer replace dashes with underscores in libraries + /// To make the names consistent across versions we also do the replacement here. + pub(crate) fn target_name(&self) -> String { + match self.target_type { + CargoTargetType::Library { .. } => self.cargo_target.name.replace("-", "_"), + _ => self.cargo_target.name.to_string(), + } } pub fn emit_cmake_target( @@ -129,6 +134,8 @@ impl CargoTarget { writeln!( out_file, " + add_library({target_name} INTERFACE) + _corrosion_initialize_properties({target_name}) _corrosion_add_library_target( WORKSPACE_MANIFEST_PATH \"{workspace_manifest_path}\" TARGET_NAME \"{target_name}\" @@ -144,7 +151,7 @@ impl CargoTarget { ) ", workspace_manifest_path = ws_manifest, - target_name = self.cargo_target.name, + target_name = self.target_name(), lib_kinds = lib_kinds, )?; } @@ -152,6 +159,8 @@ impl CargoTarget { writeln!( out_file, " + add_executable({target_name} IMPORTED GLOBAL) + _corrosion_initialize_properties({target_name}) _corrosion_add_bin_target(\"{workspace_manifest_path}\" \"{target_name}\" bin_byproduct pdb_byproduct ) @@ -159,7 +168,7 @@ impl CargoTarget { list(APPEND byproducts \"${{bin_byproduct}}\" \"${{pdb_byproduct}}\") ", workspace_manifest_path = ws_manifest, - target_name = self.cargo_target.name, + target_name = self.target_name(), )?; } }; @@ -184,27 +193,28 @@ impl CargoTarget { if(archive_byproducts) _corrosion_copy_byproducts( - {target_name} ARCHIVE_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{archive_byproducts}}\" + {target_name} INTERFACE_ARCHIVE_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{archive_byproducts}}\" FALSE ) endif() if(shared_lib_byproduct) _corrosion_copy_byproducts( - {target_name} LIBRARY_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{shared_lib_byproduct}}\" + {target_name} INTERFACE_LIBRARY_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{shared_lib_byproduct}}\" FALSE ) endif() if(pdb_byproduct) _corrosion_copy_byproducts( - {target_name} PDB_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{pdb_byproduct}}\" + {target_name} INTERFACE_PDB_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{pdb_byproduct}}\" FALSE ) endif() if(bin_byproduct) _corrosion_copy_byproducts( - {target_name} RUNTIME_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{bin_byproduct}}\" + {target_name} INTERFACE_RUNTIME_OUTPUT_DIRECTORY \"${{cargo_build_out_dir}}\" \"${{bin_byproduct}}\" TRUE ) endif() + set_property(TARGET {target_name} PROPERTY INTERFACE_COR_CARGO_PACKAGE_NAME {package_name} ) ", package_name = self.cargo_package.name, - target_name = self.cargo_target.name, + target_name = self.target_name(), package_manifest_path = self.cargo_package.manifest_path.as_str().replace("\\", "/"), workspace_manifest_path = ws_manifest, target_kinds = target_kinds, diff --git a/corrosion/test/cargo_flags/cargo_flags/CMakeLists.txt b/corrosion/test/cargo_flags/cargo_flags/CMakeLists.txt index 90b3d248..8c4d0784 100644 --- a/corrosion/test/cargo_flags/cargo_flags/CMakeLists.txt +++ b/corrosion/test/cargo_flags/cargo_flags/CMakeLists.txt @@ -5,12 +5,12 @@ include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml FLAGS --features one) add_executable(flags-exe main.cpp) -target_link_libraries(flags-exe PUBLIC flags-lib) -corrosion_set_cargo_flags(flags-lib --features two) -corrosion_set_cargo_flags(flags-lib $<TARGET_PROPERTY:flags-lib,more_flags>) +target_link_libraries(flags-exe PUBLIC flags_lib) +corrosion_set_cargo_flags(flags_lib --features two) +corrosion_set_cargo_flags(flags_lib $<TARGET_PROPERTY:flags_lib,more_flags>) set_property( - TARGET flags-lib + TARGET flags_lib APPEND PROPERTY more_flags --features three ) diff --git a/corrosion/test/cbindgen/rust2cpp/CMakeLists.txt b/corrosion/test/cbindgen/rust2cpp/CMakeLists.txt index da459430..71967796 100644 --- a/corrosion/test/cbindgen/rust2cpp/CMakeLists.txt +++ b/corrosion/test/cbindgen/rust2cpp/CMakeLists.txt @@ -3,8 +3,8 @@ project(test_project VERSION 0.1.0) include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml) -corrosion_experimental_cbindgen(TARGET rust-lib HEADER_NAME "rust-lib.h") +corrosion_experimental_cbindgen(TARGET rust_lib HEADER_NAME "rust-lib.h") add_executable(cpp-exe main.cpp) set_property(TARGET cpp-exe PROPERTY CXX_STANDARD 11) -target_link_libraries(cpp-exe PUBLIC rust-lib) +target_link_libraries(cpp-exe PUBLIC rust_lib) diff --git a/corrosion/test/custom_profiles/basic_profiles/CMakeLists.txt b/corrosion/test/custom_profiles/basic_profiles/CMakeLists.txt index 0ab33ca1..d020df4f 100644 --- a/corrosion/test/custom_profiles/basic_profiles/CMakeLists.txt +++ b/corrosion/test/custom_profiles/basic_profiles/CMakeLists.txt @@ -9,4 +9,4 @@ endif() corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml PROFILE ${CARGO_PROFILE}) add_executable(${CARGO_PROFILE}_bin main.cpp) -target_link_libraries(${CARGO_PROFILE}_bin PUBLIC cargo-profiles-lib) +target_link_libraries(${CARGO_PROFILE}_bin PUBLIC cargo_profiles_lib) diff --git a/corrosion/test/custom_profiles/custom_profiles/CMakeLists.txt b/corrosion/test/custom_profiles/custom_profiles/CMakeLists.txt index 16b0ff5c..c9b5cd2e 100644 --- a/corrosion/test/custom_profiles/custom_profiles/CMakeLists.txt +++ b/corrosion/test/custom_profiles/custom_profiles/CMakeLists.txt @@ -8,7 +8,7 @@ set(custom_profile $<IF:$<CONFIG:Debug>,dev-without-dbg,${_release_profile}>) if(CORROSION_TEST_USE_TARGET_SPECIFIC_OVERRIDE) # Select "wrong" profile here on purpose. corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml PROFILE dev) - set_target_properties(custom-profiles-lib + set_target_properties(custom_profiles_lib PROPERTIES INTERFACE_CORROSION_CARGO_PROFILE "${custom_profile}" ) @@ -17,4 +17,4 @@ else() endif() add_executable(custom-profile-exe main.cpp) -target_link_libraries(custom-profile-exe PUBLIC custom-profiles-lib) +target_link_libraries(custom-profile-exe PUBLIC custom_profiles_lib) diff --git a/corrosion/test/cxxbridge/cxxbridge_rust2cpp/CMakeLists.txt b/corrosion/test/cxxbridge/cxxbridge_rust2cpp/CMakeLists.txt index 17d71208..b8979b74 100644 --- a/corrosion/test/cxxbridge/cxxbridge_rust2cpp/CMakeLists.txt +++ b/corrosion/test/cxxbridge/cxxbridge_rust2cpp/CMakeLists.txt @@ -5,12 +5,12 @@ set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED 1) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml) -corrosion_add_cxxbridge(cxxbridge-cpp CRATE cxxbridge-crate MANIFEST_PATH rust FILES lib.rs foo/mod.rs) +corrosion_add_cxxbridge(cxxbridge-cpp CRATE cxxbridge_crate MANIFEST_PATH rust FILES lib.rs foo/mod.rs) add_executable(cxxbridge-exe main.cpp) target_link_libraries(cxxbridge-exe PUBLIC cxxbridge-cpp) if(MSVC) # Note: This is required because we use `cxx` which uses `cc` to compile and link C++ code. - corrosion_set_env_vars(cxxbridge-crate "CFLAGS=-MDd" "CXXFLAGS=-MDd") + corrosion_set_env_vars(cxxbridge_crate "CFLAGS=-MDd" "CXXFLAGS=-MDd") endif() diff --git a/corrosion/test/envvar/envvar/CMakeLists.txt b/corrosion/test/envvar/envvar/CMakeLists.txt index 20cdd655..2e0eeabd 100644 --- a/corrosion/test/envvar/envvar/CMakeLists.txt +++ b/corrosion/test/envvar/envvar/CMakeLists.txt @@ -4,7 +4,7 @@ include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH Cargo.toml) -corrosion_set_env_vars(rust-lib-requiring-envvar +corrosion_set_env_vars(rust_lib_requiring_envvar "ANOTHER_VARIABLE=ANOTHER_VALUE" "$<TARGET_PROPERTY:program_requiring_rust_lib_with_envvar,INDIRECT_VAR_TEST>" "COR_CARGO_VERSION_MAJOR=${Rust_CARGO_VERSION_MAJOR}" @@ -20,4 +20,4 @@ set_property( "REQUIRED_VARIABLE=EXPECTED_VALUE" ) -target_link_libraries(program_requiring_rust_lib_with_envvar PUBLIC rust-lib-requiring-envvar) +target_link_libraries(program_requiring_rust_lib_with_envvar PUBLIC rust_lib_requiring_envvar) diff --git a/corrosion/test/features/features/CMakeLists.txt b/corrosion/test/features/features/CMakeLists.txt index a3d5b865..7b376795 100644 --- a/corrosion/test/features/features/CMakeLists.txt +++ b/corrosion/test/features/features/CMakeLists.txt @@ -5,9 +5,9 @@ include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml FEATURES thirdfeature ALL_FEATURES) add_executable(features-cpp-exe main.cpp) -target_link_libraries(features-cpp-exe PUBLIC rust-feature-lib) +target_link_libraries(features-cpp-exe PUBLIC rust_feature_lib) -corrosion_set_features(rust-feature-lib +corrosion_set_features(rust_feature_lib ALL_FEATURES OFF NO_DEFAULT_FEATURES FEATURES diff --git a/corrosion/test/output directory/CMakeLists.txt b/corrosion/test/output directory/CMakeLists.txt index 3cd92e47..4adb6f9a 100644 --- a/corrosion/test/output directory/CMakeLists.txt +++ b/corrosion/test/output directory/CMakeLists.txt @@ -1,7 +1,3 @@ -if(CMAKE_VERSION VERSION_LESS 3.19.0) - return() -endif() - if(CMAKE_C_COMPILER) set(TEST_C_COMPILER "C_COMPILER" "${CMAKE_C_COMPILER}") endif() @@ -33,7 +29,12 @@ if(CORROSION_TESTS_INSTALL_CORROSION) set_tests_properties("output_directory_build" PROPERTIES FIXTURES_REQUIRED "fixture_corrosion_install") endif() -foreach(output_approach targetprop var) +set(test_variants "var") +if(NOT CORROSION_NATIVE_TOOLING) + list(APPEND test_variants "targetprop") +endif() + +foreach(output_approach ${test_variants}) if(output_approach STREQUAL "targetprop") set(rust_proj_suffix "1") elseif(output_approach STREQUAL "var") @@ -128,13 +129,15 @@ foreach(output_approach targetprop var) endforeach() -add_test(NAME postbuild_custom_command - COMMAND - "${CMAKE_COMMAND}" - -P "${CMAKE_CURRENT_SOURCE_DIR}/TestFileExists.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/build/another_dir/moved_bin" - ) -set_tests_properties("postbuild_custom_command" PROPERTIES FIXTURES_REQUIRED "build_fixture_output_directory") +if(NOT CORROSION_NATIVE_TOOLING) + add_test(NAME postbuild_custom_command + COMMAND + "${CMAKE_COMMAND}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/TestFileExists.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/build/another_dir/moved_bin" + ) + set_tests_properties("postbuild_custom_command" PROPERTIES FIXTURES_REQUIRED "build_fixture_output_directory") +endif() add_test(NAME "output_directory_cleanup" COMMAND "${CMAKE_COMMAND}" -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/build") set_tests_properties("output_directory_cleanup" PROPERTIES FIXTURES_CLEANUP "build_fixture_output_directory") diff --git a/corrosion/test/output directory/output directory/CMakeLists.txt b/corrosion/test/output directory/output directory/CMakeLists.txt index 5a0f2cf8..c3704cc8 100644 --- a/corrosion/test/output directory/output directory/CMakeLists.txt +++ b/corrosion/test/output directory/output directory/CMakeLists.txt @@ -2,29 +2,32 @@ cmake_minimum_required(VERSION 3.15) project(test_project VERSION 0.1.0) include(../../test_header.cmake) -corrosion_import_crate(MANIFEST_PATH proj1/Cargo.toml) - -# Note: The output directories defined here must be manually kept in sync with the expected test location. -set_target_properties(rust_bin1 - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_bin_targetprop" - PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_bin_pdb_targetprop" - -) -set_target_properties(rust_lib1 PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_archive_targetprop") -set_target_properties(rust_lib1 - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_lib_targetprop" - PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_lib_pdb_targetprop" -) - -add_custom_command(TARGET cargo-build_rust_bin1 POST_BUILD - COMMAND - ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/another_dir" - COMMAND - ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_PROPERTY:rust_bin1,LOCATION>" "${CMAKE_CURRENT_BINARY_DIR}/another_dir/moved_bin" +if(NOT CORROSION_NATIVE_TOOLING) + corrosion_import_crate(MANIFEST_PATH proj1/Cargo.toml) + + # Note: The output directories defined here must be manually kept in sync with the expected test location. + set_target_properties(rust_bin1 + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_bin_targetprop" + PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_bin_pdb_targetprop" + + ) + set_target_properties(rust_lib1 PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_archive_targetprop") + set_target_properties(rust_lib1 + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_lib_targetprop" + PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_lib_pdb_targetprop" ) + add_custom_command(TARGET cargo-build_rust_bin1 POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/another_dir" + COMMAND + ${CMAKE_COMMAND} -E copy_if_different + "$<TARGET_PROPERTY:rust_bin1,LOCATION>" + "${CMAKE_CURRENT_BINARY_DIR}/another_dir/moved_bin" + ) +endif() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_bin_var") set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_binlib_pdb_var") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/custom_archive_var") @@ -39,6 +42,6 @@ unset(CMAKE_LIBRARY_OUTPUT_DIRECTORY) unset(CMAKE_PDB_OUTPUT_DIRECTORY) add_executable(consumer consumer.cpp) -add_dependencies(consumer cargo-build_rust_lib1 cargo-build_rust_lib2) +add_dependencies(consumer cargo-build_rust_lib2) -target_link_libraries(consumer rust_lib1 rust_lib2) +target_link_libraries(consumer rust_lib2) diff --git a/corrosion/test/rust2cpp/rust2cpp/CMakeLists.txt b/corrosion/test/rust2cpp/rust2cpp/CMakeLists.txt index 71b3bee6..e14de93a 100644 --- a/corrosion/test/rust2cpp/rust2cpp/CMakeLists.txt +++ b/corrosion/test/rust2cpp/rust2cpp/CMakeLists.txt @@ -5,8 +5,8 @@ include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml) add_executable(cpp-exe main.cpp) -target_link_libraries(cpp-exe PUBLIC rust-lib) +target_link_libraries(cpp-exe PUBLIC rust_lib) add_executable(cpp-exe-shared main.cpp) target_link_libraries(cpp-exe-shared - PUBLIC rust-lib-shared) + PUBLIC rust_lib-shared) diff --git a/corrosion/test/rustflags/rustflags/CMakeLists.txt b/corrosion/test/rustflags/rustflags/CMakeLists.txt index 422c352b..9a4f25cd 100644 --- a/corrosion/test/rustflags/rustflags/CMakeLists.txt +++ b/corrosion/test/rustflags/rustflags/CMakeLists.txt @@ -5,16 +5,16 @@ include(../../test_header.cmake) corrosion_import_crate(MANIFEST_PATH rust/Cargo.toml) add_executable(rustflags-cpp-exe main.cpp) -target_link_libraries(rustflags-cpp-exe PUBLIC rustflag-test-lib) +target_link_libraries(rustflags-cpp-exe PUBLIC rustflag_test_lib) # Test --cfg=key="value" rustflag. -corrosion_add_target_rustflags(rustflag-test-lib --cfg=test_rustflag_cfg1="test_rustflag_cfg1_value") +corrosion_add_target_rustflags(rustflag_test_lib --cfg=test_rustflag_cfg1="test_rustflag_cfg1_value") # Test using a generator expression to produce a rustflag and passing multiple rustflags. -corrosion_add_target_rustflags(rustflag-test-lib +corrosion_add_target_rustflags(rustflag_test_lib --cfg=test_rustflag_cfg2="$<IF:$<OR:$<CONFIG:Debug>,$<CONFIG:>>,debug,release>" "--cfg=test_rustflag_cfg3" ) -corrosion_add_target_local_rustflags(rustflag-test-lib "--cfg=test_local_rustflag1") -corrosion_add_target_local_rustflags(rustflag-test-lib --cfg=test_local_rustflag2="value") +corrosion_add_target_local_rustflags(rustflag_test_lib "--cfg=test_local_rustflag1") +corrosion_add_target_local_rustflags(rustflag_test_lib --cfg=test_local_rustflag2="value") diff --git a/cpp_test/build_env.h b/cpp_test/build_env.h index 23a88486..13ccd576 100644 --- a/cpp_test/build_env.h +++ b/cpp_test/build_env.h @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp_test/mock_server.cpp b/cpp_test/mock_server.cpp index 71eb3654..0e8a1e85 100644 --- a/cpp_test/mock_server.cpp +++ b/cpp_test/mock_server.cpp @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,27 +27,27 @@ #include <string.h> #if defined(PLATFORM_UNIX) -#include <fcntl.h> -#include <arpa/inet.h> -#include <netdb.h> -#include <netinet/in.h> -#include <netinet/tcp.h> -#include <sys/types.h> -#include <sys/socket.h> -#include <unistd.h> +# include <fcntl.h> +# include <arpa/inet.h> +# include <netdb.h> +# include <netinet/in.h> +# include <netinet/tcp.h> +# include <sys/types.h> +# include <sys/socket.h> +# include <unistd.h> #elif defined(PLATFORM_WINDOWS) -#include <winsock2.h> -#include <ws2tcpip.h> +# include <winsock2.h> +# include <ws2tcpip.h> #endif #if defined(PLATFORM_UNIX) -#define CLOSESOCKET ::close +# define CLOSESOCKET ::close typedef const void* setsockopt_arg_t; -#ifndef INVALID_SOCKET -#define INVALID_SOCKET -1 -#endif +# ifndef INVALID_SOCKET +# define INVALID_SOCKET -1 +# endif #elif defined(PLATFORM_WINDOWS) -#define CLOSESOCKET ::closesocket +# define CLOSESOCKET ::closesocket typedef const char* setsockopt_arg_t; typedef long suseconds_t; #endif @@ -108,8 +108,8 @@ mock_server::mock_server() _listen_fd, SOL_SOCKET, SO_REUSEADDR, - static_cast<setsockopt_arg_t>(static_cast<const void*>( - &reuse_addr)), + static_cast<setsockopt_arg_t>( + static_cast<const void*>(&reuse_addr)), sizeof(reuse_addr)) != 0) { #if defined(PLATFORM_UNIX) @@ -126,10 +126,8 @@ mock_server::mock_server() listen_addr.sin_family = AF_INET; listen_addr.sin_addr.s_addr = INADDR_ANY; listen_addr.sin_port = htons(0); - if (bind( - _listen_fd, - (const sockaddr *)&listen_addr, - sizeof(listen_addr)) == -1) + if (bind(_listen_fd, (const sockaddr*)&listen_addr, sizeof(listen_addr)) == + -1) throw std::runtime_error{"Bad `bind()`."}; if (listen(_listen_fd, 1) == -1) @@ -139,9 +137,7 @@ mock_server::mock_server() memset(&resolved_addr, 0, sizeof(resolved_addr)); socklen_t resolved_addr_len = sizeof(resolved_addr); if (getsockname( - _listen_fd, - (sockaddr *)&resolved_addr, - &resolved_addr_len) == -1) + _listen_fd, (sockaddr*)&resolved_addr, &resolved_addr_len) == -1) throw std::runtime_error{"Bad `getsockname()`."}; _port = ntohs(resolved_addr.sin_port); } @@ -150,10 +146,7 @@ void mock_server::accept() { sockaddr_in remote_addr; socklen_t remote_addr_len = sizeof(remote_addr); - _conn_fd = ::accept( - _listen_fd, - (sockaddr *)&remote_addr, - &remote_addr_len); + _conn_fd = ::accept(_listen_fd, (sockaddr*)&remote_addr, &remote_addr_len); if (_conn_fd == INVALID_SOCKET) throw std::runtime_error{"Bad `accept()`."}; #if defined(PLATFORM_UNIX) @@ -169,7 +162,7 @@ bool mock_server::wait_for_data(std::optional<double> wait_timeout_sec) fd_set read_set; FD_ZERO(&read_set); FD_SET(_conn_fd, &read_set); - timeval* timeout_ptr = nullptr; // nullptr blocks indefinitely. + timeval* timeout_ptr = nullptr; // nullptr blocks indefinitely. timeval timeout; if (wait_timeout_sec) { @@ -178,9 +171,8 @@ bool mock_server::wait_for_data(std::optional<double> wait_timeout_sec) #elif defined(PLATFORM_WINDOWS) const long secs = static_cast<long>(*wait_timeout_sec); #endif - const suseconds_t usec = - static_cast<suseconds_t>( - 1000000.0 * (*wait_timeout_sec - static_cast<double>(secs))); + const suseconds_t usec = static_cast<suseconds_t>( + 1000000.0 * (*wait_timeout_sec - static_cast<double>(secs))); timeout = timeval{secs, usec}; timeout_ptr = &timeout; } @@ -209,11 +201,8 @@ size_t mock_server::recv(double wait_timeout_sec) for (;;) { wait_for_data(); - sock_ssize_t count = ::recv( - _conn_fd, - &chunk[0], - static_cast<sock_len_t>(chunk_len), - 0); + sock_ssize_t count = + ::recv(_conn_fd, &chunk[0], static_cast<sock_len_t>(chunk_len), 0); if (count == -1) throw std::runtime_error{"Bad `recv()`."}; const size_t u_count = static_cast<size_t>(count); @@ -266,4 +255,4 @@ mock_server::~mock_server() #endif } -} +} // namespace questdb::ingress::test diff --git a/cpp_test/mock_server.hpp b/cpp_test/mock_server.hpp index 9993e66c..ba66efb0 100644 --- a/cpp_test/mock_server.hpp +++ b/cpp_test/mock_server.hpp @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ #if defined(PLATFORM_UNIX) typedef int socketfd_t; #elif defined(PLATFORM_WINDOWS) -#include <winsock2.h> +# include <winsock2.h> typedef SOCKET socketfd_t; #endif @@ -45,17 +45,20 @@ namespace questdb::ingress::test /** * Bug-ridden mock server to handle line requests. * YMMV, but should be just good enough for testing. -*/ + */ class mock_server { public: mock_server(); - uint16_t port() const { return _port; } + uint16_t port() const + { + return _port; + } void accept(); - size_t recv(double wait_timeout_sec=0.1); + size_t recv(double wait_timeout_sec = 0.1); const std::vector<std::string>& msgs() const { @@ -75,4 +78,4 @@ class mock_server std::vector<std::string> _msgs; }; -} +} // namespace questdb::ingress::test diff --git a/cpp_test/test_line_sender.cpp b/cpp_test/test_line_sender.cpp index 21e9f1d2..1510aef2 100644 --- a/cpp_test/test_line_sender.cpp +++ b/cpp_test/test_line_sender.cpp @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,20 +30,28 @@ #include <questdb/ingress/line_sender.h> #include <questdb/ingress/line_sender.hpp> -#include <vector> -#include <sstream> #include <chrono> +#include <cstring> +#include <sstream> #include <thread> +#include <vector> using namespace std::string_literals; using namespace questdb::ingress::literals; template <typename F> - class on_scope_exit +class on_scope_exit { public: - explicit on_scope_exit(F&& f) : _f{std::move(f)} {} - ~on_scope_exit() { _f(); } + explicit on_scope_exit(F&& f) + : _f{std::move(f)} + { + } + ~on_scope_exit() + { + _f(); + } + private: F _f; }; @@ -52,26 +60,23 @@ TEST_CASE("line_sender c api basics") { questdb::ingress::test::mock_server server; ::line_sender_error* err = nullptr; - on_scope_exit error_free_guard{[&]{ - if (err) - ::line_sender_error_free(err); - }}; + on_scope_exit error_free_guard{[&] { + if (err) + ::line_sender_error_free(err); + }}; ::line_sender_utf8 host = {0, nullptr}; CHECK(::line_sender_utf8_init(&host, 9, "localhost", &err)); - ::line_sender_opts* opts = ::line_sender_opts_new( - ::line_sender_protocol_tcp, - host, - server.port()); + ::line_sender_opts* opts = + ::line_sender_opts_new(::line_sender_protocol_tcp, host, server.port()); CHECK_NE(opts, nullptr); ::line_sender* sender = ::line_sender_build(opts, &err); line_sender_opts_free(opts); CHECK_NE(sender, nullptr); CHECK_FALSE(::line_sender_must_close(sender)); - on_scope_exit sender_close_guard{[&] - { - ::line_sender_close(sender); - sender = nullptr; - }}; + on_scope_exit sender_close_guard{[&] { + ::line_sender_close(sender); + sender = nullptr; + }}; server.accept(); CHECK(server.recv() == 0); ::line_sender_table_name table_name{0, nullptr}; @@ -99,7 +104,8 @@ TEST_CASE("line_sender c api basics") CHECK(server.msgs().front() == "test,t1=v1 f1=0.5 10000000\n"); } -TEST_CASE("Opts service API tests") { +TEST_CASE("Opts service API tests") +{ // We just check these compile and link. line_sender_utf8 host = QDB_UTF8_LITERAL("localhost"); @@ -109,28 +115,24 @@ TEST_CASE("Opts service API tests") { ::line_sender_protocol_tcp, ::line_sender_protocol_tcps, ::line_sender_protocol_http, - ::line_sender_protocol_https - }; + ::line_sender_protocol_https}; - for (size_t index = 0; index < sizeof(protocols) / sizeof(::line_sender_protocol); ++index) + for (size_t index = 0; + index < sizeof(protocols) / sizeof(::line_sender_protocol); + ++index) { auto proto = protocols[index]; - ::line_sender_opts* opts1 = ::line_sender_opts_new_service( - proto, - host, - port); + ::line_sender_opts* opts1 = + ::line_sender_opts_new_service(proto, host, port); ::line_sender_opts_free(opts1); } - } TEST_CASE("line_sender c++ connect disconnect") { questdb::ingress::test::mock_server server; questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - server.port()}; + questdb::ingress::protocol::tcp, "localhost", server.port()}; CHECK_FALSE(sender.must_close()); server.accept(); CHECK(server.recv() == 0); @@ -148,8 +150,7 @@ TEST_CASE("line_sender c++ api basics") CHECK(server.recv() == 0); questdb::ingress::line_sender_buffer buffer; - buffer - .table("test") + buffer.table("test") .symbol("t1", "v1") .symbol("t2", "") .column("f1", 0.5) @@ -166,8 +167,7 @@ TEST_CASE("test multiple lines") { questdb::ingress::test::mock_server server; std::string conf_str = - "tcp::addr=localhost:" + - std::to_string(server.port()) + ";"; + "tcp::addr=localhost:" + std::to_string(server.port()) + ";"; questdb::ingress::line_sender sender = questdb::ingress::line_sender::from_conf(conf_str); CHECK_FALSE(sender.must_close()); @@ -176,8 +176,7 @@ TEST_CASE("test multiple lines") const auto table_name = "metric1"_tn; questdb::ingress::line_sender_buffer buffer; - buffer - .table(table_name) + buffer.table(table_name) .symbol("t1"_cn, "val1"_utf8) .symbol("t2"_cn, "val2"_utf8) .column("f1"_cn, true) @@ -187,8 +186,7 @@ TEST_CASE("test multiple lines") .column("f5"_cn, "val4"_utf8) .column("f6"_cn, "val5"_utf8) .at(questdb::ingress::timestamp_nanos{111222233333}); - buffer - .table(table_name) + buffer.table(table_name) .symbol("tag3"_cn, "value 3"_utf8) .symbol("tag 4"_cn, "value:4"_utf8) .column("field5"_cn, false) @@ -198,10 +196,12 @@ TEST_CASE("test multiple lines") CHECK(buffer.size() == 137); sender.flush(buffer); CHECK(server.recv() == 2); - CHECK(server.msgs()[0] == + CHECK( + server.msgs()[0] == ("metric1,t1=val1,t2=val2 f1=t,f2=12345i," "f3=10.75,f4=\"val3\",f5=\"val4\",f6=\"val5\" 111222233333\n")); - CHECK(server.msgs()[1] == + CHECK( + server.msgs()[1] == "metric1,tag3=value\\ 3,tag\\ 4=value:4 field5=f\n"); } @@ -250,9 +250,7 @@ TEST_CASE("One column only - server.accept() after flush, before close") { questdb::ingress::test::mock_server server; questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - server.port()}; + questdb::ingress::protocol::tcp, "localhost", server.port()}; // Does not raise - this is unlike InfluxDB spec that disallows this. questdb::ingress::line_sender_buffer buffer; @@ -272,16 +270,13 @@ TEST_CASE("Symbol after column") { questdb::ingress::test::mock_server server; questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - server.port()}; + questdb::ingress::protocol::tcp, "localhost", server.port()}; questdb::ingress::line_sender_buffer buffer; buffer.table("test").column("t1", "v1"); CHECK_THROWS_AS( - buffer.symbol("t2", "v2"), - questdb::ingress::line_sender_error); + buffer.symbol("t2", "v2"), questdb::ingress::line_sender_error); CHECK(!sender.must_close()); @@ -337,44 +332,53 @@ TEST_CASE("Validation of bad chars in key names.") questdb::ingress::line_sender_error); } - auto test_bad_name = [](std::string bad_name) + auto test_bad_name = [](std::string bad_name) { + try { - try - { - questdb::ingress::column_name_view{bad_name}; - std::stringstream ss; - ss << "Name `" << bad_name << "` ("; - for (const char& c : bad_name) - { - ss << "\\x" - << std::hex - << std::setw(2) - << std::setfill('0') - << (int)c; - } - ss << ") did not raise."; - CHECK_MESSAGE(false, ss.str()); - } - catch (const questdb::ingress::line_sender_error&) - { - return; - } - catch (...) + questdb::ingress::column_name_view{bad_name}; + std::stringstream ss; + ss << "Name `" << bad_name << "` ("; + for (const char& c : bad_name) { - CHECK_MESSAGE(false, "Other exception raised."); + ss << "\\x" << std::hex << std::setw(2) << std::setfill('0') + << (int)c; } - }; + ss << ") did not raise."; + CHECK_MESSAGE(false, ss.str()); + } + catch (const questdb::ingress::line_sender_error&) + { + return; + } + catch (...) + { + CHECK_MESSAGE(false, "Other exception raised."); + } + }; std::vector<std::string> bad_chars{ - "?"s, "."s, ","s, "'"s, "\""s, "\\"s, "/"s, "\0"s, ":"s, - ")"s, "("s, "+"s, "-"s, "*"s, "%"s, "~"s, "\xef\xbb\xbf"s}; + "?"s, + "."s, + ","s, + "'"s, + "\""s, + "\\"s, + "/"s, + "\0"s, + ":"s, + ")"s, + "("s, + "+"s, + "-"s, + "*"s, + "%"s, + "~"s, + "\xef\xbb\xbf"s}; for (const auto& bad_char : bad_chars) { std::vector<std::string> bad_names{ - bad_char + "abc", - "ab" + bad_char + "c", - "abc" + bad_char}; + bad_char + "abc", "ab" + bad_char + "c", "abc" + bad_char}; for (const auto& bad_name : bad_names) { test_bad_name(bad_name); @@ -382,6 +386,42 @@ TEST_CASE("Validation of bad chars in key names.") } } +#if __cplusplus >= 202002L +template <size_t N> +bool operator==(std::span<const std::byte> lhs, const char (&rhs)[N]) +{ + constexpr size_t bytelen = N - 1; // Exclude null terminator + const std::span<const std::byte> rhs_span{ + reinterpret_cast<const std::byte*>(rhs), bytelen}; + return lhs.size() == bytelen && std::ranges::equal(lhs, rhs_span); +} + +bool operator==(std::span<const std::byte> lhs, const std::string& rhs) +{ + const std::span<const std::byte> rhs_span{ + reinterpret_cast<const std::byte*>(rhs.data()), rhs.size()}; + return lhs.size() == rhs.size() && std::ranges::equal(lhs, rhs_span); +} +#else +template <size_t N> +bool operator==( + const questdb::ingress::buffer_view lhs_view, const char (&rhs)[N]) +{ + constexpr size_t bytelen = N - 1; // Exclude null terminator + const questdb::ingress::buffer_view rhs_view{ + reinterpret_cast<const std::byte*>(rhs), bytelen}; + return lhs_view == rhs_view; +} + +bool operator==( + const questdb::ingress::buffer_view lhs_view, const std::string& rhs) +{ + const questdb::ingress::buffer_view rhs_view{ + reinterpret_cast<const std::byte*>(rhs.data()), rhs.size()}; + return lhs_view == rhs_view; +} +#endif + TEST_CASE("Buffer move and copy ctor testing") { const size_t init_buf_size = 128; @@ -420,8 +460,6 @@ TEST_CASE("Buffer move and copy ctor testing") CHECK(buffer3.size() == 0); CHECK(buffer3.capacity() == 0); CHECK(buffer3.peek() == ""); - - } TEST_CASE("Sender move testing.") @@ -433,9 +471,7 @@ TEST_CASE("Sender move testing.") const questdb::ingress::utf8_view& host_ref = host; questdb::ingress::line_sender sender1{ - questdb::ingress::protocol::tcp, - host_ref, - server1.port()}; + questdb::ingress::protocol::tcp, host_ref, server1.port()}; questdb::ingress::line_sender_buffer buffer; buffer.table("test").column("t1", "v1").at_now(); @@ -451,8 +487,7 @@ TEST_CASE("Sender move testing.") }; CHECK_THROWS_AS( - fail_to_flush_eventually(), - questdb::ingress::line_sender_error); + fail_to_flush_eventually(), questdb::ingress::line_sender_error); CHECK(sender1.must_close()); questdb::ingress::line_sender sender2{std::move(sender1)}; @@ -461,9 +496,7 @@ TEST_CASE("Sender move testing.") CHECK(sender2.must_close()); questdb::ingress::line_sender sender3{ - questdb::ingress::protocol::tcp, - "localhost", - server2.port()}; + questdb::ingress::protocol::tcp, "localhost", server2.port()}; CHECK_FALSE(sender3.must_close()); sender3 = std::move(sender2); @@ -475,9 +508,7 @@ TEST_CASE("Bad hostname") try { questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "dummy_hostname", - "9009"}; + questdb::ingress::protocol::tcp, "dummy_hostname", "9009"}; CHECK_MESSAGE(false, "Expected exception"); } catch (const questdb::ingress::line_sender_error& se) @@ -498,9 +529,7 @@ TEST_CASE("Bad interface") try { questdb::ingress::opts opts{ - questdb::ingress::protocol::tcp, - "localhost", - "9009"}; + questdb::ingress::protocol::tcp, "localhost", "9009"}; opts.bind_interface("dummy_hostname"); questdb::ingress::line_sender sender{opts}; CHECK_MESSAGE(false, "Expected exception"); @@ -509,8 +538,7 @@ TEST_CASE("Bad interface") { std::string msg{se.what()}; CHECK_MESSAGE( - msg.rfind("Could not resolve \"dummy_hostname\": ", 0) == 0, - msg); + msg.rfind("Could not resolve \"dummy_hostname\": ", 0) == 0, msg); } catch (...) { @@ -520,27 +548,24 @@ TEST_CASE("Bad interface") TEST_CASE("Bad port") { - const auto test_bad_port = [](std::string bad_port) + const auto test_bad_port = [](std::string bad_port) { + try { - try - { - questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - bad_port}; - CHECK_MESSAGE(false, "Expected exception"); - } - catch (const questdb::ingress::line_sender_error& se) - { - std::string msg{se.what()}; - std::string exp_msg{"\"localhost:" + bad_port + "\": "}; - CHECK_MESSAGE(msg.find(exp_msg) != std::string::npos, msg); - } - catch (...) - { - CHECK_MESSAGE(false, "Other exception raised."); - } - }; + questdb::ingress::line_sender sender{ + questdb::ingress::protocol::tcp, "localhost", bad_port}; + CHECK_MESSAGE(false, "Expected exception"); + } + catch (const questdb::ingress::line_sender_error& se) + { + std::string msg{se.what()}; + std::string exp_msg{"\"localhost:" + bad_port + "\": "}; + CHECK_MESSAGE(msg.find(exp_msg) != std::string::npos, msg); + } + catch (...) + { + CHECK_MESSAGE(false, "Other exception raised."); + } + }; test_bad_port("wombat"); test_bad_port("0"); @@ -556,17 +581,13 @@ TEST_CASE("Bad connect") // Port 1 is generally the tcpmux service which one would // very much expect to never be running. questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - 1}; + questdb::ingress::protocol::tcp, "localhost", 1}; CHECK_MESSAGE(false, "Expected exception"); } catch (const questdb::ingress::line_sender_error& se) { std::string msg{se.what()}; - CHECK_MESSAGE( - msg.rfind("Could not connect", 0) == 0, - msg); + CHECK_MESSAGE(msg.rfind("Could not connect", 0) == 0, msg); } catch (...) { @@ -580,9 +601,7 @@ TEST_CASE("Bad CA path") { questdb::ingress::test::mock_server server; questdb::ingress::opts opts{ - questdb::ingress::protocol::tcps, - "localhost", - server.port()}; + questdb::ingress::protocol::tcps, "localhost", server.port()}; opts.auth_timeout(1000); @@ -590,7 +609,7 @@ TEST_CASE("Bad CA path") opts.tls_roots("/an/invalid/path/to/ca.pem"); questdb::ingress::line_sender sender{opts}; } - catch(const questdb::ingress::line_sender_error& se) + catch (const questdb::ingress::line_sender_error& se) { std::string msg{se.what()}; CHECK_MESSAGE( @@ -609,25 +628,19 @@ TEST_CASE("os certs") questdb::ingress::test::mock_server server; { questdb::ingress::opts opts{ - questdb::ingress::protocol::tcps, - "localhost", - server.port()}; + questdb::ingress::protocol::tcps, "localhost", server.port()}; opts.tls_ca(questdb::ingress::ca::webpki_roots); } { questdb::ingress::opts opts{ - questdb::ingress::protocol::https, - "localhost", - server.port()}; + questdb::ingress::protocol::https, "localhost", server.port()}; opts.tls_ca(questdb::ingress::ca::os_roots); } { questdb::ingress::opts opts{ - questdb::ingress::protocol::https, - "localhost", - server.port()}; + questdb::ingress::protocol::https, "localhost", server.port()}; opts.tls_ca(questdb::ingress::ca::webpki_and_os_roots); } } @@ -636,40 +649,29 @@ TEST_CASE("Opts copy ctor, assignment and move testing.") { { questdb::ingress::opts opts1{ - questdb::ingress::protocol::tcp, - "localhost", - "9009"}; + questdb::ingress::protocol::tcp, "localhost", "9009"}; questdb::ingress::opts opts2{std::move(opts1)}; } { questdb::ingress::opts opts1{ - questdb::ingress::protocol::tcps, - "localhost", - "9009"}; + questdb::ingress::protocol::tcps, "localhost", "9009"}; questdb::ingress::opts opts2{opts1}; } { - questdb::ingress::opts opts1{questdb::ingress::protocol::tcp, - "localhost", - "9009"}; + questdb::ingress::opts opts1{ + questdb::ingress::protocol::tcp, "localhost", "9009"}; questdb::ingress::opts opts2{ - questdb::ingress::protocol::tcp, - "altavista.digital.com", - "9009"}; + questdb::ingress::protocol::tcp, "altavista.digital.com", "9009"}; opts1 = std::move(opts2); } { questdb::ingress::opts opts1{ - questdb::ingress::protocol::https, - "localhost", - "9009"}; + questdb::ingress::protocol::https, "localhost", "9009"}; questdb::ingress::opts opts2{ - questdb::ingress::protocol::https, - "altavista.digital.com", - "9009"}; + questdb::ingress::protocol::https, "altavista.digital.com", "9009"}; opts1 = opts2; } } @@ -678,31 +680,30 @@ TEST_CASE("Test timestamp column.") { questdb::ingress::test::mock_server server; questdb::ingress::line_sender sender{ - questdb::ingress::protocol::tcp, - "localhost", - server.port()}; + questdb::ingress::protocol::tcp, "localhost", server.port()}; const auto now = std::chrono::system_clock::now(); const auto now_micros = std::chrono::duration_cast<std::chrono::microseconds>( - now.time_since_epoch()).count(); - const auto now_nanos = - std::chrono::duration_cast<std::chrono::nanoseconds>( - now.time_since_epoch()).count(); + now.time_since_epoch()) + .count(); + const auto now_nanos = std::chrono::duration_cast<std::chrono::nanoseconds>( + now.time_since_epoch()) + .count(); const auto now_nanos_ts = questdb::ingress::timestamp_nanos{now_nanos}; const auto now_micros_ts = questdb::ingress::timestamp_micros{now_micros}; questdb::ingress::line_sender_buffer buffer; - buffer - .table("test") + buffer.table("test") .column("ts1", questdb::ingress::timestamp_micros{12345}) .column("ts2", now_micros_ts) .column("ts3", now_nanos_ts) .at(now_nanos_ts); std::stringstream ss; - ss << "test ts1=12345t,ts2=" << now_micros << "t,ts3=" << now_micros << "t " << now_nanos << "\n"; + ss << "test ts1=12345t,ts2=" << now_micros << "t,ts3=" << now_micros << "t " + << now_nanos << "\n"; const auto exp = ss.str(); CHECK(buffer.peek() == exp); @@ -717,10 +718,14 @@ TEST_CASE("Test timestamp column.") CHECK(server.msgs()[0] == exp); } -TEST_CASE("test timestamp_micros and timestamp_nanos::now()") { - // Explicit in tests, just to be sure we haven't messed up the return types :-) - questdb::ingress::timestamp_micros micros_now{questdb::ingress::timestamp_micros::now()}; - questdb::ingress::timestamp_nanos nanos_now{questdb::ingress::timestamp_nanos::now()}; +TEST_CASE("test timestamp_micros and timestamp_nanos::now()") +{ + // Explicit in tests, just to be sure we haven't messed up the return types + // :-) + questdb::ingress::timestamp_micros micros_now{ + questdb::ingress::timestamp_micros::now()}; + questdb::ingress::timestamp_nanos nanos_now{ + questdb::ingress::timestamp_nanos::now()}; // Check both are not zero. CHECK(micros_now.as_micros() != 0); @@ -729,7 +734,9 @@ TEST_CASE("test timestamp_micros and timestamp_nanos::now()") { // Check both are within half second of each other. const int64_t micros_of_nanos = nanos_now.as_nanos() / 1000; const int64_t half_second_micros = 500000; - CHECK(std::abs(micros_of_nanos - micros_now.as_micros()) < half_second_micros); + CHECK( + std::abs(micros_of_nanos - micros_now.as_micros()) < + half_second_micros); } TEST_CASE("Test Marker") @@ -748,11 +755,13 @@ TEST_CASE("Test Marker") CHECK(buffer.size() == 0); // Can't rewind, no marker set: Cleared by `rewind_to_marker`. - CHECK_THROWS_AS(buffer.rewind_to_marker(), questdb::ingress::line_sender_error); + CHECK_THROWS_AS( + buffer.rewind_to_marker(), questdb::ingress::line_sender_error); buffer.table("a").symbol("b", "c"); CHECK_THROWS_AS(buffer.set_marker(), questdb::ingress::line_sender_error); - CHECK_THROWS_AS(buffer.rewind_to_marker(), questdb::ingress::line_sender_error); + CHECK_THROWS_AS( + buffer.rewind_to_marker(), questdb::ingress::line_sender_error); CHECK(buffer.peek() == "a,b=c"); buffer.at_now(); @@ -761,7 +770,8 @@ TEST_CASE("Test Marker") buffer.set_marker(); buffer.clear_marker(); buffer.clear_marker(); - CHECK_THROWS_AS(buffer.rewind_to_marker(), questdb::ingress::line_sender_error); + CHECK_THROWS_AS( + buffer.rewind_to_marker(), questdb::ingress::line_sender_error); buffer.set_marker(); buffer.table("d").symbol("e", "f"); CHECK(buffer.peek() == "a,b=c\nd,e=f"); @@ -771,10 +781,12 @@ TEST_CASE("Test Marker") buffer.clear(); CHECK(buffer.peek() == ""); - CHECK_THROWS_AS(buffer.rewind_to_marker(), questdb::ingress::line_sender_error); + CHECK_THROWS_AS( + buffer.rewind_to_marker(), questdb::ingress::line_sender_error); } -TEST_CASE("Moved View") { +TEST_CASE("Moved View") +{ auto v1 = "abc"_tn; CHECK(v1.size() == 3); questdb::ingress::table_name_view v2{std::move(v1)}; @@ -783,7 +795,8 @@ TEST_CASE("Moved View") { CHECK(v1.data() == v2.data()); } -TEST_CASE("Empty Buffer") { +TEST_CASE("Empty Buffer") +{ questdb::ingress::line_sender_buffer b1; CHECK(b1.size() == 0); questdb::ingress::line_sender_buffer b2{std::move(b1)}; @@ -801,7 +814,8 @@ TEST_CASE("Empty Buffer") { CHECK(b5.size() == 9); questdb::ingress::test::mock_server server; - questdb::ingress::line_sender sender{questdb::ingress::protocol::tcp, "localhost", server.port()}; + questdb::ingress::line_sender sender{ + questdb::ingress::protocol::tcp, "localhost", server.port()}; CHECK_THROWS_WITH_AS( sender.flush(b1), "State error: Bad call to `flush`, should have called `table` instead.", @@ -812,36 +826,36 @@ TEST_CASE("Empty Buffer") { questdb::ingress::line_sender_error); } -TEST_CASE("Opts from conf") { - questdb::ingress::opts opts1 = questdb::ingress::opts::from_conf("tcp::addr=localhost:9009;"); - questdb::ingress::opts opts2 = questdb::ingress::opts::from_conf("tcps::addr=localhost:9009;"); - questdb::ingress::opts opts3 = questdb::ingress::opts::from_conf("https::addr=localhost:9009;"); - questdb::ingress::opts opts4 = questdb::ingress::opts::from_conf("https::addr=localhost:9009;"); +TEST_CASE("Opts from conf") +{ + questdb::ingress::opts opts1 = + questdb::ingress::opts::from_conf("tcp::addr=localhost:9009;"); + questdb::ingress::opts opts2 = + questdb::ingress::opts::from_conf("tcps::addr=localhost:9009;"); + questdb::ingress::opts opts3 = + questdb::ingress::opts::from_conf("https::addr=localhost:9009;"); + questdb::ingress::opts opts4 = + questdb::ingress::opts::from_conf("https::addr=localhost:9009;"); } -TEST_CASE("HTTP basics") { +TEST_CASE("HTTP basics") +{ questdb::ingress::opts opts1{ - questdb::ingress::protocol::http, - "localhost", - 1}; + questdb::ingress::protocol::http, "localhost", 1}; questdb::ingress::opts opts1conf = questdb::ingress::opts::from_conf( - "http::addr=localhost:1;username=user;password=pass;request_timeout=5000;retry_timeout=5;"); + "http::addr=localhost:1;username=user;password=pass;request_timeout=" + "5000;retry_timeout=5;"); questdb::ingress::opts opts2{ - questdb::ingress::protocol::https, - "localhost", - "1"}; + questdb::ingress::protocol::https, "localhost", "1"}; questdb::ingress::opts opts2conf = questdb::ingress::opts::from_conf( - "http::addr=localhost:1;token=token;request_min_throughput=1000;retry_timeout=0;"); - opts1 - .username("user") + "http::addr=localhost:1;token=token;request_min_throughput=1000;retry_" + "timeout=0;"); + opts1.username("user") .password("pass") .max_buf_size(1000000) .request_timeout(5000) .retry_timeout(5); - opts2 - .token("token") - .request_min_throughput(1000) - .retry_timeout(0); + opts2.token("token").request_min_throughput(1000).retry_timeout(0); questdb::ingress::line_sender sender1{opts1}; questdb::ingress::line_sender sender1conf{opts1conf}; questdb::ingress::line_sender sender2{opts2}; @@ -856,6 +870,7 @@ TEST_CASE("HTTP basics") { CHECK_THROWS_AS(sender2conf.flush(b1), questdb::ingress::line_sender_error); CHECK_THROWS_AS( - questdb::ingress::opts::from_conf("http::addr=localhost:1;bind_interface=0.0.0.0;"), - questdb::ingress::line_sender_error); + questdb::ingress::opts::from_conf( + "http::addr=localhost:1;bind_interface=0.0.0.0;"), + questdb::ingress::line_sender_error); } \ No newline at end of file diff --git a/doc/C.md b/doc/C.md index 5968e0ee..4f6b3e56 100644 --- a/doc/C.md +++ b/doc/C.md @@ -62,21 +62,20 @@ re-usable, but a buffer may only be flushed via the sender after a call to `line_sender_buffer_at_*(..)` (preferred) or `line_sender_buffer_at_now()`. ```c -line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars"); -line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); -line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); +line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("trades"); +line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); +line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); line_sender_buffer* buffer = line_sender_buffer_new(); if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; -line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); -if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) +line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); +if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; -if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) +if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; if (!line_sender_buffer_at_nanos(buffer, line_sender_now_nanos(), &err)) diff --git a/doc/CPP.md b/doc/CPP.md index 99a9d442..72e18794 100644 --- a/doc/CPP.md +++ b/doc/CPP.md @@ -58,9 +58,9 @@ re-usable, but a buffer may only be flushed via the sender after a call to ```cpp questdb::ingress::line_sender_buffer buffer; buffer - .table("cpp_cars") - .symbol("id", "d6e5fe92-d19f-482a-a97a-c105f547f721") - .column("x", 30.5) + .table("trades") + .symbol("symbol", "ETH-USD") + .column("price", 2615.54) .at(timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/doc/DEPENDENCY.md b/doc/DEPENDENCY.md index 1fe73846..f7a525bb 100644 --- a/doc/DEPENDENCY.md +++ b/doc/DEPENDENCY.md @@ -91,7 +91,7 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/questdb/c-questdb-client.git GIT_TAG CHOSEN_RELEASE_TAG) # CHANGE ME! -FetchContent_MakeAvailable(c_questdb_client) +FetchContent_MakeAvailable(c_questdb_client_proj) add_executable( main diff --git a/doc/RELEASING.md b/doc/RELEASING.md new file mode 100644 index 00000000..ec47c000 --- /dev/null +++ b/doc/RELEASING.md @@ -0,0 +1,90 @@ +# How to publish a new release + +## 1. Ensure you're fully in sync with the remote repo + +```bash +git switch main +git pull +git status +``` + +## 2. Create the release branch + +```bash +git switch -c vX.Y.Z +``` + +## 3. Update all the occurrences of the version in the repo + +```bash +bump2version --config-file .bumpversion.cfg <increment> +``` + +The `increment` parameter can be: + +- `patch` +- `minor` +- `major` + +Use the one appropriate to the version increment you're releasing. + +## 4. Refresh `Cargo.lock` + +```bash +cd questdb-rs-ffi +cargo clean +cargo build +``` + +## 5. Merge the release branch to master + +```bash +git commit -a -m "Bump version: <current> → <new>" +git push +``` + +Replace the `<current>` and `<new>` placeholders! + +Create and merge a PR with the same name: "Bump version: \<current\> → \<new\>" + +## 6. Tag the new version + +Once the PR is merged, pull main and add the version tag: + +```bash +git switch main +git pull --prune +git tag X.Y.Z +git push --tags +``` + +## 7. Create a new release on GitHub + +[GitHub Release Page](https://github.com/questdb/c-questdb-client/releases) + +On that page you'll see all the previous releases. Follow their manually-written +style, and note that the style differs between patch, minor, and major releases. + +## 8. Publish the Rust crate to crates.io + +Ensure once more you're fully in sync with the remote repo: + +```bash +git switch main +git pull +git status +``` + +Publish the crate: + +```bash +cd questdb-rs +cargo publish --dry-run --token [your API token from crates.io] +cargo publish --token [your API token from crates.io] +``` + +## 9. Ensure the docs are online on docs.rs + +The release is immediately visible on crates.io, but there's a delay until it +becomes available on [docs.rs](https://docs.rs/questdb-rs/latest/questdb/). Watch that site and ensure it +appears there. diff --git a/doc/SECURITY.md b/doc/SECURITY.md index 3dbfa036..ccb00f6b 100644 --- a/doc/SECURITY.md +++ b/doc/SECURITY.md @@ -35,7 +35,7 @@ A few important technical details on TLS: are managed centrally. For API usage: -* Rust: `SenderBuilder`'s [`auth`](https://docs.rs/questdb-rs/4.0.1/questdb/ingress/struct.SenderBuilder.html#method.auth) - and [`tls`](https://docs.rs/questdb-rs/4.0.1/questdb/ingress/struct.SenderBuilder.html#method.tls) methods. +* Rust: `SenderBuilder`'s [`auth`](https://docs.rs/questdb-rs/4.0.4/questdb/ingress/struct.SenderBuilder.html#method.auth) + and [`tls`](https://docs.rs/questdb-rs/4.0.4/questdb/ingress/struct.SenderBuilder.html#method.tls) methods. * C: [examples/line_sender_c_example_auth.c](../examples/line_sender_c_example_auth.c) * C++: [examples/line_sender_cpp_example_auth.cpp](../examples/line_sender_cpp_example_auth.cpp) diff --git a/examples.manifest.yaml b/examples.manifest.yaml index 0e5a0beb..c7275ac3 100644 --- a/examples.manifest.yaml +++ b/examples.manifest.yaml @@ -24,7 +24,7 @@ header: |- [C client library docs](https://github.com/questdb/c-questdb-client/blob/main/doc/C.md) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac @@ -38,7 +38,7 @@ header: |- [C++ client library docs](https://github.com/questdb/c-questdb-client/blob/main/doc/CPP.md) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac @@ -52,7 +52,7 @@ header: |- [Rust client library docs](https://docs.rs/crate/questdb-rs/latest) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac @@ -67,7 +67,7 @@ header: |- [C client library docs](https://github.com/questdb/c-questdb-client/blob/main/doc/C.md) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac @@ -81,7 +81,7 @@ header: |- [C++ client library docs](https://github.com/questdb/c-questdb-client/blob/main/doc/CPP.md) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac @@ -95,7 +95,7 @@ header: |- [Rust client library docs](https://docs.rs/crate/questdb-rs/latest) auth: - kid: testUser1 + kid: admin d: 5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48 x: fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU y: Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac diff --git a/examples/line_sender_c_example.c b/examples/line_sender_c_example.c index 3e0b5786..0f6805cc 100644 --- a/examples/line_sender_c_example.c +++ b/examples/line_sender_c_example.c @@ -20,7 +20,7 @@ static bool example(const char* host, const char* port) sender = line_sender_from_conf(conf_str_utf8, &err); if (!sender) - goto on_error; + goto on_error; free(conf_str); conf_str = NULL; @@ -31,37 +31,28 @@ static bool example(const char* host, const char* port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_auth.c b/examples/line_sender_c_example_auth.c index bb7cf543..59a99421 100644 --- a/examples/line_sender_c_example_auth.c +++ b/examples/line_sender_c_example_auth.c @@ -11,7 +11,7 @@ static bool example(const char* host, const char* port) line_sender_buffer* buffer = NULL; char* conf_str = concat( "tcp::addr=", host, ":", port, ";" - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;"); @@ -25,7 +25,7 @@ static bool example(const char* host, const char* port) sender = line_sender_from_conf(conf_str_utf8, &err); if (!sender) - goto on_error; + goto on_error; free(conf_str); conf_str = NULL; @@ -36,37 +36,28 @@ static bool example(const char* host, const char* port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_auth"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_auth"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_auth_tls.c b/examples/line_sender_c_example_auth_tls.c index f727e5c5..e7454d8d 100644 --- a/examples/line_sender_c_example_auth_tls.c +++ b/examples/line_sender_c_example_auth_tls.c @@ -11,7 +11,7 @@ static bool example(const char* host, const char* port) line_sender_buffer* buffer = NULL; char* conf_str = concat( "tcps::addr=", host, ":", port, ";" - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;"); @@ -25,7 +25,7 @@ static bool example(const char* host, const char* port) sender = line_sender_from_conf(conf_str_utf8, &err); if (!sender) - goto on_error; + goto on_error; free(conf_str); conf_str = NULL; @@ -36,37 +36,28 @@ static bool example(const char* host, const char* port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_auth_tls"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_auth_tls"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_from_conf.c b/examples/line_sender_c_example_from_conf.c index 808ab6b8..6f1d4e43 100644 --- a/examples/line_sender_c_example_from_conf.c +++ b/examples/line_sender_c_example_from_conf.c @@ -12,7 +12,7 @@ int main(int argc, const char* argv[]) "tcp::addr=localhost:9009;"); line_sender* sender = line_sender_from_conf(conf, &err); if (!sender) - goto on_error; + goto on_error; buffer = line_sender_buffer_new(); line_sender_buffer_reserve(buffer, 64 * 1024); // 64KB buffer initial size. @@ -20,37 +20,28 @@ int main(int argc, const char* argv[]) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_from_conf"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_from_conf"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_from_env.c b/examples/line_sender_c_example_from_env.c index 5c636b00..e67dbe59 100644 --- a/examples/line_sender_c_example_from_env.c +++ b/examples/line_sender_c_example_from_env.c @@ -11,7 +11,7 @@ int main(int argc, const char* argv[]) // Construct a sender from the `QDB_CLIENT_CONF` environment variable. line_sender* sender = line_sender_from_env(&err); if (!sender) - goto on_error; + goto on_error; buffer = line_sender_buffer_new(); line_sender_buffer_reserve(buffer, 64 * 1024); // 64KB buffer initial size. @@ -19,37 +19,28 @@ int main(int argc, const char* argv[]) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_from_env"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_from_env"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_http.c b/examples/line_sender_c_example_http.c index 28134b2f..427ab705 100644 --- a/examples/line_sender_c_example_http.c +++ b/examples/line_sender_c_example_http.c @@ -22,7 +22,7 @@ static bool example(const char* host, const char* port) sender = line_sender_from_conf(conf_str_utf8, &err); if (!sender) - goto on_error; + goto on_error; free(conf_str); conf_str = NULL; @@ -30,40 +30,28 @@ static bool example(const char* host, const char* port) buffer = line_sender_buffer_new(); line_sender_buffer_reserve(buffer, 64 * 1024); // 64KB buffer initial size. - // We prepare all our table names and column names in advance. - // If we're inserting multiple rows, this allows us to avoid - // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_http"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_http"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_c_example_tls_ca.c b/examples/line_sender_c_example_tls_ca.c index 4636d396..63151945 100644 --- a/examples/line_sender_c_example_tls_ca.c +++ b/examples/line_sender_c_example_tls_ca.c @@ -12,7 +12,7 @@ static bool example(const char* ca_path, const char* host, const char* port) char* conf_str = concat( "tcps::addr=", host, ":", port, ";", "tls_roots=", ca_path, ";", - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;"); @@ -26,7 +26,7 @@ static bool example(const char* ca_path, const char* host, const char* port) sender = line_sender_from_conf(conf_str_utf8, &err); if (!sender) - goto on_error; + goto on_error; free(conf_str); conf_str = NULL; @@ -37,37 +37,28 @@ static bool example(const char* ca_path, const char* host, const char* port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_cars_tls_ca"); - line_sender_column_name id_name = QDB_COLUMN_NAME_LITERAL("id"); - line_sender_column_name x_name = QDB_COLUMN_NAME_LITERAL("x"); - line_sender_column_name y_name = QDB_COLUMN_NAME_LITERAL("y"); - line_sender_column_name booked_name = QDB_COLUMN_NAME_LITERAL("booked"); - line_sender_column_name passengers_name = QDB_COLUMN_NAME_LITERAL( - "passengers"); - line_sender_column_name driver_name = QDB_COLUMN_NAME_LITERAL("driver"); - - if (!line_sender_buffer_table(buffer, table_name, &err)) - goto on_error; + line_sender_table_name table_name = QDB_TABLE_NAME_LITERAL("c_trades_tls_ca"); + line_sender_column_name symbol_name = QDB_COLUMN_NAME_LITERAL("symbol"); + line_sender_column_name side_name = QDB_COLUMN_NAME_LITERAL("side"); + line_sender_column_name price_name = QDB_COLUMN_NAME_LITERAL("price"); + line_sender_column_name amount_name = QDB_COLUMN_NAME_LITERAL("amount"); - line_sender_utf8 id_value = QDB_UTF8_LITERAL( - "d6e5fe92-d19f-482a-a97a-c105f547f721"); - if (!line_sender_buffer_symbol(buffer, id_name, id_value, &err)) - goto on_error; - if (!line_sender_buffer_column_f64(buffer, x_name, 30.5, &err)) + if (!line_sender_buffer_table(buffer, table_name, &err)) goto on_error; - if (!line_sender_buffer_column_f64(buffer, y_name, -150.25, &err)) + line_sender_utf8 symbol_value = QDB_UTF8_LITERAL("ETH-USD"); + if (!line_sender_buffer_symbol(buffer, symbol_name, symbol_value, &err)) goto on_error; - if (!line_sender_buffer_column_bool(buffer, booked_name, true, &err)) + line_sender_utf8 side_value = QDB_UTF8_LITERAL("sell"); + if (!line_sender_buffer_symbol(buffer, side_name, side_value, &err)) goto on_error; - if (!line_sender_buffer_column_i64(buffer, passengers_name, 3, &err)) + if (!line_sender_buffer_column_f64(buffer, price_name, 2615.54, &err)) goto on_error; - line_sender_utf8 driver_value = QDB_UTF8_LITERAL("John Doe"); - if (!line_sender_buffer_column_str(buffer, driver_name, driver_value, &err)) + if (!line_sender_buffer_column_f64(buffer, amount_name, 0.00044, &err)) goto on_error; // 1997-07-04 04:56:55 UTC diff --git a/examples/line_sender_cpp_example.cpp b/examples/line_sender_cpp_example.cpp index 7e7dddca..2d90812f 100644 --- a/examples/line_sender_cpp_example.cpp +++ b/examples/line_sender_cpp_example.cpp @@ -14,23 +14,19 @@ static bool example(std::string_view host, std::string_view port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_auth.cpp b/examples/line_sender_cpp_example_auth.cpp index f25c2534..85e0d6e1 100644 --- a/examples/line_sender_cpp_example_auth.cpp +++ b/examples/line_sender_cpp_example_auth.cpp @@ -10,7 +10,7 @@ static bool example(std::string_view host, std::string_view port) { auto sender = questdb::ingress::line_sender::from_conf( "tcp::addr=" + std::string{host} + ":" + std::string{port} + ";" - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;"); @@ -18,23 +18,19 @@ static bool example(std::string_view host, std::string_view port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_auth"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_auth"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_auth_tls.cpp b/examples/line_sender_cpp_example_auth_tls.cpp index 7299c718..f100dd04 100644 --- a/examples/line_sender_cpp_example_auth_tls.cpp +++ b/examples/line_sender_cpp_example_auth_tls.cpp @@ -12,7 +12,7 @@ static bool example( { auto sender = questdb::ingress::line_sender::from_conf( "tcps::addr=" + std::string{host} + ":" + std::string{port} + ";" - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;"); @@ -20,23 +20,19 @@ static bool example( // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_auth_tls"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_auth_tls"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_from_conf.cpp b/examples/line_sender_cpp_example_from_conf.cpp index 1d3bb29a..bb71c6e3 100644 --- a/examples/line_sender_cpp_example_from_conf.cpp +++ b/examples/line_sender_cpp_example_from_conf.cpp @@ -14,23 +14,19 @@ int main(int argc, const char* argv[]) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_from_conf"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_from_conf"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_from_env.cpp b/examples/line_sender_cpp_example_from_env.cpp index 7526fb88..63e99b26 100644 --- a/examples/line_sender_cpp_example_from_env.cpp +++ b/examples/line_sender_cpp_example_from_env.cpp @@ -13,23 +13,19 @@ int main(int argc, const char* argv[]) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_from_env"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_from_env"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_http.cpp b/examples/line_sender_cpp_example_http.cpp index 979657f7..217845a2 100644 --- a/examples/line_sender_cpp_example_http.cpp +++ b/examples/line_sender_cpp_example_http.cpp @@ -14,23 +14,19 @@ static bool example(std::string_view host, std::string_view port) // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_http"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_http"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/examples/line_sender_cpp_example_tls_ca.cpp b/examples/line_sender_cpp_example_tls_ca.cpp index 64231f0d..c0327e96 100644 --- a/examples/line_sender_cpp_example_tls_ca.cpp +++ b/examples/line_sender_cpp_example_tls_ca.cpp @@ -13,7 +13,7 @@ static bool example( { auto sender = questdb::ingress::line_sender::from_conf( "tcps::addr=" + std::string{host} + ":" + std::string{port} + ";" - "username=testUser1;" + "username=admin;" "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;" "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;" "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;" @@ -22,23 +22,19 @@ static bool example( // We prepare all our table names and column names in advance. // If we're inserting multiple rows, this allows us to avoid // re-validating the same strings over and over again. - const auto table_name = "cpp_cars_tls_ca"_tn; - const auto id_name = "id"_cn; - const auto x_name = "x"_cn; - const auto y_name = "y"_cn; - const auto booked_name = "booked"_cn; - const auto passengers_name = "passengers"_cn; - const auto driver_name = "driver"_cn; + const auto table_name = "cpp_trades_tls_ca"_tn; + const auto symbol_name = "symbol"_cn; + const auto side_name = "side"_cn; + const auto price_name = "price"_cn; + const auto amount_name = "amount"_cn; questdb::ingress::line_sender_buffer buffer; buffer .table(table_name) - .symbol(id_name, "d6e5fe92-d19f-482a-a97a-c105f547f721"_utf8) - .column(x_name, 30.5) - .column(y_name, -150.25) - .column(booked_name, true) - .column(passengers_name, int64_t{3}) - .column(driver_name, "John Doe"_utf8) + .symbol(symbol_name, "ETH-USD"_utf8) + .symbol(side_name, "sell"_utf8) + .column(price_name, 2615.54) + .column(amount_name, 0.00044) .at(questdb::ingress::timestamp_nanos::now()); // To insert more records, call `buffer.table(..)...` again. diff --git a/include/questdb/ingress/line_sender.h b/include/questdb/ingress/line_sender.h index 16449d88..281b9d78 100644 --- a/include/questdb/ingress/line_sender.h +++ b/include/questdb/ingress/line_sender.h @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ extern "C" { # define LINESENDER_API #endif - /////////// Error handling. /** An error that occurred when using the line sender. */ typedef struct line_sender_error line_sender_error; @@ -96,15 +95,19 @@ typedef enum line_sender_protocol line_sender_protocol_https, } line_sender_protocol; -/** Possible sources of the root certificates used to validate the server's TLS certificate. */ -typedef enum line_sender_ca { +/** Possible sources of the root certificates used to validate the server's + * TLS certificate. */ +typedef enum line_sender_ca +{ /** Use the set of root certificates provided by the `webpki` crate. */ line_sender_ca_webpki_roots, - /** Use the set of root certificates provided by the operating system. */ + /** Use the set of root certificates provided by the operating system. + */ line_sender_ca_os_roots, - /** Combine the set of root certificates provided by the `webpki` crate and the operating system. */ + /** Combine the set of root certificates provided by the `webpki` crate + * and the operating system. */ line_sender_ca_webpki_and_os_roots, /** Use the root certificates provided in a PEM-encoded file. */ @@ -127,7 +130,6 @@ const char* line_sender_error_msg(const line_sender_error*, size_t* len_out); LINESENDER_API void line_sender_error_free(line_sender_error*); - /////////// Preparing strings and names /** @@ -171,6 +173,16 @@ line_sender_utf8 line_sender_utf8_assert(size_t len, const char* buf); #define QDB_UTF8_LITERAL(literal) \ line_sender_utf8_assert(sizeof(literal) - 1, (literal)) +/** + * Non-owning view of sender buffer, Modifying the buffer will invalidate + * the borrowed buffer + */ +typedef struct line_sender_buffer_view +{ + size_t len; + const uint8_t* buf; +} line_sender_buffer_view; + /** * Non-owning validated table, symbol or column name. UTF-8 encoded. * Need not be null-terminated. @@ -210,8 +222,7 @@ bool line_sender_table_name_init( */ LINESENDER_API line_sender_table_name line_sender_table_name_assert( - size_t len, - const char* buf); + size_t len, const char* buf); #define QDB_TABLE_NAME_LITERAL(literal) \ line_sender_table_name_assert(sizeof(literal) - 1, (literal)) @@ -247,21 +258,19 @@ bool line_sender_column_name_init( /** * Construct a column name object from UTF-8 encoded buffer and length. - * If the passed in buffer is not valid UTF-8, or is not a valid column name, - * the program will abort. + * If the passed in buffer is not valid UTF-8, or is not a valid column + * name, the program will abort. * * @param[in] len Length in bytes of the buffer. * @param[in] buf UTF-8 encoded buffer. */ LINESENDER_API line_sender_column_name line_sender_column_name_assert( - size_t len, - const char* buf); + size_t len, const char* buf); #define QDB_COLUMN_NAME_LITERAL(literal) \ line_sender_column_name_assert(sizeof(literal) - 1, (literal)) - /////////// Constructing ILP messages. /** @@ -271,18 +280,18 @@ line_sender_column_name line_sender_column_name_assert( typedef struct line_sender_buffer line_sender_buffer; /** - * Construct a `line_sender_buffer` with a `max_name_len` of `127`, which is the - * same as the QuestDB server default. + * Construct a `line_sender_buffer` with a `max_name_len` of `127`, which is + * the same as the QuestDB server default. */ LINESENDER_API line_sender_buffer* line_sender_buffer_new(); /** - * Construct a `line_sender_buffer` with a custom maximum length for table and - * column names. This should match the `cairo.max.file.name.length` setting of - * the QuestDB server you're connecting to. - * If the server does not configure it, the default is `127`, and you can - * call `line_sender_buffer_new()` instead. + * Construct a `line_sender_buffer` with a custom maximum length for table + * and column names. This should match the `cairo.max.file.name.length` + * setting of the QuestDB server you're connecting to. If the server does + * not configure it, the default is `127`, and you can call + * `line_sender_buffer_new()` instead. */ LINESENDER_API line_sender_buffer* line_sender_buffer_with_max_name_len(size_t max_name_len); @@ -302,9 +311,7 @@ line_sender_buffer* line_sender_buffer_clone(const line_sender_buffer* buffer); * See: `capacity`. */ LINESENDER_API -void line_sender_buffer_reserve( - line_sender_buffer* buffer, - size_t additional); +void line_sender_buffer_reserve(line_sender_buffer* buffer, size_t additional); /** Get the current capacity of the buffer. */ LINESENDER_API @@ -319,8 +326,7 @@ size_t line_sender_buffer_capacity(const line_sender_buffer* buffer); */ LINESENDER_API bool line_sender_buffer_set_marker( - line_sender_buffer* buffer, - line_sender_error** err_out); + line_sender_buffer* buffer, line_sender_error** err_out); /** * Undo all changes since the last `set_marker()` call. @@ -328,13 +334,11 @@ bool line_sender_buffer_set_marker( */ LINESENDER_API bool line_sender_buffer_rewind_to_marker( - line_sender_buffer* buffer, - line_sender_error** err_out); + line_sender_buffer* buffer, line_sender_error** err_out); /** Discard the marker. */ LINESENDER_API -void line_sender_buffer_clear_marker( - line_sender_buffer* buffer); +void line_sender_buffer_clear_marker(line_sender_buffer* buffer); /** * Remove all accumulated data and prepare the buffer for new lines. @@ -352,26 +356,23 @@ LINESENDER_API size_t line_sender_buffer_row_count(const line_sender_buffer* buffer); /** - * Tell whether the buffer is transactional. It is transactional iff it contains - * data for at most one table. Additionally, you must send the buffer over HTTP to - * get transactional behavior. + * Tell whether the buffer is transactional. It is transactional iff it + * contains data for at most one table. Additionally, you must send the + * buffer over HTTP to get transactional behavior. */ LINESENDER_API bool line_sender_buffer_transactional(const line_sender_buffer* buffer); /** - * Get a string representation of the contents of the buffer. + * Get a read-only view into the buffer's bytes contents. * * @param[in] buffer Line sender buffer object. - * @param[out] len_out The length in bytes of the returned string buffer. - * @return UTF-8 encoded buffer with the string representation of the line - * sender buffer's contents. The buffer is not nul-terminated, and the - * length is in the `len_out` parameter. + * @return read_only view with the byte representation of the line + * sender buffer's contents. */ LINESENDER_API -const char* line_sender_buffer_peek( - const line_sender_buffer* buffer, - size_t* len_out); +line_sender_buffer_view line_sender_buffer_peek( + const line_sender_buffer* buffer); /** * Start recording a new row for the given table. @@ -507,7 +508,7 @@ bool line_sender_buffer_column_ts_micros( */ LINESENDER_API bool line_sender_buffer_at_nanos( - line_sender_buffer *buffer, + line_sender_buffer* buffer, int64_t epoch_nanos, line_sender_error** err_out); @@ -528,29 +529,31 @@ bool line_sender_buffer_at_nanos( */ LINESENDER_API bool line_sender_buffer_at_micros( - line_sender_buffer *buffer, + line_sender_buffer* buffer, int64_t epoch_micros, line_sender_error** err_out); /** - * Complete the current row without providing a timestamp. The QuestDB instance - * will insert its own timestamp. + * Complete the current row without providing a timestamp. The QuestDB + * instance will insert its own timestamp. * - * Letting the server assign the timestamp can be faster since it a reliable way - * to avoid out-of-order operations in the database for maximum ingestion - * throughput. However, it removes the ability to deduplicate rows. + * Letting the server assign the timestamp can be faster since it a reliable + * way to avoid out-of-order operations in the database for maximum + * ingestion throughput. However, it removes the ability to deduplicate + * rows. * * This is NOT equivalent to calling `line_sender_buffer_at_nanos()` or - * `line_sender_buffer_at_micros()` with the current time: the QuestDB server - * will set the timestamp only after receiving the row. If you're flushing - * infrequently, the server-assigned timestamp may be significantly behind the - * time the data was recorded in the buffer. + * `line_sender_buffer_at_micros()` with the current time: the QuestDB + * server will set the timestamp only after receiving the row. If you're + * flushing infrequently, the server-assigned timestamp may be significantly + * behind the time the data was recorded in the buffer. * - * In almost all cases, you should prefer the `line_sender_buffer_at_*()` functions. + * In almost all cases, you should prefer the `line_sender_buffer_at_*()` + * functions. * - * After this call, you can start recording the next row by calling `table()` - * again, or you can send the accumulated batch by calling `flush()` or one of - * its variants. + * After this call, you can start recording the next row by calling + * `table()` again, or you can send the accumulated batch by calling + * `flush()` or one of its variants. * * @param[in] buffer Line buffer object. * @param[out] err_out Set on error. @@ -558,16 +561,15 @@ bool line_sender_buffer_at_micros( */ LINESENDER_API bool line_sender_buffer_at_now( - line_sender_buffer* buffer, - line_sender_error** err_out); + line_sender_buffer* buffer, line_sender_error** err_out); /////////// Connecting, sending and disconnecting. /** * Inserts data into QuestDB via the InfluxDB Line Protocol. * - * Batch up rows in a `line_sender_buffer`, then call `line_sender_flush()` or - * one of its variants with this object to send them. + * Batch up rows in a `line_sender_buffer`, then call `line_sender_flush()` + * or one of its variants with this object to send them. */ typedef struct line_sender line_sender; @@ -577,50 +579,47 @@ typedef struct line_sender line_sender; typedef struct line_sender_opts line_sender_opts; /** - * Create a new `line_sender_opts` instance from the given configuration string. - * The format of the string is: "tcp::addr=host:port;key=value;...;" + * Create a new `line_sender_opts` instance from the given configuration + * string. The format of the string is: "tcp::addr=host:port;key=value;...;" * Instead of "tcp" you can also specify "tcps", "http", and "https". * - * The accepted keys match one-for-one with the functions on `line_sender_opts`. - * For example, this is a valid configuration string: + * The accepted keys match one-for-one with the functions on + * `line_sender_opts`. For example, this is a valid configuration string: * * "https::addr=host:port;username=alice;password=secret;" * * and there are matching functions `line_sender_opts_username()` and - * `line_sender_opts_password()`. The value for `addr=` is supplied directly to - * `line_sender_opts_new`, so there's no function with a matching name. + * `line_sender_opts_password()`. The value for `addr=` is supplied directly + * to `line_sender_opts_new`, so there's no function with a matching name. * - * For the full list of keys, search this header for `bool line_sender_opts_`. + * For the full list of keys, search this header for `bool + * line_sender_opts_`. */ LINESENDER_API line_sender_opts* line_sender_opts_from_conf( - line_sender_utf8 config, - line_sender_error** err_out); + line_sender_utf8 config, line_sender_error** err_out); /** - * Create a new `line_sender_opts` instance from the configuration stored in the - * `QDB_CLIENT_CONF` environment variable. + * Create a new `line_sender_opts` instance from the configuration stored in + * the `QDB_CLIENT_CONF` environment variable. */ LINESENDER_API -line_sender_opts* line_sender_opts_from_env( - line_sender_error** err_out); +line_sender_opts* line_sender_opts_from_env(line_sender_error** err_out); /** - * Create a new `line_sender_opts` instance with the given protocol, hostname and - * port. + * Create a new `line_sender_opts` instance with the given protocol, + * hostname and port. * @param[in] protocol The protocol to use. * @param[in] host The QuestDB database host. * @param[in] port The QuestDB ILP TCP port. */ LINESENDER_API line_sender_opts* line_sender_opts_new( - line_sender_protocol protocol, - line_sender_utf8 host, - uint16_t port); + line_sender_protocol protocol, line_sender_utf8 host, uint16_t port); /** - * Create a new `line_sender_opts` instance with the given protocol, hostname and - * service name. + * Create a new `line_sender_opts` instance with the given protocol, + * hostname and service name. */ LINESENDER_API line_sender_opts* line_sender_opts_new_service( @@ -701,9 +700,7 @@ bool line_sender_opts_token_y( */ LINESENDER_API bool line_sender_opts_auth_timeout( - line_sender_opts* opts, - uint64_t millis, - line_sender_error** err_out); + line_sender_opts* opts, uint64_t millis, line_sender_error** err_out); /** * Set to `false` to disable TLS certificate verification. @@ -714,9 +711,7 @@ bool line_sender_opts_auth_timeout( */ LINESENDER_API bool line_sender_opts_tls_verify( - line_sender_opts* opts, - bool verify, - line_sender_error** err_out); + line_sender_opts* opts, bool verify, line_sender_error** err_out); /** * Specify where to find the root certificates used to validate the @@ -724,32 +719,27 @@ bool line_sender_opts_tls_verify( */ LINESENDER_API bool line_sender_opts_tls_ca( - line_sender_opts* opts, - line_sender_ca ca, - line_sender_error** err_out); + line_sender_opts* opts, line_sender_ca ca, line_sender_error** err_out); /** * Set the path to a custom root certificate `.pem` file. - * This is used to validate the server's certificate during the TLS handshake. + * This is used to validate the server's certificate during the TLS + * handshake. * * See notes on how to test with self-signed certificates: * https://github.com/questdb/c-questdb-client/tree/main/tls_certs. */ LINESENDER_API bool line_sender_opts_tls_roots( - line_sender_opts* opts, - line_sender_utf8 path, - line_sender_error** err_out); + line_sender_opts* opts, line_sender_utf8 path, line_sender_error** err_out); /** - * Set the maximum buffer size in bytes that the client will flush to the server. - * The default is 100 MiB. + * Set the maximum buffer size in bytes that the client will flush to the + * server. The default is 100 MiB. */ LINESENDER_API bool line_sender_opts_max_buf_size( - line_sender_opts* opts, - size_t max_buf_size, - line_sender_error** err_out); + line_sender_opts* opts, size_t max_buf_size, line_sender_error** err_out); /** * Set the cumulative duration spent in retries. @@ -757,16 +747,14 @@ bool line_sender_opts_max_buf_size( */ LINESENDER_API bool line_sender_opts_retry_timeout( - line_sender_opts* opts, - uint64_t millis, - line_sender_error** err_out); + line_sender_opts* opts, uint64_t millis, line_sender_error** err_out); /** - * Set the minimum acceptable throughput while sending a buffer to the server. - * The sender will divide the payload size by this number to determine for how - * long to keep sending the payload before timing out. - * The value is in bytes per second, and the default is 100 KiB/s. - * The timeout calculated from minimum throughput is adedd to the value of + * Set the minimum acceptable throughput while sending a buffer to the + * server. The sender will divide the payload size by this number to + * determine for how long to keep sending the payload before timing out. The + * value is in bytes per second, and the default is 100 KiB/s. The timeout + * calculated from minimum throughput is adedd to the value of * `request_timeout`. * * See also: `line_sender_opts_request_timeout()` @@ -778,17 +766,16 @@ bool line_sender_opts_request_min_throughput( line_sender_error** err_out); /** - * Set the additional time to wait on top of that calculated from the minimum - * throughput. This accounts for the fixed latency of the HTTP request-response - * roundtrip. The value is in milliseconds, and the default is 10 seconds. + * Set the additional time to wait on top of that calculated from the + * minimum throughput. This accounts for the fixed latency of the HTTP + * request-response roundtrip. The value is in milliseconds, and the default + * is 10 seconds. * * See also: `line_sender_opts_request_min_throughput()` */ LINESENDER_API bool line_sender_opts_request_timeout( - line_sender_opts* opts, - uint64_t millis, - line_sender_error** err_out); + line_sender_opts* opts, uint64_t millis, line_sender_error** err_out); // Do not call: Private API for the C++ and Python bindings. bool line_sender_opts_user_agent( @@ -801,8 +788,7 @@ bool line_sender_opts_user_agent( * Both old and new objects will have to be freed. */ LINESENDER_API -line_sender_opts* line_sender_opts_clone( - line_sender_opts* opts); +line_sender_opts* line_sender_opts_clone(line_sender_opts* opts); /** Release the `line_sender_opts` object. */ LINESENDER_API @@ -811,8 +797,8 @@ void line_sender_opts_free(line_sender_opts* opts); /** * Create a new line sender instance from the given options object. * - * In the case of TCP, this synchronously establishes the TCP connection, and - * returns once the connection is fully established. If the connection + * In the case of TCP, this synchronously establishes the TCP connection, + * and returns once the connection is fully established. If the connection * requires authentication or TLS, these will also be completed before * returning. * @@ -822,27 +808,27 @@ void line_sender_opts_free(line_sender_opts* opts); */ LINESENDER_API line_sender* line_sender_build( - const line_sender_opts* opts, - line_sender_error** err_out); + const line_sender_opts* opts, line_sender_error** err_out); /** * Create a new line sender instance from the given configuration string. * The format of the string is: "tcp::addr=host:port;key=value;...;" * Instead of "tcp" you can also specify "tcps", "http", and "https". * - * The accepted keys match one-for-one with the functions on `line_sender_opts`. - * For example, this is a valid configuration string: + * The accepted keys match one-for-one with the functions on + * `line_sender_opts`. For example, this is a valid configuration string: * * "https::addr=host:port;username=alice;password=secret;" * * and there are matching functions `line_sender_opts_username()` and - * `line_sender_opts_password()`. The value for `addr=` is supplied directly to - * `line_sender_opts_new`, so there's no function with a matching name. + * `line_sender_opts_password()`. The value for `addr=` is supplied directly + * to `line_sender_opts_new`, so there's no function with a matching name. * - * For the full list of keys, search this header for `bool line_sender_opts_`. + * For the full list of keys, search this header for `bool + * line_sender_opts_`. * - * In the case of TCP, this synchronously establishes the TCP connection, and - * returns once the connection is fully established. If the connection + * In the case of TCP, this synchronously establishes the TCP connection, + * and returns once the connection is fully established. If the connection * requires authentication or TLS, these will also be completed before * returning. * @@ -850,23 +836,21 @@ line_sender* line_sender_build( */ LINESENDER_API line_sender* line_sender_from_conf( - line_sender_utf8 config, - line_sender_error** err_out); + line_sender_utf8 config, line_sender_error** err_out); /** * Create a new `line_sender` instance from the configuration stored in the * `QDB_CLIENT_CONF` environment variable. * - * In the case of TCP, this synchronously establishes the TCP connection, and - * returns once the connection is fully established. If the connection + * In the case of TCP, this synchronously establishes the TCP connection, + * and returns once the connection is fully established. If the connection * requires authentication or TLS, these will also be completed before * returning. * * The sender should be accessed by only a single thread a time. */ LINESENDER_API -line_sender* line_sender_from_env( - line_sender_error** err_out); +line_sender* line_sender_from_env(line_sender_error** err_out); /** * Tell whether the sender is no longer usable and must be closed. @@ -888,26 +872,27 @@ void line_sender_close(line_sender* sender); /** * Send the given buffer of rows to the QuestDB server, clearing the buffer. * - * After this function returns, the buffer is empty and ready for the next batch. - * If you want to preserve the buffer contents, call `line_sender_flush_and_keep`. - * If you want to ensure the flush is transactional, call - * `line_sender_flush_and_keep_with_flags`. + * After this function returns, the buffer is empty and ready for the next + * batch. If you want to preserve the buffer contents, call + * `line_sender_flush_and_keep`. If you want to ensure the flush is + * transactional, call `line_sender_flush_and_keep_with_flags`. * * With ILP-over-HTTP, this function sends an HTTP request and waits for the - * response. If the server responds with an error, it returns a descriptive error. - * In the case of a network error, it retries until it has exhausted the retry time - * budget. + * response. If the server responds with an error, it returns a descriptive + * error. In the case of a network error, it retries until it has exhausted + * the retry time budget. * - * With ILP-over-TCP, the function blocks only until the buffer is flushed to the - * underlying OS-level network socket, without waiting to actually send it to the - * server. In the case of an error, the server will quietly disconnect: consult the - * server logs for error messages. + * With ILP-over-TCP, the function blocks only until the buffer is flushed + * to the underlying OS-level network socket, without waiting to actually + * send it to the server. In the case of an error, the server will quietly + * disconnect: consult the server logs for error messages. * - * HTTP should be the first choice, but use TCP if you need to continuously send - * data to the server at a high rate. + * HTTP should be the first choice, but use TCP if you need to continuously + * send data to the server at a high rate. * - * To improve the HTTP performance, send larger buffers (with more rows), and - * consider parallelizing writes using multiple senders from multiple threads. + * To improve the HTTP performance, send larger buffers (with more rows), + * and consider parallelizing writes using multiple senders from multiple + * threads. * * @param[in] sender Line sender object. * @param[in] buffer Line buffer object. @@ -922,37 +907,41 @@ bool line_sender_flush( /** * Send the given buffer of rows to the QuestDB server. * - * All the data stays in the buffer. Clear the buffer before starting a new batch. + * All the data stays in the buffer. Clear the buffer before starting a new + * batch. * - * To send and clear in one step, call `line_sender_flush` instead. Also, see the docs - * on that function for more important details on flushing. + * To send and clear in one step, call `line_sender_flush` instead. Also, + * see the docs on that function for more important details on flushing. * @param[in] sender Line sender object. * @param[in] buffer Line buffer object. * @return true on success, false on error. */ LINESENDER_API bool line_sender_flush_and_keep( - line_sender *sender, + line_sender* sender, const line_sender_buffer* buffer, line_sender_error** err_out); /** - * Send the batch of rows in the buffer to the QuestDB server, and, if the parameter - * `transactional` is true, ensure the flush will be transactional. + * Send the batch of rows in the buffer to the QuestDB server, and, if the + * parameter `transactional` is true, ensure the flush will be + * transactional. * - * A flush is transactional iff all the rows belong to the same table. This allows - * QuestDB to treat the flush as a single database transaction, because it doesn't - * support transactions spanning multiple tables. Additionally, only ILP-over-HTTP - * supports transactional flushes. + * A flush is transactional iff all the rows belong to the same table. This + * allows QuestDB to treat the flush as a single database transaction, + * because it doesn't support transactions spanning multiple tables. + * Additionally, only ILP-over-HTTP supports transactional flushes. * - * If the flush wouldn't be transactional, this function returns an error and - * doesn't flush any data. + * If the flush wouldn't be transactional, this function returns an error + * and doesn't flush any data. * - * The function sends an HTTP request and waits for the response. If the server - * responds with an error, it returns a descriptive error. In the case of a network - * error, it retries until it has exhausted the retry time budget. + * The function sends an HTTP request and waits for the response. If the + * server responds with an error, it returns a descriptive error. In the + * case of a network error, it retries until it has exhausted the retry time + * budget. * - * All the data stays in the buffer. Clear the buffer before starting a new batch. + * All the data stays in the buffer. Clear the buffer before starting a new + * batch. */ LINESENDER_API bool line_sender_flush_and_keep_with_flags( @@ -971,7 +960,6 @@ int64_t line_sender_now_nanos(); LINESENDER_API int64_t line_sender_now_micros(); - #ifdef __cplusplus } #endif diff --git a/include/questdb/ingress/line_sender.hpp b/include/questdb/ingress/line_sender.hpp index e70f8308..e118a22f 100644 --- a/include/questdb/ingress/line_sender.hpp +++ b/include/questdb/ingress/line_sender.hpp @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,1267 +26,1264 @@ #include "line_sender.h" -#include <string> -#include <string_view> -#include <stdexcept> +#include <chrono> +#include <cstddef> #include <cstdint> #include <optional> -#include <chrono> +#include <stdexcept> +#include <string> #include <type_traits> +#if __cplusplus >= 202002L +# include <span> +#endif namespace questdb::ingress { - constexpr const char* inaddr_any = "0.0.0.0"; +constexpr const char* inaddr_any = "0.0.0.0"; - class line_sender; - class line_sender_buffer; - class opts; +class line_sender; +class line_sender_buffer; +class opts; - /** Category of error. */ - enum class line_sender_error_code - { - /** The host, port, or interface was incorrect. */ - could_not_resolve_addr, +/** Category of error. */ +enum class line_sender_error_code +{ + /** The host, port, or interface was incorrect. */ + could_not_resolve_addr, - /** Called methods in the wrong order. E.g. `symbol` after `column`. */ - invalid_api_call, + /** Called methods in the wrong order. E.g. `symbol` after `column`. */ + invalid_api_call, - /** A network error connecting or flushing data out. */ - socket_error, + /** A network error connecting or flushing data out. */ + socket_error, - /** The string or symbol field is not encoded in valid UTF-8. */ - invalid_utf8, + /** The string or symbol field is not encoded in valid UTF-8. */ + invalid_utf8, - /** The table name or column name contains bad characters. */ - invalid_name, + /** The table name or column name contains bad characters. */ + invalid_name, - /** The supplied timestamp is invalid. */ - invalid_timestamp, + /** The supplied timestamp is invalid. */ + invalid_timestamp, - /** Error during the authentication process. */ - auth_error, + /** Error during the authentication process. */ + auth_error, - /** Error during TLS handshake. */ - tls_error, + /** Error during TLS handshake. */ + tls_error, - /** The server does not support ILP over HTTP. */ - http_not_supported, + /** The server does not support ILP over HTTP. */ + http_not_supported, - /** Error sent back from the server during flush. */ - server_flush_error, + /** Error sent back from the server during flush. */ + server_flush_error, - /** Bad configuration. */ - config_error, - }; + /** Bad configuration. */ + config_error, +}; - /** The protocol used to connect with. */ - enum class protocol - { - /** InfluxDB Line Protocol over TCP. */ - tcp, +/** The protocol used to connect with. */ +enum class protocol +{ + /** InfluxDB Line Protocol over TCP. */ + tcp, - /** InfluxDB Line Protocol over TCP with TLS. */ - tcps, + /** InfluxDB Line Protocol over TCP with TLS. */ + tcps, - /** InfluxDB Line Protocol over HTTP. */ - http, + /** InfluxDB Line Protocol over HTTP. */ + http, - /** InfluxDB Line Protocol over HTTP with TLS. */ - https, - }; + /** InfluxDB Line Protocol over HTTP with TLS. */ + https, +}; - /* Possible sources of the root certificates used to validate the server's TLS certificate. */ - enum class ca { - /** Use the set of root certificates provided by the `webpki` crate. */ - webpki_roots, +/* Possible sources of the root certificates used to validate the server's TLS + * certificate. */ +enum class ca +{ + /** Use the set of root certificates provided by the `webpki` crate. */ + webpki_roots, - /** Use the set of root certificates provided by the operating system. */ - os_roots, + /** Use the set of root certificates provided by the operating system. */ + os_roots, - /** Combine the set of root certificates provided by the `webpki` crate and the operating system. */ - webpki_and_os_roots, + /** Combine the set of root certificates provided by the `webpki` crate and + * the operating system. */ + webpki_and_os_roots, - /** Use the root certificates provided in a PEM-encoded file. */ - pem_file, - }; + /** Use the root certificates provided in a PEM-encoded file. */ + pem_file, +}; - /** - * An error that occurred when using the line sender. - * - * Call `.what()` to obtain the ASCII-encoded error message. - */ - class line_sender_error : public std::runtime_error +/** + * An error that occurred when using the line sender. + * + * Call `.what()` to obtain the ASCII-encoded error message. + */ +class line_sender_error : public std::runtime_error +{ +public: + line_sender_error(line_sender_error_code code, const std::string& what) + : std::runtime_error{what} + , _code{code} { - public: - line_sender_error(line_sender_error_code code, const std::string& what) - : std::runtime_error{what} - , _code{code} - {} - - /** Error code categorizing the error. */ - line_sender_error_code code() const noexcept { return _code; } - - private: - inline static line_sender_error from_c(::line_sender_error* c_err) - { - line_sender_error_code code = - static_cast<line_sender_error_code>( - static_cast<int>( - ::line_sender_error_get_code(c_err))); - size_t c_len{0}; - const char* c_msg{::line_sender_error_msg(c_err, &c_len)}; - std::string msg{c_msg, c_len}; - line_sender_error err{code, msg}; - ::line_sender_error_free(c_err); - return err; - } + } - template <typename F, typename... Args> - inline static auto wrapped_call(F&& f, Args&&... args) - { - ::line_sender_error* c_err{nullptr}; - auto obj = f(std::forward<Args>(args)..., &c_err); - if (obj) - return obj; - else - throw from_c(c_err); - } + /** Error code categorizing the error. */ + line_sender_error_code code() const noexcept + { + return _code; + } - friend class line_sender; - friend class line_sender_buffer; - friend class opts; +private: + inline static line_sender_error from_c(::line_sender_error* c_err) + { + line_sender_error_code code = static_cast<line_sender_error_code>( + static_cast<int>(::line_sender_error_get_code(c_err))); + size_t c_len{0}; + const char* c_msg{::line_sender_error_msg(c_err, &c_len)}; + std::string msg{c_msg, c_len}; + line_sender_error err{code, msg}; + ::line_sender_error_free(c_err); + return err; + } - template < - typename T, - bool (*F)(T*, size_t, const char*, ::line_sender_error**)> - friend class basic_view; + template <typename F, typename... Args> + inline static auto wrapped_call(F&& f, Args&&... args) + { + ::line_sender_error* c_err{nullptr}; + auto obj = f(std::forward<Args>(args)..., &c_err); + if (obj) + return obj; + else + throw from_c(c_err); + } - line_sender_error_code _code; - }; + friend class line_sender; + friend class line_sender_buffer; + friend class opts; - /** - * Non-owning validated string. - * - * See `table_name_view`, `column_name_view` and `utf8_view` along with the - * `_utf8`, `_tn` and `_cn` literal suffixes in the `literals` namespace. - */ template < typename T, bool (*F)(T*, size_t, const char*, ::line_sender_error**)> - class basic_view + friend class basic_view; + + line_sender_error_code _code; +}; + +/** + * Non-owning validated string. + * + * See `table_name_view`, `column_name_view` and `utf8_view` along with the + * `_utf8`, `_tn` and `_cn` literal suffixes in the `literals` namespace. + */ +template <typename T, bool (*F)(T*, size_t, const char*, ::line_sender_error**)> +class basic_view +{ +public: + basic_view(const char* buf, size_t len) + : _impl{0, nullptr} { - public: - basic_view(const char* buf, size_t len) - : _impl{0, nullptr} - { - line_sender_error::wrapped_call( - F, - &_impl, - len, - buf); - } + line_sender_error::wrapped_call(F, &_impl, len, buf); + } + + template <size_t N> + basic_view(const char (&buf)[N]) + : basic_view{buf, N - 1} + { + } + + basic_view(std::string_view s_view) + : basic_view{s_view.data(), s_view.size()} + { + } - template <size_t N> - basic_view(const char (&buf)[N]) - : basic_view{buf, N - 1} - {} + basic_view(const std::string& s) + : basic_view{s.data(), s.size()} + { + } - basic_view(std::string_view s_view) - : basic_view{s_view.data(), s_view.size()} - {} + size_t size() const noexcept + { + return _impl.len; + } - basic_view(const std::string& s) - : basic_view{s.data(), s.size()} - {} + const char* data() const noexcept + { + return _impl.buf; + } - size_t size() const noexcept { return _impl.len; } + std::string_view to_string_view() const noexcept + { + return std::string_view{_impl.buf, _impl.len}; + } - const char* data() const noexcept { return _impl.buf; } +private: + T _impl; - std::string_view to_string_view() const noexcept - { - return std::string_view{_impl.buf, _impl.len}; - } + friend class line_sender; + friend class line_sender_buffer; + friend class opts; +}; + +using utf8_view = basic_view<::line_sender_utf8, ::line_sender_utf8_init>; - private: - T _impl; +using table_name_view = + basic_view<::line_sender_table_name, ::line_sender_table_name_init>; - friend class line_sender; - friend class line_sender_buffer; - friend class opts; - }; +using column_name_view = + basic_view<::line_sender_column_name, ::line_sender_column_name_init>; - using utf8_view = basic_view< - ::line_sender_utf8, - ::line_sender_utf8_init>; +namespace literals +{ +/** + * Utility to construct `utf8_view` objects from string literals. + * @code {.cpp} + * auto validated = "A UTF-8 encoded string"_utf8; + * @endcode + */ +inline utf8_view operator"" _utf8(const char* buf, size_t len) +{ + return utf8_view{buf, len}; +} - using table_name_view = basic_view< - ::line_sender_table_name, - ::line_sender_table_name_init>; +/** + * Utility to construct `table_name_view` objects from string literals. + * @code {.cpp} + * auto table_name = "events"_tn; + * @endcode + */ +inline table_name_view operator"" _tn(const char* buf, size_t len) +{ + return table_name_view{buf, len}; +} - using column_name_view = basic_view< - ::line_sender_column_name, - ::line_sender_column_name_init>; +/** + * Utility to construct `column_name_view` objects from string literals. + * @code {.cpp} + * auto column_name = "events"_cn; + * @endcode + */ +inline column_name_view operator"" _cn(const char* buf, size_t len) +{ + return column_name_view{buf, len}; +} +} // namespace literals - namespace literals +class timestamp_micros +{ +public: + template <typename ClockT, typename DurationT> + explicit timestamp_micros(std::chrono::time_point<ClockT, DurationT> tp) + : _ts{std::chrono::duration_cast<std::chrono::microseconds>( + tp.time_since_epoch()) + .count()} { - /** - * Utility to construct `utf8_view` objects from string literals. - * @code {.cpp} - * auto validated = "A UTF-8 encoded string"_utf8; - * @endcode - */ - inline utf8_view operator "" _utf8(const char* buf, size_t len) - { - return utf8_view{buf, len}; - } + } - /** - * Utility to construct `table_name_view` objects from string literals. - * @code {.cpp} - * auto table_name = "events"_tn; - * @endcode - */ - inline table_name_view operator "" _tn(const char* buf, size_t len) - { - return table_name_view{buf, len}; - } + explicit timestamp_micros(int64_t ts) noexcept + : _ts{ts} + { + } - /** - * Utility to construct `column_name_view` objects from string literals. - * @code {.cpp} - * auto column_name = "events"_cn; - * @endcode - */ - inline column_name_view operator "" _cn(const char* buf, size_t len) - { - return column_name_view{buf, len}; - } + int64_t as_micros() const noexcept + { + return _ts; } - class timestamp_micros + static inline timestamp_micros now() noexcept { - public: - template <typename ClockT, typename DurationT> - explicit timestamp_micros(std::chrono::time_point<ClockT, DurationT> tp) - : _ts{std::chrono::duration_cast<std::chrono::microseconds>( - tp.time_since_epoch()).count()} - {} + return timestamp_micros{::line_sender_now_micros()}; + } - explicit timestamp_micros(int64_t ts) noexcept - : _ts{ts} - {} +private: + int64_t _ts; +}; - int64_t as_micros() const noexcept { return _ts; } +class timestamp_nanos +{ +public: + template <typename ClockT, typename DurationT> + explicit timestamp_nanos(std::chrono::time_point<ClockT, DurationT> tp) + : _ts{std::chrono::duration_cast<std::chrono::nanoseconds>( + tp.time_since_epoch()) + .count()} + { + } - static inline timestamp_micros now() noexcept - { - return timestamp_micros{::line_sender_now_micros()}; - } + explicit timestamp_nanos(int64_t ts) noexcept + : _ts{ts} + { + } - private: - int64_t _ts; - }; + int64_t as_nanos() const noexcept + { + return _ts; + } - class timestamp_nanos + static inline timestamp_nanos now() noexcept { - public: - template <typename ClockT, typename DurationT> - explicit timestamp_nanos(std::chrono::time_point<ClockT, DurationT> tp) - : _ts{std::chrono::duration_cast<std::chrono::nanoseconds>( - tp.time_since_epoch()).count()} - {} + return timestamp_nanos{::line_sender_now_nanos()}; + } - explicit timestamp_nanos(int64_t ts) noexcept - : _ts{ts} - {} +private: + int64_t _ts; +}; - int64_t as_nanos() const noexcept { return _ts; } +#if __cplusplus < 202002L +class buffer_view final +{ +public: + /// @brief Default constructor. Creates an empty buffer view. + buffer_view() noexcept = default; + + /// @brief Constructs a buffer view from raw byte data. + /// @param data Pointer to the underlying byte array (may be nullptr if + /// length=0). + /// @param length Number of bytes in the array. + constexpr buffer_view(const std::byte* data, size_t length) noexcept + : buf(data) + , len(length) + { + } - static inline timestamp_nanos now() noexcept - { - return timestamp_nanos{::line_sender_now_nanos()}; - } + /// @brief Gets a pointer to the underlying byte array. + /// @return Const pointer to the data (may be nullptr if empty()). + constexpr const std::byte* data() const noexcept + { + return buf; + } - private: - int64_t _ts; - }; + /// @brief Gets the number of bytes in the view. + /// @return Size in bytes. + constexpr size_t size() const noexcept + { + return len; + } - class line_sender_buffer + /// @brief Checks if the view contains no bytes. + /// @return true if size() == 0. + constexpr bool empty() const noexcept { - public: - explicit line_sender_buffer(size_t init_buf_size = 64 * 1024) noexcept - : line_sender_buffer{init_buf_size, 127} - {} + return len == 0; + } - line_sender_buffer( - size_t init_buf_size, - size_t max_name_len) noexcept - : _impl{nullptr} - , _init_buf_size{init_buf_size} - , _max_name_len{max_name_len} - { - } + /// @brief Checks byte-wise equality between two buffer views. + /// @return true if both views have identical size and byte content. + friend bool operator==( + const buffer_view& lhs, const buffer_view& rhs) noexcept + { + return lhs.size() == rhs.size() && + std::equal(lhs.buf, lhs.buf + lhs.len, rhs.buf); + } - line_sender_buffer(const line_sender_buffer& other) noexcept - : _impl{::line_sender_buffer_clone(other._impl)} - , _init_buf_size{other._init_buf_size} - , _max_name_len{other._max_name_len} - {} +private: + const std::byte* buf{nullptr}; + size_t len{0}; +}; +#endif - line_sender_buffer(line_sender_buffer&& other) noexcept - : _impl{other._impl} - , _init_buf_size{other._init_buf_size} - , _max_name_len{other._max_name_len} - { - other._impl = nullptr; - } +class line_sender_buffer +{ +public: + explicit line_sender_buffer(size_t init_buf_size = 64 * 1024) noexcept + : line_sender_buffer{init_buf_size, 127} + { + } - line_sender_buffer& operator=(const line_sender_buffer& other) noexcept - { - if (this != &other) - { - ::line_sender_buffer_free(_impl); - if (other._impl) - _impl = ::line_sender_buffer_clone(other._impl); - else - _impl = nullptr; - _init_buf_size = other._init_buf_size; - _max_name_len = other._max_name_len; - } - return *this; - } + line_sender_buffer(size_t init_buf_size, size_t max_name_len) noexcept + : _impl{nullptr} + , _init_buf_size{init_buf_size} + , _max_name_len{max_name_len} + { + } - line_sender_buffer& operator=(line_sender_buffer&& other) noexcept - { - if (this != &other) - { - ::line_sender_buffer_free(_impl); - _impl = other._impl; - _init_buf_size = other._init_buf_size; - _max_name_len = other._max_name_len; - other._impl = nullptr; - } - return *this; - } + line_sender_buffer(const line_sender_buffer& other) noexcept + : _impl{::line_sender_buffer_clone(other._impl)} + , _init_buf_size{other._init_buf_size} + , _max_name_len{other._max_name_len} + { + } - /** - * Pre-allocate to ensure the buffer has enough capacity for at least - * the specified additional byte count. This may be rounded up. - * This does not allocate if such additional capacity is already - * satisfied. - * See: `capacity`. - */ - void reserve(size_t additional) - { - may_init(); - ::line_sender_buffer_reserve(_impl, additional); - } + line_sender_buffer(line_sender_buffer&& other) noexcept + : _impl{other._impl} + , _init_buf_size{other._init_buf_size} + , _max_name_len{other._max_name_len} + { + other._impl = nullptr; + } - /** Get the current capacity of the buffer. */ - size_t capacity() const noexcept + line_sender_buffer& operator=(const line_sender_buffer& other) noexcept + { + if (this != &other) { - if (_impl) - return ::line_sender_buffer_capacity(_impl); + ::line_sender_buffer_free(_impl); + if (other._impl) + _impl = ::line_sender_buffer_clone(other._impl); else - return 0; + _impl = nullptr; + _init_buf_size = other._init_buf_size; + _max_name_len = other._max_name_len; } + return *this; + } - /** The number of bytes accumulated in the buffer. */ - size_t size() const noexcept + line_sender_buffer& operator=(line_sender_buffer&& other) noexcept + { + if (this != &other) { - if (_impl) - return ::line_sender_buffer_size(_impl); - else - return 0; + ::line_sender_buffer_free(_impl); + _impl = other._impl; + _init_buf_size = other._init_buf_size; + _max_name_len = other._max_name_len; + other._impl = nullptr; } + return *this; + } - /** The number of rows accumulated in the buffer. */ - size_t row_count() const noexcept - { - if (_impl) - return ::line_sender_buffer_row_count(_impl); - else - return 0; - } + /** + * Pre-allocate to ensure the buffer has enough capacity for at least + * the specified additional byte count. This may be rounded up. + * This does not allocate if such additional capacity is already + * satisfied. + * See: `capacity`. + */ + void reserve(size_t additional) + { + may_init(); + ::line_sender_buffer_reserve(_impl, additional); + } - /** - * Tell whether the buffer is transactional. It is transactional iff it contains - * data for at most one table. Additionally, you must send the buffer over HTTP to - * get transactional behavior. - */ - bool transactional() const noexcept - { - if (_impl) - return ::line_sender_buffer_transactional(_impl); - else - return 0; - } + /** Get the current capacity of the buffer. */ + size_t capacity() const noexcept + { + if (_impl) + return ::line_sender_buffer_capacity(_impl); + else + return 0; + } - /** - * Get a string representation of the contents of the buffer. - */ - std::string_view peek() const noexcept - { - if (_impl) - { - size_t len = 0; - const char* buf = ::line_sender_buffer_peek(_impl, &len); - return {buf, len}; - } - else - { - return {}; - } - } + /** The number of bytes accumulated in the buffer. */ + size_t size() const noexcept + { + if (_impl) + return ::line_sender_buffer_size(_impl); + else + return 0; + } - /** - * Mark a rewind point. - * This allows undoing accumulated changes to the buffer for one or more - * rows by calling `rewind_to_marker`. - * Any previous marker will be discarded. - * Once the marker is no longer needed, call `clear_marker`. - */ - void set_marker() - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_set_marker, _impl); - } + /** The number of rows accumulated in the buffer. */ + size_t row_count() const noexcept + { + if (_impl) + return ::line_sender_buffer_row_count(_impl); + else + return 0; + } - /** - * Undo all changes since the last `set_marker` call. - * As a side-effect, this also clears the marker. - */ - void rewind_to_marker() - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_rewind_to_marker, _impl); - } + /** + * Tell whether the buffer is transactional. It is transactional iff it + * contains data for at most one table. Additionally, you must send the + * buffer over HTTP to get transactional behavior. + */ + bool transactional() const noexcept + { + if (_impl) + return ::line_sender_buffer_transactional(_impl); + else + return 0; + } - /** Discard the marker. */ - void clear_marker() noexcept - { - if (_impl) - ::line_sender_buffer_clear_marker(_impl); - } +#if __cplusplus >= 202002L + using buffer_view = std::span<const std::byte>; +#endif - /** - * Remove all accumulated data and prepare the buffer for new lines. - * This does not affect the buffer's capacity. - */ - void clear() noexcept + /** + * Get a bytes view of the contents of the buffer + * (not guaranteed to be an encoded string) + */ + buffer_view peek() const noexcept + { + if (_impl) { - if (_impl) - ::line_sender_buffer_clear(_impl); + auto view = ::line_sender_buffer_peek(_impl); + return {reinterpret_cast<const std::byte*>(view.buf), view.len}; } + return {}; + } - /** - * Start recording a new row for the given table. - * @param name Table name. - */ - line_sender_buffer& table(table_name_view name) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_table, - _impl, - name._impl); - return *this; - } + /** + * Mark a rewind point. + * This allows undoing accumulated changes to the buffer for one or more + * rows by calling `rewind_to_marker`. + * Any previous marker will be discarded. + * Once the marker is no longer needed, call `clear_marker`. + */ + void set_marker() + { + may_init(); + line_sender_error::wrapped_call(::line_sender_buffer_set_marker, _impl); + } - /** - * Record a symbol value for the given column. - * Make sure you record all the symbol columns before any other column type. - * @param name Column name. - * @param value Column value. - */ - line_sender_buffer& symbol(column_name_view name, utf8_view value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_symbol, - _impl, - name._impl, - value._impl); - return *this; - } + /** + * Undo all changes since the last `set_marker` call. + * As a side-effect, this also clears the marker. + */ + void rewind_to_marker() + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_rewind_to_marker, _impl); + } - // Require specific overloads of `column` to avoid - // involuntary usage of the `bool` overload. - template <typename T> - line_sender_buffer& column(column_name_view name, T value) = delete; - - /** - * Record a boolean value for the given column. - * @param name Column name. - * @param value Column value. - */ - line_sender_buffer& column(column_name_view name, bool value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_bool, - _impl, - name._impl, - value); - return *this; - } + /** Discard the marker. */ + void clear_marker() noexcept + { + if (_impl) + ::line_sender_buffer_clear_marker(_impl); + } - /** - * Record an integer value for the given column. - * @param name Column name. - * @param value Column value. - */ - line_sender_buffer& column(column_name_view name, int64_t value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_i64, - _impl, - name._impl, - value); - return *this; - } + /** + * Remove all accumulated data and prepare the buffer for new lines. + * This does not affect the buffer's capacity. + */ + void clear() noexcept + { + if (_impl) + ::line_sender_buffer_clear(_impl); + } - /** - * Record a floating-point value for the given column. - * @param name Column name. - * @param value Column value. - */ - line_sender_buffer& column(column_name_view name, double value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_f64, - _impl, - name._impl, - value); - return *this; - } + /** + * Start recording a new row for the given table. + * @param name Table name. + */ + line_sender_buffer& table(table_name_view name) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_table, _impl, name._impl); + return *this; + } - /** - * Record a string value for the given column. - * @param name Column name. - * @param value Column value. - */ - line_sender_buffer& column(column_name_view name, utf8_view value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_str, - _impl, - name._impl, - value._impl); - return *this; - } + /** + * Record a symbol value for the given column. + * Make sure you record all the symbol columns before any other column type. + * @param name Column name. + * @param value Column value. + */ + line_sender_buffer& symbol(column_name_view name, utf8_view value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_symbol, _impl, name._impl, value._impl); + return *this; + } - template <size_t N> - line_sender_buffer& column( - column_name_view name, - const char (&value)[N]) - { - return column(name, utf8_view{value}); - } + // Require specific overloads of `column` to avoid + // involuntary usage of the `bool` overload. + template <typename T> + line_sender_buffer& column(column_name_view name, T value) = delete; - line_sender_buffer& column( - column_name_view name, - std::string_view value) - { - return column(name, utf8_view{value}); - } + /** + * Record a boolean value for the given column. + * @param name Column name. + * @param value Column value. + */ + line_sender_buffer& column(column_name_view name, bool value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_bool, _impl, name._impl, value); + return *this; + } - line_sender_buffer& column( - column_name_view name, - const std::string& value) - { - return column(name, utf8_view{value}); - } + /** + * Record an integer value for the given column. + * @param name Column name. + * @param value Column value. + */ + line_sender_buffer& column(column_name_view name, int64_t value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_i64, _impl, name._impl, value); + return *this; + } - /** Record a nanosecond timestamp value for the given column. */ - template <typename ClockT> - line_sender_buffer& column( - column_name_view name, - std::chrono::time_point<ClockT, std::chrono::nanoseconds> tp) - { - timestamp_nanos nanos{tp}; - return column(name, nanos); - } + /** + * Record a floating-point value for the given column. + * @param name Column name. + * @param value Column value. + */ + line_sender_buffer& column(column_name_view name, double value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_f64, _impl, name._impl, value); + return *this; + } - /** Record a timestamp value for the given column, specified as a - * `DurationT`. */ - template <typename ClockT, typename DurationT> - line_sender_buffer& column( - column_name_view name, - std::chrono::time_point<ClockT, DurationT> tp) - { - timestamp_micros micros{tp}; - return column(name, micros); - } + /** + * Record a string value for the given column. + * @param name Column name. + * @param value Column value. + */ + line_sender_buffer& column(column_name_view name, utf8_view value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_str, _impl, name._impl, value._impl); + return *this; + } - line_sender_buffer& column( - column_name_view name, - timestamp_nanos value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_ts_nanos, - _impl, - name._impl, - value.as_nanos()); - return *this; - } + template <size_t N> + line_sender_buffer& column(column_name_view name, const char (&value)[N]) + { + return column(name, utf8_view{value}); + } - line_sender_buffer& column( - column_name_view name, - timestamp_micros value) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_column_ts_micros, - _impl, - name._impl, - value.as_micros()); - return *this; - } + line_sender_buffer& column(column_name_view name, std::string_view value) + { + return column(name, utf8_view{value}); + } - /** - * Complete the current row with the designated timestamp in nanoseconds. - * - * After this call, you can start recording the next row by calling - * `table()` again, or you can send the accumulated batch by calling - * `flush()` or one of its variants. - * - * If you want to pass the current system timestamp, call `at_now()`. - * - * @param timestamp Number of nanoseconds since the Unix epoch. - */ - void at(timestamp_nanos timestamp) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_at_nanos, - _impl, - timestamp.as_nanos()); - } + line_sender_buffer& column(column_name_view name, const std::string& value) + { + return column(name, utf8_view{value}); + } - /** - * Complete the current row with the designated timestamp in microseconds. - * - * After this call, you can start recording the next row by calling - * `table()` again, or you can send the accumulated batch by calling - * `flush()` or one of its variants. - * - * @param timestamp Number of microseconds since the Unix epoch. - */ - void at(timestamp_micros timestamp) - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_at_micros, - _impl, - timestamp.as_micros()); - } + /** Record a nanosecond timestamp value for the given column. */ + template <typename ClockT> + line_sender_buffer& column( + column_name_view name, + std::chrono::time_point<ClockT, std::chrono::nanoseconds> tp) + { + timestamp_nanos nanos{tp}; + return column(name, nanos); + } - /** - * Complete the current row without providing a timestamp. The QuestDB instance - * will insert its own timestamp. - * - * Letting the server assign the timestamp can be faster since it a reliable way - * to avoid out-of-order operations in the database for maximum ingestion - * throughput. However, it removes the ability to deduplicate rows. - * - * This is NOT equivalent to calling `line_sender_buffer_at_nanos()` or - * `line_sender_buffer_at_micros()` with the current time: the QuestDB server - * will set the timestamp only after receiving the row. If you're flushing - * infrequently, the server-assigned timestamp may be significantly behind the - * time the data was recorded in the buffer. - * - * In almost all cases, you should prefer the `at()`/`at_now()` methods. - * - * After this call, you can start recording the next row by calling `table()` - * again, or you can send the accumulated batch by calling `flush()` or one of - * its variants. - */ - void at_now() - { - may_init(); - line_sender_error::wrapped_call( - ::line_sender_buffer_at_now, - _impl); - } + /** Record a timestamp value for the given column, specified as a + * `DurationT`. */ + template <typename ClockT, typename DurationT> + line_sender_buffer& column( + column_name_view name, std::chrono::time_point<ClockT, DurationT> tp) + { + timestamp_micros micros{tp}; + return column(name, micros); + } - ~line_sender_buffer() noexcept - { - if (_impl) - ::line_sender_buffer_free(_impl); - } - private: - inline void may_init() - { - if (!_impl) - { - _impl = ::line_sender_buffer_with_max_name_len(_max_name_len); - ::line_sender_buffer_reserve(_impl, _init_buf_size); - } - } + line_sender_buffer& column(column_name_view name, timestamp_nanos value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_ts_nanos, + _impl, + name._impl, + value.as_nanos()); + return *this; + } - ::line_sender_buffer* _impl; - size_t _init_buf_size; - size_t _max_name_len; + line_sender_buffer& column(column_name_view name, timestamp_micros value) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_column_ts_micros, + _impl, + name._impl, + value.as_micros()); + return *this; + } - friend class line_sender; - }; + /** + * Complete the current row with the designated timestamp in nanoseconds. + * + * After this call, you can start recording the next row by calling + * `table()` again, or you can send the accumulated batch by calling + * `flush()` or one of its variants. + * + * If you want to pass the current system timestamp, call `at_now()`. + * + * @param timestamp Number of nanoseconds since the Unix epoch. + */ + void at(timestamp_nanos timestamp) + { + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_at_nanos, _impl, timestamp.as_nanos()); + } - class _user_agent + /** + * Complete the current row with the designated timestamp in microseconds. + * + * After this call, you can start recording the next row by calling + * `table()` again, or you can send the accumulated batch by calling + * `flush()` or one of its variants. + * + * @param timestamp Number of microseconds since the Unix epoch. + */ + void at(timestamp_micros timestamp) { - private: - static inline ::line_sender_utf8 name() + may_init(); + line_sender_error::wrapped_call( + ::line_sender_buffer_at_micros, _impl, timestamp.as_micros()); + } + + /** + * Complete the current row without providing a timestamp. The QuestDB + * instance will insert its own timestamp. + * + * Letting the server assign the timestamp can be faster since it a reliable + * way to avoid out-of-order operations in the database for maximum + * ingestion throughput. However, it removes the ability to deduplicate + * rows. + * + * This is NOT equivalent to calling `line_sender_buffer_at_nanos()` or + * `line_sender_buffer_at_micros()` with the current time: the QuestDB + * server will set the timestamp only after receiving the row. If you're + * flushing infrequently, the server-assigned timestamp may be significantly + * behind the time the data was recorded in the buffer. + * + * In almost all cases, you should prefer the `at()`/`at_now()` methods. + * + * After this call, you can start recording the next row by calling + * `table()` again, or you can send the accumulated batch by calling + * `flush()` or one of its variants. + */ + void at_now() + { + may_init(); + line_sender_error::wrapped_call(::line_sender_buffer_at_now, _impl); + } + + ~line_sender_buffer() noexcept + { + if (_impl) + ::line_sender_buffer_free(_impl); + } + +private: + inline void may_init() + { + if (!_impl) { - // Maintained by .bumpversion.cfg - static const char user_agent[] = "questdb/c++/4.0.1"; - ::line_sender_utf8 utf8 = ::line_sender_utf8_assert( - sizeof(user_agent) - 1, - user_agent); - return utf8; + _impl = ::line_sender_buffer_with_max_name_len(_max_name_len); + ::line_sender_buffer_reserve(_impl, _init_buf_size); } + } + + ::line_sender_buffer* _impl; + size_t _init_buf_size; + size_t _max_name_len; + + friend class line_sender; +}; - friend class opts; - }; - - class opts - { - public: - /** - * Create a new `opts` instance from the given configuration string. - * The format of the string is: "tcp::addr=host:port;key=value;...;" - * Instead of "tcp" you can also specify "tcps", "http", and "https". - * - * The accepted keys match one-for-one with the methods on `opts`. - * For example, this is a valid configuration string: - * - * "https::addr=host:port;username=alice;password=secret;" - * - * and there are matching methods `opts.username()` and `opts.password()`. - * The value for `addr=` is supplied directly to `opts()`, so there's no - * function with a matching name. - */ - static inline opts from_conf(utf8_view conf) - { - return {line_sender_error::wrapped_call( - ::line_sender_opts_from_conf, - conf._impl)}; - } - - /** - * Create a new `opts` instance from the configuration stored in the - * `QDB_CLIENT_CONF` environment variable. - */ - static inline opts from_env() - { - opts impl{line_sender_error::wrapped_call( - ::line_sender_opts_from_env)}; - line_sender_error::wrapped_call( - ::line_sender_opts_user_agent, - impl._impl, - _user_agent::name()); - return impl; - } - - /** - * Create a new `opts` instance with the given protocol, hostname and port. - * @param[in] protocol The protocol to use. - * @param[in] host The QuestDB database host. - * @param[in] port The QuestDB tcp or http port. - */ - opts( - protocol protocol, - utf8_view host, - uint16_t port) noexcept - : _impl{ - ::line_sender_opts_new( - static_cast<::line_sender_protocol>(protocol), - host._impl, - port) - } - { - line_sender_error::wrapped_call( - ::line_sender_opts_user_agent, - _impl, - _user_agent::name()); - } - - /** - * Create a new `opts` instance with the given protocol, hostname and service name. - * @param[in] protocol The protocol to use. - * @param[in] host The QuestDB database host. - * @param[in] port The QuestDB tcp or http port as service name. - */ - opts( - protocol protocol, - utf8_view host, - utf8_view port) noexcept - : _impl{ - ::line_sender_opts_new_service( - static_cast<::line_sender_protocol>(protocol), - host._impl, port._impl) - } - { - line_sender_error::wrapped_call( - ::line_sender_opts_user_agent, - _impl, - _user_agent::name()); - } - - opts(const opts& other) noexcept - : _impl{::line_sender_opts_clone(other._impl)} - {} - - opts(opts&& other) noexcept - : _impl{other._impl} - { - other._impl = nullptr; - } - - opts& operator=(const opts& other) noexcept - { - if (this != &other) - { - reset(); - _impl = ::line_sender_opts_clone(other._impl); - } - return *this; - } - - opts& operator=(opts&& other) noexcept - { - if (this != &other) - { - reset(); - _impl = other._impl; - other._impl = nullptr; - } - return *this; - } - - /** - * Select local outbound network "bind" interface. - * - * This may be relevant if your machine has multiple network interfaces. - * - * The default is `0.0.0.0`. - */ - opts& bind_interface(utf8_view bind_interface) - { - line_sender_error::wrapped_call( - ::line_sender_opts_bind_interface, - _impl, - bind_interface._impl); - return *this; - } - - /** - * Set the username for authentication. - * - * For TCP this is the `kid` part of the ECDSA key set. - * The other fields are `token` `token_x` and `token_y`. - * - * For HTTP this is part of basic authentication. - * See also: `password()`. - */ - opts& username(utf8_view username) - { - line_sender_error::wrapped_call( - ::line_sender_opts_username, - _impl, - username._impl); - return *this; - } - - /** - * Set the password for basic HTTP authentication. - * See also: `username()`. - */ - opts& password(utf8_view password) - { - line_sender_error::wrapped_call( - ::line_sender_opts_password, - _impl, - password._impl); - return *this; - } - - /** - * Set the Token (Bearer) Authentication parameter for HTTP, - * or the ECDSA private key for TCP authentication. - */ - opts& token(utf8_view token) - { - line_sender_error::wrapped_call( - ::line_sender_opts_token, - _impl, - token._impl); - return *this; - } - - /** - * Set the ECDSA public key X for TCP authentication. - */ - opts& token_x(utf8_view token_x) - { - line_sender_error::wrapped_call( - ::line_sender_opts_token_x, - _impl, - token_x._impl); - return *this; - } - - /** - * Set the ECDSA public key Y for TCP authentication. - */ - opts& token_y(utf8_view token_y) - { - line_sender_error::wrapped_call( - ::line_sender_opts_token_y, - _impl, - token_y._impl); - return *this; - } - - /** - * Configure how long to wait for messages from the QuestDB server during - * the TLS handshake and authentication process. - * The value is in milliseconds, and the default is 15 seconds. - */ - opts& auth_timeout(uint64_t millis) - { - line_sender_error::wrapped_call( - ::line_sender_opts_auth_timeout, - _impl, - millis); - return *this; - } - - /** - * Set to `false` to disable TLS certificate verification. - * This should only be used for debugging purposes as it reduces security. - * - * For testing, consider specifying a path to a `.pem` file instead via - * the `tls_roots` setting. - */ - opts& tls_verify(bool verify) - { - line_sender_error::wrapped_call( - ::line_sender_opts_tls_verify, - _impl, - verify); - return *this; - } - - /** - * Specify where to find the certificate authority used to validate the - * server's TLS certificate. - */ - opts& tls_ca(ca ca) - { - ::line_sender_ca ca_impl = static_cast<::line_sender_ca>(ca); - line_sender_error::wrapped_call( - ::line_sender_opts_tls_ca, - _impl, - ca_impl); - return *this; - } - - /** - * Set the path to a custom root certificate `.pem` file. - * This is used to validate the server's certificate during the TLS handshake. - * - * See notes on how to test with self-signed certificates: - * https://github.com/questdb/c-questdb-client/tree/main/tls_certs. - */ - opts& tls_roots(utf8_view path) - { - line_sender_error::wrapped_call( - ::line_sender_opts_tls_roots, - _impl, - path._impl); - return *this; - } - - /** - * The maximum buffer size in bytes that the client will flush to the server. - * The default is 100 MiB. - */ - opts& max_buf_size(size_t max_buf_size) - { - line_sender_error::wrapped_call( - ::line_sender_opts_max_buf_size, - _impl, - max_buf_size); - return *this; - } - - /** - * Set the cumulative duration spent in retries. - * The value is in milliseconds, and the default is 10 seconds. - */ - opts& retry_timeout(uint64_t millis) - { - line_sender_error::wrapped_call( - ::line_sender_opts_retry_timeout, - _impl, - millis); - return *this; - } - - /** - * Set the minimum acceptable throughput while sending a buffer to the server. - * The sender will divide the payload size by this number to determine for how - * long to keep sending the payload before timing out. - * The value is in bytes per second, and the default is 100 KiB/s. - * The timeout calculated from minimum throughput is adedd to the value of - * `request_timeout`. - * - * See also: `request_timeout()` - */ - opts& request_min_throughput(uint64_t bytes_per_sec) - { - line_sender_error::wrapped_call( - ::line_sender_opts_request_min_throughput, - _impl, - bytes_per_sec); - return *this; - } - - /** - * Additional time to wait on top of that calculated from the minimum throughput. - * This accounts for the fixed latency of the HTTP request-response roundtrip. - * The value is in milliseconds, and the default is 10 seconds. - * - * See also: `request_min_throughput()` - */ - opts& request_timeout(uint64_t millis) - { - line_sender_error::wrapped_call( - ::line_sender_opts_request_timeout, - _impl, - millis); - return *this; - } - - ~opts() noexcept - { - reset(); - } - private: - opts(::line_sender_opts* impl) : _impl{impl} - {} - - void reset() noexcept - { - if (_impl) - { - ::line_sender_opts_free(_impl); - _impl = nullptr; - } - } - - friend class line_sender; - - ::line_sender_opts* _impl; - }; +class _user_agent +{ +private: + static inline ::line_sender_utf8 name() + { + // Maintained by .bumpversion.cfg + static const char user_agent[] = "questdb/c++/4.0.4"; + ::line_sender_utf8 utf8 = + ::line_sender_utf8_assert(sizeof(user_agent) - 1, user_agent); + return utf8; + } + + friend class opts; +}; +class opts +{ +public: /** - * Inserts data into QuestDB via the InfluxDB Line Protocol. + * Create a new `opts` instance from the given configuration string. + * The format of the string is: "tcp::addr=host:port;key=value;...;" + * Instead of "tcp" you can also specify "tcps", "http", and "https". + * + * The accepted keys match one-for-one with the methods on `opts`. + * For example, this is a valid configuration string: * - * Batch up rows in a `line_sender_buffer` object, then call - * `.flush()` or one of its variants to send. + * "https::addr=host:port;username=alice;password=secret;" * - * When you use ILP-over-TCP, the `line_sender` object connects on construction. - * If you want to connect later, wrap it in an std::optional. + * and there are matching methods `opts.username()` and `opts.password()`. + * The value for `addr=` is supplied directly to `opts()`, so there's no + * function with a matching name. + */ + static inline opts from_conf(utf8_view conf) + { + return {line_sender_error::wrapped_call( + ::line_sender_opts_from_conf, conf._impl)}; + } + + /** + * Create a new `opts` instance from the configuration stored in the + * `QDB_CLIENT_CONF` environment variable. + */ + static inline opts from_env() + { + opts impl{line_sender_error::wrapped_call(::line_sender_opts_from_env)}; + line_sender_error::wrapped_call( + ::line_sender_opts_user_agent, impl._impl, _user_agent::name()); + return impl; + } + + /** + * Create a new `opts` instance with the given protocol, hostname and port. + * @param[in] protocol The protocol to use. + * @param[in] host The QuestDB database host. + * @param[in] port The QuestDB tcp or http port. + */ + opts(protocol protocol, utf8_view host, uint16_t port) noexcept + : _impl{::line_sender_opts_new( + static_cast<::line_sender_protocol>(protocol), host._impl, port)} + { + line_sender_error::wrapped_call( + ::line_sender_opts_user_agent, _impl, _user_agent::name()); + } + + /** + * Create a new `opts` instance with the given protocol, hostname and + * service name. + * @param[in] protocol The protocol to use. + * @param[in] host The QuestDB database host. + * @param[in] port The QuestDB tcp or http port as service name. */ - class line_sender - { - public: - /** - * Create a new line sender instance from the given configuration string. - * The format of the string is: "tcp::addr=host:port;key=value;...;" - * Instead of "tcp" you can also specify "tcps", "http", and "https". - * - * The accepted keys match one-for-one with the methods on `opts`. - * For example, this is a valid configuration string: - * - * "https::addr=host:port;username=alice;password=secret;" - * - * and there are matching methods `opts.username()` and `opts.password()`. The - * value for `addr=` is supplied directly to `opts()`, so there's no function - * with a matching name. - * - * In the case of TCP, this synchronously establishes the TCP connection, and - * returns once the connection is fully established. If the connection - * requires authentication or TLS, these will also be completed before - * returning. - * - * The sender should be accessed by only a single thread a time. - */ - static inline line_sender from_conf(utf8_view conf) + opts(protocol protocol, utf8_view host, utf8_view port) noexcept + : _impl{::line_sender_opts_new_service( + static_cast<::line_sender_protocol>(protocol), + host._impl, + port._impl)} + { + line_sender_error::wrapped_call( + ::line_sender_opts_user_agent, _impl, _user_agent::name()); + } + + opts(const opts& other) noexcept + : _impl{::line_sender_opts_clone(other._impl)} + { + } + + opts(opts&& other) noexcept + : _impl{other._impl} + { + other._impl = nullptr; + } + + opts& operator=(const opts& other) noexcept + { + if (this != &other) { - return {opts::from_conf(conf)}; + reset(); + _impl = ::line_sender_opts_clone(other._impl); } + return *this; + } - /** - * Create a new `line_sender` instance from the configuration stored in the - * `QDB_CLIENT_CONF` environment variable. - * - * In the case of TCP, this synchronously establishes the TCP connection, and - * returns once the connection is fully established. If the connection - * requires authentication or TLS, these will also be completed before - * returning. - * - * The sender should be accessed by only a single thread a time. - */ - static inline line_sender from_env() + opts& operator=(opts&& other) noexcept + { + if (this != &other) { - return {opts::from_env()}; + reset(); + _impl = other._impl; + other._impl = nullptr; } + return *this; + } - line_sender(protocol protocol, utf8_view host, uint16_t port) - : line_sender{opts{protocol, host, port}} - {} + /** + * Select local outbound network "bind" interface. + * + * This may be relevant if your machine has multiple network interfaces. + * + * The default is `0.0.0.0`. + */ + opts& bind_interface(utf8_view bind_interface) + { + line_sender_error::wrapped_call( + ::line_sender_opts_bind_interface, _impl, bind_interface._impl); + return *this; + } - line_sender(protocol protocol, utf8_view host, utf8_view port) - : line_sender{opts{protocol, host, port}} - {} + /** + * Set the username for authentication. + * + * For TCP this is the `kid` part of the ECDSA key set. + * The other fields are `token` `token_x` and `token_y`. + * + * For HTTP this is part of basic authentication. + * See also: `password()`. + */ + opts& username(utf8_view username) + { + line_sender_error::wrapped_call( + ::line_sender_opts_username, _impl, username._impl); + return *this; + } + + /** + * Set the password for basic HTTP authentication. + * See also: `username()`. + */ + opts& password(utf8_view password) + { + line_sender_error::wrapped_call( + ::line_sender_opts_password, _impl, password._impl); + return *this; + } + + /** + * Set the Token (Bearer) Authentication parameter for HTTP, + * or the ECDSA private key for TCP authentication. + */ + opts& token(utf8_view token) + { + line_sender_error::wrapped_call( + ::line_sender_opts_token, _impl, token._impl); + return *this; + } + + /** + * Set the ECDSA public key X for TCP authentication. + */ + opts& token_x(utf8_view token_x) + { + line_sender_error::wrapped_call( + ::line_sender_opts_token_x, _impl, token_x._impl); + return *this; + } - line_sender(const opts& opts) - : _impl{line_sender_error::wrapped_call( - ::line_sender_build, opts._impl)} - {} + /** + * Set the ECDSA public key Y for TCP authentication. + */ + opts& token_y(utf8_view token_y) + { + line_sender_error::wrapped_call( + ::line_sender_opts_token_y, _impl, token_y._impl); + return *this; + } - line_sender(const line_sender&) = delete; + /** + * Configure how long to wait for messages from the QuestDB server during + * the TLS handshake and authentication process. + * The value is in milliseconds, and the default is 15 seconds. + */ + opts& auth_timeout(uint64_t millis) + { + line_sender_error::wrapped_call( + ::line_sender_opts_auth_timeout, _impl, millis); + return *this; + } + + /** + * Set to `false` to disable TLS certificate verification. + * This should only be used for debugging purposes as it reduces security. + * + * For testing, consider specifying a path to a `.pem` file instead via + * the `tls_roots` setting. + */ + opts& tls_verify(bool verify) + { + line_sender_error::wrapped_call( + ::line_sender_opts_tls_verify, _impl, verify); + return *this; + } - line_sender(line_sender&& other) noexcept - : _impl{other._impl} + /** + * Specify where to find the certificate authority used to validate the + * server's TLS certificate. + */ + opts& tls_ca(ca ca) + { + ::line_sender_ca ca_impl = static_cast<::line_sender_ca>(ca); + line_sender_error::wrapped_call( + ::line_sender_opts_tls_ca, _impl, ca_impl); + return *this; + } + + /** + * Set the path to a custom root certificate `.pem` file. + * This is used to validate the server's certificate during the TLS + * handshake. + * + * See notes on how to test with self-signed certificates: + * https://github.com/questdb/c-questdb-client/tree/main/tls_certs. + */ + opts& tls_roots(utf8_view path) + { + line_sender_error::wrapped_call( + ::line_sender_opts_tls_roots, _impl, path._impl); + return *this; + } + + /** + * The maximum buffer size in bytes that the client will flush to the + * server. The default is 100 MiB. + */ + opts& max_buf_size(size_t max_buf_size) + { + line_sender_error::wrapped_call( + ::line_sender_opts_max_buf_size, _impl, max_buf_size); + return *this; + } + + /** + * Set the cumulative duration spent in retries. + * The value is in milliseconds, and the default is 10 seconds. + */ + opts& retry_timeout(uint64_t millis) + { + line_sender_error::wrapped_call( + ::line_sender_opts_retry_timeout, _impl, millis); + return *this; + } + + /** + * Set the minimum acceptable throughput while sending a buffer to the + * server. The sender will divide the payload size by this number to + * determine for how long to keep sending the payload before timing out. The + * value is in bytes per second, and the default is 100 KiB/s. The timeout + * calculated from minimum throughput is adedd to the value of + * `request_timeout`. + * + * See also: `request_timeout()` + */ + opts& request_min_throughput(uint64_t bytes_per_sec) + { + line_sender_error::wrapped_call( + ::line_sender_opts_request_min_throughput, _impl, bytes_per_sec); + return *this; + } + + /** + * Additional time to wait on top of that calculated from the minimum + * throughput. This accounts for the fixed latency of the HTTP + * request-response roundtrip. The value is in milliseconds, and the default + * is 10 seconds. + * + * See also: `request_min_throughput()` + */ + opts& request_timeout(uint64_t millis) + { + line_sender_error::wrapped_call( + ::line_sender_opts_request_timeout, _impl, millis); + return *this; + } + + ~opts() noexcept + { + reset(); + } + +private: + opts(::line_sender_opts* impl) + : _impl{impl} + { + } + + void reset() noexcept + { + if (_impl) { - other._impl = nullptr; + ::line_sender_opts_free(_impl); + _impl = nullptr; } + } + + friend class line_sender; - line_sender& operator=(const line_sender&) = delete; + ::line_sender_opts* _impl; +}; + +/** + * Inserts data into QuestDB via the InfluxDB Line Protocol. + * + * Batch up rows in a `line_sender_buffer` object, then call + * `.flush()` or one of its variants to send. + * + * When you use ILP-over-TCP, the `line_sender` object connects on construction. + * If you want to connect later, wrap it in an std::optional. + */ +class line_sender +{ +public: + /** + * Create a new line sender instance from the given configuration string. + * The format of the string is: "tcp::addr=host:port;key=value;...;" + * Instead of "tcp" you can also specify "tcps", "http", and "https". + * + * The accepted keys match one-for-one with the methods on `opts`. + * For example, this is a valid configuration string: + * + * "https::addr=host:port;username=alice;password=secret;" + * + * and there are matching methods `opts.username()` and `opts.password()`. + * The value for `addr=` is supplied directly to `opts()`, so there's no + * function with a matching name. + * + * In the case of TCP, this synchronously establishes the TCP connection, + * and returns once the connection is fully established. If the connection + * requires authentication or TLS, these will also be completed before + * returning. + * + * The sender should be accessed by only a single thread a time. + */ + static inline line_sender from_conf(utf8_view conf) + { + return {opts::from_conf(conf)}; + } - line_sender& operator=(line_sender&& other) noexcept + /** + * Create a new `line_sender` instance from the configuration stored in the + * `QDB_CLIENT_CONF` environment variable. + * + * In the case of TCP, this synchronously establishes the TCP connection, + * and returns once the connection is fully established. If the connection + * requires authentication or TLS, these will also be completed before + * returning. + * + * The sender should be accessed by only a single thread a time. + */ + static inline line_sender from_env() + { + return {opts::from_env()}; + } + + line_sender(protocol protocol, utf8_view host, uint16_t port) + : line_sender{opts{protocol, host, port}} + { + } + + line_sender(protocol protocol, utf8_view host, utf8_view port) + : line_sender{opts{protocol, host, port}} + { + } + + line_sender(const opts& opts) + : _impl{ + line_sender_error::wrapped_call(::line_sender_build, opts._impl)} + { + } + + line_sender(const line_sender&) = delete; + + line_sender(line_sender&& other) noexcept + : _impl{other._impl} + { + other._impl = nullptr; + } + + line_sender& operator=(const line_sender&) = delete; + + line_sender& operator=(line_sender&& other) noexcept + { + if (this != &other) { - if (this != &other) - { - close(); - _impl = other._impl; - other._impl = nullptr; - } - return *this; + close(); + _impl = other._impl; + other._impl = nullptr; } + return *this; + } + + /** + * Send the given buffer of rows to the QuestDB server, clearing the buffer. + * + * After this function returns, the buffer is empty and ready for the next + * batch. If you want to preserve the buffer contents, call + * `flush_and_keep()`. If you want to ensure the flush is transactional, + * call `flush_and_keep_with_flags()`. + * + * With ILP-over-HTTP, this function sends an HTTP request and waits for the + * response. If the server responds with an error, it returns a descriptive + * error. In the case of a network error, it retries until it has exhausted + * the retry time budget. + * + * With ILP-over-TCP, the function blocks only until the buffer is flushed + * to the underlying OS-level network socket, without waiting to actually + * send it to the server. In the case of an error, the server will quietly + * disconnect: consult the server logs for error messages. + * + * HTTP should be the first choice, but use TCP if you need to continuously + * send data to the server at a high rate. + * + * To improve the HTTP performance, send larger buffers (with more rows), + * and consider parallelizing writes using multiple senders from multiple + * threads. + */ + void flush(line_sender_buffer& buffer) + { + buffer.may_init(); + ensure_impl(); + line_sender_error::wrapped_call( + ::line_sender_flush, _impl, buffer._impl); + } - /** - * Send the given buffer of rows to the QuestDB server, clearing the buffer. - * - * After this function returns, the buffer is empty and ready for the next batch. - * If you want to preserve the buffer contents, call `flush_and_keep()`. If you - * want to ensure the flush is transactional, call `flush_and_keep_with_flags()`. - * - * With ILP-over-HTTP, this function sends an HTTP request and waits for the - * response. If the server responds with an error, it returns a descriptive error. - * In the case of a network error, it retries until it has exhausted the retry time - * budget. - * - * With ILP-over-TCP, the function blocks only until the buffer is flushed to the - * underlying OS-level network socket, without waiting to actually send it to the - * server. In the case of an error, the server will quietly disconnect: consult the - * server logs for error messages. - * - * HTTP should be the first choice, but use TCP if you need to continuously send - * data to the server at a high rate. - * - * To improve the HTTP performance, send larger buffers (with more rows), and - * consider parallelizing writes using multiple senders from multiple threads. - */ - void flush(line_sender_buffer& buffer) + /** + * Send the given buffer of rows to the QuestDB server. + * + * All the data stays in the buffer. Clear the buffer before starting a new + * batch. + * + * To send and clear in one step, call `flush()` instead. Also, see the docs + * on that method for more important details on flushing. + */ + void flush_and_keep(const line_sender_buffer& buffer) + { + if (buffer._impl) { - buffer.may_init(); ensure_impl(); line_sender_error::wrapped_call( - ::line_sender_flush, - _impl, - buffer._impl); + ::line_sender_flush_and_keep, _impl, buffer._impl); } - - /** - * Send the given buffer of rows to the QuestDB server. - * - * All the data stays in the buffer. Clear the buffer before starting a new batch. - * - * To send and clear in one step, call `flush()` instead. Also, see the docs - * on that method for more important details on flushing. - */ - void flush_and_keep(const line_sender_buffer& buffer) + else { - if (buffer._impl) - { - ensure_impl(); - line_sender_error::wrapped_call( - ::line_sender_flush_and_keep, - _impl, - buffer._impl); - } - else - { - line_sender_buffer buffer2{0}; - buffer2.may_init(); - line_sender_error::wrapped_call( - ::line_sender_flush_and_keep, - _impl, - buffer2._impl); - } + line_sender_buffer buffer2{0}; + buffer2.may_init(); + line_sender_error::wrapped_call( + ::line_sender_flush_and_keep, _impl, buffer2._impl); } + } - /** - * Check if an error occurred previously and the sender must be closed. - * This happens when there was an earlier failure. - * This method is specific to ILP-over-TCP and is not relevant for ILP-over-HTTP. - * @return true if an error occurred with a sender and it must be closed. - */ - bool must_close() const noexcept - { - return _impl - ? ::line_sender_must_close(_impl) - : false; - } + /** + * Check if an error occurred previously and the sender must be closed. + * This happens when there was an earlier failure. + * This method is specific to ILP-over-TCP and is not relevant for + * ILP-over-HTTP. + * @return true if an error occurred with a sender and it must be closed. + */ + bool must_close() const noexcept + { + return _impl ? ::line_sender_must_close(_impl) : false; + } - /** - * Close the connection. Does not flush. Idempotent. - */ - void close() noexcept + /** + * Close the connection. Does not flush. Idempotent. + */ + void close() noexcept + { + if (_impl) { - if (_impl) - { - ::line_sender_close(_impl); - _impl = nullptr; - } + ::line_sender_close(_impl); + _impl = nullptr; } + } - ~line_sender() noexcept - { - close(); - } + ~line_sender() noexcept + { + close(); + } - private: - void ensure_impl() - { - if (!_impl) - throw line_sender_error{ - line_sender_error_code::invalid_api_call, - "Sender closed."}; - } +private: + void ensure_impl() + { + if (!_impl) + throw line_sender_error{ + line_sender_error_code::invalid_api_call, "Sender closed."}; + } - ::line_sender* _impl; - }; + ::line_sender* _impl; +}; -} +} // namespace questdb::ingress diff --git a/questdb-rs-ffi/Cargo.lock b/questdb-rs-ffi/Cargo.lock index 8992364f..5666057c 100644 --- a/questdb-rs-ffi/Cargo.lock +++ b/questdb-rs-ffi/Cargo.lock @@ -1,48 +1,92 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] [[package]] -name = "autocfg" -version = "1.1.0" +name = "aws-lc-rs" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] -name = "bitflags" -version = "1.3.2" +name = "bindgen" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] [[package]] name = "bitflags" -version = "2.4.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cbindgen" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "heck", "indexmap", @@ -51,18 +95,29 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 1.0.109", + "syn", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.0.83" +version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ + "jobserver", "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -72,30 +127,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "core-foundation" -version = "0.9.4" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ - "core-foundation-sys", + "glob", "libc", + "libloading", ] [[package]] -name = "core-foundation-sys" -version = "0.8.6" +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "core-foundation" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ - "cfg-if", + "core-foundation-sys", + "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "dns-lookup" version = "2.0.4" @@ -108,57 +174,86 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "flate2" -version = "1.0.28" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "getrandom" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "percent-encoding", + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.12" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -167,104 +262,151 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "hoot" -version = "0.1.3" +name = "home" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "httparse", - "log", + "windows-sys 0.59.0", ] [[package]] -name = "hootbin" -version = "0.1.1" +name = "http" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "fastrand", - "hoot", - "serde", - "serde_json", - "thiserror", + "bytes", + "fnv", + "itoa", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "idna" -version = "0.5.0" +name = "indexmap" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "equivalent", + "hashbrown", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ - "autocfg", - "hashbrown", + "either", ] [[package]] -name = "indoc" -version = "2.0.4" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "itoa" -version = "1.0.10" +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] -name = "miniz_oxide" -version = "0.7.2" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "adler", + "memchr", + "minimal-lexical", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "percent-encoding" @@ -274,38 +416,52 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "questdb-confstr" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85ae0d477308986d89496bce6227833e11490cd27b98b809e4704b105000fd8" +checksum = "7aceffde1cbf8e67f34cdfd70d2436396176d6ff648fa719e0231fb9856ef3e9" [[package]] name = "questdb-confstr-ffi" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a745c819f8ef71ca240dff7119bad4db368e0b11489c6e5e07b8e5cf2c775561" +checksum = "123e9600ed7e3ca168902c5729ae621e7bde1f5fb5e3a80bd5ceee8410751448" dependencies = [ "questdb-confstr", ] [[package]] name = "questdb-rs" -version = "4.0.1" +version = "4.0.4" dependencies = [ + "aws-lc-rs", "base64ct", "dns-lookup", "indoc", @@ -313,7 +469,6 @@ dependencies = [ "libc", "questdb-confstr", "rand", - "ring", "rustls", "rustls-native-certs", "rustls-pemfile", @@ -330,7 +485,7 @@ dependencies = [ [[package]] name = "questdb-rs-ffi" -version = "4.0.1" +version = "4.0.4" dependencies = [ "cbindgen", "libc", @@ -340,29 +495,35 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" -version = "0.8.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "libc", "rand_chacha", "rand_core", + "zerocopy", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -370,48 +531,97 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ring" -version = "0.17.7" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "getrandom", + "cfg-if", + "getrandom 0.2.15", "libc", - "spin", - "untrusted", - "windows-sys 0.48.0", + "untrusted 0.9.0", + "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.22.2" +version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ + "aws-lc-rs", "log", - "ring", + "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", @@ -420,12 +630,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", @@ -433,53 +642,53 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.2.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "security-framework" -version = "2.9.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -488,9 +697,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -498,35 +707,51 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slugify" version = "0.1.0" @@ -538,42 +763,25 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[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", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.48" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -582,80 +790,56 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", - "rustix", - "windows-sys 0.52.0", -] - -[[package]] -name = "thiserror" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" -dependencies = [ - "thiserror-impl", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.5", + "windows-sys 0.59.0", ] [[package]] -name = "thiserror-impl" -version = "1.0.56" +name = "toml" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "toml_datetime" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ - "tinyvec_macros", + "serde", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "toml_edit" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ + "indexmap", "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" 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", -] +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unidecode" @@ -663,6 +847,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -671,48 +861,75 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.5" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d" +checksum = "4b0351ca625c7b41a8e4f9bb6c5d9755f67f62c2187ebedecacd9974674b271d" dependencies = [ "base64", - "flate2", - "hootbin", "log", - "once_cell", + "percent-encoding", "rustls", + "rustls-pemfile", "rustls-pki-types", - "rustls-webpki", - "url", + "ureq-proto", + "utf-8", "webpki-roots", ] [[package]] -name = "url" -version = "2.5.0" +name = "ureq-proto" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "ae239d0a3341aebc94259414d1dc67cfce87d41cbebc816772c91b77902fafa4" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", + "base64", + "http", + "httparse", + "log", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -750,7 +967,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -770,17 +996,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -791,9 +1018,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -803,9 +1030,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -815,9 +1042,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -827,9 +1060,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -839,9 +1072,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -851,9 +1084,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -863,12 +1096,50 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/questdb-rs-ffi/Cargo.toml b/questdb-rs-ffi/Cargo.toml index 1636d907..d9d8409c 100644 --- a/questdb-rs-ffi/Cargo.toml +++ b/questdb-rs-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "questdb-rs-ffi" -version = "4.0.1" +version = "4.0.4" edition = "2021" publish = false @@ -12,10 +12,10 @@ crate-type = ["cdylib", "staticlib"] questdb-rs = { path = "../questdb-rs", features = [ "insecure-skip-verify", "tls-native-certs", "ilp-over-http"] } libc = "0.2" -questdb-confstr-ffi = { version = "0.1.0", optional = true } +questdb-confstr-ffi = { version = "0.1.1", optional = true } [build-dependencies] -cbindgen = { version = "0.26.0", optional = true, default-features = false } +cbindgen = { version = "0.28.0", optional = true, default-features = false } [features] # Expose the config parsing C API. diff --git a/questdb-rs-ffi/src/lib.rs b/questdb-rs-ffi/src/lib.rs index a792a1f7..2db6a917 100644 --- a/questdb-rs-ffi/src/lib.rs +++ b/questdb-rs-ffi/src/lib.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -672,20 +672,29 @@ pub unsafe extern "C" fn line_sender_buffer_transactional( buffer.transactional() } -/// Get a string representation of the contents of the buffer. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct line_sender_buffer_view { + len: size_t, + buf: *const u8, +} + +/// Provides a read-only view into the buffer's bytes content. /// /// @param[in] buffer Line buffer object. -/// @param[out] len_out The length in bytes of the accumulated buffer. -/// @return UTF-8 encoded buffer. The buffer is not nul-terminated. +/// @return A [`line_sender_buffer_view`] struct containing: +/// - `buf`: Immutable pointer to the byte stream +/// - `len`: Exact byte length of the data #[no_mangle] pub unsafe extern "C" fn line_sender_buffer_peek( buffer: *const line_sender_buffer, - len_out: *mut size_t, -) -> *const c_char { +) -> line_sender_buffer_view { let buffer = unwrap_buffer(buffer); - let buf: &[u8] = buffer.as_str().as_bytes(); - *len_out = buf.len(); - buf.as_ptr() as *const c_char + let buf: &[u8] = buffer.as_bytes(); + line_sender_buffer_view { + len: buf.len(), + buf: buf.as_ptr(), + } } /// Start recording a new row for the given table. diff --git a/questdb-rs/Cargo.toml b/questdb-rs/Cargo.toml index da87eaad..90ace7f3 100644 --- a/questdb-rs/Cargo.toml +++ b/questdb-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "questdb-rs" -version = "4.0.1" +version = "4.0.4" edition = "2021" license = "Apache-2.0" description = "QuestDB Client Library for Rust" @@ -21,20 +21,25 @@ crate-type = ["lib"] libc = "0.2" socket2 = "0.5.5" dns-lookup = "2.0.4" -base64ct = { version = "1.6.0", features = ["alloc"] } +base64ct = { version = "1.7", features = ["alloc"] } rustls-pemfile = "2.0.0" -ryu = "1.0.15" -itoa = "1.0.9" -ring = "0.17.5" +ryu = "1.0" +itoa = "1.0" +aws-lc-rs = { version = "1.13", optional = true } +ring = { version = "0.17.14", optional = true } rustls-pki-types = "1.0.1" -rustls = "0.22.0" -rustls-native-certs = { version = "0.7.0", optional = true } -webpki-roots = { version = "0.26.0", optional = true } -chrono = { version = "0.4.30", optional = true } -ureq = { version = "2.9.4", optional = true } -serde_json = { version = "1.0.108", optional = true } -questdb-confstr = "0.1.0" -rand = { version = "0.8.5", optional = true } +rustls = { version = "0.23.25", default-features = false, features = ["logging", "std", "tls12"] } +rustls-native-certs = { version = "0.8.1", optional = true } +webpki-roots = { version = "0.26.8", default-features = false, optional = true } +chrono = { version = "0.4.40", optional = true } + +# We need to limit the `ureq` version to 3.0.x since we use +# the `ureq::unversioned` module which does not respect semantic versioning. +ureq = { version = "3.0.10, <3.1.0", default-features = false, features = ["rustls-no-provider"], optional = true } +serde_json = { version = "1", optional = true } +questdb-confstr = "0.1.1" +rand = { version = "0.9.0", optional = true } +no-panic = { version = "0.1", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["ws2def"] } @@ -43,15 +48,16 @@ winapi = { version = "0.3.9", features = ["ws2def"] } serde_json = { version = "1.0.108" } serde = { version = "1.0.193", features = ["derive"] } slugify = "0.1.0" -indoc = "2.0.4" +indoc = "2" [dev-dependencies] -mio = { version = "0.8.10", features = ["os-poll", "net"] } +mio = { version = "1", features = ["os-poll", "net"] } chrono = "0.4.31" -tempfile = "3.2.0" +tempfile = "3" +webpki-roots = "0.26.8" [features] -default = ["tls-webpki-certs", "ilp-over-http"] +default = ["tls-webpki-certs", "ilp-over-http", "aws-lc-crypto"] # Include support for ILP over HTTP. ilp-over-http = ["dep:ureq", "dep:serde_json", "dep:rand"] @@ -62,6 +68,12 @@ tls-native-certs = ["dep:rustls-native-certs"] # Allow use of the `webpki-roots` crate to validate TLS certificates. tls-webpki-certs = ["dep:webpki-roots"] +# Use `aws-lc-rs` as the cryto library. +aws-lc-crypto = ["dep:aws-lc-rs", "rustls/aws-lc-rs"] + +# Use `ring` as the crypto library. +ring-crypto = ["dep:ring", "rustls/ring"] + # Allow skipping verification of insecure certificates. insecure-skip-verify = [] @@ -71,6 +83,19 @@ json_tests = [] # Enable methods to create timestamp objects from chrono::DateTime objects. chrono_timestamp = ["chrono"] +# The `aws-lc-crypto` and `ring-crypto` features are mutually exclusive, +# thus compiling with `--all-features` will not work. +# Instead compile with `--features almost-all-features`. +# This is useful for quickly running `cargo test` or `cargo clippy`. +almost-all-features = [ + "tls-webpki-certs", + "ilp-over-http", + "aws-lc-crypto", + "insecure-skip-verify", + "json_tests", + "chrono_timestamp" +] + [[example]] name = "basic" required-features = ["chrono_timestamp"] diff --git a/questdb-rs/README.md b/questdb-rs/README.md index 6af085ea..093fd6de 100644 --- a/questdb-rs/README.md +++ b/questdb-rs/README.md @@ -32,10 +32,11 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf("http::addr=localhost:9000;")?; let mut buffer = Buffer::new(); buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(TimestampNanos::now())?; sender.flush(&mut buffer)?; Ok(()) @@ -45,7 +46,7 @@ fn main() -> Result<()> { ## Docs Most of the client documentation is on the -[`ingress`](https://docs.rs/questdb-rs/4.0.1/questdb/ingress/) module page. +[`ingress`](https://docs.rs/questdb-rs/4.0.4/questdb/ingress/) module page. ## Crate features diff --git a/questdb-rs/build.rs b/questdb-rs/build.rs index 522c872d..636f55e8 100644 --- a/questdb-rs/build.rs +++ b/questdb-rs/build.rs @@ -98,9 +98,9 @@ pub mod json_tests { use crate::{Result, ingress::{Buffer}}; use crate::tests::{TestResult}; - fn matches_any_line(line: &str, expected: &[&str]) -> bool { + fn matches_any_line(line: &[u8], expected: &[&str]) -> bool { for &exp in expected { - if line == exp { + if line == exp.as_bytes() { return true; } } @@ -171,7 +171,7 @@ pub mod json_tests { if let Some(ref line) = expected.line { let exp_ln = format!("{}\n", line); writeln!(output, " let exp = {:?};", exp_ln)?; - writeln!(output, " assert_eq!(buffer.as_str(), exp);")?; + writeln!(output, " assert_eq!(buffer.as_bytes(), exp.as_bytes());")?; } else { let any: Vec<String> = expected .any_lines @@ -187,7 +187,7 @@ pub mod json_tests { writeln!(output, " ];")?; writeln!( output, - " assert!(matches_any_line(buffer.as_str(), &any));" + " assert!(matches_any_line(buffer.as_bytes(), &any));" )?; } } else { @@ -202,6 +202,17 @@ pub mod json_tests { } fn main() -> Result<(), Box<dyn std::error::Error>> { + #[cfg(not(any(feature = "tls-webpki-certs", feature = "tls-native-certs")))] + compile_error!( + "At least one of `tls-webpki-certs` or `tls-native-certs` features must be enabled." + ); + + #[cfg(not(any(feature = "aws-lc-crypto", feature = "ring-crypto")))] + compile_error!("You must enable exactly one of the `aws-lc-crypto` or `ring-crypto` features, but none are enabled."); + + #[cfg(all(feature = "aws-lc-crypto", feature = "ring-crypto"))] + compile_error!("You must enable exactly one of the `aws-lc-crypto` or `ring-crypto` features, but both are enabled."); + #[cfg(feature = "json_tests")] { println!("cargo:rerun-if-changed=build.rs"); diff --git a/questdb-rs/examples/auth.rs b/questdb-rs/examples/auth.rs index 642d2ffd..2dd6e66d 100644 --- a/questdb-rs/examples/auth.rs +++ b/questdb-rs/examples/auth.rs @@ -10,7 +10,7 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf(format!( concat!( "tcp::addr={}:{};", - "username=testUser1;", + "username=admin;", "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;", "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;", "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;" @@ -21,10 +21,11 @@ fn main() -> Result<()> { let designated_timestamp = TimestampNanos::from_datetime(Utc.with_ymd_and_hms(1997, 7, 4, 4, 56, 55).unwrap())?; buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(designated_timestamp)?; //// If you want to pass the current system timestamp, replace with: diff --git a/questdb-rs/examples/auth_tls.rs b/questdb-rs/examples/auth_tls.rs index cb2132eb..19225027 100644 --- a/questdb-rs/examples/auth_tls.rs +++ b/questdb-rs/examples/auth_tls.rs @@ -10,7 +10,7 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf(format!( concat!( "tcps::addr={}:{};", - "username=testUser1;", + "username=admin;", "token=5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48;", "token_x=fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU;", "token_y=Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac;", @@ -21,10 +21,11 @@ fn main() -> Result<()> { let designated_timestamp = TimestampNanos::from_datetime(Utc.with_ymd_and_hms(1997, 7, 4, 4, 56, 55).unwrap())?; buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(designated_timestamp)?; //// If you want to pass the current system timestamp, replace with: diff --git a/questdb-rs/examples/basic.rs b/questdb-rs/examples/basic.rs index 336fbf2c..6fa665d3 100644 --- a/questdb-rs/examples/basic.rs +++ b/questdb-rs/examples/basic.rs @@ -12,10 +12,11 @@ fn main() -> Result<()> { let designated_timestamp = TimestampNanos::from_datetime(Utc.with_ymd_and_hms(1997, 7, 4, 4, 56, 55).unwrap())?; buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(designated_timestamp)?; //// If you want to pass the current system timestamp, replace with: diff --git a/questdb-rs/examples/from_conf.rs b/questdb-rs/examples/from_conf.rs index 524da8be..d328c31c 100644 --- a/questdb-rs/examples/from_conf.rs +++ b/questdb-rs/examples/from_conf.rs @@ -7,10 +7,11 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf("tcp::addr=localhost:9009;")?; let mut buffer = Buffer::new(); buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(TimestampNanos::now())?; sender.flush(&mut buffer)?; Ok(()) diff --git a/questdb-rs/examples/from_env.rs b/questdb-rs/examples/from_env.rs index a3dbf768..ca338851 100644 --- a/questdb-rs/examples/from_env.rs +++ b/questdb-rs/examples/from_env.rs @@ -8,10 +8,11 @@ fn main() -> Result<()> { let mut sender = Sender::from_env()?; let mut buffer = Buffer::new(); buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(TimestampNanos::now())?; sender.flush(&mut buffer)?; Ok(()) diff --git a/questdb-rs/examples/http.rs b/questdb-rs/examples/http.rs index 3eb72d6f..74b2f3e9 100644 --- a/questdb-rs/examples/http.rs +++ b/questdb-rs/examples/http.rs @@ -7,10 +7,11 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf("https::addr=localhost:9000;username=foo;password=bar;")?; let mut buffer = Buffer::new(); buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(TimestampNanos::now())?; sender.flush(&mut buffer)?; Ok(()) diff --git a/questdb-rs/src/error.rs b/questdb-rs/src/error.rs index 6848280a..45f56650 100644 --- a/questdb-rs/src/error.rs +++ b/questdb-rs/src/error.rs @@ -66,6 +66,31 @@ impl Error { } } + #[cfg(feature = "ilp-over-http")] + pub(crate) fn from_ureq_error(err: ureq::Error, url: &str) -> Error { + match err { + ureq::Error::StatusCode(code) => { + if code == 404 { + fmt!( + HttpNotSupported, + "Could not flush buffer: HTTP endpoint does not support ILP." + ) + } else if [401, 403].contains(&code) { + fmt!( + AuthError, + "Could not flush buffer: HTTP endpoint authentication error [code: {}]", + code + ) + } else { + fmt!(SocketError, "Could not flush buffer: {}: {}", url, err) + } + } + e => { + fmt!(SocketError, "Could not flush buffer: {}: {}", url, e) + } + } + } + /// Get the error code (category) of this error. pub fn code(&self) -> ErrorCode { self.code diff --git a/questdb-rs/src/gai.rs b/questdb-rs/src/gai.rs index 0270b69e..145dac3e 100644 --- a/questdb-rs/src/gai.rs +++ b/questdb-rs/src/gai.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/questdb-rs/src/ingress/conf.rs b/questdb-rs/src/ingress/conf.rs index 011d8620..f5a9e272 100644 --- a/questdb-rs/src/ingress/conf.rs +++ b/questdb-rs/src/ingress/conf.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/questdb-rs/src/ingress/http.rs b/questdb-rs/src/ingress/http.rs index 177c87ed..323a4a92 100644 --- a/questdb-rs/src/ingress/http.rs +++ b/questdb-rs/src/ingress/http.rs @@ -1,12 +1,24 @@ +use super::conf::ConfigSetting; use crate::{error, Error}; use base64ct::Base64; use base64ct::Encoding; use rand::Rng; -use std::fmt::Write; +use rustls::{ClientConnection, StreamOwned}; +use rustls_pki_types::ServerName; +use std::fmt; +use std::fmt::{Debug, Write}; +use std::io::{Read, Write as IoWrite}; +use std::sync::Arc; use std::thread::sleep; use std::time::Duration; +use ureq::http::Response; +use ureq::unversioned::transport::{ + Buffers, Connector, LazyBuffers, NextTimeout, Transport, TransportAdapter, +}; -use super::conf::ConfigSetting; +use ureq::unversioned::*; +use ureq::Error::*; +use ureq::{http, Body}; #[derive(PartialEq, Debug, Clone)] pub(super) struct BasicAuthParams { @@ -72,7 +84,163 @@ pub(super) struct HttpHandlerState { pub(super) config: HttpConfig, } -pub(super) fn parse_json_error(json: &serde_json::Value, msg: &str) -> Error { +impl HttpHandlerState { + fn send_request( + &self, + buf: &[u8], + request_timeout: Duration, + ) -> (bool, Result<Response<Body>, ureq::Error>) { + let request = self + .agent + .post(&self.url) + .config() + .timeout_per_call(Some(request_timeout)) + .build() + .query_pairs([("precision", "n")]) + .content_type("text/plain; charset=utf-8"); + + let request = match self.auth.as_ref() { + Some(auth) => request.header("Authorization", auth), + None => request, + }; + let response = request.send(buf); + match &response { + Ok(res) => (need_retry(Ok(res.status())), response), + Err(err) => (need_retry(Err(err)), response), + } + } +} + +#[derive(Debug)] +pub struct TlsConnector { + tls_config: Option<Arc<rustls::ClientConfig>>, +} + +impl<In: Transport> Connector<In> for TlsConnector { + type Out = transport::Either<In, TlsTransport>; + + fn connect( + &self, + details: &transport::ConnectionDetails, + chained: Option<In>, + ) -> std::result::Result<Option<Self::Out>, ureq::Error> { + let transport = match chained { + Some(t) => t, + None => return Ok(None), + }; + + // Only add TLS if we are connecting via HTTPS, otherwise use chained transport as is. + if !details.needs_tls() { + return Ok(Some(transport::Either::A(transport))); + } + + match self.tls_config.as_ref() { + Some(config) => { + let name_borrowed: ServerName<'_> = details + .uri + .authority() + .expect("uri authority for tls") + .host() + .try_into() + .map_err(|_e| ureq::Error::Tls("tls invalid dns name error"))?; + + let name = name_borrowed.to_owned(); + let conn = ClientConnection::new(config.clone(), name) + .map_err(|_e| ureq::Error::Tls("tls client connection error"))?; + let stream = StreamOwned { + conn, + sock: TransportAdapter::new(transport.boxed()), + }; + let buffers = LazyBuffers::new( + details.config.input_buffer_size(), + details.config.output_buffer_size(), + ); + + let transport = TlsTransport { buffers, stream }; + Ok(Some(transport::Either::B(transport))) + } + _ => Ok(Some(transport::Either::A(transport))), + } + } +} + +impl TlsConnector { + pub fn new(tls_config: Option<Arc<rustls::ClientConfig>>) -> Self { + TlsConnector { tls_config } + } +} + +pub struct TlsTransport { + buffers: LazyBuffers, + stream: StreamOwned<ClientConnection, TransportAdapter>, +} + +impl Debug for TlsTransport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TlsTransport") + .field("chained", &self.stream.sock.inner()) + .finish() + } +} + +impl Transport for TlsTransport { + fn buffers(&mut self) -> &mut dyn Buffers { + &mut self.buffers + } + + fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), ureq::Error> { + self.stream.get_mut().set_timeout(timeout); + let output = &self.buffers.output()[..amount]; + self.stream.write_all(output)?; + Ok(()) + } + + fn await_input(&mut self, timeout: NextTimeout) -> Result<bool, ureq::Error> { + if self.buffers.can_use_input() { + return Ok(true); + } + + self.stream.get_mut().set_timeout(timeout); + let input = self.buffers.input_append_buf(); + let amount = self.stream.read(input)?; + self.buffers.input_appended(amount); + Ok(amount > 0) + } + + fn is_open(&mut self) -> bool { + self.stream.get_mut().get_mut().is_open() + } + + fn is_tls(&self) -> bool { + true + } +} + +fn need_retry(res: Result<http::status::StatusCode, &ureq::Error>) -> bool { + match res { + Ok(status) => { + status.is_server_error() + && matches!( + status.as_u16(), + // Official HTTP codes + 500 | // Internal Server Error + 503 | // Service Unavailable + 504 | // Gateway Timeout + + // Unofficial extensions + 507 | // Insufficient Storage + 509 | // Bandwidth Limit Exceeded + 523 | // Origin is Unreachable + 524 | // A Timeout Occurred + 529 | // Site is overloaded + 599 // Network Connect Timeout Error + ) + } + Err(err) => matches!(err, Timeout(_) | ConnectionFailed | TooManyRedirects), + } +} + +fn parse_json_error(json: &serde_json::Value, msg: &str) -> Error { let mut description = msg.to_string(); error::fmt!(ServerFlushError, "Could not flush buffer: {}", msg); @@ -113,14 +281,16 @@ pub(super) fn parse_json_error(json: &serde_json::Value, msg: &str) -> Error { error::fmt!(ServerFlushError, "Could not flush buffer: {}", description) } -pub(super) fn parse_http_error(http_status_code: u16, response: ureq::Response) -> Error { +pub(super) fn parse_http_error(http_status_code: u16, response: Response<Body>) -> Error { + let (head, body) = response.into_parts(); + let body_content = body.into_with_config().lossy_utf8(true).read_to_string(); if http_status_code == 404 { return error::fmt!( HttpNotSupported, "Could not flush buffer: HTTP endpoint does not support ILP." ); } else if [401, 403].contains(&http_status_code) { - let description = match response.into_string() { + let description = match body_content { Ok(msg) if !msg.is_empty() => format!(": {}", msg), _ => "".to_string(), }; @@ -132,10 +302,14 @@ pub(super) fn parse_http_error(http_status_code: u16, response: ureq::Response) ); } - let is_json = response - .content_type() - .eq_ignore_ascii_case("application/json"); - match response.into_string() { + let is_json = match head.headers.get("Content-Type") { + Some(header_value) => match header_value.to_str() { + Ok(s) => s.eq_ignore_ascii_case("application/json"), + Err(_) => false, + }, + None => false, + }; + match body_content { Ok(msg) => { let string_err = || error::fmt!(ServerFlushError, "Could not flush buffer: {}", msg); @@ -150,11 +324,11 @@ pub(super) fn parse_http_error(http_status_code: u16, response: ureq::Response) } }; - return if let Some(serde_json::Value::String(ref msg)) = json.get("message") { + if let Some(serde_json::Value::String(ref msg)) = json.get("message") { parse_json_error(&json, msg) } else { string_err() - }; + } } Err(err) => { error::fmt!(SocketError, "Could not flush buffer: {}", err) @@ -162,74 +336,50 @@ pub(super) fn parse_http_error(http_status_code: u16, response: ureq::Response) } } -pub(super) fn is_retriable_error(err: &ureq::Error) -> bool { - use ureq::Error::*; - match err { - Transport(_) => true, - - // Official HTTP codes - Status(500, _) | // Internal Server Error - Status(503, _) | // Service Unavailable - Status(504, _) | // Gateway Timeout - - // Unofficial extensions - Status(507, _) | // Insufficient Storage - Status(509, _) | // Bandwidth Limit Exceeded - Status(523, _) | // Origin is Unreachable - Status(524, _) | // A Timeout Occurred - Status(529, _) | // Site is overloaded - Status(599, _) => { // Network Connect Timeout Error - true - } - _ => false - } -} - #[allow(clippy::result_large_err)] // `ureq::Error` is large enough to cause this warning. fn retry_http_send( - request: ureq::Request, + state: &HttpHandlerState, buf: &[u8], + request_timeout: Duration, retry_timeout: Duration, - mut last_err: ureq::Error, -) -> Result<ureq::Response, ureq::Error> { - let mut rng = rand::thread_rng(); + mut last_rep: Result<Response<Body>, ureq::Error>, +) -> Result<Response<Body>, ureq::Error> { + let mut rng = rand::rng(); let retry_end = std::time::Instant::now() + retry_timeout; let mut retry_interval_ms = 10; + let mut need_retry; loop { - let jitter_ms = rng.gen_range(-5i32..5); + let jitter_ms = rng.random_range(-5i32..5); let to_sleep_ms = retry_interval_ms + jitter_ms; let to_sleep = Duration::from_millis(to_sleep_ms as u64); if (std::time::Instant::now() + to_sleep) > retry_end { - return Err(last_err); + return last_rep; } sleep(to_sleep); - last_err = match request.clone().send_bytes(buf) { - Ok(res) => return Ok(res), - Err(err) => { - if !is_retriable_error(&err) { - return Err(err); - } - err - } - }; + if let Ok(last_rep) = last_rep { + // Actively consume the reader to return the connection to the connection pool. + // see https://github.com/algesten/ureq/issues/94 + _ = last_rep.into_body().read_to_vec(); + } + (need_retry, last_rep) = state.send_request(buf, request_timeout); + if !need_retry { + return last_rep; + } retry_interval_ms = (retry_interval_ms * 2).min(1000); } } #[allow(clippy::result_large_err)] // `ureq::Error` is large enough to cause this warning. pub(super) fn http_send_with_retries( - request: ureq::Request, + state: &HttpHandlerState, buf: &[u8], + request_timeout: Duration, retry_timeout: Duration, -) -> Result<ureq::Response, ureq::Error> { - let last_err = match request.clone().send_bytes(buf) { - Ok(res) => return Ok(res), - Err(err) => err, - }; - - if retry_timeout.is_zero() || !is_retriable_error(&last_err) { - return Err(last_err); +) -> Result<Response<Body>, ureq::Error> { + let (need_retry, last_rep) = state.send_request(buf, request_timeout); + if !need_retry || retry_timeout.is_zero() { + return last_rep; } - retry_http_send(request, buf, retry_timeout, last_err) + retry_http_send(state, buf, request_timeout, retry_timeout, last_rep) } diff --git a/questdb-rs/src/ingress/mod.md b/questdb-rs/src/ingress/mod.md index c2874412..aed342af 100644 --- a/questdb-rs/src/ingress/mod.md +++ b/questdb-rs/src/ingress/mod.md @@ -21,10 +21,11 @@ fn main() -> Result<()> { let mut sender = Sender::from_conf("http::addr=localhost:9000;")?; let mut buffer = Buffer::new(); buffer - .table("sensors")? - .symbol("id", "toronto1")? - .column_f64("temperature", 20.0)? - .column_i64("humidity", 50)? + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? .at(TimestampNanos::now())?; sender.flush(&mut buffer)?; Ok(()) @@ -65,27 +66,6 @@ QuestDB instances), call The two supported transport modes, HTTP and TCP, handle errors very differently. In a nutshell, HTTP is much better at error handling. -# Health Check - -The QuestDB server has a "ping" endpoint you can access to see if it's alive, -and confirm the version of InfluxDB Line Protocol with which you are -interacting: - -```shell -curl -I http://localhost:9000/ping -``` - -Example of the expected response: - -```shell -HTTP/1.1 204 OK -Server: questDB/1.0 -Date: Fri, 2 Feb 2024 17:09:38 GMT -Transfer-Encoding: chunked -Content-Type: text/plain; charset=utf-8 -X-Influxdb-Version: v2.7.4 -``` - ## TCP TCP doesn't report errors at all to the sender; instead, the server quietly @@ -110,6 +90,27 @@ message. After the sender has signalled an error, it remains usable. You can handle the error as appropriate and continue using it. +# Health Check + +The QuestDB server has a "ping" endpoint you can access to see if it's alive, +and confirm the version of InfluxDB Line Protocol with which you are +interacting: + +```shell +curl -I http://localhost:9000/ping +``` + +Example of the expected response: + +```plain +HTTP/1.1 204 OK +Server: questDB/1.0 +Date: Fri, 2 Feb 2024 17:09:38 GMT +Transfer-Encoding: chunked +Content-Type: text/plain; charset=utf-8 +X-Influxdb-Version: v2.7.4 +``` + # Configuration Parameters In the examples below, we'll use configuration strings. We also provide the @@ -161,7 +162,7 @@ let mut sender = Sender::from_conf( # use questdb::{Result, ingress::Sender}; # fn main() -> Result<()> { let mut sender = Sender::from_conf( - "tcps::addr=localhost:9009;username=testUser1;token=5UjEA0;token_x=fLKYa9;token_y=bS1dEfy" + "tcps::addr=localhost:9009;username=testUser1;token=5UjEA0;token_x=fLKYa9;token_y=bS1dEfy;" )?; # Ok(()) # } @@ -256,18 +257,26 @@ with a high-latency network connection. ### Timestamp Column Name InfluxDB Line Protocol (ILP) does not give a name to the designated timestamp, -so if you let this client auto-create the table, it will have the default name. -To use a custom name, create the table using a DDL statement: +so if you let this client auto-create the table, it will have the default `timestamp` name. +To use a custom name, say `my_ts`, pre-create the table with the desired +timestamp column name: + +To address this, issue a `CREATE TABLE` statement to create the table in advance. +Note the `timestamp(my_ts)` clause at the end specifies the designated timestamp. ```sql -CREATE TABLE sensors ( - my_ts timestamp, - id symbol, - temperature double, - humidity double, -) timestamp(my_ts); +CREATE TABLE IF NOT EXISTS 'trades' ( + symbol SYMBOL capacity 256 CACHE, + side SYMBOL capacity 256 CACHE, + price DOUBLE, + amount DOUBLE, + my_ts TIMESTAMP +) timestamp (my_ts) PARTITION BY DAY WAL; ``` +You can use the `CREATE TABLE IF NOT EXISTS` construct to make sure the table is +created, but without raising an error if the table already exists. + ## Sequential Coupling in the Buffer API The fluent API of [`Buffer`] has sequential coupling: there's a certain order in @@ -291,10 +300,10 @@ use questdb::ingress::{ TimestampNanos}; # fn main() -> Result<()> { let mut buffer = Buffer::new(); -let tide_name = TableName::new("tide")?; -let water_level_name = ColumnName::new("water_level")?; -buffer.table(tide_name)?.column_f64(water_level_name, 20.4)?.at(TimestampNanos::now())?; -buffer.table(tide_name)?.column_f64(water_level_name, 17.2)?.at(TimestampNanos::now())?; +let table_name = TableName::new("trades")?; +let price_name = ColumnName::new("price")?; +buffer.table(table_name)?.column_f64(price_name, 2615.54)?.at(TimestampNanos::now())?; +buffer.table(table_name)?.column_f64(price_name, 39269.98)?.at(TimestampNanos::now())?; # Ok(()) # } ``` @@ -326,4 +335,4 @@ Instead, on error, the server terminates the connection, and logs any error messages in [server logs](https://questdb.io/docs/troubleshooting/log/). To inspect or log a buffer's contents before you send it, call -[`buffer.as_str()`](Buffer::as_str). +[`buffer.as_bytes()`](Buffer::as_bytes). diff --git a/questdb-rs/src/ingress/mod.rs b/questdb-rs/src/ingress/mod.rs index 0239eac5..fab0fe35 100644 --- a/questdb-rs/src/ingress/mod.rs +++ b/questdb-rs/src/ingress/mod.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,11 @@ pub use self::timestamp::*; use crate::error::{self, Error, Result}; use crate::gai; use crate::ingress::conf::ConfigSetting; +use base64ct::{Base64, Base64UrlUnpadded, Encoding}; use core::time::Duration; +use rustls::{ClientConnection, RootCertStore, StreamOwned}; +use rustls_pki_types::ServerName; +use socket2::{Domain, Protocol as SockProtocol, SockAddr, Socket, Type}; use std::collections::HashMap; use std::convert::Infallible; use std::fmt::{Debug, Display, Formatter, Write}; @@ -39,12 +43,17 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use base64ct::{Base64, Base64UrlUnpadded, Encoding}; -use ring::rand::SystemRandom; -use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}; -use rustls::{ClientConnection, RootCertStore, StreamOwned}; -use rustls_pki_types::ServerName; -use socket2::{Domain, Protocol as SockProtocol, SockAddr, Socket, Type}; +#[cfg(feature = "aws-lc-crypto")] +use aws_lc_rs::{ + rand::SystemRandom, + signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}, +}; + +#[cfg(feature = "ring-crypto")] +use ring::{ + rand::SystemRandom, + signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}, +}; #[derive(Debug, Copy, Clone)] enum Op { @@ -251,12 +260,11 @@ impl From<Infallible> for Error { } } -fn write_escaped_impl<Q, C>(check_escape_fn: C, quoting_fn: Q, output: &mut String, s: &str) +fn write_escaped_impl<Q, C>(check_escape_fn: C, quoting_fn: Q, output: &mut Vec<u8>, s: &str) where C: Fn(u8) -> bool, Q: Fn(&mut Vec<u8>), { - let output_vec = unsafe { output.as_mut_vec() }; let mut to_escape = 0usize; for b in s.bytes() { if check_escape_fn(b) { @@ -264,32 +272,32 @@ where } } - quoting_fn(output_vec); + quoting_fn(output); if to_escape == 0 { // output.push_str(s); - output_vec.extend_from_slice(s.as_bytes()); + output.extend_from_slice(s.as_bytes()); } else { let additional = s.len() + to_escape; - output_vec.reserve(additional); - let mut index = output_vec.len(); - unsafe { output_vec.set_len(index + additional) }; + output.reserve(additional); + let mut index = output.len(); + unsafe { output.set_len(index + additional) }; for b in s.bytes() { if check_escape_fn(b) { unsafe { - *output_vec.get_unchecked_mut(index) = b'\\'; + *output.get_unchecked_mut(index) = b'\\'; } index += 1; } unsafe { - *output_vec.get_unchecked_mut(index) = b; + *output.get_unchecked_mut(index) = b; } index += 1; } } - quoting_fn(output_vec); + quoting_fn(output); } fn must_escape_unquoted(c: u8) -> bool { @@ -300,11 +308,11 @@ fn must_escape_quoted(c: u8) -> bool { matches!(c, b'\n' | b'\r' | b'"' | b'\\') } -fn write_escaped_unquoted(output: &mut String, s: &str) { +fn write_escaped_unquoted(output: &mut Vec<u8>, s: &str) { write_escaped_impl(must_escape_unquoted, |_output| (), output, s); } -fn write_escaped_quoted(output: &mut String, s: &str) { +fn write_escaped_quoted(output: &mut Vec<u8>, s: &str) { write_escaped_impl(must_escape_quoted, |output| output.push(b'"'), output, s) } @@ -543,7 +551,7 @@ impl BufferState { /// #[derive(Debug, Clone)] pub struct Buffer { - output: String, + output: Vec<u8>, state: BufferState, marker: Option<(usize, BufferState)>, max_name_len: usize, @@ -554,7 +562,7 @@ impl Buffer { /// QuestDB server default. pub fn new() -> Self { Self { - output: String::new(), + output: Vec::new(), state: BufferState::new(), marker: None, max_name_len: 127, @@ -608,8 +616,7 @@ impl Buffer { self.output.capacity() } - /// A string representation of the buffer's contents. Useful for debugging. - pub fn as_str(&self) -> &str { + pub fn as_bytes(&self) -> &[u8] { &self.output } @@ -796,9 +803,9 @@ impl Buffer { let name: ColumnName<'a> = name.try_into()?; self.validate_max_name_len(name.name)?; self.check_op(Op::Symbol)?; - self.output.push(','); + self.output.push(b','); write_escaped_unquoted(&mut self.output, name.name); - self.output.push('='); + self.output.push(b'='); write_escaped_unquoted(&mut self.output, value.as_ref()); self.state.op_case = OpCase::SymbolWritten; Ok(self) @@ -814,12 +821,12 @@ impl Buffer { self.check_op(Op::Column)?; self.output .push(if (self.state.op_case as isize & Op::Symbol as isize) > 0 { - ' ' + b' ' } else { - ',' + b',' }); write_escaped_unquoted(&mut self.output, name.name); - self.output.push('='); + self.output.push(b'='); self.state.op_case = OpCase::ColumnWritten; Ok(self) } @@ -858,7 +865,7 @@ impl Buffer { Error: From<N::Error>, { self.write_column_key(name)?; - self.output.push(if value { 't' } else { 'f' }); + self.output.push(if value { b't' } else { b'f' }); Ok(self) } @@ -898,8 +905,8 @@ impl Buffer { self.write_column_key(name)?; let mut buf = itoa::Buffer::new(); let printed = buf.format(value); - self.output.push_str(printed); - self.output.push('i'); + self.output.extend_from_slice(printed.as_bytes()); + self.output.push(b'i'); Ok(self) } @@ -938,7 +945,7 @@ impl Buffer { { self.write_column_key(name)?; let mut ser = F64Serializer::new(value); - self.output.push_str(ser.as_str()); + self.output.extend_from_slice(ser.as_str().as_bytes()); Ok(self) } @@ -1059,8 +1066,8 @@ impl Buffer { let timestamp: TimestampMicros = timestamp.try_into()?; let mut buf = itoa::Buffer::new(); let printed = buf.format(timestamp.as_i64()); - self.output.push_str(printed); - self.output.push('t'); + self.output.extend_from_slice(printed.as_bytes()); + self.output.push(b't'); Ok(self) } @@ -1122,9 +1129,9 @@ impl Buffer { } let mut buf = itoa::Buffer::new(); let printed = buf.format(epoch_nanos); - self.output.push(' '); - self.output.push_str(printed); - self.output.push('\n'); + self.output.push(b' '); + self.output.extend_from_slice(printed.as_bytes()); + self.output.push(b'\n'); self.state.op_case = OpCase::MayFlushOrTable; self.state.row_count += 1; Ok(()) @@ -1160,7 +1167,7 @@ impl Buffer { /// ``` pub fn at_now(&mut self) -> Result<()> { self.check_op(Op::At)?; - self.output.push('\n'); + self.output.push(b'\n'); self.state.op_case = OpCase::MayFlushOrTable; self.state.row_count += 1; Ok(()) @@ -1309,8 +1316,16 @@ mod danger { Ok(HandshakeSignatureValid::assertion()) } + #[cfg(feature = "aws-lc-crypto")] + fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { + rustls::crypto::aws_lc_rs::default_provider() + .signature_verification_algorithms + .supported_schemes() + } + + #[cfg(feature = "ring-crypto")] fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { - rustls::crypto::ring::default_provider() + rustls::crypto::aws_lc_rs::default_provider() .signature_verification_algorithms .supported_schemes() } @@ -1325,14 +1340,27 @@ fn add_webpki_roots(root_store: &mut RootCertStore) { } #[cfg(feature = "tls-native-certs")] -fn add_os_roots(root_store: &mut RootCertStore) -> Result<()> { - let os_certs = rustls_native_certs::load_native_certs().map_err(|io_err| { - error::fmt!( +fn unpack_os_native_certs( + res: rustls_native_certs::CertificateResult, +) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>> { + if !res.errors.is_empty() { + return Err(error::fmt!( TlsError, "Could not load OS native TLS certificates: {}", - io_err - ) - })?; + res.errors + .iter() + .map(|e| e.to_string()) + .collect::<Vec<_>>() + .join(", ") + )); + } + + Ok(res.certs) +} + +#[cfg(feature = "tls-native-certs")] +fn add_os_roots(root_store: &mut RootCertStore) -> Result<()> { + let os_certs = unpack_os_native_certs(rustls_native_certs::load_native_certs())?; let (valid_count, invalid_count) = root_store.add_parsable_certificates(os_certs); if valid_count == 0 && invalid_count > 0 { @@ -1919,7 +1947,7 @@ impl SenderBuilder { } /// Configure how long to wait for messages from the QuestDB server during - /// the TLS handshake and authentication process. + /// the TLS handshake and authentication process. This only applies to TCP. /// The default is 15 seconds. pub fn auth_timeout(mut self, value: Duration) -> Result<Self> { self.auth_timeout.set_specified("auth_timeout", value)?; @@ -2016,6 +2044,8 @@ impl SenderBuilder { /// The timeout calculated from minimum throughput is adedd to the value of /// [`request_timeout`](SenderBuilder::request_timeout) to get the total timeout /// value. + /// A value of 0 disables this feature, so it's similar to setting "infinite" + /// minimum throughput. The total timeout will then be equal to `request_timeout`. pub fn request_min_throughput(mut self, value: u64) -> Result<Self> { if let Some(http) = &mut self.http { http.request_min_throughput @@ -2032,10 +2062,16 @@ impl SenderBuilder { #[cfg(feature = "ilp-over-http")] /// Additional time to wait on top of that calculated from the minimum throughput. /// This accounts for the fixed latency of the HTTP request-response roundtrip. - /// The value is in milliseconds, and the default is 10 seconds. + /// The default is 10 seconds. /// See also: [`request_min_throughput`](SenderBuilder::request_min_throughput). pub fn request_timeout(mut self, value: Duration) -> Result<Self> { if let Some(http) = &mut self.http { + if value.is_zero() { + return Err(error::fmt!( + ConfigError, + "\"request_timeout\" must be greater than 0." + )); + } http.request_timeout .set_specified("request_timeout", value)?; } else { @@ -2267,6 +2303,8 @@ impl SenderBuilder { Protocol::Tcp | Protocol::Tcps => self.connect_tcp(&auth)?, #[cfg(feature = "ilp-over-http")] Protocol::Http | Protocol::Https => { + use ureq::unversioned::transport::Connector; + use ureq::unversioned::transport::TcpConnector; if self.net_interface.is_some() { // See: https://github.com/algesten/ureq/issues/692 return Err(error::fmt!( @@ -2275,8 +2313,11 @@ impl SenderBuilder { )); } - let user_agent = self.http.as_ref().unwrap().user_agent.as_str(); - let agent_builder = ureq::AgentBuilder::new() + let http_config = self.http.as_ref().unwrap(); + let user_agent = http_config.user_agent.as_str(); + let connector = TcpConnector::default(); + + let agent_builder = ureq::Agent::config_builder() .user_agent(user_agent) .no_delay(true); @@ -2286,15 +2327,13 @@ impl SenderBuilder { #[cfg(not(feature = "insecure-skip-verify"))] let tls_verify = true; - let agent_builder = match configure_tls( + let connector = connector.chain(TlsConnector::new(configure_tls( self.protocol.tls_enabled(), tls_verify, *self.tls_ca, self.tls_roots.deref(), - )? { - Some(tls_config) => agent_builder.tls_config(tls_config), - None => agent_builder, - }; + )?)); + let auth = match auth { Some(AuthParams::Basic(ref auth)) => Some(auth.to_header_string()), Some(AuthParams::Token(ref auth)) => Some(auth.to_header_string()?), @@ -2307,7 +2346,14 @@ impl SenderBuilder { } None => None, }; - let agent = agent_builder.build(); + let agent_builder = agent_builder + .timeout_connect(Some(*http_config.request_timeout.deref())) + .http_status_as_error(false); + let agent = ureq::Agent::with_parts( + agent_builder.build(), + connector, + ureq::unversioned::resolver::DefaultResolver::default(), + ); let proto = self.protocol.schema(); let url = format!( "{}://{}:{}/write", @@ -2426,14 +2472,26 @@ fn parse_public_key(pub_key_x: &str, pub_key_y: &str) -> Result<Vec<u8>> { fn parse_key_pair(auth: &EcdsaAuthParams) -> Result<EcdsaKeyPair> { let private_key = b64_decode("private authentication key", auth.priv_key.as_str())?; let public_key = parse_public_key(auth.pub_key_x.as_str(), auth.pub_key_y.as_str())?; - let system_random = SystemRandom::new(); - EcdsaKeyPair::from_private_key_and_public_key( + + #[cfg(feature = "aws-lc-crypto")] + let res = EcdsaKeyPair::from_private_key_and_public_key( &ECDSA_P256_SHA256_FIXED_SIGNING, &private_key[..], &public_key[..], - &system_random, - ) - .map_err(|key_rejected| { + ); + + #[cfg(feature = "ring-crypto")] + let res = { + let system_random = SystemRandom::new(); + EcdsaKeyPair::from_private_key_and_public_key( + &ECDSA_P256_SHA256_FIXED_SIGNING, + &private_key[..], + &public_key[..], + &system_random, + ) + }; + + res.map_err(|key_rejected| { error::fmt!( AuthError, "Misconfigured ILP authentication keys: {}. Hint: Check the keys for a possible typo.", @@ -2538,7 +2596,7 @@ impl Sender { )); } - let bytes = buf.as_str().as_bytes(); + let bytes = buf.as_bytes(); if bytes.is_empty() { return Ok(()); } @@ -2570,34 +2628,23 @@ impl Sender { } else { 0.0f64 }; - let timeout = *state.config.request_timeout + Duration::from_secs_f64(extra_time); - let request = state - .agent - .post(&state.url) - .query_pairs([("precision", "n")]) - .timeout(timeout) - .set("Content-Type", "text/plain; charset=utf-8"); - let request = match state.auth.as_ref() { - Some(auth) => request.set("Authorization", auth), - None => request, - }; - let response_or_err = - http_send_with_retries(request, bytes, *state.config.retry_timeout); - match response_or_err { - Ok(_response) => { - // on success, there's no information in the response. - } - Err(ureq::Error::Status(http_status_code, response)) => { - return Err(parse_http_error(http_status_code, response)); - } - Err(ureq::Error::Transport(transport)) => { - return Err(error::fmt!( - SocketError, - "Could not flush buffer: {}", - transport - )); + + return match http_send_with_retries( + state, + bytes, + *state.config.request_timeout + Duration::from_secs_f64(extra_time), + *state.config.retry_timeout, + ) { + Ok(res) => { + if res.status().is_client_error() || res.status().is_server_error() { + Err(parse_http_error(res.status().as_u16(), res)) + } else { + res.into_body(); + Ok(()) + } } - } + Err(err) => Err(Error::from_ureq_error(err, &state.url)), + }; } } Ok(()) diff --git a/questdb-rs/src/ingress/tests.rs b/questdb-rs/src/ingress/tests.rs index d5c1c1c6..8d83a7b0 100644 --- a/questdb-rs/src/ingress/tests.rs +++ b/questdb-rs/src/ingress/tests.rs @@ -20,7 +20,12 @@ fn https_simple() { assert_specified_eq(&builder.host, "localhost"); assert_specified_eq(&builder.port, Protocol::Https.default_port()); assert!(builder.protocol.tls_enabled()); + + #[cfg(feature = "tls-webpki-certs")] assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::WebpkiRoots); + + #[cfg(not(feature = "tls-webpki-certs"))] + assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::OsRoots); } #[test] @@ -39,7 +44,12 @@ fn tcps_simple() { assert_specified_eq(&builder.host, "localhost"); assert_specified_eq(&builder.port, Protocol::Tcps.default_port()); assert!(builder.protocol.tls_enabled()); + + #[cfg(feature = "tls-webpki-certs")] assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::WebpkiRoots); + + #[cfg(not(feature = "tls-webpki-certs"))] + assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::OsRoots); } #[test] @@ -126,6 +136,21 @@ fn incomplete_basic_auth() { ); } +#[cfg(feature = "ilp-over-http")] +#[test] +fn zero_timeout_forbidden() { + assert_conf_err( + SenderBuilder::from_conf("http::addr=localhost;username=user123;request_timeout=0;"), + "\"request_timeout\" must be greater than 0.", + ); + + assert_conf_err( + SenderBuilder::new(Protocol::Http, "localhost", 9000) + .request_timeout(Duration::from_millis(0)), + "\"request_timeout\" must be greater than 0.", + ); +} + #[cfg(feature = "ilp-over-http")] #[test] fn misspelled_basic_auth() { @@ -284,7 +309,12 @@ fn misspelled_tcp_ecdsa_auth() { fn tcps_tls_verify_on() { let builder = SenderBuilder::from_conf("tcps::addr=localhost;tls_verify=on;").unwrap(); assert!(builder.protocol.tls_enabled()); + + #[cfg(feature = "tls-webpki-certs")] assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::WebpkiRoots); + + #[cfg(not(feature = "tls-webpki-certs"))] + assert_defaulted_eq(&builder.tls_ca, CertificateAuthority::OsRoots); } #[cfg(feature = "insecure-skip-verify")] @@ -306,10 +336,21 @@ fn tcps_tls_verify_invalid() { #[test] fn tcps_tls_roots_webpki() { - let builder = SenderBuilder::from_conf("tcps::addr=localhost;tls_ca=webpki_roots;").unwrap(); - assert!(builder.protocol.tls_enabled()); - assert_specified_eq(&builder.tls_ca, CertificateAuthority::WebpkiRoots); - assert_defaulted_eq(&builder.tls_roots, None); + let builder = SenderBuilder::from_conf("tcps::addr=localhost;tls_ca=webpki_roots;"); + + #[cfg(feature = "tls-webpki-certs")] + { + let builder = builder.unwrap(); + assert!(builder.protocol.tls_enabled()); + assert_specified_eq(&builder.tls_ca, CertificateAuthority::WebpkiRoots); + assert_defaulted_eq(&builder.tls_roots, None); + } + + #[cfg(not(feature = "tls-webpki-certs"))] + assert_eq!( + "Config parameter \"tls_ca=webpki_roots\" requires the \"tls-webpki-certs\" feature", + builder.unwrap_err().msg() + ); } #[cfg(feature = "tls-native-certs")] @@ -398,6 +439,33 @@ fn http_retry_timeout() { assert_specified_eq(&http_config.retry_timeout, Duration::from_millis(100)); } +#[cfg(feature = "ilp-over-http")] +#[test] +fn connect_timeout_uses_request_timeout() { + use std::time::Instant; + let request_timeout = Duration::from_millis(10); + let builder = SenderBuilder::new(Protocol::Http, "127.0.0.2", "1111") + .request_timeout(request_timeout) + .unwrap() + .retry_timeout(Duration::from_millis(10)) + .unwrap() + .request_min_throughput(0) + .unwrap(); + let mut sender = builder.build().unwrap(); + let mut buf = Buffer::new(); + buf.table("x") + .unwrap() + .symbol("x", "x") + .unwrap() + .at_now() + .unwrap(); + let start = Instant::now(); + sender + .flush(&mut buf) + .expect_err("Request did not time out"); + assert!(Instant::now() - start < Duration::from_secs(10)); +} + #[test] fn auto_flush_off() { SenderBuilder::from_conf("tcps::addr=localhost;auto_flush=off;").unwrap(); diff --git a/questdb-rs/src/lib.rs b/questdb-rs/src/lib.rs index e72b88d2..dc18bd8c 100644 --- a/questdb-rs/src/lib.rs +++ b/questdb-rs/src/lib.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/questdb-rs/src/tests/f64_serializer.rs b/questdb-rs/src/tests/f64_serializer.rs index 3ba023e3..633d2bdd 100644 --- a/questdb-rs/src/tests/f64_serializer.rs +++ b/questdb-rs/src/tests/f64_serializer.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/questdb-rs/src/tests/http.rs b/questdb-rs/src/tests/http.rs index 7f40ca5c..90ec1222 100644 --- a/questdb-rs/src/tests/http.rs +++ b/questdb-rs/src/tests/http.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ fn test_two_lines() -> TestResult { req.header("user-agent"), Some(concat!("questdb/rust/", env!("CARGO_PKG_VERSION"))) ); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; @@ -97,7 +97,7 @@ fn test_text_plain_error() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -143,7 +143,7 @@ fn test_bad_json_error() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -191,7 +191,7 @@ fn test_json_error() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -236,9 +236,9 @@ fn test_no_connection() -> TestResult { assert!(res.is_err()); let err = res.unwrap_err(); assert_eq!(err.code(), ErrorCode::SocketError); - assert!(err.msg().starts_with( - "Could not flush buffer: http://127.0.0.1:1/write?precision=n: Connection Failed" - )); + assert!(err + .msg() + .starts_with("Could not flush buffer: http://127.0.0.1:1/write: io: Connection refused")); Ok(()) } @@ -261,7 +261,7 @@ fn test_old_server_without_ilp_http_support() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -316,7 +316,7 @@ fn test_http_basic_auth() -> TestResult { req.header("authorization"), Some("Basic QWxhZGRpbjpPcGVuU2VzYW1l") ); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; @@ -353,7 +353,7 @@ fn test_unauthenticated() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -402,7 +402,7 @@ fn test_token_auth() -> TestResult { assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); assert_eq!(req.header("authorization"), Some("Bearer 0123456789")); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; @@ -441,7 +441,7 @@ fn test_request_timeout() -> TestResult { assert!(res.is_err()); let err = res.unwrap_err(); assert_eq!(err.code(), ErrorCode::SocketError); - assert!(err.msg().contains("timed out reading response")); + assert!(err.msg().contains("per call")); assert!(time_elapsed >= request_timeout); Ok(()) } @@ -467,7 +467,7 @@ fn test_tls() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.method(), "POST"); assert_eq!(req.path(), "/write?precision=n"); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; @@ -502,7 +502,7 @@ fn test_user_agent() -> TestResult { let req = server.recv_http_q()?; assert_eq!(req.header("user-agent"), Some("wallabies/1.2.99")); - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; @@ -541,7 +541,7 @@ fn test_two_retries() -> TestResult { server.accept()?; let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -552,7 +552,7 @@ fn test_two_retries() -> TestResult { let start_time = std::time::Instant::now(); let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); let elapsed = std::time::Instant::now().duration_since(start_time); assert!(elapsed > Duration::from_millis(5)); @@ -565,7 +565,7 @@ fn test_two_retries() -> TestResult { let start_time = std::time::Instant::now(); let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); let elapsed = std::time::Instant::now().duration_since(start_time); assert!(elapsed > Duration::from_millis(15)); @@ -604,7 +604,7 @@ fn test_one_retry() -> TestResult { server.accept()?; let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -613,7 +613,7 @@ fn test_one_retry() -> TestResult { )?; let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer2.as_str()); + assert_eq!(req.body(), buffer2.as_bytes()); server.send_http_response_q( HttpResponse::empty() @@ -628,7 +628,7 @@ fn test_one_retry() -> TestResult { return Err(io::Error::new( ErrorKind::InvalidInput, "unexpected retry response", - )) + )); } Err(err) => err, }; @@ -681,7 +681,7 @@ fn test_transactional() -> TestResult { server.accept()?; let req = server.recv_http_q()?; - assert_eq!(req.body_str().unwrap(), buffer3.as_str()); + assert_eq!(req.body(), buffer3.as_bytes()); server.send_http_response_q(HttpResponse::empty())?; diff --git a/questdb-rs/src/tests/mock.rs b/questdb-rs/src/tests/mock.rs index 5d2dc38e..289b41cb 100644 --- a/questdb-rs/src/tests/mock.rs +++ b/questdb-rs/src/tests/mock.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,10 +104,6 @@ impl HttpRequest { pub fn body(&self) -> &[u8] { &self.body } - - pub fn body_str(&self) -> Result<&str, std::str::Utf8Error> { - std::str::from_utf8(self.body()) - } } #[cfg(feature = "ilp-over-http")] diff --git a/questdb-rs/src/tests/mod.rs b/questdb-rs/src/tests/mod.rs index 50266006..f817287e 100644 --- a/questdb-rs/src/tests/mod.rs +++ b/questdb-rs/src/tests/mod.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/questdb-rs/src/tests/sender.rs b/questdb-rs/src/tests/sender.rs index 4c2fa850..041e78b5 100644 --- a/questdb-rs/src/tests/sender.rs +++ b/questdb-rs/src/tests/sender.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,13 +71,14 @@ fn test_basics() -> TestResult { ts_nanos_num / 1000i64, ts_nanos_num ); - assert_eq!(buffer.as_str(), exp); + let exp_byte = exp.as_bytes(); + assert_eq!(buffer.as_bytes(), exp_byte); assert_eq!(buffer.len(), exp.len()); sender.flush(&mut buffer)?; assert_eq!(buffer.len(), 0); - assert_eq!(buffer.as_str(), ""); + assert_eq!(buffer.as_bytes(), b""); assert_eq!(server.recv_q()?, 1); - assert_eq!(server.msgs[0].as_str(), exp); + assert_eq!(server.msgs[0].as_bytes(), exp_byte); Ok(()) } @@ -172,11 +173,17 @@ fn test_auth_bad_base64_private_key() -> TestResult { #[test] fn test_auth_private_key_too_long() -> TestResult { + #[cfg(feature = "aws-lc-crypto")] + let expected = "Misconfigured ILP authentication keys: InvalidEncoding. Hint: Check the keys for a possible typo."; + + #[cfg(feature = "ring-crypto")] + let expected = "Misconfigured ILP authentication keys: InvalidComponent. Hint: Check the keys for a possible typo."; + test_bad_key( "ZkxLWUVhb0ViOWxybjNua3dMREEtTV94bnVGT2RTdDl5MFo3X3ZXU0hMVWZMS1lFYW9FYjlscm4zbmt3TERBLU1feG51Rk9kU3Q5eTBaN192V1NITFU", "fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU", // x "Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac", // y - "Misconfigured ILP authentication keys: InvalidComponent. Hint: Check the keys for a possible typo." + expected ) } @@ -229,7 +236,7 @@ fn test_bad_key( let server = MockServer::new()?; let lsb = server .lsb_tcp() - .username("testUser1")? + .username("admin")? .token(priv_key)? .token_x(pub_key_x)? .token_y(pub_key_y)?; @@ -286,8 +293,9 @@ fn test_timestamp_overloads() -> TestResult { let exp = concat!( "tbl_name a=12345t,b=-100000000t,c=12345t,d=-12345t,e=-1t,f=-10t 1000\n", "tbl_name a=1000000t 5000000000\n" - ); - assert_eq!(buffer.as_str(), exp); + ) + .as_bytes(); + assert_eq!(buffer.as_bytes(), exp); Ok(()) } @@ -304,8 +312,8 @@ fn test_chrono_timestamp() -> TestResult { let mut buffer = Buffer::new(); buffer.table(tbl_name)?.column_ts("a", ts)?.at(ts)?; - let exp = "tbl_name a=1000000t 1000000000\n"; - assert_eq!(buffer.as_str(), exp); + let exp = b"tbl_name a=1000000t 1000000000\n"; + assert_eq!(buffer.as_bytes(), exp); Ok(()) } @@ -368,12 +376,12 @@ fn test_tls_with_file_ca() -> TestResult { .at(TimestampNanos::new(10000000))?; assert_eq!(server.recv_q()?, 0); - let exp = "test,t1=v1 f1=0.5 10000000\n"; - assert_eq!(buffer.as_str(), exp); + let exp = b"test,t1=v1 f1=0.5 10000000\n"; + assert_eq!(buffer.as_bytes(), exp); assert_eq!(buffer.len(), exp.len()); sender.flush(&mut buffer)?; assert_eq!(server.recv_q()?, 1); - assert_eq!(server.msgs[0].as_str(), exp); + assert_eq!(server.msgs[0].as_bytes(), exp); Ok(()) } @@ -461,12 +469,12 @@ fn test_tls_insecure_skip_verify() -> TestResult { .at(TimestampNanos::new(10000000))?; assert_eq!(server.recv_q()?, 0); - let exp = "test,t1=v1 f1=0.5 10000000\n"; - assert_eq!(buffer.as_str(), exp); + let exp = b"test,t1=v1 f1=0.5 10000000\n"; + assert_eq!(buffer.as_bytes(), exp); assert_eq!(buffer.len(), exp.len()); sender.flush(&mut buffer)?; assert_eq!(server.recv_q()?, 1); - assert_eq!(server.msgs[0].as_str(), exp); + assert_eq!(server.msgs[0].as_bytes(), exp); Ok(()) } @@ -484,7 +492,6 @@ fn bad_uppercase_addr() { let res = Sender::from_conf("tcp::ADDR=localhost:9009;"); assert!(res.is_err()); let err = res.unwrap_err(); - eprint!("err: {:?}", err); assert!(err.code() == ErrorCode::ConfigError); assert!(err.msg() == "Missing \"addr\" parameter in config string"); } diff --git a/system_test/fixture.py b/system_test/fixture.py index 88e802dc..40dca629 100644 --- a/system_test/fixture.py +++ b/system_test/fixture.py @@ -6,7 +6,7 @@ ## \__\_\\__,_|\___||___/\__|____/|____/ ## ## Copyright (c) 2014-2019 Appsicle -## Copyright (c) 2019-2024 QuestDB +## Copyright (c) 2019-2025 QuestDB ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. @@ -43,13 +43,13 @@ from pprint import pformat -AUTH_TXT = """testUser1 ec-p-256-sha256 fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac +AUTH_TXT = """admin ec-p-256-sha256 fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac # [key/user id] [key type] {keyX keyY}""" # Valid keys as registered with the QuestDB fixture. AUTH = dict( - username="testUser1", + username="admin", token="5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48", token_x="fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU", token_y="Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac") @@ -314,11 +314,11 @@ def check_http_up(): if self._proc.poll() is not None: raise RuntimeError('QuestDB died during startup.') req = urllib.request.Request( - f'http://localhost:{self.http_server_port}', + f'http://localhost:{self.http_server_port}/ping', method='GET') try: resp = urllib.request.urlopen(req, timeout=1) - if resp.status == 200: + if resp.status == 204: return True except socket.timeout: pass diff --git a/system_test/questdb_line_sender.py b/system_test/questdb_line_sender.py index f7471258..5d4eca85 100644 --- a/system_test/questdb_line_sender.py +++ b/system_test/questdb_line_sender.py @@ -6,7 +6,7 @@ ## \__\_\\__,_|\___||___/\__|____/|____/ ## ## Copyright (c) 2014-2019 Appsicle -## Copyright (c) 2019-2024 QuestDB +## Copyright (c) 2019-2025 QuestDB ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. @@ -55,6 +55,7 @@ c_int, c_int64, c_double, + c_uint8, c_uint16, c_uint64, c_void_p, @@ -104,6 +105,10 @@ class c_line_sender_utf8(ctypes.Structure): class c_line_sender_table_name(ctypes.Structure): _fields_ = [("len", c_size_t), ("buf", c_char_p)] +class line_sender_buffer_view(ctypes.Structure): + _fields_ = [("len", c_size_t), + ("buf", ctypes.POINTER(c_uint8))] + c_line_sender_table_name_p = ctypes.POINTER(c_line_sender_table_name) class c_line_sender_column_name(ctypes.Structure): _fields_ = [("len", c_size_t), @@ -185,9 +190,8 @@ def set_sig(fn, restype, *argtypes): c_line_sender_buffer_p) set_sig( dll.line_sender_buffer_peek, - c_char_p, - c_line_sender_buffer_p, - c_size_t_p) + line_sender_buffer_view, + c_line_sender_buffer_p) set_sig( dll.line_sender_buffer_clear, None, @@ -417,8 +421,8 @@ def set_sig(fn, restype, *argtypes): _PY_DLL = ctypes.pythonapi _PY_DLL.PyUnicode_FromKindAndData.restype = ctypes.py_object _PY_DLL.PyUnicode_FromKindAndData.argtypes = [c_int, c_void_p, c_ssize_t] -_PY_DLL.PyUnicode_FromStringAndSize.restype = ctypes.py_object -_PY_DLL.PyUnicode_FromStringAndSize.argtypes = [c_char_p, c_ssize_t] +_PY_DLL.PyBytes_FromStringAndSize.restype = ctypes.py_object +_PY_DLL.PyBytes_FromStringAndSize.argtypes = [ctypes.c_char_p, ctypes.c_ssize_t] class SenderError(Exception): @@ -543,11 +547,10 @@ def peek(self) -> str: # https://docs.python.org/3/c-api/buffer.html # This way we would not need to `bytes(..)` the object to keep it alive. # Then we could call `PyMemoryView_FromObject`. - size = c_size_t(0) - buf = _DLL.line_sender_buffer_peek(self._impl, ctypes.byref(size)) - if size: - size = c_ssize_t(size.value) - return _PY_DLL.PyUnicode_FromStringAndSize(buf, size) + view = _DLL.line_sender_buffer_peek(self._impl) + if view.len: + c_buf = ctypes.cast(view.buf, c_char_p) # uint8_t* → char* + return _PY_DLL.PyBytes_FromStringAndSize(c_buf, view.len) else: return '' diff --git a/system_test/test.py b/system_test/test.py index 9d90bac6..c28ccd6d 100755 --- a/system_test/test.py +++ b/system_test/test.py @@ -8,7 +8,7 @@ ## \__\_\\__,_|\___||___/\__|____/|____/ ## ## Copyright (c) 2014-2019 Appsicle -## Copyright (c) 2019-2024 QuestDB +## Copyright (c) 2019-2025 QuestDB ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. @@ -142,7 +142,7 @@ def test_insert_three_rows(self): {'name': 'name_b', 'type': 'BOOLEAN'}, {'name': 'name_c', 'type': 'LONG'}, {'name': 'name_d', 'type': 'DOUBLE'}, - {'name': 'name_e', 'type': 'STRING'}, + {'name': 'name_e', 'type': 'VARCHAR'}, {'name': 'timestamp', 'type': 'TIMESTAMP'}] self.assertEqual(resp['columns'], exp_columns) @@ -240,8 +240,8 @@ def test_two_columns(self): resp = retry_check_table(table_name, log_ctx=pending) exp_columns = [ - {'name': 'a', 'type': 'STRING'}, - {'name': 'b', 'type': 'STRING'}, + {'name': 'a', 'type': 'VARCHAR'}, + {'name': 'b', 'type': 'VARCHAR'}, {'name': 'timestamp', 'type': 'TIMESTAMP'}] self.assertEqual(resp['columns'], exp_columns) @@ -255,7 +255,7 @@ def test_mismatched_types_across_rows(self): with self._mk_linesender() as sender: (sender .table(table_name) - .column('a', 'A') # STRING + .column('a', 1) # LONG .at_now()) (sender .table(table_name) @@ -271,8 +271,8 @@ def test_mismatched_types_across_rows(self): raise e self.assertIn('Could not flush buffer', str(e)) self.assertIn('cast error from', str(e)) - self.assertIn('STRING', str(e)) - self.assertIn('code: invalid, line: 2', str(e)) + self.assertIn('LONG', str(e)) + self.assertIn('error in line 2', str(e)) if QDB_FIXTURE.http: # If HTTP, the error should cause the whole batch to be ignored. @@ -283,11 +283,11 @@ def test_mismatched_types_across_rows(self): # We only ever get the first row back. resp = retry_check_table(table_name, log_ctx=pending) exp_columns = [ - {'name': 'a', 'type': 'STRING'}, + {'name': 'a', 'type': 'LONG'}, {'name': 'timestamp', 'type': 'TIMESTAMP'}] self.assertEqual(resp['columns'], exp_columns) - exp_dataset = [['A']] # Comparison excludes timestamp column. + exp_dataset = [[1]] # Comparison excludes timestamp column. scrubbed_dataset = [row[:-1] for row in resp['dataset']] self.assertEqual(scrubbed_dataset, exp_dataset) @@ -352,7 +352,7 @@ def test_timestamp_col(self): exp_dataset = [['1969-12-31T23:59:59.000000Z'], ['1970-01-01T00:00:01.000000Z']] scrubbed_dataset = [row[:-1] for row in resp['dataset']] self.assertEqual(scrubbed_dataset, exp_dataset) - + def test_underscores(self): table_name = f'_{uuid.uuid4().hex}_' @@ -507,22 +507,14 @@ def _test_example(self, bin_name, table_name, tls=False): # Check inserted data. resp = retry_check_table(table_name) exp_columns = [ - {'name': 'id', 'type': 'SYMBOL'}, - {'name': 'x', 'type': 'DOUBLE'}, - {'name': 'y', 'type': 'DOUBLE'}, - {'name': 'booked', 'type': 'BOOLEAN'}, - {'name': 'passengers', 'type': 'LONG'}, - {'name': 'driver', 'type': 'STRING'}, + {'name': 'symbol', 'type': 'SYMBOL'}, + {'name': 'side', 'type': 'SYMBOL'}, + {'name': 'price', 'type': 'DOUBLE'}, + {'name': 'amount', 'type': 'DOUBLE'}, {'name': 'timestamp', 'type': 'TIMESTAMP'}] self.assertEqual(resp['columns'], exp_columns) - exp_dataset = [[ - 'd6e5fe92-d19f-482a-a97a-c105f547f721', - 30.5, - -150.25, - True, - 3, - 'John Doe']] # Comparison excludes timestamp column. + exp_dataset = [['ETH-USD', 'sell', 2615.54, 0.00044]] # Comparison excludes timestamp column. scrubbed_dataset = [row[:-1] for row in resp['dataset']] self.assertEqual(scrubbed_dataset, exp_dataset) @@ -531,25 +523,25 @@ def test_c_example(self): suffix += '_http' if QDB_FIXTURE.http else '' self._test_example( f'line_sender_c_example{suffix}', - f'c_cars{suffix}') + f'c_trades{suffix}') def test_cpp_example(self): suffix = '_auth' if QDB_FIXTURE.auth else '' suffix += '_http' if QDB_FIXTURE.http else '' self._test_example( f'line_sender_cpp_example{suffix}', - f'cpp_cars{suffix}') + f'cpp_trades{suffix}') def test_c_tls_example(self): self._test_example( 'line_sender_c_example_tls_ca', - 'c_cars_tls_ca', + 'c_trades_tls_ca', tls=True) def test_cpp_tls_example(self): self._test_example( 'line_sender_cpp_example_tls_ca', - 'cpp_cars_tls_ca', + 'cpp_trades_tls_ca', tls=True) def test_opposite_auth(self): @@ -618,7 +610,7 @@ def test_malformed_auth1(self): with self.assertRaisesRegex( qls.SenderError, - r'Misconfigured ILP authentication keys: InconsistentComponents. Hint: Check the keys for a possible typo.'): + r'Misconfigured ILP authentication keys: .*. Hint: Check the keys for a possible typo.'): sender.connect() def test_malformed_auth2(self): @@ -726,8 +718,8 @@ def test_http_transactions(self): raise e self.assertIn('Could not flush buffer', str(e)) self.assertIn('cast error from', str(e)) - self.assertIn('STRING', str(e)) - self.assertIn('code: invalid, line: 3', str(e)) + self.assertIn('VARCHAR', str(e)) + self.assertIn('error in line 3', str(e)) with self.assertRaises(TimeoutError): retry_check_table(table_name, timeout_sec=1, log=False) diff --git a/system_test/tls_proxy/src/lib.rs b/system_test/tls_proxy/src/lib.rs index 19e3a3b8..8dee12c9 100644 --- a/system_test/tls_proxy/src/lib.rs +++ b/system_test/tls_proxy/src/lib.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/system_test/tls_proxy/src/main.rs b/system_test/tls_proxy/src/main.rs index 3836e3c0..02f85898 100644 --- a/system_test/tls_proxy/src/main.rs +++ b/system_test/tls_proxy/src/main.rs @@ -6,7 +6,7 @@ * \__\_\\__,_|\___||___/\__|____/|____/ * * Copyright (c) 2014-2019 Appsicle - * Copyright (c) 2019-2024 QuestDB + * Copyright (c) 2019-2025 QuestDB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.