diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md index e7aa64d..081e58c 100644 --- a/.github/instructions/tests.instructions.md +++ b/.github/instructions/tests.instructions.md @@ -29,10 +29,20 @@ TEST_CASE("descriptive name", "[tag]") { ``` ## Running Tests -- Build: `ninja -C build` +- Build: `cmake -B build -G Ninja -DBUILD_TESTS=ON -DPLEXDB_LOG_ENABLED=ON -DPLEXDB_DEBUG=ON && ninja -C build` - Run all: `./build/plexdb/plexdb_tests --skip-benchmarks` or `./build/objstore/objstore_tests --skip-benchmarks` - Run specific tags: `./build/objstore/objstore_tests --skip-benchmarks "[tagname]"` +## Debugging Test Failures +- Build with `-DPLEXDB_LOG_ENABLED=ON` so log messages appear in failing tests. +- All `plexdb::log` messages are routed to Catch2 `UNSCOPED_INFO` by the test log consumer (`test/log_consumer_helper.test.cpp`). When a test fails, all log messages emitted during that test case are printed alongside the failure. +- To add diagnostic logging in production code, create a `plexdb::log::Producer` and use `plexdb::log::message(producer, Level::Debug, str8)` where `str8` is a `String8`. This is zero-overhead when logging is disabled at build time. Registration is lazy — the producer registers itself on first use. +- For numeric metrics without string formatting overhead, create a `plexdb::log::Stat` with a `StatType` (`Counter` or `Gauge`) and use `plexdb::log::stat(s, value)`. The stat's name and type are registered automatically on first fire. Default type is `Gauge`. +- When a consumer registers, all known producers and stat metadata are replayed (catch-up). +- Custom log consumers can be registered via the plugin ABI (`plexdb_log_register_consumer` in `plexdb/log/log_abi.h`). Write a consumer to filter by producer ID, log level, or stat ID. +- The OTLP plugin (`objstore/plugins/log_otel/log_otel_plugin.cpp`) exports metrics via OpenTelemetry (OTLP/HTTP JSON). +- Parse errors are reported via `UNSCOPED_INFO` through `objstore/test/parsers_error_reporter.helper.cppm` and also through the log system (`objstore::log::cql_parse_error` at `Level::Error`). + ## Benchmarks - Catch2 supports benchmarks but use them sparingly - Always use `--skip-benchmarks` for normal test runs diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a304906..c0cffcc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -22,7 +22,8 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 nodejs npm + sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 nodejs npm \ + libgrpc++-dev libprotobuf-dev protobuf-compiler protobuf-compiler-grpc - name: Configure CMake run: cmake -B build -G Ninja -DBUILD_TESTS=ON -DPLEXDB_LOG_ENABLED=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 @@ -30,6 +31,13 @@ jobs: - name: Build run: ninja -C build + - name: Build OTLP plugin + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 + ninja -C build + working-directory: objstore/plugins/log_otel + - name: Install test_cql.js dependencies run: npm install working-directory: extra/node diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a7d8f6..a01ed85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,8 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 + sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 \ + libgrpc++-dev libprotobuf-dev protobuf-compiler protobuf-compiler-grpc - name: Configure run: | @@ -41,10 +42,20 @@ jobs: cp macros/macros.h pkg/plexdb/macros/ tar -czf plexdb-linux-x64.tar.gz -C pkg plexdb + - name: Build OTLP plugin + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 + ninja -C build + working-directory: objstore/plugins/log_otel + - name: Package objstore run: | cp build/objstore/objstore_server objstore-linux-x64 + - name: Package OTLP plugin + run: cp objstore/plugins/log_otel/build/libobjstore_log_otel.so . + - name: Upload objstore uses: actions/upload-artifact@v4 with: @@ -57,6 +68,12 @@ jobs: name: plexdb-linux-x64 path: plexdb-linux-x64.tar.gz + - name: Upload OTLP plugin + uses: actions/upload-artifact@v4 + with: + name: libobjstore_log_otel + path: libobjstore_log_otel.so + release: needs: build runs-on: ubuntu-latest @@ -74,9 +91,15 @@ jobs: with: name: plexdb-linux-x64 + - name: Download OTLP plugin + uses: actions/download-artifact@v4 + with: + name: libobjstore_log_otel + - name: Create release uses: softprops/action-gh-release@v2 with: files: | objstore-linux-x64 plexdb-linux-x64.tar.gz + libobjstore_log_otel.so diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a28824..2541a90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,14 +16,29 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 + sudo apt-get install -y ninja-build liburing-dev clang-19 clang-tools-19 \ + libgrpc++-dev libprotobuf-dev protobuf-compiler protobuf-compiler-grpc - name: Configure - run: cmake -B build -G Ninja -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 + run: cmake -B build -G Ninja -DBUILD_TESTS=ON -DPLEXDB_LOG_ENABLED=ON -DPLEXDB_DEBUG=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 - name: Build run: ninja -C build + - name: Build OTLP plugin + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 + ninja -C build + working-directory: objstore/plugins/log_otel + + - name: Build file plugin + run: | + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 + ninja -C build + working-directory: objstore/plugins/log_file + - name: Run plexdb tests run: ./build/plexdb/plexdb_tests --skip-benchmarks diff --git a/.gitignore b/.gitignore index 3122a80..ca3e1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ out/ _codeql_detected_source_root *.db +*.stats objstore.pid **/.venv -**/node_modules \ No newline at end of file +**/node_modules +**/__pycache__ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 0b1faf4..d17a769 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,4 +14,16 @@ ## Testing - Tests use [Catch2](https://github.com/catchorg/Catch2/blob/devel/docs/). Create tests for your changes. -- Test executables: `build/plexdb/plexdb_tests`, `build/objstore/objstore_tests`. Use `--skip-benchmarks` for normal runs. \ No newline at end of file +- Test executables: `build/plexdb/plexdb_tests`, `build/objstore/objstore_tests`. Use `--skip-benchmarks` for normal runs. + +## Logging & debugging test failures +- Build with `-DPLEXDB_LOG_ENABLED=ON` to enable the structured logging system. +- All log messages are routed to Catch2's `UNSCOPED_INFO` via the test log consumer (`test/log_consumer_helper.test.cpp`). They appear automatically in the output of any failing test case. +- The log system uses levels: `Trace`, `Debug`, `Info`, `Warn`, `Error` (see `plexdb::log::Level`). +- Registration is lazy: `Producer` and `Stat` objects register themselves on first use, so construction order does not matter. +- To add diagnostic logging, create a `plexdb::log::Producer` and call `plexdb::log::message(producer, Level::Debug, str8)` where `str8` is a `String8`. +- For structured numeric metrics, create a `plexdb::log::Stat` with a `StatType` (`Counter` or `Gauge`) and call `plexdb::log::stat(s, value)` — no string formatting overhead. The stat's name and type are registered automatically on first fire. +- Stat types: `StatType::Counter` for monotonically increasing cumulative values, `StatType::Gauge` for point-in-time measurements. Default is `Gauge`. +- When a consumer registers, all known producers and stat metadata (including stat types) are replayed (catch-up), so the consumer always has a complete view. +- The plugin system (`plexdb/log/log_abi.h`) allows custom consumers. Register via `plexdb_log_register_consumer` to filter or redirect logs. See `objstore/plugins/log_file/log_file_plugin.cpp` for a reference plugin. +- The OTLP plugin (`objstore/plugins/log_otel/log_otel_plugin.cpp`) exports metrics via OpenTelemetry (OTLP/gRPC). Built as a standalone project in `objstore/plugins/log_otel/`; load via `LD_PRELOAD`. Uses plaintext gRPC (`use_ssl_credentials = false`). \ No newline at end of file diff --git a/README.md b/README.md index 2d5d175..f53f329 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Pre-built binaries and the plexdb static library are attached to each [GitHub re |---|---| | `objstore` | Server executable (Linux x86-64) | | `plexdb-linux-x64.tar.gz` | Static library + C++20 module sources | +| `libobjstore_log_otel.so` | OTLP/gRPC metrics plugin (load via `LD_PRELOAD`) | ## Usage @@ -32,3 +33,29 @@ cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_C_COMPILER=clang-19 ninja -C build ``` + +## Plugins + +### OTLP metrics plugin + +Exports structured log stats to an OpenTelemetry collector via OTLP/gRPC. Requires gRPC and protobuf system libraries. + +```sh +cd objstore/plugins/log_otel +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +ninja -C build +``` + +Load at server startup: + +```sh +LD_PRELOAD=objstore/plugins/log_otel/build/libobjstore_log_otel.so \ + PLEXDB_OTLP_ENDPOINT=localhost:4317 \ + ./build/objstore/objstore_server +``` + +| Env var | Default | Description | +|---|---|---| +| `PLEXDB_OTLP_ENDPOINT` | `localhost:4317` | OTLP/gRPC collector endpoint | +| `PLEXDB_OTLP_INTERVAL_MS` | `10000` | Export interval in milliseconds | +| `PLEXDB_OTLP_SERVICE` | `plexdb` | Service name reported to collector | diff --git a/TODO.md b/TODO.md index f6023bb..e058c1c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ # TODO -- Parse CQL in objstore with lexy - - Parse cache with string hash as key +- Parse cache with string hash as key - Query planning - Very simple for basic CQL commands - Again cache planning result if complex diff --git a/extra/grafana/dashboards/plexdb.json b/extra/grafana/dashboards/plexdb.json new file mode 100644 index 0000000..5c0b2af --- /dev/null +++ b/extra/grafana/dashboards/plexdb.json @@ -0,0 +1,214 @@ +{ + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "title": "Active Connections", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto" + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "targets": [ + { + "expr": "db_client_connection_count", + "legendFormat": "active connections", + "refId": "A" + } + ] + }, + { + "title": "Max Connections", + "type": "stat", + "gridPos": { "h": 8, "w": 6, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 80 }, + { "color": "red", "value": 100 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "reduceOptions": { "calcs": ["lastNotNull"] } + }, + "targets": [ + { + "expr": "db_client_connection_max", + "legendFormat": "max", + "refId": "A" + } + ] + }, + { + "title": "Connection Utilization", + "type": "gauge", + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 60 }, + { "color": "red", "value": 90 } + ] + }, + "min": 0, + "max": 100, + "unit": "percent" + }, + "overrides": [] + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] } + }, + "targets": [ + { + "expr": "db_client_connection_count / db_client_connection_max * 100", + "legendFormat": "utilization", + "refId": "A" + } + ] + }, + { + "title": "Operation Duration", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto" + }, + "unit": "\u00b5s" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "targets": [ + { + "expr": "db_client_operation_duration", + "legendFormat": "duration (\u00b5s)", + "refId": "A" + } + ] + }, + { + "title": "Returned Rows", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 10, + "lineWidth": 2, + "pointSize": 5, + "showPoints": "auto" + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "targets": [ + { + "expr": "db_client_response_returned_rows", + "legendFormat": "returned rows", + "refId": "A" + } + ] + }, + { + "title": "All Metrics (auto-discovery)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "fillOpacity": 10, + "lineWidth": 2 + } + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "expr": "{job=\"otel-collector\", __name__=~\"db_.*\"}", + "legendFormat": "{{__name__}}", + "refId": "A" + } + ] + } + ], + "schemaVersion": 39, + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "type": "datasource" + } + ] + }, + "time": { "from": "now-15m", "to": "now" }, + "timepicker": {}, + "timezone": "browser", + "title": "plexdb", + "uid": "plexdb-overview", + "version": 1, + "refresh": "5s" +} diff --git a/extra/grafana/provisioning/dashboards/dashboard.yml b/extra/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..ded14e6 --- /dev/null +++ b/extra/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: plexdb + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false diff --git a/extra/grafana/provisioning/datasources/datasource.yml b/extra/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..1a57b69 --- /dev/null +++ b/extra/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/extra/otel-config.yaml b/extra/otel-config.yaml new file mode 100644 index 0000000..4e70d91 --- /dev/null +++ b/extra/otel-config.yaml @@ -0,0 +1,22 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +exporters: + prometheus: + endpoint: "0.0.0.0:9464" + debug: + verbosity: basic + +service: + pipelines: + metrics: + receivers: [otlp] + exporters: [prometheus] + logs: + receivers: [otlp] + exporters: [debug] \ No newline at end of file diff --git a/extra/otlp-docker-compose.yml b/extra/otlp-docker-compose.yml new file mode 100644 index 0000000..ee6edc2 --- /dev/null +++ b/extra/otlp-docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.9" + +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + container_name: otel-collector + command: ["--config=/etc/otel/config.yaml"] + volumes: + - ./otel-config.yaml:/etc/otel/config.yaml + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "9464:9464" # Prometheus metrics export + depends_on: + - prometheus + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + grafana: + image: grafana/grafana-oss:latest + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana-data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/var/lib/grafana/dashboards + +volumes: + grafana-data: \ No newline at end of file diff --git a/extra/prometheus.yml b/extra/prometheus.yml new file mode 100644 index 0000000..63d326b --- /dev/null +++ b/extra/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: "otel-collector" + static_configs: + - targets: ["otel-collector:9464"] \ No newline at end of file diff --git a/objstore/CMakeLists.txt b/objstore/CMakeLists.txt index f2815f1..f258322 100644 --- a/objstore/CMakeLists.txt +++ b/objstore/CMakeLists.txt @@ -117,24 +117,6 @@ target_link_libraries(objstore foonathan::lexy::ext ) -# ============================================================================= -# Plugins -# ============================================================================= -if(PLEXDB_LOG_ENABLED) - add_library(objstore_log_file SHARED) - target_compile_features(objstore_log_file PRIVATE cxx_std_23) - - target_sources(objstore_log_file - PRIVATE - plugins/log_file/log_file_plugin.cpp - ) - - target_include_directories(objstore_log_file - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../plexdb/log - ) -endif() - # ============================================================================= # Executables # ============================================================================= @@ -170,7 +152,7 @@ if(BUILD_TESTS) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.11.0 + GIT_TAG v3.13.0 ) FetchContent_MakeAvailable(Catch2) endif() @@ -181,7 +163,7 @@ if(BUILD_TESTS) "${CMAKE_CURRENT_SOURCE_DIR}/**/*.test.cpp" ) file(GLOB_RECURSE OBJSTORE_TEST_MODULE_SOURCES CONFIGURE_DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/**/*.helper.cppm" + "${CMAKE_CURRENT_SOURCE_DIR}/**/*.test.cppm" ) target_sources(objstore_tests PRIVATE @@ -195,6 +177,7 @@ if(BUILD_TESTS) target_include_directories(objstore_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../macros + ${CMAKE_CURRENT_SOURCE_DIR}/../plexdb/log ) target_link_libraries(objstore_tests diff --git a/objstore/entry/main.cpp b/objstore/entry/main.cpp index 53c826b..8f2720e 100644 --- a/objstore/entry/main.cpp +++ b/objstore/entry/main.cpp @@ -13,7 +13,7 @@ using namespace objstore; using namespace plexdb; void assert_handler(const char* msg, const char* file_name, const char* function_name, unsigned line_number) { - println("assert failed with message \"", msg, "\""); + println("Assert failed \"", msg, "\" at ", function_name, " in ", file_name, ":", to_str(line_number)); #if PLEXDB_DEBUG PLEXDB_TRAP; diff --git a/objstore/log/log.cpp b/objstore/log/log.cpp index 37a4362..b3d0b7b 100644 --- a/objstore/log/log.cpp +++ b/objstore/log/log.cpp @@ -4,21 +4,36 @@ import plexdb.base; import plexdb.log; namespace objstore::log { - plexdb::log::Producer producer{"objstore::parser::cql"}; - - void cql_parse_query(plexdb::String8 query) { - plexdb::log::fire_message( - producer.id, - reinterpret_cast(query.data), - query.length - ); + plexdb::log::Producer objstore_parser_cql_producer{"objstore::parser::cql"}; + + void cql_parse_error(const plexdb::String8& error) { + plexdb::log::message(objstore_parser_cql_producer, plexdb::log::Level::Error, error); + } + + plexdb::log::Producer otlp_db_producer{"otlp.db"}; + + void db_query_text(const plexdb::String8& query) { + plexdb::log::message(otlp_db_producer, plexdb::log::Level::Debug, query, "query.text"); + } + + plexdb::log::Stat stat_connection_count{&otlp_db_producer, "client.connection.count", plexdb::log::StatType::Gauge}; + plexdb::log::Stat stat_connection_max{&otlp_db_producer, "client.connection.max", plexdb::log::StatType::Gauge}; + plexdb::log::Stat stat_operation_duration{&otlp_db_producer, "client.operation.duration", plexdb::log::StatType::Gauge}; + plexdb::log::Stat stat_returned_rows{&otlp_db_producer, "client.response.returned_rows", plexdb::log::StatType::Gauge}; + + void db_connection_count(plexdb::S64 active_connections) { + plexdb::log::stat(stat_connection_count, active_connections); + } + + void db_connection_max(plexdb::S64 max_connections) { + plexdb::log::stat(stat_connection_max, max_connections); + } + + void db_operation_duration(plexdb::S64 microseconds) { + plexdb::log::stat(stat_operation_duration, microseconds); } - void cql_parse_error(const char* text, plexdb::U64 len) { - plexdb::log::fire_message( - producer.id, - text, - len - ); + void db_response_returned_rows(plexdb::S64 row_count) { + plexdb::log::stat(stat_returned_rows, row_count); } } diff --git a/objstore/log/log.cppm b/objstore/log/log.cppm index 8d12894..a05bf40 100644 --- a/objstore/log/log.cppm +++ b/objstore/log/log.cppm @@ -4,6 +4,12 @@ import plexdb.base; import plexdb.log; export namespace objstore::log { - void cql_parse_query(plexdb::String8 query); - void cql_parse_error(const char* text, plexdb::U64 len); + void db_query_text(const plexdb::String8& query); + void cql_parse_error(const plexdb::String8& error); + + // OTel database metrics (https://opentelemetry.io/docs/specs/semconv/db/database-metrics/) + void db_connection_count(plexdb::S64 active_connections); + void db_connection_max(plexdb::S64 max_connections); + void db_operation_duration(plexdb::S64 microseconds); + void db_response_returned_rows(plexdb::S64 row_count); } diff --git a/objstore/native/native.cppm b/objstore/native/native.cppm index f489139..d534caf 100644 --- a/objstore/native/native.cppm +++ b/objstore/native/native.cppm @@ -13,6 +13,7 @@ import objstore.tcp; import objstore.parsers; import objstore.engine; import objstore.engine.statements; +import objstore.log; using namespace plexdb; using namespace objstore; @@ -479,6 +480,7 @@ namespace objstore::native { } break; case opcode::QUERY: { + S64 t0 = os::monotonic_us(); const U8* p = body; String8 query = read_cql_long_string(p, body_end); // Remaining bytes are query parameters (consistency, flags, etc.) - ignored @@ -487,6 +489,7 @@ namespace objstore::native { if (!cql_opt) { auto frame = make_native_frame(conn, &chunk, write, opcode::ERROR, stream); append_error_body(frame, engine::ExecutionStatus::SyntaxError, "Failed to parse CQL"); + objstore::log::db_operation_duration(os::monotonic_us() - t0); break; } @@ -496,6 +499,7 @@ namespace objstore::native { auto frame = make_native_frame(conn, &chunk, write, opcode::ERROR, stream); String8 msg = result.message.length ? result.message : engine::to_str(result.status); append_error_body(frame, result.status, msg); + objstore::log::db_operation_duration(os::monotonic_us() - t0); break; } @@ -521,13 +525,16 @@ namespace objstore::native { assert_true(tbl != nullptr, "table not found for rows result"); auto frame = make_native_frame(conn, &chunk, write, opcode::RESULT, stream); append_result_rows(frame, result, tbl); + objstore::log::db_response_returned_rows(btree::size(tbl->btree)); }break; case engine::ResultKind::VirtualRows:{ assert_true(result.virtual_rows.has_value(), "virtual rows missing"); auto frame = make_native_frame(conn, &chunk, write, opcode::RESULT, stream); append_result_virtual_rows(frame, *result.virtual_rows); + objstore::log::db_response_returned_rows(result.virtual_rows->rows.length); }break; } + objstore::log::db_operation_duration(os::monotonic_us() - t0); } break; case opcode::REGISTER: { diff --git a/objstore/parsers/parsers.cpp b/objstore/parsers/parsers.cpp index 96ef080..fea59ce 100644 --- a/objstore/parsers/parsers.cpp +++ b/objstore/parsers/parsers.cpp @@ -54,7 +54,7 @@ namespace { error.message()); } - fn(msg.c_str, msg.length); + fn(msg); ++_count; } @@ -69,8 +69,6 @@ namespace { return _sink{0, fn}; } }; - - using LogErrorCallback = ErrorCallback; } namespace objstore::parsers::cql { @@ -1645,23 +1643,8 @@ namespace objstore::parsers::cql { }; } - Optional parse(String8 bytes, bool report_errors) { - log::cql_parse_query(bytes); - - auto input = lexy::string_input(bytes.data, bytes.length); - - auto try_parse = [&](auto callback) -> Optional { - auto result = lexy::parse(input, callback); - if (result.has_value()) return result.value(); - return {}; - }; - - if (report_errors) return try_parse(LogErrorCallback{log::cql_parse_error}); - return try_parse(lexy::noop); - } - - Optional parse(String8 bytes, void(*error_fn)(const char*, size_t)) { - log::cql_parse_query(bytes); + Optional parse(String8 bytes, void(*error_fn)(const String8& error)) { + log::db_query_text(bytes); auto input = lexy::string_input(bytes.data, bytes.length); @@ -1671,6 +1654,6 @@ namespace objstore::parsers::cql { return {}; }; - return try_parse(ErrorCallback{error_fn}); + return try_parse(ErrorCallback{error_fn}); } } diff --git a/objstore/parsers/parsers.cppm b/objstore/parsers/parsers.cppm index 93b9c09..b195fda 100644 --- a/objstore/parsers/parsers.cppm +++ b/objstore/parsers/parsers.cppm @@ -5,6 +5,7 @@ import plexdb.log; import plexdb.tagged_union; import objstore.engine.dtype; import objstore.engine.statements; +import objstore.log; using namespace plexdb; @@ -13,7 +14,6 @@ namespace objstore::parsers { // cassandra query language (CQL) // ======================================================================== namespace cql { - export Optional parse(String8 bytes, bool report_errors=log::enabled); - export Optional parse(String8 bytes, void(*error_fn)(const char*, size_t)); + export Optional parse(String8 bytes, void(*error_fn)(const String8& error) = &log::cql_parse_error); } } diff --git a/objstore/parsers/parsers.test.cpp b/objstore/parsers/parsers.test.cpp index ecaea0f..1618563 100644 --- a/objstore/parsers/parsers.test.cpp +++ b/objstore/parsers/parsers.test.cpp @@ -8,7 +8,6 @@ import plexdb.os.dynamic_tagged_union; import objstore.parsers; import objstore.engine.statements; import objstore.engine.dtype; -import objstore.test.parsers_error_reporter; using namespace plexdb; using namespace objstore; @@ -52,13 +51,13 @@ TEST_CASE("CQL DROP KEYSPACE", "[objstore.cql]") { TEST_CASE("CQL CREATE KEYSPACE statements", "[objstore.parser]") { SECTION("Basic CREATE KEYSPACE") { auto query = "CREATE KEYSPACE my_keyspace WITH replication = 'SimpleStrategy';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); } } TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("simple table with inline primary key") { - auto result = cql::parse("CREATE TABLE ks.tbl (id int PRIMARY KEY, name text, age int);", catch2_cql_parse_error); + auto result = cql::parse("CREATE TABLE ks.tbl (id int PRIMARY KEY, name text, age int);"); REQUIRE(result.has_value()); auto& stmt = get(result->value); @@ -68,7 +67,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE IF NOT EXISTS") { auto query = "CREATE KEYSPACE IF NOT EXISTS test_ks WITH replication = 'NetworkTopologyStrategy';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -85,7 +84,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE with multiple options") { auto query = "CREATE KEYSPACE prod WITH replication = 'SimpleStrategy' AND durable_writes = 'true';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -105,7 +104,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE with three options") { auto query = "CREATE KEYSPACE multi_opt WITH replication = 'NetworkTopologyStrategy' AND durable_writes = 'true' AND strategy_class = 'SimpleStrategy';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -119,7 +118,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE case insensitive") { auto query = "create keyspace TestKS with replication = 'test';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -130,7 +129,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE with underscore in name") { auto query = "CREATE KEYSPACE my_test_keyspace WITH replication = 'SimpleStrategy';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ks = get(result->value); @@ -139,7 +138,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE with mixed case IF NOT EXISTS") { auto query = "CREATE KEYSPACE If Not Exists mixed_case WITH replication = 'test';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ks = get(result->value); @@ -150,7 +149,7 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { SECTION("CREATE KEYSPACE with quoted option value") { // @note CQL uses '' to escape single quotes inside strings, not backslash auto query = "CREATE KEYSPACE ks WITH replication = '{''class'': ''SimpleStrategy''}';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ks = get(result->value); @@ -161,11 +160,11 @@ TEST_CASE("CQL CREATE TABLE", "[objstore.cql]") { TEST_CASE("CQL CREATE TABLE statements", "[objstore.parser]") { SECTION("Basic CREATE TABLE with single column") { auto query = "CREATE TABLE ks.users (id int PRIMARY KEY);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); } SECTION("if not exists") { - auto result = cql::parse("CREATE TABLE IF NOT EXISTS tbl (id int PRIMARY KEY);", catch2_cql_parse_error); + auto result = cql::parse("CREATE TABLE IF NOT EXISTS tbl (id int PRIMARY KEY);"); REQUIRE(result.has_value()); auto& stmt = get(result->value); REQUIRE(stmt.if_not_exists == true); @@ -189,7 +188,7 @@ TEST_CASE("CQL TRUNCATE", "[objstore.cql]") { TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("with column names and values") { - auto result = cql::parse("INSERT INTO tbl (id, name) VALUES (1, 'hello');", catch2_cql_parse_error); + auto result = cql::parse("INSERT INTO tbl (id, name) VALUES (1, 'hello');"); REQUIRE(result.has_value()); auto& stmt = get(result->value); REQUIRE(stmt.table.table_name == "tbl"); @@ -200,7 +199,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with multiple columns") { auto query = "CREATE TABLE ks.users (id int PRIMARY KEY, name text, age int);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -224,7 +223,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE IF NOT EXISTS") { auto query = "CREATE TABLE IF NOT EXISTS ks.products (sku int PRIMARY KEY, name text, price int);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -237,7 +236,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with various data types") { auto query = "CREATE TABLE ks.data (id int PRIMARY KEY, name text, count bigint, created timestamp, active boolean);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -251,7 +250,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with FLOAT and DOUBLE types") { auto query = "CREATE TABLE prod.metrics (id int PRIMARY KEY, temperature float, precision_value double);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -262,7 +261,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with UUID type") { auto query = "CREATE TABLE ks.sessions (session_id uuid PRIMARY KEY, user_id int);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -273,7 +272,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE case insensitive") { auto query = "create table ks.TestTable (Id INT primary key, Name TEXT);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -284,7 +283,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with non-primary key as last column") { auto query = "CREATE TABLE ks.test (id int PRIMARY KEY, data text);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -294,7 +293,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with primary key in middle") { auto query = "CREATE TABLE ks.test (name text, id int PRIMARY KEY, email text);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -306,7 +305,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with many columns") { auto query = "CREATE TABLE ks.large (c1 int PRIMARY KEY, c2 text, c3 bigint, c4 timestamp, c5 boolean, c6 float, c7 double);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -315,7 +314,7 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { SECTION("CREATE TABLE with underscore column names") { auto query = "CREATE TABLE ks.test (user_id int PRIMARY KEY, first_name text, last_name text);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& tbl = get(result->value); @@ -328,11 +327,11 @@ TEST_CASE("CQL INSERT INTO", "[objstore.cql]") { TEST_CASE("CQL INSERT INTO statements", "[objstore.parser]") { SECTION("INSERT INTO with integer values") { auto query = "INSERT INTO ks.users VALUES (1, 2, 3);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); } SECTION("if not exists") { - auto result = cql::parse("INSERT INTO tbl (id) VALUES (1) IF NOT EXISTS;", catch2_cql_parse_error); + auto result = cql::parse("INSERT INTO tbl (id) VALUES (1) IF NOT EXISTS;"); REQUIRE(result.has_value()); auto& stmt = get(result->value); REQUIRE(stmt.if_not_exists == true); @@ -341,7 +340,7 @@ TEST_CASE("CQL INSERT INTO statements", "[objstore.parser]") { TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("select star") { - auto result = cql::parse("SELECT * FROM tbl;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM tbl;"); REQUIRE(result.has_value()); auto& stmt = get(result->value); REQUIRE(stmt.from.table_name == "tbl"); @@ -361,11 +360,11 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with mixed values") { auto query = "INSERT INTO app.users VALUES (123, 'John Doe', 'john@example.com');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); } SECTION("select with where") { - auto result = cql::parse("SELECT * FROM tbl WHERE id = 1;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM tbl WHERE id = 1;"); REQUIRE(result.has_value()); auto& stmt = get(result->value); } @@ -385,7 +384,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with negative integers") { // @note negation is represented as UnaryMinusArithmeticOperation, not a folded Constant auto query = "INSERT INTO ks.data VALUES (-100, -50, -1);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -403,7 +402,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with large integer") { auto query = "INSERT INTO ks.data VALUES (9223372036854775807);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -413,7 +412,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO case insensitive") { auto query = "insert into ks.tbl values (1, 'test');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -421,7 +420,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with empty string") { auto query = "INSERT INTO ks.tbl VALUES ('');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -431,7 +430,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with string containing spaces") { auto query = "INSERT INTO ks.tbl VALUES ('hello world', 'foo bar baz');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -443,7 +442,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with escaped quotes") { // @note CQL uses '' to escape single quotes inside strings, not backslash auto query = "INSERT INTO ks.tbl VALUES ('''quoted''');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -453,7 +452,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with zero value") { auto query = "INSERT INTO ks.tbl VALUES (0);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -462,7 +461,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { SECTION("INSERT INTO with multiple string values") { auto query = "INSERT INTO app.messages VALUES ('msg1', 'msg2', 'msg3', 'msg4', 'msg5');"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& ins = get(result->value); @@ -476,7 +475,7 @@ TEST_CASE("CQL SELECT", "[objstore.cql]") { TEST_CASE("CQL SELECT FROM statements", "[objstore.parser]") { SECTION("Basic SELECT FROM") { auto query = "SELECT * FROM ks.users;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); @@ -500,7 +499,7 @@ TEST_CASE("CQL SELECT FROM statements", "[objstore.parser]") { SECTION("SELECT FROM with underscores") { auto query = "SELECT * FROM my_table;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& sel = get(result->value)); @@ -518,7 +517,7 @@ TEST_CASE("CQL SELECT FROM statements", "[objstore.parser]") { SECTION("SELECT FROM with extra whitespace") { auto query = "SELECT * FROM ks.users ;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE(result.has_value()); const auto& sel = get(result->value); @@ -540,13 +539,13 @@ TEST_CASE("CQL SELECT FROM statements", "[objstore.parser]") { TEST_CASE("CQL Invalid syntax handling", "[objstore.parser]") { SECTION("Invalid keyword") { auto query = "INVALID STATEMENT;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing table name in CREATE TABLE") { auto query = "CREATE TABLE;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } @@ -556,66 +555,66 @@ TEST_CASE("CQL Invalid syntax handling", "[objstore.parser]") { SECTION("Missing parentheses in CREATE TABLE") { auto query = "CREATE TABLE ks.users id int PRIMARY KEY;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing WITH in CREATE KEYSPACE") { auto query = "CREATE KEYSPACE ks replication = 'test';"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Empty query") { auto query = ""; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Only whitespace") { auto query = " \n\t "; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Unclosed string in INSERT") { auto query = "INSERT INTO ks.tbl VALUES ('unclosed);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing comma between columns") { auto query = "CREATE TABLE ks.test (id int PRIMARY KEY name text);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing comma between values") { auto query = "INSERT INTO ks.tbl VALUES (1 2 3);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Invalid data type") { auto query = "CREATE TABLE ks.test (id invalidtype PRIMARY KEY);"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing closing parenthesis in INSERT") { auto query = "INSERT INTO ks.tbl VALUES (1, 2, 3;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("Missing closing parenthesis in CREATE TABLE") { auto query = "CREATE TABLE ks.test (id int PRIMARY KEY;"; - auto result = cql::parse(query, catch2_cql_parse_error); + auto result = cql::parse(query); REQUIRE_FALSE(result.has_value()); } SECTION("select json") { - auto result = cql::parse("SELECT JSON * FROM tbl;", catch2_cql_parse_error); + auto result = cql::parse("SELECT JSON * FROM tbl;"); REQUIRE(result.has_value()); auto& stmt = get(result->value)); const auto& stmt = get(result->value)); const auto& stmt = get(result->value)); const auto& stmt = get(result->value)); const auto& stmt = get(result->value)); const auto& stmt = get(result->value)); const auto& stmt = get(result->value); REQUIRE(stmt.order_by->columns.length == 1); @@ -848,7 +847,7 @@ TEST_CASE("Parse SELECT with ORDER BY", "[objstore.parser]") { } SECTION("ORDER BY default ascending") { - auto result = cql::parse("SELECT * FROM ks.users ORDER BY name;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM ks.users ORDER BY name;"); REQUIRE(result.has_value()); const auto& stmt = get(result->value); // @todo @@ -866,7 +865,7 @@ TEST_CASE("Parse SELECT with ORDER BY", "[objstore.parser]") { TEST_CASE("Parse SELECT with ALLOW FILTERING", "[objstore.parser]") { SECTION("Basic ALLOW FILTERING") { - auto result = cql::parse("SELECT * FROM ks.users WHERE age = 25 ALLOW FILTERING;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM ks.users WHERE age = 25 ALLOW FILTERING;"); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value); @@ -874,7 +873,7 @@ TEST_CASE("Parse SELECT with ALLOW FILTERING", "[objstore.parser]") { } SECTION("ALLOW FILTERING with ORDER BY and LIMIT") { - auto result = cql::parse("SELECT * FROM ks.users WHERE age = 25 ORDER BY name LIMIT 100 ALLOW FILTERING;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM ks.users WHERE age = 25 ORDER BY name LIMIT 100 ALLOW FILTERING;"); REQUIRE(result.has_value()); const auto& stmt = get(result->value)); const auto& stmt = get(result->value); // @todo } SECTION("GROUP BY with WHERE and ORDER BY") { - auto result = cql::parse("SELECT * FROM ks.events WHERE user_id = 1 GROUP BY event_type ORDER BY created_at DESC;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM ks.events WHERE user_id = 1 GROUP BY event_type ORDER BY created_at DESC;"); REQUIRE(result.has_value()); const auto& stmt = get(result->value); // @todo @@ -916,7 +915,7 @@ TEST_CASE("Parse SELECT with GROUP BY", "[objstore.parser]") { TEST_CASE("Parse CREATE KEYSPACE with map literal replication", "[objstore.parser]") { SECTION("Simple map replication") { - auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy'};", catch2_cql_parse_error); + auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy'};"); REQUIRE(result.has_value()); REQUIRE(type_matches_tag(result->value)); const auto& ks = get(result->value); @@ -928,7 +927,7 @@ TEST_CASE("Parse CREATE KEYSPACE with map literal replication", "[objstore.parse } SECTION("Map with multiple entries") { - auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};", catch2_cql_parse_error); + auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};"); REQUIRE(result.has_value()); const auto& ks = get(result->value); const auto& map = get(ks.options.identifier_values[0].second); @@ -936,7 +935,7 @@ TEST_CASE("Parse CREATE KEYSPACE with map literal replication", "[objstore.parse } SECTION("NetworkTopologyStrategy with datacenter configs") { - auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3, 'dc2': 2};", catch2_cql_parse_error); + auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3, 'dc2': 2};"); REQUIRE(result.has_value()); const auto& ks = get(result->value); const auto& map = get(ks.options.identifier_values[0].second); @@ -944,7 +943,7 @@ TEST_CASE("Parse CREATE KEYSPACE with map literal replication", "[objstore.parse } SECTION("Mix of map and scalar options") { - auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy'} AND durable_writes = 'true';", catch2_cql_parse_error); + auto result = cql::parse("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy'} AND durable_writes = 'true';"); REQUIRE(result.has_value()); const auto& ks = get(result->value); REQUIRE(ks.options.identifier_values.length == 2); @@ -957,40 +956,40 @@ TEST_CASE("Parse CREATE KEYSPACE with map literal replication", "[objstore.parse TEST_CASE("CQL parse error reporting", "[objstore.parser]") { SECTION("Invalid syntax returns empty with Catch2 reporter") { - auto result = cql::parse("INVALID STATEMENT;", catch2_cql_parse_error); + auto result = cql::parse("INVALID STATEMENT;"); REQUIRE_FALSE(result.has_value()); } SECTION("Valid query succeeds with Catch2 reporter") { - auto result = cql::parse("SELECT * FROM ks.tbl;", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM ks.tbl;"); REQUIRE(result.has_value()); } SECTION("Empty query returns empty with Catch2 reporter") { - auto result = cql::parse("", catch2_cql_parse_error); + auto result = cql::parse(""); REQUIRE_FALSE(result.has_value()); } SECTION("Unclosed string returns empty with Catch2 reporter") { - auto result = cql::parse("INSERT INTO ks.tbl VALUES ('unclosed);", catch2_cql_parse_error); + auto result = cql::parse("INSERT INTO ks.tbl VALUES ('unclosed);"); REQUIRE_FALSE(result.has_value()); } SECTION("Invalid syntax returns empty with bool reporter") { - auto result = cql::parse("CREATE TABLE;", true); + auto result = cql::parse("CREATE TABLE;"); REQUIRE_FALSE(result.has_value()); } } TEST_CASE("CQL quoted identifiers", "[objstore.cql]") { - auto result = cql::parse("SELECT * FROM \"MyTable\";", catch2_cql_parse_error); + auto result = cql::parse("SELECT * FROM \"MyTable\";"); REQUIRE(result.has_value()); auto& stmt = get