From 0c46e24d2aabd4ffdbe231d4697fc252f1208686 Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:37 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .gitignore | 1 - .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 28 +- .vscode/tasks.json | 12 + CONTRIBUTING.md | 12 +- README.md | 7 + examples/AsyncFileUpload/AsyncFileUpload.ino | 181 ++++---- .../AsyncLargeFileUpload.ino | 321 ++++++++------- examples/BulkOperations/BulkOperations.ino | 98 ++--- examples/CacheDisabled/CacheDisabled.ino | 73 ++-- examples/Collections/Collections.ino | 26 +- .../ConfigManagement/ConfigManagement.ino | 216 +++++----- examples/CreateMany/CreateMany.ino | 75 ++-- examples/ExternalFs/ExternalFs.ino | 104 ++--- examples/FileStreaming/FileStreaming.ino | 248 +++++------ examples/FindOne/FindOne.ino | 67 +-- .../LargeFileStreaming/LargeFileStreaming.ino | 230 ++++++----- examples/QuickStart/QuickStart.ino | 52 ++- examples/References/References.ino | 76 ++-- .../SchemaValidation/SchemaValidation.ino | 51 +-- examples/UniqueFields/UniqueFields.ino | 105 ++--- examples/UpdateOne/UpdateOne.ino | 99 ++--- scripts/format_cpp.sh | 24 ++ src/esp_jsondb/collection/collection.cpp | 388 ++++++++++-------- src/esp_jsondb/collection/collection.h | 335 ++++++++------- src/esp_jsondb/db.cpp | 271 +++++++----- src/esp_jsondb/db.h | 147 ++++--- src/esp_jsondb/db_files.cpp | 133 +++--- src/esp_jsondb/db_files_async.cpp | 46 ++- src/esp_jsondb/document/document.cpp | 101 +++-- src/esp_jsondb/document/document.h | 55 +-- src/esp_jsondb/utils/dbTypes.h | 80 ++-- src/esp_jsondb/utils/fr_mutex.h | 15 +- src/esp_jsondb/utils/fs_utils.h | 18 +- src/esp_jsondb/utils/jsondb_allocator.h | 25 +- src/esp_jsondb/utils/objectId.cpp | 24 +- src/esp_jsondb/utils/objectId.h | 6 +- src/esp_jsondb/utils/refs.h | 13 +- src/esp_jsondb/utils/schema.h | 43 +- src/esp_jsondb/utils/time_utils.h | 3 +- test/bulkTests.cpp | 100 ++--- test/collectionTests.cpp | 88 ++-- test/dbTest.h | 78 ++-- test/documentTests.cpp | 49 +-- test/fileTests.cpp | 85 ++-- test/refTests.cpp | 78 ++-- test/schemaTests.cpp | 72 ++-- 50 files changed, 2440 insertions(+), 1969 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1301145..3b39243 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "files.associations": { + "*.ino": "cpp", "array": "cpp", "deque": "cpp", "string": "cpp", @@ -8,5 +9,30 @@ "string_view": "cpp", "initializer_list": "cpp", "cstdint": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true } -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 293c87b..4af4a20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,19 +134,9 @@ Please keep these in mind when contributing: - **I/O**: Use `StreamUtils::WriteBufferingStream` for buffered writes. - **Validation**: Run schema hooks on create/update; on failure revert and return a validation error. - **Naming**: lowerCamelCase for methods/vars, UpperCamelCase for types, ALL_CAPS for simple constants/enums. -- **Formatting**: Use a consistent `clang-format` (LLVM/Google); keep lines ≤ 120 cols. +- **Formatting**: Follow the repository `.clang-format` + `.editorconfig` baseline from `esptoolkit-template` (LLVM-derived style, `ColumnLimit: 100`, tabs with width `4`, `BinPackArguments/Parameters: false`, `AllowShortFunctionsOnASingleLine: None`). - **Allocations**: Avoid hidden allocations in hot paths and inside event callbacks & sync loops. -Optional `.clang-format` starter (Google-like): -```yaml -BasedOnStyle: Google -IndentWidth: 4 -ColumnLimit: 120 -DerivePointerAlignment: false -PointerAlignment: Left -AllowShortFunctionsOnASingleLine: Empty -``` - --- ## Commit messages & branches diff --git a/README.md b/README.md index e92e080..d394670 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,13 @@ Stack sizes are expressed in bytes. ## Tests An integration harness (`test/`) runs CRUD, bulk, schema, reference, and diagnostic scenarios via the `DbTester` class. Build it as a PlatformIO test or ESP-IDF component (include `test/dbTest.cpp` in your project) and run it on hardware to validate changes. Contributions that expand automated coverage are welcome. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/AsyncFileUpload/AsyncFileUpload.ino b/examples/AsyncFileUpload/AsyncFileUpload.ino index 3c61be6..917f2d2 100644 --- a/examples/AsyncFileUpload/AsyncFileUpload.ino +++ b/examples/AsyncFileUpload/AsyncFileUpload.ino @@ -4,9 +4,9 @@ ESPJsonDB db; struct UploadContext { - const uint8_t *data = nullptr; - size_t total = 0; - size_t offset = 0; + const uint8_t *data = nullptr; + size_t total = 0; + size_t offset = 0; }; static UploadContext gUploadCtx; @@ -16,106 +16,111 @@ static volatile bool gUploadSuccess = false; static volatile size_t gUploadBytes = 0; DbStatus uploadPull(size_t requested, uint8_t *buffer, size_t &produced, bool &eof) { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; - - if (gUploadCtx.offset >= gUploadCtx.total) { - produced = 0; - eof = true; - return {DbStatusCode::Ok, ""}; - } - - size_t remaining = gUploadCtx.total - gUploadCtx.offset; - size_t take = remaining < requested ? remaining : requested; - memcpy(buffer, gUploadCtx.data + gUploadCtx.offset, take); - gUploadCtx.offset += take; - - produced = take; - eof = (gUploadCtx.offset >= gUploadCtx.total); - return {DbStatusCode::Ok, ""}; + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; + + if (gUploadCtx.offset >= gUploadCtx.total) { + produced = 0; + eof = true; + return {DbStatusCode::Ok, ""}; + } + + size_t remaining = gUploadCtx.total - gUploadCtx.offset; + size_t take = remaining < requested ? remaining : requested; + memcpy(buffer, gUploadCtx.data + gUploadCtx.offset, take); + gUploadCtx.offset += take; + + produced = take; + eof = (gUploadCtx.offset >= gUploadCtx.total); + return {DbStatusCode::Ok, ""}; } void onUploadDone(uint32_t uploadId, const DbStatus &st, size_t bytesWritten) { - Serial.printf("Upload %lu done: %s (%u bytes)\n", - static_cast(uploadId), - st.ok() ? "ok" : st.message, - static_cast(bytesWritten)); - gUploadBytes = bytesWritten; - gUploadSuccess = st.ok(); - gUploadDone = true; + Serial.printf( + "Upload %lu done: %s (%u bytes)\n", + static_cast(uploadId), + st.ok() ? "ok" : st.message, + static_cast(bytesWritten) + ); + gUploadBytes = bytesWritten; + gUploadSuccess = st.ok(); + gUploadDone = true; } void setup() { - Serial.begin(115200); + Serial.begin(115200); - ESPJsonDBConfig cfg; - cfg.autosync = false; + ESPJsonDBConfig cfg; + cfg.autosync = false; - auto initStatus = db.init("/async_file_demo", cfg); - if (!initStatus.ok()) { - Serial.printf("DB init failed: %s\n", initStatus.message); - return; - } + auto initStatus = db.init("/async_file_demo", cfg); + if (!initStatus.ok()) { + Serial.printf("DB init failed: %s\n", initStatus.message); + return; + } - static uint8_t payload[1500]; - for (size_t i = 0; i < sizeof(payload); ++i) { - payload[i] = static_cast(i & 0xFF); - } + static uint8_t payload[1500]; + for (size_t i = 0; i < sizeof(payload); ++i) { + payload[i] = static_cast(i & 0xFF); + } - gUploadCtx.data = payload; - gUploadCtx.total = sizeof(payload); - gUploadCtx.offset = 0; + gUploadCtx.data = payload; + gUploadCtx.total = sizeof(payload); + gUploadCtx.offset = 0; - ESPJsonDBFileOptions opts; - opts.chunkSize = 128; - opts.overwrite = true; + ESPJsonDBFileOptions opts; + opts.chunkSize = 128; + opts.overwrite = true; - auto start = db.writeFileStreamAsync("uploads/telemetry.bin", uploadPull, opts, onUploadDone); - if (!start.status.ok()) { - Serial.printf("writeFileStreamAsync failed: %s\n", start.status.message); - return; - } + auto start = db.writeFileStreamAsync("uploads/telemetry.bin", uploadPull, opts, onUploadDone); + if (!start.status.ok()) { + Serial.printf("writeFileStreamAsync failed: %s\n", start.status.message); + return; + } - gUploadId = start.value; - Serial.printf("Started upload %lu\n", static_cast(gUploadId)); + gUploadId = start.value; + Serial.printf("Started upload %lu\n", static_cast(gUploadId)); - // setup() returns immediately; upload continues on background worker. + // setup() returns immediately; upload continues on background worker. } void loop() { - if (gUploadId == 0) { - delay(200); - return; - } - - if (!gUploadDone) { - auto state = db.getFileUploadState(gUploadId); - if (state.status.ok()) { - Serial.printf("Upload state: %u\n", static_cast(state.value)); - } - delay(250); - return; - } - - // One-time verification after completion. - static bool verified = false; - if (!verified) { - verified = true; - - if (!gUploadSuccess) { - Serial.println("Upload failed"); - return; - } - - auto file = db.readFile("uploads/telemetry.bin"); - if (!file.status.ok()) { - Serial.printf("readFile failed: %s\n", file.status.message); - return; - } - - Serial.printf("readFile bytes: %u (done callback bytes: %u)\n", - static_cast(file.value.size()), - static_cast(gUploadBytes)); - } - - delay(1000); + if (gUploadId == 0) { + delay(200); + return; + } + + if (!gUploadDone) { + auto state = db.getFileUploadState(gUploadId); + if (state.status.ok()) { + Serial.printf("Upload state: %u\n", static_cast(state.value)); + } + delay(250); + return; + } + + // One-time verification after completion. + static bool verified = false; + if (!verified) { + verified = true; + + if (!gUploadSuccess) { + Serial.println("Upload failed"); + return; + } + + auto file = db.readFile("uploads/telemetry.bin"); + if (!file.status.ok()) { + Serial.printf("readFile failed: %s\n", file.status.message); + return; + } + + Serial.printf( + "readFile bytes: %u (done callback bytes: %u)\n", + static_cast(file.value.size()), + static_cast(gUploadBytes) + ); + } + + delay(1000); } diff --git a/examples/AsyncLargeFileUpload/AsyncLargeFileUpload.ino b/examples/AsyncLargeFileUpload/AsyncLargeFileUpload.ino index 3959813..d2b9e1b 100644 --- a/examples/AsyncLargeFileUpload/AsyncLargeFileUpload.ino +++ b/examples/AsyncLargeFileUpload/AsyncLargeFileUpload.ino @@ -9,44 +9,52 @@ static constexpr uint32_t kFnvOffset = 2166136261UL; static constexpr uint32_t kFnvPrime = 16777619UL; static uint32_t fnv1aUpdate(uint32_t hash, const uint8_t *data, size_t size) { - for (size_t i = 0; i < size; ++i) { - hash ^= data[i]; - hash *= kFnvPrime; - } - return hash; + for (size_t i = 0; i < size; ++i) { + hash ^= data[i]; + hash *= kFnvPrime; + } + return hash; } static uint8_t payloadByteAt(size_t index) { - return static_cast((index * 37UL + 11UL) & 0xFFU); + return static_cast((index * 37UL + 11UL) & 0xFFU); } struct UploadContext { - size_t offset = 0; - uint32_t hash = kFnvOffset; + size_t offset = 0; + uint32_t hash = kFnvOffset; }; class HashingSink : public Stream { public: - size_t total = 0; - uint32_t hash = kFnvOffset; - - int available() override { return 0; } - int read() override { return -1; } - int peek() override { return -1; } - void flush() override {} - - size_t write(uint8_t b) override { - hash = fnv1aUpdate(hash, &b, 1); - total += 1; - return 1; - } - - size_t write(const uint8_t *buffer, size_t size) override { - if (buffer == nullptr && size > 0) return 0; - hash = fnv1aUpdate(hash, buffer, size); - total += size; - return size; - } + size_t total = 0; + uint32_t hash = kFnvOffset; + + int available() override { + return 0; + } + int read() override { + return -1; + } + int peek() override { + return -1; + } + void flush() override { + } + + size_t write(uint8_t b) override { + hash = fnv1aUpdate(hash, &b, 1); + total += 1; + return 1; + } + + size_t write(const uint8_t *buffer, size_t size) override { + if (buffer == nullptr && size > 0) + return 0; + hash = fnv1aUpdate(hash, buffer, size); + total += size; + return size; + } }; static UploadContext gUploadCtx; @@ -58,132 +66,151 @@ static volatile size_t gUploadedProgress = 0; static volatile uint32_t gExpectedHash = kFnvOffset; DbStatus largeUploadPull(size_t requested, uint8_t *buffer, size_t &produced, bool &eof) { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; - - if (gUploadCtx.offset >= kPayloadSize) { - produced = 0; - eof = true; - return {DbStatusCode::Ok, ""}; - } - - size_t remaining = kPayloadSize - gUploadCtx.offset; - size_t take = remaining < requested ? remaining : requested; - for (size_t i = 0; i < take; ++i) { - buffer[i] = payloadByteAt(gUploadCtx.offset + i); - } - - gUploadCtx.hash = fnv1aUpdate(gUploadCtx.hash, buffer, take); - gUploadCtx.offset += take; - gUploadedProgress = gUploadCtx.offset; - gExpectedHash = gUploadCtx.hash; - - produced = take; - eof = (gUploadCtx.offset >= kPayloadSize); - return {DbStatusCode::Ok, ""}; + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; + + if (gUploadCtx.offset >= kPayloadSize) { + produced = 0; + eof = true; + return {DbStatusCode::Ok, ""}; + } + + size_t remaining = kPayloadSize - gUploadCtx.offset; + size_t take = remaining < requested ? remaining : requested; + for (size_t i = 0; i < take; ++i) { + buffer[i] = payloadByteAt(gUploadCtx.offset + i); + } + + gUploadCtx.hash = fnv1aUpdate(gUploadCtx.hash, buffer, take); + gUploadCtx.offset += take; + gUploadedProgress = gUploadCtx.offset; + gExpectedHash = gUploadCtx.hash; + + produced = take; + eof = (gUploadCtx.offset >= kPayloadSize); + return {DbStatusCode::Ok, ""}; } void onLargeUploadDone(uint32_t uploadId, const DbStatus &st, size_t bytesWritten) { - Serial.printf("Upload %lu done: %s (%u bytes)\n", - static_cast(uploadId), - st.ok() ? "ok" : st.message, - static_cast(bytesWritten)); - gUploadBytes = bytesWritten; - gUploadOk = st.ok(); - gUploadDone = true; + Serial.printf( + "Upload %lu done: %s (%u bytes)\n", + static_cast(uploadId), + st.ok() ? "ok" : st.message, + static_cast(bytesWritten) + ); + gUploadBytes = bytesWritten; + gUploadOk = st.ok(); + gUploadDone = true; } void setup() { - Serial.begin(115200); - - ESPJsonDBConfig cfg; - cfg.autosync = false; - - auto initStatus = db.init("/async_large_demo", cfg); - if (!initStatus.ok()) { - Serial.printf("DB init failed: %s\n", initStatus.message); - return; - } - - gUploadCtx.offset = 0; - gUploadCtx.hash = kFnvOffset; - gUploadDone = false; - gUploadOk = false; - gUploadBytes = 0; - gUploadedProgress = 0; - gExpectedHash = kFnvOffset; - - ESPJsonDBFileOptions opts; - opts.chunkSize = kChunkSize; - opts.overwrite = true; - - auto start = db.writeFileStreamAsync("uploads/large_payload.bin", largeUploadPull, opts, onLargeUploadDone); - if (!start.status.ok()) { - Serial.printf("writeFileStreamAsync failed: %s\n", start.status.message); - return; - } - - gUploadId = start.value; - Serial.printf("Started async upload %lu for %u bytes\n", - static_cast(gUploadId), - static_cast(kPayloadSize)); + Serial.begin(115200); + + ESPJsonDBConfig cfg; + cfg.autosync = false; + + auto initStatus = db.init("/async_large_demo", cfg); + if (!initStatus.ok()) { + Serial.printf("DB init failed: %s\n", initStatus.message); + return; + } + + gUploadCtx.offset = 0; + gUploadCtx.hash = kFnvOffset; + gUploadDone = false; + gUploadOk = false; + gUploadBytes = 0; + gUploadedProgress = 0; + gExpectedHash = kFnvOffset; + + ESPJsonDBFileOptions opts; + opts.chunkSize = kChunkSize; + opts.overwrite = true; + + auto start = db.writeFileStreamAsync( + "uploads/large_payload.bin", + largeUploadPull, + opts, + onLargeUploadDone + ); + if (!start.status.ok()) { + Serial.printf("writeFileStreamAsync failed: %s\n", start.status.message); + return; + } + + gUploadId = start.value; + Serial.printf( + "Started async upload %lu for %u bytes\n", + static_cast(gUploadId), + static_cast(kPayloadSize) + ); } void loop() { - if (gUploadId == 0) { - delay(250); - return; - } - - if (!gUploadDone) { - auto state = db.getFileUploadState(gUploadId); - if (state.status.ok()) { - Serial.printf("state=%u progress=%u/%u\n", - static_cast(state.value), - static_cast(gUploadedProgress), - static_cast(kPayloadSize)); - } - delay(500); - return; - } - - static bool verified = false; - if (!verified) { - verified = true; - - if (!gUploadOk || gUploadBytes != kPayloadSize) { - Serial.println("Upload failed or byte count mismatch"); - return; - } - - auto sizeRes = db.fileSize("uploads/large_payload.bin"); - if (!sizeRes.status.ok() || sizeRes.value != kPayloadSize) { - Serial.printf("fileSize mismatch: %s size=%u\n", - sizeRes.status.message, - static_cast(sizeRes.value)); - return; - } - - HashingSink sink; - auto readRes = db.readFileStream("uploads/large_payload.bin", sink, kReadChunkSize); - if (!readRes.status.ok()) { - Serial.printf("readFileStream failed: %s\n", readRes.status.message); - return; - } - - if (readRes.value != kPayloadSize || sink.total != kPayloadSize || sink.hash != gExpectedHash) { - Serial.printf("verify failed read=%u sink=%u expected=%u hash=0x%08lX/0x%08lX\n", - static_cast(readRes.value), - static_cast(sink.total), - static_cast(kPayloadSize), - static_cast(sink.hash), - static_cast(gExpectedHash)); - return; - } - - Serial.printf("Async large file verified: %u bytes, hash=0x%08lX\n", - static_cast(sink.total), - static_cast(sink.hash)); - } - - delay(1000); + if (gUploadId == 0) { + delay(250); + return; + } + + if (!gUploadDone) { + auto state = db.getFileUploadState(gUploadId); + if (state.status.ok()) { + Serial.printf( + "state=%u progress=%u/%u\n", + static_cast(state.value), + static_cast(gUploadedProgress), + static_cast(kPayloadSize) + ); + } + delay(500); + return; + } + + static bool verified = false; + if (!verified) { + verified = true; + + if (!gUploadOk || gUploadBytes != kPayloadSize) { + Serial.println("Upload failed or byte count mismatch"); + return; + } + + auto sizeRes = db.fileSize("uploads/large_payload.bin"); + if (!sizeRes.status.ok() || sizeRes.value != kPayloadSize) { + Serial.printf( + "fileSize mismatch: %s size=%u\n", + sizeRes.status.message, + static_cast(sizeRes.value) + ); + return; + } + + HashingSink sink; + auto readRes = db.readFileStream("uploads/large_payload.bin", sink, kReadChunkSize); + if (!readRes.status.ok()) { + Serial.printf("readFileStream failed: %s\n", readRes.status.message); + return; + } + + if (readRes.value != kPayloadSize || sink.total != kPayloadSize || + sink.hash != gExpectedHash) { + Serial.printf( + "verify failed read=%u sink=%u expected=%u hash=0x%08lX/0x%08lX\n", + static_cast(readRes.value), + static_cast(sink.total), + static_cast(kPayloadSize), + static_cast(sink.hash), + static_cast(gExpectedHash) + ); + return; + } + + Serial.printf( + "Async large file verified: %u bytes, hash=0x%08lX\n", + static_cast(sink.total), + static_cast(sink.hash) + ); + } + + delay(1000); } diff --git a/examples/BulkOperations/BulkOperations.ino b/examples/BulkOperations/BulkOperations.ino index 4fd68ed..490b5db 100644 --- a/examples/BulkOperations/BulkOperations.ino +++ b/examples/BulkOperations/BulkOperations.ino @@ -3,55 +3,55 @@ ESPJsonDB db; void setup() { - Serial.begin(115200); - - if (!db.init("/bulk_db").ok()) { - Serial.println("DB init failed"); - return; - } - - for (int index = 0; index < 10; ++index) { - JsonDocument userDoc; - userDoc["email"] = "espjsondb_" + String(index) + "@gmail.com"; - userDoc["role"] = index % 2 ? "admin" : "user"; - db.create("users", userDoc.as()); - } - - auto removed = db.removeMany("users", [](const DocView &doc){ - return doc["role"].as() == "admin"; - }); - Serial.printf("Removed %d admins\n", removed.value); - - JsonDocument patch; - patch["role"] = "admin"; - JsonDocument filter; - filter["role"] = "user"; - db.updateMany("users", patch, filter); - - auto found = db.findMany("users", [](const DocView &doc){ - return doc["role"].as() == "admin"; - }); - Serial.printf("Found %d admins\n", found.value.size()); - - // Find first matching document using a predicate - auto firstAdmin = db.findOne("users", [](const DocView &doc){ - return doc["role"].as() == "admin"; - }); - if (firstAdmin.status.ok()) { - std::string email = firstAdmin.value["email"].as(); - Serial.printf("First admin email: %s\n", email.c_str()); - } else { - Serial.println("No admin found"); - } - - // Or find using a JSON filter (key == value pairs) - JsonDocument userFilter; - userFilter["role"] = "user"; - auto firstUser = db.findOne("users", userFilter); - if (firstUser.status.ok()) { - std::string email = firstUser.value["email"].as(); - Serial.printf("First user email: %s\n", email.c_str()); - } + Serial.begin(115200); + + if (!db.init("/bulk_db").ok()) { + Serial.println("DB init failed"); + return; + } + + for (int index = 0; index < 10; ++index) { + JsonDocument userDoc; + userDoc["email"] = "espjsondb_" + String(index) + "@gmail.com"; + userDoc["role"] = index % 2 ? "admin" : "user"; + db.create("users", userDoc.as()); + } + + auto removed = db.removeMany("users", [](const DocView &doc) { + return doc["role"].as() == "admin"; + }); + Serial.printf("Removed %d admins\n", removed.value); + + JsonDocument patch; + patch["role"] = "admin"; + JsonDocument filter; + filter["role"] = "user"; + db.updateMany("users", patch, filter); + + auto found = db.findMany("users", [](const DocView &doc) { + return doc["role"].as() == "admin"; + }); + Serial.printf("Found %d admins\n", found.value.size()); + + // Find first matching document using a predicate + auto firstAdmin = db.findOne("users", [](const DocView &doc) { + return doc["role"].as() == "admin"; + }); + if (firstAdmin.status.ok()) { + std::string email = firstAdmin.value["email"].as(); + Serial.printf("First admin email: %s\n", email.c_str()); + } else { + Serial.println("No admin found"); + } + + // Or find using a JSON filter (key == value pairs) + JsonDocument userFilter; + userFilter["role"] = "user"; + auto firstUser = db.findOne("users", userFilter); + if (firstUser.status.ok()) { + std::string email = firstUser.value["email"].as(); + Serial.printf("First user email: %s\n", email.c_str()); + } } void loop() { diff --git a/examples/CacheDisabled/CacheDisabled.ino b/examples/CacheDisabled/CacheDisabled.ino index 72dbc4b..29631b3 100644 --- a/examples/CacheDisabled/CacheDisabled.ino +++ b/examples/CacheDisabled/CacheDisabled.ino @@ -7,41 +7,42 @@ ESPJsonDB db; * This sketch now demonstrates low-frequency autosync with the cache enabled. */ void setup() { - Serial.begin(115200); - - ESPJsonDBConfig cfg; - cfg.autosync = true; - cfg.intervalMs = 3000; - - if (!db.init("/nocache_db", cfg).ok()) { - Serial.println("DB init failed"); - return; - } - - auto events = db.collection("events"); - if (!events.status.ok()) { - Serial.printf("Failed to get events collection: %s\n", events.status.message); - return; - } - - JsonDocument eventDoc; - eventDoc["type"] = "boot"; - eventDoc["ts"] = millis(); - auto created = db.create("events", eventDoc.as()); - if (!created.status.ok()) { - Serial.printf("Create failed: %s\n", created.status.message); - return; - } - Serial.printf("Created event %s (queued for sync task flush)\n", created.value.c_str()); - - // Reads are served from in-memory cache. - auto fetched = db.findById("events", created.value); - if (fetched.status.ok()) { - std::string kind = fetched.value["type"].as(); - Serial.printf("Reloaded event type: %s\n", kind.empty() ? "(null)" : kind.c_str()); - } else { - Serial.printf("Reload failed: %s\n", fetched.status.message); - } + Serial.begin(115200); + + ESPJsonDBConfig cfg; + cfg.autosync = true; + cfg.intervalMs = 3000; + + if (!db.init("/nocache_db", cfg).ok()) { + Serial.println("DB init failed"); + return; + } + + auto events = db.collection("events"); + if (!events.status.ok()) { + Serial.printf("Failed to get events collection: %s\n", events.status.message); + return; + } + + JsonDocument eventDoc; + eventDoc["type"] = "boot"; + eventDoc["ts"] = millis(); + auto created = db.create("events", eventDoc.as()); + if (!created.status.ok()) { + Serial.printf("Create failed: %s\n", created.status.message); + return; + } + Serial.printf("Created event %s (queued for sync task flush)\n", created.value.c_str()); + + // Reads are served from in-memory cache. + auto fetched = db.findById("events", created.value); + if (fetched.status.ok()) { + std::string kind = fetched.value["type"].as(); + Serial.printf("Reloaded event type: %s\n", kind.empty() ? "(null)" : kind.c_str()); + } else { + Serial.printf("Reload failed: %s\n", fetched.status.message); + } } -void loop() {} +void loop() { +} diff --git a/examples/Collections/Collections.ino b/examples/Collections/Collections.ino index ebc1836..95d0c1e 100644 --- a/examples/Collections/Collections.ino +++ b/examples/Collections/Collections.ino @@ -3,22 +3,22 @@ ESPJsonDB db; void setup() { - Serial.begin(115200); + Serial.begin(115200); - if (!db.init("/collections_db").ok()) { - Serial.println("DB init failed"); - return; - } + if (!db.init("/collections_db").ok()) { + Serial.println("DB init failed"); + return; + } - if (db.collection("sensors").status.ok()) { - Serial.println("Created 'sensors' collection"); - } - db.dropCollection("sensors"); + if (db.collection("sensors").status.ok()) { + Serial.println("Created 'sensors' collection"); + } + db.dropCollection("sensors"); - for (int index = 0; index < 3; ++index) { - db.collection("test_" + String(index)); - } - db.dropAll(); + for (int index = 0; index < 3; ++index) { + db.collection("test_" + String(index)); + } + db.dropAll(); } void loop() { diff --git a/examples/ConfigManagement/ConfigManagement.ino b/examples/ConfigManagement/ConfigManagement.ino index adc93de..c31b388 100644 --- a/examples/ConfigManagement/ConfigManagement.ino +++ b/examples/ConfigManagement/ConfigManagement.ino @@ -9,120 +9,130 @@ ESPJsonDB db; // - getOr with sensible defaults // - updateOne with upsert=true to create or update config -const char* CONF_COLLECTION = "config"; +const char *CONF_COLLECTION = "config"; // Simple validation helpers -bool validHostname(const char* s) { - if (!s) return false; - size_t n = strlen(s); - if (n == 0 || n > 32) return false; - for (size_t i = 0; i < n; ++i) { - char c = s[i]; - if (!(isalnum(c) || c == '-' || c == '_')) return false; - } - return true; +bool validHostname(const char *s) { + if (!s) + return false; + size_t n = strlen(s); + if (n == 0 || n > 32) + return false; + for (size_t i = 0; i < n; ++i) { + char c = s[i]; + if (!(isalnum(c) || c == '-' || c == '_')) + return false; + } + return true; } -bool validSsid(const char* s) { - if (!s) return false; - size_t n = strlen(s); - return n > 0 && n <= 32; +bool validSsid(const char *s) { + if (!s) + return false; + size_t n = strlen(s); + return n > 0 && n <= 32; } -bool validPassword(const char* s) { - if (!s) return false; - size_t n = strlen(s); - // allow open networks (empty) or WPA(8..63) - return n == 0 || (n >= 8 && n <= 63); +bool validPassword(const char *s) { + if (!s) + return false; + size_t n = strlen(s); + // allow open networks (empty) or WPA(8..63) + return n == 0 || (n >= 8 && n <= 63); } ValidationError netConfigValidate(const JsonObjectConst &doc) { - // Require a confType matching this schema - const char* confType = doc["confType"].as(); - if (!confType || strcmp(confType, "netConf") != 0) { - return {false, "confType must be 'netConf'"}; - } - - const char* ssid = doc["ssid"].as(); - if (!validSsid(ssid)) return {false, "ssid invalid"}; - - const char* password = doc["password"].as(); - if (!validPassword(password)) return {false, "password invalid"}; - - const char* hostname = doc["hostname"].as(); - if (!validHostname(hostname)) return {false, "hostname invalid"}; - - // Optional nested object: ipConfig { mode: dhcp|static, ip, gw, mask } - auto ipCfg = doc["ipConfig"]; - if (!ipCfg.isNull()) { - const char* mode = ipCfg["mode"].as(); - if (!mode || (strcmp(mode, "dhcp") != 0 && strcmp(mode, "static") != 0)) { - return {false, "ipConfig.mode must be 'dhcp' or 'static'"}; - } - if (strcmp(mode, "static") == 0) { - if (!ipCfg["ip"].is() || !ipCfg["gw"].is() || !ipCfg["mask"].is()) { - return {false, "ipConfig requires ip, gw, mask"}; - } - } - } - return {true, ""}; + // Require a confType matching this schema + const char *confType = doc["confType"].as(); + if (!confType || strcmp(confType, "netConf") != 0) { + return {false, "confType must be 'netConf'"}; + } + + const char *ssid = doc["ssid"].as(); + if (!validSsid(ssid)) + return {false, "ssid invalid"}; + + const char *password = doc["password"].as(); + if (!validPassword(password)) + return {false, "password invalid"}; + + const char *hostname = doc["hostname"].as(); + if (!validHostname(hostname)) + return {false, "hostname invalid"}; + + // Optional nested object: ipConfig { mode: dhcp|static, ip, gw, mask } + auto ipCfg = doc["ipConfig"]; + if (!ipCfg.isNull()) { + const char *mode = ipCfg["mode"].as(); + if (!mode || (strcmp(mode, "dhcp") != 0 && strcmp(mode, "static") != 0)) { + return {false, "ipConfig.mode must be 'dhcp' or 'static'"}; + } + if (strcmp(mode, "static") == 0) { + if (!ipCfg["ip"].is() || !ipCfg["gw"].is() || + !ipCfg["mask"].is()) { + return {false, "ipConfig requires ip, gw, mask"}; + } + } + } + return {true, ""}; } void setup() { - Serial.begin(115200); - - if (!db.init("/config_db").ok()) { - Serial.println("DB init failed"); - return; - } - - // Register a schema for network configuration - Schema netSchema; - netSchema.fields = { - {"confType", FieldType::String, "netConf"}, - {"ssid", FieldType::String, ""}, - {"password", FieldType::String, ""}, - {"hostname", FieldType::String, "ESP_DEVICE"}, - {"autoReconnect", FieldType::Bool, "true"}, - // Ensure object exists by providing any defaultValue (unused for Object) - {"ipConfig", FieldType::Object, ""} - }; - netSchema.validate = netConfigValidate; - db.registerSchema(CONF_COLLECTION, netSchema); - - // Upsert a configuration document using updateOne(filter, patch, true) - JsonDocument filter; - filter["confType"] = "netConf"; - - JsonDocument patch; // supply values typically sourced from user input - patch["confType"] = "netConf"; - patch["ssid"] = "MyWiFi"; - patch["password"] = "supersecret"; - patch["hostname"] = "ESP_MAIN"; - patch["autoReconnect"] = true; - JsonObject ip = patch["ipConfig"].to(); - ip["mode"] = "dhcp"; // or "static" with ip/gw/mask - - auto up = db.updateOne(CONF_COLLECTION, filter, patch, /*create=*/true); - Serial.printf("Config upsert: %s\n", up.ok() ? "OK" : up.message); - - // Retrieve and read using getOr with defaults - JsonDocument findFilter; - findFilter["confType"] = "netConf"; - auto confResult = db.findOne(CONF_COLLECTION, findFilter); - - const char *ssid = confResult.value.getOr("ssid", "FallbackSSID"); - const char *password = confResult.value.getOr("password", ""); - const char *hostname = confResult.value.getOr("hostname", "ESP_DEVICE"); - bool autoReconnect = confResult.value.getOr("autoReconnect", true); - - if (!confResult.status.ok()) { - Serial.println("Config not found. Using defaults."); - } - - Serial.printf("SSID: %s\n", ssid); - Serial.printf("Hostname: %s\n", hostname); - Serial.printf("AutoReconnect: %s\n", autoReconnect ? "true" : "false"); + Serial.begin(115200); + + if (!db.init("/config_db").ok()) { + Serial.println("DB init failed"); + return; + } + + // Register a schema for network configuration + Schema netSchema; + netSchema.fields = { + {"confType", FieldType::String, "netConf"}, + {"ssid", FieldType::String, ""}, + {"password", FieldType::String, ""}, + {"hostname", FieldType::String, "ESP_DEVICE"}, + {"autoReconnect", FieldType::Bool, "true"}, + // Ensure object exists by providing any defaultValue (unused for Object) + {"ipConfig", FieldType::Object, ""} + }; + netSchema.validate = netConfigValidate; + db.registerSchema(CONF_COLLECTION, netSchema); + + // Upsert a configuration document using updateOne(filter, patch, true) + JsonDocument filter; + filter["confType"] = "netConf"; + + JsonDocument patch; // supply values typically sourced from user input + patch["confType"] = "netConf"; + patch["ssid"] = "MyWiFi"; + patch["password"] = "supersecret"; + patch["hostname"] = "ESP_MAIN"; + patch["autoReconnect"] = true; + JsonObject ip = patch["ipConfig"].to(); + ip["mode"] = "dhcp"; // or "static" with ip/gw/mask + + auto up = db.updateOne(CONF_COLLECTION, filter, patch, /*create=*/true); + Serial.printf("Config upsert: %s\n", up.ok() ? "OK" : up.message); + + // Retrieve and read using getOr with defaults + JsonDocument findFilter; + findFilter["confType"] = "netConf"; + auto confResult = db.findOne(CONF_COLLECTION, findFilter); + + const char *ssid = confResult.value.getOr("ssid", "FallbackSSID"); + const char *password = confResult.value.getOr("password", ""); + const char *hostname = confResult.value.getOr("hostname", "ESP_DEVICE"); + bool autoReconnect = confResult.value.getOr("autoReconnect", true); + + if (!confResult.status.ok()) { + Serial.println("Config not found. Using defaults."); + } + + Serial.printf("SSID: %s\n", ssid); + Serial.printf("Hostname: %s\n", hostname); + Serial.printf("AutoReconnect: %s\n", autoReconnect ? "true" : "false"); } -void loop() {} +void loop() { +} diff --git a/examples/CreateMany/CreateMany.ino b/examples/CreateMany/CreateMany.ino index ecd7ecf..5e80673 100644 --- a/examples/CreateMany/CreateMany.ino +++ b/examples/CreateMany/CreateMany.ino @@ -5,42 +5,43 @@ ESPJsonDB db; // Demonstrates bulk insert with createMany and reading returned ids. void setup() { - Serial.begin(115200); - - if (!db.init("/createmany_db").ok()) { - Serial.println("DB init failed"); - return; - } - - // Prepare an array of documents - JsonDocument arrDoc; - JsonArray arr = arrDoc.to(); - for (int i = 0; i < 5; ++i) { - JsonObject o = arr.add(); - o["email"] = String("bulk_") + i + "@example.com"; - o["index"] = i; - o["active"] = (i % 2) == 0; - } - - auto res = db.createMany("users", arrDoc); - if (!res.status.ok()) { - Serial.printf("createMany failed: %s\n", res.status.message); - return; - } - - Serial.printf("Inserted %u documents\n", (unsigned)res.value.size()); - for (auto& id : res.value) { - Serial.printf(" - _id: %s\n", id.c_str()); - } - - // Find one of them by filter to confirm - JsonDocument filter; - filter["index"] = 3; - auto f = db.findOne("users", filter); - if (f.status.ok()) { - std::string email = f.value["email"].as(); - Serial.printf("Found index=3: %s\n", email.c_str()); - } + Serial.begin(115200); + + if (!db.init("/createmany_db").ok()) { + Serial.println("DB init failed"); + return; + } + + // Prepare an array of documents + JsonDocument arrDoc; + JsonArray arr = arrDoc.to(); + for (int i = 0; i < 5; ++i) { + JsonObject o = arr.add(); + o["email"] = String("bulk_") + i + "@example.com"; + o["index"] = i; + o["active"] = (i % 2) == 0; + } + + auto res = db.createMany("users", arrDoc); + if (!res.status.ok()) { + Serial.printf("createMany failed: %s\n", res.status.message); + return; + } + + Serial.printf("Inserted %u documents\n", (unsigned)res.value.size()); + for (auto &id : res.value) { + Serial.printf(" - _id: %s\n", id.c_str()); + } + + // Find one of them by filter to confirm + JsonDocument filter; + filter["index"] = 3; + auto f = db.findOne("users", filter); + if (f.status.ok()) { + std::string email = f.value["email"].as(); + Serial.printf("Found index=3: %s\n", email.c_str()); + } } -void loop() {} +void loop() { +} diff --git a/examples/ExternalFs/ExternalFs.ino b/examples/ExternalFs/ExternalFs.ino index 85ebfac..2037f1a 100644 --- a/examples/ExternalFs/ExternalFs.ino +++ b/examples/ExternalFs/ExternalFs.ino @@ -4,58 +4,58 @@ ESPJsonDB db; void setup() { - Serial.begin(115200); - while (!Serial && millis() < 2000) { - delay(10); - } - - Serial.println(); - Serial.println("[ExternalFs] Mounting SPIFFS..."); - - if (!SPIFFS.begin(true)) { - Serial.println("SPIFFS mount failed. Check partition table."); - return; - } - - ESPJsonDBConfig syncCfg; - syncCfg.fs = &SPIFFS; // use the already-mounted SPIFFS instance - syncCfg.initFileSystem = false; // skip internal LittleFS.begin() - syncCfg.autosync = false; // we'll call syncNow() manually for clarity - - auto initStatus = db.init("/external_db", syncCfg); - if (!initStatus.ok()) { - Serial.printf("DB init failed: %s\n", initStatus.message); - return; - } - - Serial.println("Database ready (SPIFFS backend)."); - - JsonDocument doc; - doc["name"] = "custom-fs"; - doc["timestamp"] = millis(); - - auto createRes = db.create("settings", doc.as()); - if (!createRes.status.ok()) { - Serial.printf("Create failed: %s\n", createRes.status.message); - return; - } - - Serial.printf("Created doc id: %s\n", createRes.value.c_str()); - - auto syncStatus = db.syncNow(); - if (!syncStatus.ok()) { - Serial.printf("syncNow failed: %s\n", syncStatus.message); - return; - } - - auto findRes = db.findById("settings", createRes.value); - if (findRes.status.ok()) { - Serial.print("Fetched doc: "); - serializeJsonPretty(findRes.value.asObjectConst(), Serial); - Serial.println(); - } else { - Serial.printf("Lookup failed: %s\n", findRes.status.message); - } + Serial.begin(115200); + while (!Serial && millis() < 2000) { + delay(10); + } + + Serial.println(); + Serial.println("[ExternalFs] Mounting SPIFFS..."); + + if (!SPIFFS.begin(true)) { + Serial.println("SPIFFS mount failed. Check partition table."); + return; + } + + ESPJsonDBConfig syncCfg; + syncCfg.fs = &SPIFFS; // use the already-mounted SPIFFS instance + syncCfg.initFileSystem = false; // skip internal LittleFS.begin() + syncCfg.autosync = false; // we'll call syncNow() manually for clarity + + auto initStatus = db.init("/external_db", syncCfg); + if (!initStatus.ok()) { + Serial.printf("DB init failed: %s\n", initStatus.message); + return; + } + + Serial.println("Database ready (SPIFFS backend)."); + + JsonDocument doc; + doc["name"] = "custom-fs"; + doc["timestamp"] = millis(); + + auto createRes = db.create("settings", doc.as()); + if (!createRes.status.ok()) { + Serial.printf("Create failed: %s\n", createRes.status.message); + return; + } + + Serial.printf("Created doc id: %s\n", createRes.value.c_str()); + + auto syncStatus = db.syncNow(); + if (!syncStatus.ok()) { + Serial.printf("syncNow failed: %s\n", syncStatus.message); + return; + } + + auto findRes = db.findById("settings", createRes.value); + if (findRes.status.ok()) { + Serial.print("Fetched doc: "); + serializeJsonPretty(findRes.value.asObjectConst(), Serial); + Serial.println(); + } else { + Serial.printf("Lookup failed: %s\n", findRes.status.message); + } } void loop() { diff --git a/examples/FileStreaming/FileStreaming.ino b/examples/FileStreaming/FileStreaming.ino index 345aad2..88a8870 100644 --- a/examples/FileStreaming/FileStreaming.ino +++ b/examples/FileStreaming/FileStreaming.ino @@ -4,131 +4,135 @@ ESPJsonDB db; static bool writeSeedSourceFile(const char *path) { - if (LittleFS.exists(path)) { - LittleFS.remove(path); - } - - File out = LittleFS.open(path, FILE_WRITE); - if (!out) return false; - - for (uint16_t i = 0; i < 1024; ++i) { - uint8_t b = static_cast(i & 0xFF); - if (out.write(&b, 1) != 1) { - out.close(); - return false; - } - } - - out.close(); - return true; + if (LittleFS.exists(path)) { + LittleFS.remove(path); + } + + File out = LittleFS.open(path, FILE_WRITE); + if (!out) + return false; + + for (uint16_t i = 0; i < 1024; ++i) { + uint8_t b = static_cast(i & 0xFF); + if (out.write(&b, 1) != 1) { + out.close(); + return false; + } + } + + out.close(); + return true; } void setup() { - Serial.begin(115200); - - ESPJsonDBConfig cfg; - cfg.autosync = false; - - auto st = db.init("/stream_demo_db", cfg); - if (!st.ok()) { - Serial.printf("DB init failed: %s\n", st.message); - return; - } - - // 1) Plain text file - st = db.writeTextFile("docs/readme.txt", "Hello from ESPJsonDB file storage.\nLine #2."); - if (!st.ok()) { - Serial.printf("writeTextFile(txt) failed: %s\n", st.message); - return; - } - - // 2) JSON file (stored as text payload) - st = db.writeTextFile("configs/app.json", "{\"mode\":\"demo\",\"intervalMs\":1000}"); - if (!st.ok()) { - Serial.printf("writeTextFile(json) failed: %s\n", st.message); - return; - } - - // 3) CSV file - st = db.writeTextFile("exports/metrics.csv", "ts,temp,humidity\n1,21.4,48\n2,21.7,47\n"); - if (!st.ok()) { - Serial.printf("writeTextFile(csv) failed: %s\n", st.message); - return; - } - - // 4) Binary file using direct byte write - const uint8_t binPayload[] = {0x00, 0xA1, 0xFF, 0x10, 0x22, 0x33, 0x44, 0x55}; - st = db.writeFile("firmware/chunk.bin", binPayload, sizeof(binPayload)); - if (!st.ok()) { - Serial.printf("writeFile(bin) failed: %s\n", st.message); - return; - } - - // 5) Custom extension file (any file type works) - const uint8_t customPayload[] = {'M', 'O', 'D', 'L', 0x01, 0x02, 0x03, 0x04}; - st = db.writeFile("assets/model.dat", customPayload, sizeof(customPayload)); - if (!st.ok()) { - Serial.printf("writeFile(dat) failed: %s\n", st.message); - return; - } - - // 6) Stream write: stream source -> DB managed file - const char *seedPath = "/seed_source.raw"; - if (!writeSeedSourceFile(seedPath)) { - Serial.println("Failed to prepare source stream file"); - return; - } - - ESPJsonDBFileOptions opts; - opts.overwrite = true; - opts.chunkSize = 128; - st = db.writeFileFromPath("streams/raw_capture.raw", seedPath, opts); - if (!st.ok()) { - Serial.printf("writeFileFromPath failed: %s\n", st.message); - return; - } - - // 7) Stream read: DB managed file -> sink stream - if (LittleFS.exists("/stream_sink.raw")) { - LittleFS.remove("/stream_sink.raw"); - } - - File sink = LittleFS.open("/stream_sink.raw", FILE_WRITE); - if (!sink) { - Serial.println("Failed to open sink stream file"); - return; - } - - auto streamOut = db.readFileStream("streams/raw_capture.raw", sink, 96); - sink.close(); - if (!streamOut.status.ok()) { - Serial.printf("readFileStream failed: %s\n", streamOut.status.message); - return; - } - - // 8) Verify with helper reads - auto txt = db.readTextFile("docs/readme.txt"); - auto jsn = db.readTextFile("configs/app.json"); - auto csv = db.readTextFile("exports/metrics.csv"); - auto bin = db.readFile("firmware/chunk.bin"); - auto modelSize = db.fileSize("assets/model.dat"); - - if (!txt.status.ok() || !jsn.status.ok() || !csv.status.ok() || !bin.status.ok() || !modelSize.status.ok()) { - Serial.println("One of the verification reads failed"); - return; - } - - Serial.println("Stored under /stream_demo_db/_files:"); - Serial.printf("txt bytes: %u\n", static_cast(txt.value.size())); - Serial.printf("json bytes: %u\n", static_cast(jsn.value.size())); - Serial.printf("csv bytes: %u\n", static_cast(csv.value.size())); - Serial.printf("bin bytes: %u\n", static_cast(bin.value.size())); - Serial.printf("model.dat bytes: %u\n", static_cast(modelSize.value)); - Serial.printf("streamed raw bytes out: %u\n", static_cast(streamOut.value)); - - auto exists = db.fileExists("streams/raw_capture.raw"); - Serial.printf("streams/raw_capture.raw exists: %s\n", - (exists.status.ok() && exists.value) ? "yes" : "no"); + Serial.begin(115200); + + ESPJsonDBConfig cfg; + cfg.autosync = false; + + auto st = db.init("/stream_demo_db", cfg); + if (!st.ok()) { + Serial.printf("DB init failed: %s\n", st.message); + return; + } + + // 1) Plain text file + st = db.writeTextFile("docs/readme.txt", "Hello from ESPJsonDB file storage.\nLine #2."); + if (!st.ok()) { + Serial.printf("writeTextFile(txt) failed: %s\n", st.message); + return; + } + + // 2) JSON file (stored as text payload) + st = db.writeTextFile("configs/app.json", "{\"mode\":\"demo\",\"intervalMs\":1000}"); + if (!st.ok()) { + Serial.printf("writeTextFile(json) failed: %s\n", st.message); + return; + } + + // 3) CSV file + st = db.writeTextFile("exports/metrics.csv", "ts,temp,humidity\n1,21.4,48\n2,21.7,47\n"); + if (!st.ok()) { + Serial.printf("writeTextFile(csv) failed: %s\n", st.message); + return; + } + + // 4) Binary file using direct byte write + const uint8_t binPayload[] = {0x00, 0xA1, 0xFF, 0x10, 0x22, 0x33, 0x44, 0x55}; + st = db.writeFile("firmware/chunk.bin", binPayload, sizeof(binPayload)); + if (!st.ok()) { + Serial.printf("writeFile(bin) failed: %s\n", st.message); + return; + } + + // 5) Custom extension file (any file type works) + const uint8_t customPayload[] = {'M', 'O', 'D', 'L', 0x01, 0x02, 0x03, 0x04}; + st = db.writeFile("assets/model.dat", customPayload, sizeof(customPayload)); + if (!st.ok()) { + Serial.printf("writeFile(dat) failed: %s\n", st.message); + return; + } + + // 6) Stream write: stream source -> DB managed file + const char *seedPath = "/seed_source.raw"; + if (!writeSeedSourceFile(seedPath)) { + Serial.println("Failed to prepare source stream file"); + return; + } + + ESPJsonDBFileOptions opts; + opts.overwrite = true; + opts.chunkSize = 128; + st = db.writeFileFromPath("streams/raw_capture.raw", seedPath, opts); + if (!st.ok()) { + Serial.printf("writeFileFromPath failed: %s\n", st.message); + return; + } + + // 7) Stream read: DB managed file -> sink stream + if (LittleFS.exists("/stream_sink.raw")) { + LittleFS.remove("/stream_sink.raw"); + } + + File sink = LittleFS.open("/stream_sink.raw", FILE_WRITE); + if (!sink) { + Serial.println("Failed to open sink stream file"); + return; + } + + auto streamOut = db.readFileStream("streams/raw_capture.raw", sink, 96); + sink.close(); + if (!streamOut.status.ok()) { + Serial.printf("readFileStream failed: %s\n", streamOut.status.message); + return; + } + + // 8) Verify with helper reads + auto txt = db.readTextFile("docs/readme.txt"); + auto jsn = db.readTextFile("configs/app.json"); + auto csv = db.readTextFile("exports/metrics.csv"); + auto bin = db.readFile("firmware/chunk.bin"); + auto modelSize = db.fileSize("assets/model.dat"); + + if (!txt.status.ok() || !jsn.status.ok() || !csv.status.ok() || !bin.status.ok() || + !modelSize.status.ok()) { + Serial.println("One of the verification reads failed"); + return; + } + + Serial.println("Stored under /stream_demo_db/_files:"); + Serial.printf("txt bytes: %u\n", static_cast(txt.value.size())); + Serial.printf("json bytes: %u\n", static_cast(jsn.value.size())); + Serial.printf("csv bytes: %u\n", static_cast(csv.value.size())); + Serial.printf("bin bytes: %u\n", static_cast(bin.value.size())); + Serial.printf("model.dat bytes: %u\n", static_cast(modelSize.value)); + Serial.printf("streamed raw bytes out: %u\n", static_cast(streamOut.value)); + + auto exists = db.fileExists("streams/raw_capture.raw"); + Serial.printf( + "streams/raw_capture.raw exists: %s\n", + (exists.status.ok() && exists.value) ? "yes" : "no" + ); } void loop() { diff --git a/examples/FindOne/FindOne.ino b/examples/FindOne/FindOne.ino index 0fadc5b..e1926d6 100644 --- a/examples/FindOne/FindOne.ino +++ b/examples/FindOne/FindOne.ino @@ -5,38 +5,39 @@ ESPJsonDB db; // Demonstrates findOne with predicate and with a JSON filter. void setup() { - Serial.begin(115200); - - if (!db.init("/findone_db").ok()) { - Serial.println("DB init failed"); - return; - } - - // Create a few docs - for (int i = 0; i < 4; ++i) { - JsonDocument d; - d["name"] = String("device-") + i; - d["status"] = (i % 2) ? "offline" : "online"; - db.create("devices", d.as()); - } - - // 1) findOne with predicate - auto online = db.findOne("devices", [](const DocView& doc) { - return doc["status"].as() == std::string("online"); - }); - if (online.status.ok()) { - std::string name = online.value["name"].as(); - Serial.printf("First online: %s\n", name.c_str()); - } - - // 2) findOne with JSON filter - JsonDocument filter; - filter["status"] = "offline"; - auto offline = db.findOne("devices", filter); - if (offline.status.ok()) { - std::string name = offline.value["name"].as(); - Serial.printf("First offline: %s\n", name.c_str()); - } + Serial.begin(115200); + + if (!db.init("/findone_db").ok()) { + Serial.println("DB init failed"); + return; + } + + // Create a few docs + for (int i = 0; i < 4; ++i) { + JsonDocument d; + d["name"] = String("device-") + i; + d["status"] = (i % 2) ? "offline" : "online"; + db.create("devices", d.as()); + } + + // 1) findOne with predicate + auto online = db.findOne("devices", [](const DocView &doc) { + return doc["status"].as() == std::string("online"); + }); + if (online.status.ok()) { + std::string name = online.value["name"].as(); + Serial.printf("First online: %s\n", name.c_str()); + } + + // 2) findOne with JSON filter + JsonDocument filter; + filter["status"] = "offline"; + auto offline = db.findOne("devices", filter); + if (offline.status.ok()) { + std::string name = offline.value["name"].as(); + Serial.printf("First offline: %s\n", name.c_str()); + } } -void loop() {} +void loop() { +} diff --git a/examples/LargeFileStreaming/LargeFileStreaming.ino b/examples/LargeFileStreaming/LargeFileStreaming.ino index 7e41212..4317159 100644 --- a/examples/LargeFileStreaming/LargeFileStreaming.ino +++ b/examples/LargeFileStreaming/LargeFileStreaming.ino @@ -9,125 +9,143 @@ static constexpr uint32_t kFnvOffset = 2166136261UL; static constexpr uint32_t kFnvPrime = 16777619UL; static uint32_t fnv1aUpdate(uint32_t hash, const uint8_t *data, size_t size) { - for (size_t i = 0; i < size; ++i) { - hash ^= data[i]; - hash *= kFnvPrime; - } - return hash; + for (size_t i = 0; i < size; ++i) { + hash ^= data[i]; + hash *= kFnvPrime; + } + return hash; } static uint8_t payloadByteAt(size_t index) { - return static_cast((index * 37UL + 11UL) & 0xFFU); + return static_cast((index * 37UL + 11UL) & 0xFFU); } struct PayloadProducer { - size_t offset = 0; - uint32_t hash = kFnvOffset; + size_t offset = 0; + uint32_t hash = kFnvOffset; }; class HashingSink : public Stream { public: - size_t total = 0; - uint32_t hash = kFnvOffset; - - int available() override { return 0; } - int read() override { return -1; } - int peek() override { return -1; } - void flush() override {} - - size_t write(uint8_t b) override { - hash = fnv1aUpdate(hash, &b, 1); - total += 1; - return 1; - } - - size_t write(const uint8_t *buffer, size_t size) override { - if (buffer == nullptr && size > 0) return 0; - hash = fnv1aUpdate(hash, buffer, size); - total += size; - return size; - } + size_t total = 0; + uint32_t hash = kFnvOffset; + + int available() override { + return 0; + } + int read() override { + return -1; + } + int peek() override { + return -1; + } + void flush() override { + } + + size_t write(uint8_t b) override { + hash = fnv1aUpdate(hash, &b, 1); + total += 1; + return 1; + } + + size_t write(const uint8_t *buffer, size_t size) override { + if (buffer == nullptr && size > 0) + return 0; + hash = fnv1aUpdate(hash, buffer, size); + total += size; + return size; + } }; void setup() { - Serial.begin(115200); - - ESPJsonDBConfig cfg; - cfg.autosync = false; - - auto st = db.init("/large_stream_db", cfg); - if (!st.ok()) { - Serial.printf("DB init failed: %s\n", st.message); - return; - } - - PayloadProducer producer; - DbFileUploadPullCb pullCb = [&producer](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; - - if (producer.offset >= kPayloadSize) { - produced = 0; - eof = true; - return {DbStatusCode::Ok, ""}; - } - - size_t remaining = kPayloadSize - producer.offset; - size_t take = remaining < requested ? remaining : requested; - for (size_t i = 0; i < take; ++i) { - buffer[i] = payloadByteAt(producer.offset + i); - } - - producer.hash = fnv1aUpdate(producer.hash, buffer, take); - producer.offset += take; - produced = take; - eof = (producer.offset >= kPayloadSize); - - if ((producer.offset % (64 * 1024)) == 0 || eof) { - Serial.printf("write progress: %u / %u bytes\n", - static_cast(producer.offset), - static_cast(kPayloadSize)); - } - - return {DbStatusCode::Ok, ""}; - }; - - ESPJsonDBFileOptions opts; - opts.overwrite = true; - opts.chunkSize = kWriteChunkSize; - - const char *path = "firmware/large_payload.bin"; - st = db.writeFileStream(path, pullCb, opts); - if (!st.ok()) { - Serial.printf("writeFileStream failed: %s\n", st.message); - return; - } - - auto sizeRes = db.fileSize(path); - if (!sizeRes.status.ok() || sizeRes.value != kPayloadSize) { - Serial.printf("fileSize mismatch: status=%s size=%u\n", - sizeRes.status.message, - static_cast(sizeRes.value)); - return; - } - - HashingSink sink; - auto readRes = db.readFileStream(path, sink, kReadChunkSize); - if (!readRes.status.ok()) { - Serial.printf("readFileStream failed: %s\n", readRes.status.message); - return; - } - - if (readRes.value != kPayloadSize || sink.total != kPayloadSize || sink.hash != producer.hash) { - Serial.printf("verification failed (read=%u sink=%u expected=%u)\n", - static_cast(readRes.value), - static_cast(sink.total), - static_cast(kPayloadSize)); - return; - } - - Serial.printf("Large chunked stream verified: %u bytes, hash=0x%08lX\n", - static_cast(sink.total), - static_cast(sink.hash)); + Serial.begin(115200); + + ESPJsonDBConfig cfg; + cfg.autosync = false; + + auto st = db.init("/large_stream_db", cfg); + if (!st.ok()) { + Serial.printf("DB init failed: %s\n", st.message); + return; + } + + PayloadProducer producer; + DbFileUploadPullCb pullCb = + [&producer](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; + + if (producer.offset >= kPayloadSize) { + produced = 0; + eof = true; + return {DbStatusCode::Ok, ""}; + } + + size_t remaining = kPayloadSize - producer.offset; + size_t take = remaining < requested ? remaining : requested; + for (size_t i = 0; i < take; ++i) { + buffer[i] = payloadByteAt(producer.offset + i); + } + + producer.hash = fnv1aUpdate(producer.hash, buffer, take); + producer.offset += take; + produced = take; + eof = (producer.offset >= kPayloadSize); + + if ((producer.offset % (64 * 1024)) == 0 || eof) { + Serial.printf( + "write progress: %u / %u bytes\n", + static_cast(producer.offset), + static_cast(kPayloadSize) + ); + } + + return {DbStatusCode::Ok, ""}; + }; + + ESPJsonDBFileOptions opts; + opts.overwrite = true; + opts.chunkSize = kWriteChunkSize; + + const char *path = "firmware/large_payload.bin"; + st = db.writeFileStream(path, pullCb, opts); + if (!st.ok()) { + Serial.printf("writeFileStream failed: %s\n", st.message); + return; + } + + auto sizeRes = db.fileSize(path); + if (!sizeRes.status.ok() || sizeRes.value != kPayloadSize) { + Serial.printf( + "fileSize mismatch: status=%s size=%u\n", + sizeRes.status.message, + static_cast(sizeRes.value) + ); + return; + } + + HashingSink sink; + auto readRes = db.readFileStream(path, sink, kReadChunkSize); + if (!readRes.status.ok()) { + Serial.printf("readFileStream failed: %s\n", readRes.status.message); + return; + } + + if (readRes.value != kPayloadSize || sink.total != kPayloadSize || sink.hash != producer.hash) { + Serial.printf( + "verification failed (read=%u sink=%u expected=%u)\n", + static_cast(readRes.value), + static_cast(sink.total), + static_cast(kPayloadSize) + ); + return; + } + + Serial.printf( + "Large chunked stream verified: %u bytes, hash=0x%08lX\n", + static_cast(sink.total), + static_cast(sink.hash) + ); } void loop() { diff --git a/examples/QuickStart/QuickStart.ino b/examples/QuickStart/QuickStart.ino index f3f8fd4..bb8680c 100644 --- a/examples/QuickStart/QuickStart.ino +++ b/examples/QuickStart/QuickStart.ino @@ -3,35 +3,31 @@ ESPJsonDB db; void setup() { - Serial.begin(115200); - - ESPJsonDBConfig syncCfg; - syncCfg.intervalMs = 3000; // autosync every 3s - syncCfg.autosync = true; - - if (!db.init("/example_db", syncCfg).ok()) { - Serial.println("DB init failed"); - return; - } - - db.onEvent([](DBEventType event) { - Serial.printf("Event: %s\n", dbEventTypeToString(event)); - }); - - db.onError([](const DbStatus& status) { - Serial.printf("Error: %s\n", status.message); - }); - - JsonDocument userDoc; - userDoc["email"] = "espjsondb@gmail.com"; - userDoc["username"] = "esp-jsondb"; - auto createRes = db.create("users", userDoc.as()); - if (createRes.status.ok()) { - Serial.printf("Created user %s\n", createRes.value.c_str()); - db.removeById("users", createRes.value); - } + Serial.begin(115200); + + ESPJsonDBConfig syncCfg; + syncCfg.intervalMs = 3000; // autosync every 3s + syncCfg.autosync = true; + + if (!db.init("/example_db", syncCfg).ok()) { + Serial.println("DB init failed"); + return; + } + + db.onEvent([](DBEventType event) { Serial.printf("Event: %s\n", dbEventTypeToString(event)); }); + + db.onError([](const DbStatus &status) { Serial.printf("Error: %s\n", status.message); }); + + JsonDocument userDoc; + userDoc["email"] = "espjsondb@gmail.com"; + userDoc["username"] = "esp-jsondb"; + auto createRes = db.create("users", userDoc.as()); + if (createRes.status.ok()) { + Serial.printf("Created user %s\n", createRes.value.c_str()); + db.removeById("users", createRes.value); + } } void loop() { - // Call db.deinit() before shutting down the DB-owning feature. + // Call db.deinit() before shutting down the DB-owning feature. } diff --git a/examples/References/References.ino b/examples/References/References.ino index 96a2c62..4ed91f1 100644 --- a/examples/References/References.ino +++ b/examples/References/References.ino @@ -4,44 +4,44 @@ ESPJsonDB db; void setup() { - Serial.begin(115200); - - if (!db.init("/refs_db").ok()) { - Serial.println("DB init failed"); - return; - } - - JsonDocument authorDoc; - authorDoc["name"] = "John Doe"; - auto authorCreateRes = db.create("authors", authorDoc.as()); - if (!authorCreateRes.status.ok()) { - Serial.printf("Author create failed: %s\n", authorCreateRes.status.message); - return; - } - - DocRef authorRef{"authors", authorCreateRes.value}; - JsonDocument book; - book["title"] = "Example Book"; - JsonObject authorRefObj = book["author"].to(); - authorRefObj["collection"] = authorRef.collection; - authorRefObj["_id"] = authorRef.id; - auto bookCreateRes = db.create("books", book.as()); - if (!bookCreateRes.status.ok()) { - Serial.printf("Book create failed: %s\n", bookCreateRes.status.message); - return; - } - - auto bookFindRes = db.findById("books", bookCreateRes.value); - if (bookFindRes.status.ok()) { - auto populated = bookFindRes.value.populate("author"); - if (db.lastError().ok()) { - std::string name = populated["name"].as(); - Serial.printf("Author: %s\n", name.c_str()); - } - } - - db.dropCollection("books"); - db.dropCollection("authors"); + Serial.begin(115200); + + if (!db.init("/refs_db").ok()) { + Serial.println("DB init failed"); + return; + } + + JsonDocument authorDoc; + authorDoc["name"] = "John Doe"; + auto authorCreateRes = db.create("authors", authorDoc.as()); + if (!authorCreateRes.status.ok()) { + Serial.printf("Author create failed: %s\n", authorCreateRes.status.message); + return; + } + + DocRef authorRef{"authors", authorCreateRes.value}; + JsonDocument book; + book["title"] = "Example Book"; + JsonObject authorRefObj = book["author"].to(); + authorRefObj["collection"] = authorRef.collection; + authorRefObj["_id"] = authorRef.id; + auto bookCreateRes = db.create("books", book.as()); + if (!bookCreateRes.status.ok()) { + Serial.printf("Book create failed: %s\n", bookCreateRes.status.message); + return; + } + + auto bookFindRes = db.findById("books", bookCreateRes.value); + if (bookFindRes.status.ok()) { + auto populated = bookFindRes.value.populate("author"); + if (db.lastError().ok()) { + std::string name = populated["name"].as(); + Serial.printf("Author: %s\n", name.c_str()); + } + } + + db.dropCollection("books"); + db.dropCollection("authors"); } void loop() { diff --git a/examples/SchemaValidation/SchemaValidation.ino b/examples/SchemaValidation/SchemaValidation.ino index 01d6f02..c3c697f 100644 --- a/examples/SchemaValidation/SchemaValidation.ino +++ b/examples/SchemaValidation/SchemaValidation.ino @@ -2,37 +2,38 @@ ESPJsonDB db; -static ValidationError usersValidate(const JsonObjectConst& doc) { - if (doc["username"].isNull() || doc["password"].isNull()) - return {false, "username and password are required"}; - return {true, ""}; +static ValidationError usersValidate(const JsonObjectConst &doc) { + if (doc["username"].isNull() || doc["password"].isNull()) + return {false, "username and password are required"}; + return {true, ""}; } void setup() { - Serial.begin(115200); + Serial.begin(115200); - if (!db.init("/schema_db").ok()) { - Serial.println("DB init failed"); - return; - } + if (!db.init("/schema_db").ok()) { + Serial.println("DB init failed"); + return; + } - Schema userSchema; - userSchema.fields = { - {"email", FieldType::String, "a@b.c"}, - {"username", FieldType::String}, - {"role", FieldType::String, "user"}, - {"password", FieldType::String}, - {"age", FieldType::Int}}; - userSchema.validate = usersValidate; - db.registerSchema("users", userSchema); + Schema userSchema; + userSchema.fields = { + {"email", FieldType::String, "a@b.c"}, + {"username", FieldType::String}, + {"role", FieldType::String, "user"}, + {"password", FieldType::String}, + {"age", FieldType::Int} + }; + userSchema.validate = usersValidate; + db.registerSchema("users", userSchema); - JsonDocument userDoc; - userDoc["username"] = "esp-jsondb"; - userDoc["password"] = "secret"; - auto createRes = db.create("users", userDoc.as()); - if (createRes.status.ok()) { - Serial.printf("Created user %s\n", createRes.value.c_str()); - } + JsonDocument userDoc; + userDoc["username"] = "esp-jsondb"; + userDoc["password"] = "secret"; + auto createRes = db.create("users", userDoc.as()); + if (createRes.status.ok()) { + Serial.printf("Created user %s\n", createRes.value.c_str()); + } } void loop() { diff --git a/examples/UniqueFields/UniqueFields.ino b/examples/UniqueFields/UniqueFields.ino index beb2f64..5df1080 100644 --- a/examples/UniqueFields/UniqueFields.ino +++ b/examples/UniqueFields/UniqueFields.ino @@ -5,64 +5,69 @@ ESPJsonDB db; // Demonstrates schema-level unique fields on create and update. void setup() { - Serial.begin(115200); + Serial.begin(115200); - if (!db.init("/unique_db").ok()) { - Serial.println("DB init failed"); - return; - } + if (!db.init("/unique_db").ok()) { + Serial.println("DB init failed"); + return; + } - // Define a schema where email and username must be unique - Schema userSchema; - userSchema.fields = { - {"email", FieldType::String, nullptr, true}, // unique - {"username", FieldType::String, nullptr, true}, // unique - {"role", FieldType::String, "user"}}; - db.registerSchema("users", userSchema); + // Define a schema where email and username must be unique + Schema userSchema; + userSchema.fields = { + {"email", FieldType::String, nullptr, true}, // unique + {"username", FieldType::String, nullptr, true}, // unique + {"role", FieldType::String, "user"} + }; + db.registerSchema("users", userSchema); - // Create first user - JsonDocument u1; - u1["email"] = "a@x.com"; - u1["username"] = "alpha"; - auto r1 = db.create("users", u1.as()); - Serial.printf("Create u1: %s\n", r1.status.ok() ? r1.value.c_str() : r1.status.message); + // Create first user + JsonDocument u1; + u1["email"] = "a@x.com"; + u1["username"] = "alpha"; + auto r1 = db.create("users", u1.as()); + Serial.printf("Create u1: %s\n", r1.status.ok() ? r1.value.c_str() : r1.status.message); - // Attempt to create a duplicate email (should fail) - JsonDocument dup; - dup["email"] = "a@x.com"; - dup["username"] = "alpha2"; - auto rd = db.create("users", dup.as()); - Serial.printf("Create duplicate email: %s\n", rd.status.ok() ? "OK (unexpected)" : rd.status.message); + // Attempt to create a duplicate email (should fail) + JsonDocument dup; + dup["email"] = "a@x.com"; + dup["username"] = "alpha2"; + auto rd = db.create("users", dup.as()); + Serial.printf( + "Create duplicate email: %s\n", + rd.status.ok() ? "OK (unexpected)" : rd.status.message + ); - // Create second unique user - JsonDocument u2; - u2["email"] = "b@x.com"; - u2["username"] = "bravo"; - auto r2 = db.create("users", u2.as()); - Serial.printf("Create u2: %s\n", r2.status.ok() ? r2.value.c_str() : r2.status.message); + // Create second unique user + JsonDocument u2; + u2["email"] = "b@x.com"; + u2["username"] = "bravo"; + auto r2 = db.create("users", u2.as()); + Serial.printf("Create u2: %s\n", r2.status.ok() ? r2.value.c_str() : r2.status.message); - if (!r2.status.ok()) return; + if (!r2.status.ok()) + return; - // Try to update u2's email to an existing one (should fail unique check) - auto stFail = db.updateById("users", r2.value, [](DocView& v) { - v["email"].set("a@x.com"); - }); - Serial.printf("Update to duplicate email: %s\n", stFail.ok() ? "OK (unexpected)" : stFail.message); + // Try to update u2's email to an existing one (should fail unique check) + auto stFail = db.updateById("users", r2.value, [](DocView &v) { v["email"].set("a@x.com"); }); + Serial.printf( + "Update to duplicate email: %s\n", + stFail.ok() ? "OK (unexpected)" : stFail.message + ); - // Update u2's email to a new unique value (should succeed) - auto stOk = db.updateById("users", r2.value, [](DocView& v) { - v["email"].set("c@x.com"); - }); - Serial.printf("Update to new email: %s\n", stOk.ok() ? "OK" : stOk.message); + // Update u2's email to a new unique value (should succeed) + auto stOk = db.updateById("users", r2.value, [](DocView &v) { v["email"].set("c@x.com"); }); + Serial.printf("Update to new email: %s\n", stOk.ok() ? "OK" : stOk.message); - // Verify with a findOne filter - JsonDocument filter; - filter["email"] = "c@x.com"; - auto found = db.findOne("users", filter); - if (found.status.ok()) { - std::string username = found.value["username"].as(); - Serial.printf("Found updated user: %s\n", username.c_str()); - } + // Verify with a findOne filter + JsonDocument filter; + filter["email"] = "c@x.com"; + auto found = db.findOne("users", filter); + if (found.status.ok()) { + std::string username = found.value["username"].as(); + Serial.printf("Found updated user: %s\n", username.c_str()); + } } -void loop() {} +void loop() { +} diff --git a/examples/UpdateOne/UpdateOne.ino b/examples/UpdateOne/UpdateOne.ino index d730bcc..3c3aa79 100644 --- a/examples/UpdateOne/UpdateOne.ino +++ b/examples/UpdateOne/UpdateOne.ino @@ -5,60 +5,65 @@ ESPJsonDB db; // Demonstrates updateOne with both predicate+mutator and JSON filter+patch. void setup() { - Serial.begin(115200); + Serial.begin(115200); - if (!db.init("/updateone_db").ok()) { - Serial.println("DB init failed"); - return; - } + if (!db.init("/updateone_db").ok()) { + Serial.println("DB init failed"); + return; + } - // Seed a document - JsonDocument seed; - seed["email"] = "user@example.com"; - seed["visits"] = 1; - auto idRes = db.create("users", seed.as()); - if (!idRes.status.ok()) { - Serial.printf("Seed failed: %s\n", idRes.status.message); - return; - } + // Seed a document + JsonDocument seed; + seed["email"] = "user@example.com"; + seed["visits"] = 1; + auto idRes = db.create("users", seed.as()); + if (!idRes.status.ok()) { + Serial.printf("Seed failed: %s\n", idRes.status.message); + return; + } - // 1) updateOne with predicate + mutator (increment a counter) - auto st1 = db.updateOne( - "users", - [&](const DocView& v) { - return v["email"].as() == std::string("user@example.com"); // match by email - }, - [&](DocView& v) { - int visits = v["visits"].as(); - v["visits"].set(visits + 1); - }); - Serial.printf("updateOne (predicate/mutator): %s\n", st1.ok() ? "OK" : st1.message); + // 1) updateOne with predicate + mutator (increment a counter) + auto st1 = db.updateOne( + "users", + [&](const DocView &v) { + return v["email"].as() == + std::string("user@example.com"); // match by email + }, + [&](DocView &v) { + int visits = v["visits"].as(); + v["visits"].set(visits + 1); + } + ); + Serial.printf("updateOne (predicate/mutator): %s\n", st1.ok() ? "OK" : st1.message); - // 2) updateOne with JSON filter + patch (upsert=true) - JsonDocument filter; - filter["email"] = "newuser@example.com"; + // 2) updateOne with JSON filter + patch (upsert=true) + JsonDocument filter; + filter["email"] = "newuser@example.com"; - JsonDocument patch; - patch["email"] = "newuser@example.com"; - patch["visits"] = 1; - patch["role"] = "user"; + JsonDocument patch; + patch["email"] = "newuser@example.com"; + patch["visits"] = 1; + patch["role"] = "user"; - auto st2 = db.updateOne("users", filter, patch, /*create=*/true); - Serial.printf("updateOne (filter/patch upsert): %s\n", st2.ok() ? "OK" : st2.message); + auto st2 = db.updateOne("users", filter, patch, /*create=*/true); + Serial.printf("updateOne (filter/patch upsert): %s\n", st2.ok() ? "OK" : st2.message); - // Verify results - auto foundExisting = db.findOne("users", [&](const DocView& v) { return v["email"].as() == std::string("user@example.com"); }); - if (foundExisting.status.ok()) { - Serial.printf("Existing visits: %d\n", foundExisting.value["visits"].as()); - } + // Verify results + auto foundExisting = db.findOne("users", [&](const DocView &v) { + return v["email"].as() == std::string("user@example.com"); + }); + if (foundExisting.status.ok()) { + Serial.printf("Existing visits: %d\n", foundExisting.value["visits"].as()); + } - JsonDocument f2; - f2["email"] = "newuser@example.com"; - auto foundUpserted = db.findOne("users", f2); - if (foundUpserted.status.ok()) { - std::string role = foundUpserted.value["role"].as(); - Serial.printf("Upserted role: %s\n", role.c_str()); - } + JsonDocument f2; + f2["email"] = "newuser@example.com"; + auto foundUpserted = db.findOne("users", f2); + if (foundUpserted.status.ok()) { + std::string role = foundUpserted.value["role"].as(); + Serial.printf("Upserted role: %s\n", role.c_str()); + } } -void loop() {} +void loop() { +} diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_jsondb/collection/collection.cpp b/src/esp_jsondb/collection/collection.cpp index 9e562b3..1efddda 100644 --- a/src/esp_jsondb/collection/collection.cpp +++ b/src/esp_jsondb/collection/collection.cpp @@ -3,20 +3,17 @@ #include "../utils/fs_utils.h" #include "../utils/time_utils.h" -Collection::Collection(ESPJsonDB &db, - const std::string &name, - const Schema &schema, - std::string baseDir, - bool cacheEnabled, - bool usePSRAMBuffers, - fs::FS &fs) - : _db(&db), - _name(name), - _schema(schema), - _baseDir(std::move(baseDir)), - _cacheEnabled(true), - _usePSRAMBuffers(usePSRAMBuffers), - _fs(&fs) { +Collection::Collection( + ESPJsonDB &db, + const std::string &name, + const Schema &schema, + std::string baseDir, + bool cacheEnabled, + bool usePSRAMBuffers, + fs::FS &fs +) + : _db(&db), _name(name), _schema(schema), _baseDir(std::move(baseDir)), _cacheEnabled(true), + _usePSRAMBuffers(usePSRAMBuffers), _fs(&fs) { (void)cacheEnabled; } @@ -30,32 +27,38 @@ DbStatus Collection::recordStatus(const DbStatus &st) const { } void Collection::emitEvent(DBEventType ev) const { - if (_db) _db->emitEvent(ev); + if (_db) + _db->emitEvent(ev); } void Collection::noteDeletedInDiag(size_t count) const { - if (count == 0 || !_db) return; + if (count == 0 || !_db) + return; _db->noteDocumentDeleted(_name, static_cast(count)); } DbStatus Collection::checkUniqueFieldsInCache(JsonObjectConst obj, const std::string &selfId) { - // Scan schema for fields marked unique and ensure no other doc has same value - for (const auto &f : _schema.fields) { - if (!f.unique) continue; - // Only enforce on scalar types - if (f.type == FieldType::Object || f.type == FieldType::Array) continue; - JsonVariantConst v = obj[f.name]; - if (v.isNull()) continue; - for (const auto &kv : _docs) { - if (!selfId.empty() && kv.first == selfId) continue; - DocView other(kv.second, &_schema, nullptr, _db); - JsonVariantConst ov = other[f.name]; - if (!ov.isNull() && ov == v) { - return recordStatus({DbStatusCode::ValidationFailed, "unique constraint violated"}); - } - } - } - return recordStatus({DbStatusCode::Ok, ""}); + // Scan schema for fields marked unique and ensure no other doc has same value + for (const auto &f : _schema.fields) { + if (!f.unique) + continue; + // Only enforce on scalar types + if (f.type == FieldType::Object || f.type == FieldType::Array) + continue; + JsonVariantConst v = obj[f.name]; + if (v.isNull()) + continue; + for (const auto &kv : _docs) { + if (!selfId.empty() && kv.first == selfId) + continue; + DocView other(kv.second, &_schema, nullptr, _db); + JsonVariantConst ov = other[f.name]; + if (!ov.isNull() && ov == v) { + return recordStatus({DbStatusCode::ValidationFailed, "unique constraint violated"}); + } + } + } + return recordStatus({DbStatusCode::Ok, ""}); } DbStatus Collection::checkUniqueFieldsOnDisk(JsonObjectConst obj, const std::string &selfId) { @@ -80,26 +83,32 @@ DbStatus Collection::checkUniqueFieldsOnDisk(JsonObjectConst obj, const std::str return recordStatus({DbStatusCode::IoError, "open dir failed"}); } for (File f = d.openNextFile(); f; f = d.openNextFile()) { - if (f.isDirectory()) continue; + if (f.isDirectory()) + continue; String name = f.name(); std::string fname = name.c_str(); - if (fname.size() < 3 || fname.substr(fname.size() - 3) != ".mp") continue; + if (fname.size() < 3 || fname.substr(fname.size() - 3) != ".mp") + continue; ids.push_back(fname.substr(0, fname.size() - 3)); } } } for (const auto &docId : ids) { - if (!selfId.empty() && docId == selfId) continue; + if (!selfId.empty() && docId == selfId) + continue; auto rr = readDocFromFile(_baseDir, docId); if (!rr.status.ok()) { return rr.status; } DocView view(rr.value, &_schema, nullptr, _db); for (const auto &field : _schema.fields) { - if (!field.unique) continue; - if (field.type == FieldType::Object || field.type == FieldType::Array) continue; + if (!field.unique) + continue; + if (field.type == FieldType::Object || field.type == FieldType::Array) + continue; JsonVariantConst newVal = obj[field.name]; - if (newVal.isNull()) continue; + if (newVal.isNull()) + continue; JsonVariantConst existingVal = view[field.name]; if (!existingVal.isNull() && existingVal == newVal) { return recordStatus({DbStatusCode::ValidationFailed, "unique constraint violated"}); @@ -114,30 +123,30 @@ DbStatus Collection::checkUniqueFields(JsonObjectConst obj, const std::string &s } DbResult Collection::create(JsonObjectConst data) { - DbResult res{}; - JsonDocument workDoc; - workDoc.set(data); - JsonObject obj = workDoc.as(); - if (_schema.hasValidate()) { - auto ve = _schema.runPreSave(obj); - if (!ve.valid) { - res.status = {DbStatusCode::ValidationFailed, ve.message}; - recordStatus(res.status); - return res; - } - } - bool emit = false; - std::shared_ptr rec; - std::string id; - { - FrLock lk(_mu); - // Enforce unique constraints before creating the record - auto ust = checkUniqueFields(obj, ""); - if (!ust.ok()) { - res.status = ust; - recordStatus(res.status); - return res; - } + DbResult res{}; + JsonDocument workDoc; + workDoc.set(data); + JsonObject obj = workDoc.as(); + if (_schema.hasValidate()) { + auto ve = _schema.runPreSave(obj); + if (!ve.valid) { + res.status = {DbStatusCode::ValidationFailed, ve.message}; + recordStatus(res.status); + return res; + } + } + bool emit = false; + std::shared_ptr rec; + std::string id; + { + FrLock lk(_mu); + // Enforce unique constraints before creating the record + auto ust = checkUniqueFields(obj, ""); + if (!ust.ok()) { + res.status = ust; + recordStatus(res.status); + return res; + } rec = std::make_shared(_usePSRAMBuffers); rec->meta.createdAt = nowUtcMs(); rec->meta.updatedAt = rec->meta.createdAt; @@ -154,17 +163,18 @@ DbResult Collection::create(JsonObjectConst data) { return res; } - id = rec->meta.id; - _docs.emplace(id, rec); - _dirty = true; + id = rec->meta.id; + _docs.emplace(id, rec); + _dirty = true; res.status = {DbStatusCode::Ok, ""}; recordStatus(res.status); res.value = id; emit = true; - } + } if (emit) { - if (_db) _db->noteDocumentCreated(_name); + if (_db) + _db->noteDocumentCreated(_name); emitEvent(DBEventType::DocumentCreated); } return res; @@ -205,22 +215,23 @@ DbResult> Collection::createMany(JsonArrayConst arr) { DbResult> Collection::createMany(const JsonDocument &arrDoc) { if (!arrDoc.is()) { DbResult> res{}; - res.status = recordStatus({DbStatusCode::InvalidArgument, "document must be an array of objects"}); + res.status = + recordStatus({DbStatusCode::InvalidArgument, "document must be an array of objects"}); return res; } return createMany(arrDoc.as()); } DbResult Collection::findById(const std::string &id) { - { - FrLock lk(_mu); - auto it = _docs.find(id); - if (it != _docs.end()) { - DbStatus st{DbStatusCode::Ok, ""}; - recordStatus(st); - return {st, makeView(it->second)}; - } - } + { + FrLock lk(_mu); + auto it = _docs.find(id); + if (it != _docs.end()) { + DbStatus st{DbStatusCode::Ok, ""}; + recordStatus(st); + return {st, makeView(it->second)}; + } + } DbStatus st{DbStatusCode::NotFound, "document not found"}; recordStatus(st); @@ -228,17 +239,17 @@ DbResult Collection::findById(const std::string &id) { } DbResult> Collection::findMany(std::function pred) { - DbResult> res{}; - FrLock lk(_mu); - for (auto &kv : _docs) { - DocView v(kv.second, &_schema, nullptr, _db); - if (!pred || pred(v)) { - res.value.emplace_back(makeView(kv.second)); - } - } - res.status = {DbStatusCode::Ok, ""}; - recordStatus(res.status); - return res; + DbResult> res{}; + FrLock lk(_mu); + for (auto &kv : _docs) { + DocView v(kv.second, &_schema, nullptr, _db); + if (!pred || pred(v)) { + res.value.emplace_back(makeView(kv.second)); + } + } + res.status = {DbStatusCode::Ok, ""}; + recordStatus(res.status); + return res; } DbResult Collection::findOne(std::function pred) { @@ -251,9 +262,9 @@ DbResult Collection::findOne(std::function pred) return {st, makeView(kv.second)}; } } - DbStatus st{DbStatusCode::NotFound, "document not found"}; - recordStatus(st); - return {st, DocView(nullptr, &_schema, &_mu, _db, nullptr, _usePSRAMBuffers)}; + DbStatus st{DbStatusCode::NotFound, "document not found"}; + recordStatus(st); + return {st, DocView(nullptr, &_schema, &_mu, _db, nullptr, _usePSRAMBuffers)}; } DbResult Collection::findOne(const JsonDocument &filter) { @@ -269,9 +280,9 @@ DbResult Collection::findOne(const JsonDocument &filter) { return findOne(std::move(pred)); } -DbStatus Collection::updateOne(std::function pred, - std::function mutator, - bool create) { +DbStatus Collection::updateOne( + std::function pred, std::function mutator, bool create +) { bool updated = false; bool created = false; DbStatus st{DbStatusCode::NotFound, "document not found"}; @@ -296,7 +307,8 @@ DbStatus Collection::updateOne(std::function pred, } } st = v.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); // Only flag collection and emit update if record actually changed if (kv.second->meta.dirty) { _dirty = true; @@ -333,7 +345,8 @@ DbStatus Collection::updateOne(std::function pred, } } st = v.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); const std::string id = rec->meta.id; _docs.emplace(id, std::move(rec)); _dirty = true; @@ -341,7 +354,8 @@ DbStatus Collection::updateOne(std::function pred, st = {DbStatusCode::Ok, ""}; } if (created) { - if (_db) _db->noteDocumentCreated(_name); + if (_db) + _db->noteDocumentCreated(_name); emitEvent(DBEventType::DocumentCreated); } else if (updated) { emitEvent(DBEventType::DocumentUpdated); @@ -349,9 +363,7 @@ DbStatus Collection::updateOne(std::function pred, return recordStatus(st); } -DbStatus Collection::updateOne(const JsonDocument &filter, - const JsonDocument &patch, - bool create) { +DbStatus Collection::updateOne(const JsonDocument &filter, const JsonDocument &patch, bool create) { bool updated = false; bool created = false; DbStatus st{DbStatusCode::NotFound, "document not found"}; @@ -385,7 +397,8 @@ DbStatus Collection::updateOne(const JsonDocument &filter, } } st = v.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); if (kv.second->meta.dirty) { _dirty = true; updated = true; @@ -424,7 +437,8 @@ DbStatus Collection::updateOne(const JsonDocument &filter, } } st = v.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); const std::string id = rec->meta.id; _docs.emplace(id, std::move(rec)); _dirty = true; @@ -432,7 +446,8 @@ DbStatus Collection::updateOne(const JsonDocument &filter, st = {DbStatusCode::Ok, ""}; } if (created) { - if (_db) _db->noteDocumentCreated(_name); + if (_db) + _db->noteDocumentCreated(_name); emitEvent(DBEventType::DocumentCreated); } else if (updated) { emitEvent(DBEventType::DocumentUpdated); @@ -441,8 +456,8 @@ DbStatus Collection::updateOne(const JsonDocument &filter, } DbStatus Collection::updateById(const std::string &id, std::function mutator) { - bool updated = false; - DbStatus st{DbStatusCode::Ok, ""}; + bool updated = false; + DbStatus st{DbStatusCode::Ok, ""}; FrLock lk(_mu); auto it = _docs.find(id); if (it == _docs.end()) { @@ -465,21 +480,24 @@ DbStatus Collection::updateById(const std::string &id, std::functionsecond->meta.dirty) { _dirty = true; updated = true; } - if (updated) emitEvent(DBEventType::DocumentUpdated); + if (updated) + emitEvent(DBEventType::DocumentUpdated); return recordStatus(st); } DbStatus Collection::removeById(const std::string &id) { - bool removed = false; - DbStatus st{DbStatusCode::Ok, ""}; + bool removed = false; + DbStatus st{DbStatusCode::Ok, ""}; FrLock lk(_mu); auto it = _docs.find(id); - if (it == _docs.end()) return recordStatus({DbStatusCode::NotFound, "document not found"}); + if (it == _docs.end()) + return recordStatus({DbStatusCode::NotFound, "document not found"}); // Mark record as logically removed so outstanding views fail on commit it->second->meta.removed = true; _deletedIds.push_back(id); // ensure file removal on sync @@ -487,14 +505,15 @@ DbStatus Collection::removeById(const std::string &id) { _dirty = true; removed = true; if (removed) { - if (_db) _db->noteDocumentDeleted(_name); + if (_db) + _db->noteDocumentDeleted(_name); emitEvent(DBEventType::DocumentDeleted); } return recordStatus(st); } DbStatus Collection::writeDocToFile(const std::string &baseDir, const DocumentRecord &r) { - FrLock fs(g_fsMutex); + FrLock fs(g_fsMutex); std::string dir = joinPath(baseDir, _name); if (!fsEnsureDir(*_fs, dir)) { return recordStatus({DbStatusCode::IoError, "mkdir failed"}); @@ -503,7 +522,8 @@ DbStatus Collection::writeDocToFile(const std::string &baseDir, const DocumentRe std::string tmpPath = finalPath + ".tmp"; // Write to temp then rename for atomicity File f = _fs->open(tmpPath.c_str(), FILE_WRITE); - if (!f) return recordStatus({DbStatusCode::IoError, "open for write failed"}); + if (!f) + return recordStatus({DbStatusCode::IoError, "open for write failed"}); // Buffer writes to coalesce small chunks if any WriteBufferingStream bufferedFile(f, 256); size_t w = bufferedFile.write(r.msgpack.data(), r.msgpack.size()); @@ -520,21 +540,22 @@ DbStatus Collection::writeDocToFile(const std::string &baseDir, const DocumentRe return recordStatus({DbStatusCode::Ok, ""}); } -DbResult> Collection::readDocFromFile(const std::string &baseDir, const std::string &id) { - DbResult> res{}; - std::string path = joinPath(joinPath(baseDir, _name), id + ".mp"); - FrLock fs(g_fsMutex); - File f = _fs->open(path.c_str(), FILE_READ); - if (!f) { - res.status = {DbStatusCode::NotFound, "file not found"}; - recordStatus(res.status); - return res; - } - auto rec = std::make_shared(_usePSRAMBuffers); - rec->meta.id = id; - rec->meta.createdAt = nowUtcMs(); - rec->meta.updatedAt = rec->meta.createdAt; - rec->meta.dirty = false; +DbResult> +Collection::readDocFromFile(const std::string &baseDir, const std::string &id) { + DbResult> res{}; + std::string path = joinPath(joinPath(baseDir, _name), id + ".mp"); + FrLock fs(g_fsMutex); + File f = _fs->open(path.c_str(), FILE_READ); + if (!f) { + res.status = {DbStatusCode::NotFound, "file not found"}; + recordStatus(res.status); + return res; + } + auto rec = std::make_shared(_usePSRAMBuffers); + rec->meta.id = id; + rec->meta.createdAt = nowUtcMs(); + rec->meta.updatedAt = rec->meta.createdAt; + rec->meta.dirty = false; size_t sz = f.size(); rec->msgpack.resize(sz); @@ -545,10 +566,10 @@ DbResult> Collection::readDocFromFile(const std: recordStatus(res.status); return res; } - res.status = {DbStatusCode::Ok, ""}; - recordStatus(res.status); - res.value = std::move(rec); - return res; + res.status = {DbStatusCode::Ok, ""}; + recordStatus(res.status); + res.value = std::move(rec); + return res; } std::vector Collection::listDocumentIdsFromFs() const { @@ -556,14 +577,18 @@ std::vector Collection::listDocumentIdsFromFs() const { std::string dir = joinPath(_baseDir, _name); { FrLock fs(g_fsMutex); - if (!_fs->exists(dir.c_str())) return ids; + if (!_fs->exists(dir.c_str())) + return ids; File d = _fs->open(dir.c_str()); - if (!d || !d.isDirectory()) return ids; + if (!d || !d.isDirectory()) + return ids; for (File f = d.openNextFile(); f; f = d.openNextFile()) { - if (f.isDirectory()) continue; + if (f.isDirectory()) + continue; String name = f.name(); std::string fname = name.c_str(); - if (fname.size() < 3 || fname.substr(fname.size() - 3) != ".mp") continue; + if (fname.size() < 3 || fname.substr(fname.size() - 3) != ".mp") + continue; ids.push_back(fname.substr(0, fname.size() - 3)); } } @@ -579,7 +604,8 @@ DbStatus Collection::persistImmediate(const std::shared_ptr &rec return recordStatus({DbStatusCode::InvalidArgument, "no record"}); } auto st = writeDocToFile(_baseDir, *rec); - if (!st.ok()) return st; + if (!st.ok()) + return st; FrLock lk(_mu); rec->meta.dirty = false; rec->meta.removed = false; @@ -591,11 +617,13 @@ DocView Collection::makeView(std::shared_ptr rec) { return DocView(std::move(rec), &_schema, &_mu, _db, nullptr, _usePSRAMBuffers); } -DbStatus Collection::updateOneNoCache(std::function pred, - std::function mutator, - bool create, - bool &created, - bool &updated) { +DbStatus Collection::updateOneNoCache( + std::function pred, + std::function mutator, + bool create, + bool &created, + bool &updated +) { DbStatus st{DbStatusCode::NotFound, "document not found"}; auto ids = listDocumentIdsFromFs(); for (const auto &id : ids) { @@ -621,7 +649,8 @@ DbStatus Collection::updateOneNoCache(std::function pred, } } st = view.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); updated = true; return recordStatus(st); } @@ -649,18 +678,17 @@ DbStatus Collection::updateOneNoCache(std::function pred, } } st = view.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); created = true; return recordStatus(st); } return recordStatus(st); } -DbStatus Collection::updateOneJsonNoCache(const JsonDocument &filter, - const JsonDocument &patch, - bool create, - bool &created, - bool &updated) { +DbStatus Collection::updateOneJsonNoCache( + const JsonDocument &filter, const JsonDocument &patch, bool create, bool &created, bool &updated +) { DbStatus st{DbStatusCode::NotFound, "document not found"}; auto ids = listDocumentIdsFromFs(); for (const auto &id : ids) { @@ -677,7 +705,8 @@ DbStatus Collection::updateOneJsonNoCache(const JsonDocument &filter, break; } } - if (!match) continue; + if (!match) + continue; for (auto kvp : patch.as()) { view[kvp.key().c_str()].set(kvp.value()); } @@ -695,7 +724,8 @@ DbStatus Collection::updateOneJsonNoCache(const JsonDocument &filter, } } st = view.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); updated = true; return recordStatus(st); } @@ -726,18 +756,20 @@ DbStatus Collection::updateOneJsonNoCache(const JsonDocument &filter, } } st = view.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); created = true; return recordStatus(st); } return recordStatus(st); } -DbStatus Collection::updateByIdNoCache(const std::string &id, - std::function mutator, - bool &updated) { +DbStatus Collection::updateByIdNoCache( + const std::string &id, std::function mutator, bool &updated +) { auto rr = readDocFromFile(_baseDir, id); - if (!rr.status.ok()) return recordStatus(rr.status); + if (!rr.status.ok()) + return recordStatus(rr.status); auto view = makeView(rr.value); mutator(view); if (_schema.hasValidate()) { @@ -754,7 +786,8 @@ DbStatus Collection::updateByIdNoCache(const std::string &id, } } auto st = view.commit(); - if (!st.ok()) return recordStatus(st); + if (!st.ok()) + return recordStatus(st); updated = true; return recordStatus(st); } @@ -789,7 +822,8 @@ DbStatus Collection::loadFromFs(const std::string &baseDir) { return recordStatus({DbStatusCode::IoError, "open dir failed"}); } for (File f = d.openNextFile(); f; f = d.openNextFile()) { - if (f.isDirectory()) continue; + if (f.isDirectory()) + continue; String name = f.name(); f.close(); std::string n = name.c_str(); @@ -801,13 +835,13 @@ DbStatus Collection::loadFromFs(const std::string &baseDir) { } // Now, outside FS mutex, read each document file (readDocFromFile acquires FS mutex per file) - for (const auto &id : ids) { - auto rr = readDocFromFile(baseDir, id); - if (rr.status.ok()) { - _docs.emplace(id, std::move(rr.value)); - } - } - return recordStatus({DbStatusCode::Ok, ""}); + for (const auto &id : ids) { + auto rr = readDocFromFile(baseDir, id); + if (rr.status.ok()) { + _docs.emplace(id, std::move(rr.value)); + } + } + return recordStatus({DbStatusCode::Ok, ""}); } DbStatus Collection::flushDirtyToFs(const std::string &baseDir, bool &didWork) { @@ -819,7 +853,8 @@ DbStatus Collection::flushDirtyToFs(const std::string &baseDir, bool &didWork) { JsonDbVector bytes; explicit PendingWrite(bool usePSRAMBuffers) - : bytes(JsonDbAllocator(usePSRAMBuffers)) {} + : bytes(JsonDbAllocator(usePSRAMBuffers)) { + } }; std::vector toWrite; { @@ -854,13 +889,14 @@ DbStatus Collection::flushDirtyToFs(const std::string &baseDir, bool &didWork) { } // Flush writes - for (auto &pw : toWrite) { - DocumentRecord tmp(_usePSRAMBuffers); - tmp.meta.id = pw.id; - tmp.msgpack = std::move(pw.bytes); - auto st = writeDocToFile(baseDir, tmp); - if (!st.ok()) return recordStatus(st); - didWork = true; - } - return recordStatus({DbStatusCode::Ok, ""}); + for (auto &pw : toWrite) { + DocumentRecord tmp(_usePSRAMBuffers); + tmp.meta.id = pw.id; + tmp.msgpack = std::move(pw.bytes); + auto st = writeDocToFile(baseDir, tmp); + if (!st.ok()) + return recordStatus(st); + didWork = true; + } + return recordStatus({DbStatusCode::Ok, ""}); } diff --git a/src/esp_jsondb/collection/collection.h b/src/esp_jsondb/collection/collection.h index 49e942b..100657f 100644 --- a/src/esp_jsondb/collection/collection.h +++ b/src/esp_jsondb/collection/collection.h @@ -24,16 +24,22 @@ class ESPJsonDB; class Collection { public: - Collection(ESPJsonDB &db, - const std::string &name, - const Schema &schema, - std::string baseDir, - bool cacheEnabled, - bool usePSRAMBuffers, - fs::FS &fs); - const std::string &name() const { return _name; } - bool cacheEnabled() const { return _cacheEnabled; } - void setCacheEnabled(bool enabled); + Collection( + ESPJsonDB &db, + const std::string &name, + const Schema &schema, + std::string baseDir, + bool cacheEnabled, + bool usePSRAMBuffers, + fs::FS &fs + ); + const std::string &name() const { + return _name; + } + bool cacheEnabled() const { + return _cacheEnabled; + } + void setCacheEnabled(bool enabled); // Create from JsonObjectConst (validated) DbResult create(JsonObjectConst data); // returns new _id @@ -58,14 +64,15 @@ class Collection { DbResult findOne(const JsonDocument &filter); // Update the first document matching predicate; optionally create if not found - DbStatus updateOne(std::function pred, - std::function mutator, - bool create); + DbStatus updateOne( + std::function pred, + std::function mutator, + bool create + ); - // Update the first document matching a JSON filter with a JSON patch; optionally create if not found - DbStatus updateOne(const JsonDocument &filter, - const JsonDocument &patch, - bool create); + // Update the first document matching a JSON filter with a JSON patch; optionally create if not + // found + DbStatus updateOne(const JsonDocument &filter, const JsonDocument &patch, bool create); // Update single by id (mutate via view) DbStatus updateById(const std::string &id, std::function mutator); @@ -74,26 +81,36 @@ class Collection { DbStatus removeById(const std::string &id); // Bulk (cheap, flexible) - template - DbResult removeMany(Pred &&p); - - template , JsonDocument> && !std::is_same_v, JsonDocument>>> + template DbResult removeMany(Pred &&p); + + template < + typename Pred, + typename Mut, + typename = std::enable_if_t< + !std::is_same_v, JsonDocument> && + !std::is_same_v, JsonDocument>>> DbResult updateMany(Pred &&p, Mut &&m); - template , JsonDocument>>> + template < + typename Mut, + typename = std::enable_if_t, JsonDocument>>> DbResult updateMany(Mut &&m); - template , JsonDocument>>> + template < + typename Pred, + typename = std::enable_if_t, JsonDocument>>> DbResult updateMany(const JsonDocument &patch, Pred &&p); DbResult updateMany(const JsonDocument &patch, const JsonDocument &filter); // Dirty tracking - bool isDirty() const { return _dirty; } - void clearDirty() { _dirty = false; } + bool isDirty() const { + return _dirty; + } + void clearDirty() { + _dirty = false; + } // Persistence hooks used by ESPJsonDB DbStatus loadFromFs(const std::string &baseDir); @@ -101,161 +118,168 @@ class Collection { // written or removed during this call. DbStatus flushDirtyToFs(const std::string &baseDir, bool &didWork); - // Optional: stats - size_t size() const { return _docs.size(); } + // Optional: stats + size_t size() const { + return _docs.size(); + } - // Mark all records as removed (used when dropping a collection) - void markAllRemoved() { - FrLock lk(_mu); - for (auto &kv : _docs) { - kv.second->meta.removed = true; - } - } + // Mark all records as removed (used when dropping a collection) + void markAllRemoved() { + FrLock lk(_mu); + for (auto &kv : _docs) { + kv.second->meta.removed = true; + } + } private: ESPJsonDB *_db = nullptr; std::string _name; Schema _schema; - // Use shared_ptr to keep records alive while views exist + // Use shared_ptr to keep records alive while views exist std::map> _docs; bool _dirty = false; std::vector _deletedIds; // files to remove on next flush - FrMutex _mu; // guards _docs, _deletedIds + FrMutex _mu; // guards _docs, _deletedIds std::string _baseDir; bool _cacheEnabled = true; bool _usePSRAMBuffers = false; fs::FS *_fs = nullptr; // active filesystem (owned by caller) DbStatus writeDocToFile(const std::string &baseDir, const DocumentRecord &r); - DbResult> readDocFromFile(const std::string &baseDir, const std::string &id); - DbStatus checkUniqueFieldsInCache(JsonObjectConst obj, const std::string &selfId); - DbStatus checkUniqueFieldsOnDisk(JsonObjectConst obj, const std::string &selfId); - DbStatus checkUniqueFields(JsonObjectConst obj, const std::string &selfId); + DbResult> + readDocFromFile(const std::string &baseDir, const std::string &id); + DbStatus checkUniqueFieldsInCache(JsonObjectConst obj, const std::string &selfId); + DbStatus checkUniqueFieldsOnDisk(JsonObjectConst obj, const std::string &selfId); + DbStatus checkUniqueFields(JsonObjectConst obj, const std::string &selfId); std::vector listDocumentIdsFromFs() const; - DbStatus persistImmediate(const std::shared_ptr &rec); - size_t countDocumentsFromFs() const; - DocView makeView(std::shared_ptr rec); - DbStatus updateOneNoCache(std::function pred, - std::function mutator, - bool create, - bool &created, - bool &updated); - DbStatus updateOneJsonNoCache(const JsonDocument &filter, - const JsonDocument &patch, - bool create, - bool &created, - bool &updated); - DbStatus updateByIdNoCache(const std::string &id, - std::function mutator, - bool &updated); - DbStatus removeByIdNoCache(const std::string &id, bool &removed); + DbStatus persistImmediate(const std::shared_ptr &rec); + size_t countDocumentsFromFs() const; + DocView makeView(std::shared_ptr rec); + DbStatus updateOneNoCache( + std::function pred, + std::function mutator, + bool create, + bool &created, + bool &updated + ); + DbStatus updateOneJsonNoCache( + const JsonDocument &filter, + const JsonDocument &patch, + bool create, + bool &created, + bool &updated + ); + DbStatus + updateByIdNoCache(const std::string &id, std::function mutator, bool &updated); + DbStatus removeByIdNoCache(const std::string &id, bool &removed); DbStatus recordStatus(const DbStatus &st) const; void emitEvent(DBEventType ev) const; void noteDeletedInDiag(size_t count) const; }; -template -DbResult Collection::removeMany(Pred &&p) { - DbResult res{}; - std::vector toErase; - { - FrLock lk(_mu); - toErase.reserve(_docs.size()); - for (auto &kv : _docs) { - DocView v(kv.second, &_schema, nullptr, _db); - if (p(v)) { - toErase.push_back(kv.first); - } - } - for (auto &id : toErase) { - auto it = _docs.find(id); - if (it != _docs.end()) { - it->second->meta.removed = true; - _deletedIds.push_back(id); - _docs.erase(it); - } - } - if (!toErase.empty()) _dirty = true; - } - res.value = toErase.size(); - noteDeletedInDiag(res.value); - res.status = {DbStatusCode::Ok, ""}; - recordStatus(res.status); - return res; +template DbResult Collection::removeMany(Pred &&p) { + DbResult res{}; + std::vector toErase; + { + FrLock lk(_mu); + toErase.reserve(_docs.size()); + for (auto &kv : _docs) { + DocView v(kv.second, &_schema, nullptr, _db); + if (p(v)) { + toErase.push_back(kv.first); + } + } + for (auto &id : toErase) { + auto it = _docs.find(id); + if (it != _docs.end()) { + it->second->meta.removed = true; + _deletedIds.push_back(id); + _docs.erase(it); + } + } + if (!toErase.empty()) + _dirty = true; + } + res.value = toErase.size(); + noteDeletedInDiag(res.value); + res.status = {DbStatusCode::Ok, ""}; + recordStatus(res.status); + return res; } template DbResult Collection::updateMany(Pred &&p, Mut &&m) { - DbResult res{}; - size_t count = 0; - FrLock lk(_mu); - for (auto &kv : _docs) { - DocView v(kv.second, &_schema, nullptr, _db); - if (p(v)) { - m(v); - if (_schema.hasValidate()) { - auto obj = v.asObject(); - auto ve = _schema.runPreSave(obj); - if (!ve.valid) { - v.discard(); - continue; - } - // Unique constraints - auto ust = checkUniqueFields(obj, kv.second->meta.id); - if (!ust.ok()) { - v.discard(); - continue; - } - } - auto st = v.commit(); - if (st.ok()) { - ++count; - } - } - } - if (count) _dirty = true; - res.status = {DbStatusCode::Ok, ""}; - recordStatus(res.status); - res.value = count; - return res; + DbResult res{}; + size_t count = 0; + FrLock lk(_mu); + for (auto &kv : _docs) { + DocView v(kv.second, &_schema, nullptr, _db); + if (p(v)) { + m(v); + if (_schema.hasValidate()) { + auto obj = v.asObject(); + auto ve = _schema.runPreSave(obj); + if (!ve.valid) { + v.discard(); + continue; + } + // Unique constraints + auto ust = checkUniqueFields(obj, kv.second->meta.id); + if (!ust.ok()) { + v.discard(); + continue; + } + } + auto st = v.commit(); + if (st.ok()) { + ++count; + } + } + } + if (count) + _dirty = true; + res.status = {DbStatusCode::Ok, ""}; + recordStatus(res.status); + res.value = count; + return res; } -template -DbResult Collection::updateMany(Mut &&m) { - DbResult res{}; - size_t count = 0; - FrLock lk(_mu); - for (auto &kv : _docs) { - DocView v(kv.second, &_schema, nullptr, _db); - if (m(v)) { - if (_schema.hasValidate()) { - auto obj = v.asObject(); - auto ve = _schema.runPreSave(obj); - if (!ve.valid) { - v.discard(); - continue; - } - // Unique constraints - auto ust = checkUniqueFields(obj, kv.second->meta.id); - if (!ust.ok()) { - v.discard(); - continue; - } - } - auto st = v.commit(); - if (st.ok()) { - ++count; - } - } else { - v.discard(); - } - } - if (count) _dirty = true; - res.status = {DbStatusCode::Ok, ""}; - recordStatus(res.status); - res.value = count; - return res; +template DbResult Collection::updateMany(Mut &&m) { + DbResult res{}; + size_t count = 0; + FrLock lk(_mu); + for (auto &kv : _docs) { + DocView v(kv.second, &_schema, nullptr, _db); + if (m(v)) { + if (_schema.hasValidate()) { + auto obj = v.asObject(); + auto ve = _schema.runPreSave(obj); + if (!ve.valid) { + v.discard(); + continue; + } + // Unique constraints + auto ust = checkUniqueFields(obj, kv.second->meta.id); + if (!ust.ok()) { + v.discard(); + continue; + } + } + auto st = v.commit(); + if (st.ok()) { + ++count; + } + } else { + v.discard(); + } + } + if (count) + _dirty = true; + res.status = {DbStatusCode::Ok, ""}; + recordStatus(res.status); + res.value = count; + return res; } template @@ -268,7 +292,8 @@ DbResult Collection::updateMany(const JsonDocument &patch, Pred &&p) { return updateMany(std::forward(p), mut); } -inline DbResult Collection::updateMany(const JsonDocument &patch, const JsonDocument &filter) { +inline DbResult +Collection::updateMany(const JsonDocument &patch, const JsonDocument &filter) { auto pred = [&](const DocView &v) { for (auto kv : filter.as()) { if (v[kv.key().c_str()] != kv.value()) { diff --git a/src/esp_jsondb/db.cpp b/src/esp_jsondb/db.cpp index af61fea..a242283 100644 --- a/src/esp_jsondb/db.cpp +++ b/src/esp_jsondb/db.cpp @@ -17,7 +17,8 @@ DbStatus ESPJsonDB::ensureFsReady() { return setLastError({DbStatusCode::InvalidArgument, "filesystem handle is null"}); } if (_cfg.initFileSystem && _fs == &LittleFS) { - if (!LittleFS.begin(_cfg.formatOnFail, "/littlefs", _cfg.maxOpenFiles, _cfg.partitionLabel)) { + if (!LittleFS + .begin(_cfg.formatOnFail, "/littlefs", _cfg.maxOpenFiles, _cfg.partitionLabel)) { return setLastError({DbStatusCode::IoError, "LittleFS.begin failed"}); } } @@ -44,18 +45,23 @@ uint32_t ESPJsonDB::stackBytesToWords(uint32_t stackBytes) { bool ESPJsonDB::createTask(TaskFunction_t entry, const char *name, TaskHandle_t &outHandle) { const uint32_t stackDepthWords = stackBytesToWords(_cfg.stackSize); - BaseType_t rc = xTaskCreatePinnedToCore(entry, - name, - stackDepthWords, - this, - _cfg.priority, - &outHandle, - _cfg.coreId); + BaseType_t rc = xTaskCreatePinnedToCore( + entry, + name, + stackDepthWords, + this, + _cfg.priority, + &outHandle, + _cfg.coreId + ); return rc == pdPASS; } -void ESPJsonDB::stopTask(TaskHandle_t &taskHandle, std::atomic &stopRequested, std::atomic &taskExited) { - if (taskHandle == nullptr) return; +void ESPJsonDB::stopTask( + TaskHandle_t &taskHandle, std::atomic &stopRequested, std::atomic &taskExited +) { + if (taskHandle == nullptr) + return; stopRequested.store(true, std::memory_order_release); const uint32_t startMs = millis(); while (!taskExited.load(std::memory_order_acquire)) { @@ -95,7 +101,8 @@ void ESPJsonDB::deinit() { stopSyncTaskUnlocked(); for (auto &kv : _cols) { - if (kv.second) kv.second->markAllRemoved(); + if (kv.second) + kv.second->markAllRemoved(); } _cols.clear(); _schemas.clear(); @@ -132,7 +139,9 @@ DbStatus ESPJsonDB::init(const char *baseDir, const ESPJsonDBConfig &cfg) { deinit(); } if (!cfg.cacheEnabled) { - return setLastError({DbStatusCode::InvalidArgument, "cacheEnabled=false is no longer supported"}); + return setLastError( + {DbStatusCode::InvalidArgument, "cacheEnabled=false is no longer supported"} + ); } _initialized.store(false, std::memory_order_release); _baseDir = baseDir ? baseDir : std::string("/db"); @@ -154,7 +163,8 @@ DbStatus ESPJsonDB::init(const char *baseDir, const ESPJsonDBConfig &cfg) { _syncCompletedSeq.store(0, std::memory_order_release); _fileUploadStopRequested.store(false, std::memory_order_release); auto st = ensureFsReady(); - if (!st.ok()) return st; + if (!st.ok()) + return st; { FrLock lk(_mu); @@ -218,8 +228,12 @@ void ESPJsonDB::onError(const std::function &cb) { void ESPJsonDB::onSync(const std::function &cb) { // Wrap sync-only callback into event form - if (!cb) return; - onEvent([cb](DBEventType ev) { if (ev == DBEventType::Sync) cb(); }); + if (!cb) + return; + onEvent([cb](DBEventType ev) { + if (ev == DBEventType::Sync) + cb(); + }); } DbStatus ESPJsonDB::dropCollection(const std::string &name) { @@ -246,7 +260,8 @@ DbStatus ESPJsonDB::dropCollection(const std::string &name) { auto dit = _diagCache.docsPerCollection.find(name); if (dit != _diagCache.docsPerCollection.end()) { _diagCache.docsPerCollection.erase(dit); - if (_diagCache.collections > 0) --_diagCache.collections; + if (_diagCache.collections > 0) + --_diagCache.collections; } _diagCache.lastRefreshMs = millis(); } @@ -284,9 +299,11 @@ DbResult ESPJsonDB::collection(const std::string &name) { { FrLock lk(_mu); auto sit = _schemas.find(name); - if (sit != _schemas.end()) sc = sit->second; + if (sit != _schemas.end()) + sc = sit->second; } - auto col = std::make_unique(*this, name, sc, _baseDir, true, _cfg.usePSRAMBuffers, *_fs); + auto col = + std::make_unique(*this, name, sc, _baseDir, true, _cfg.usePSRAMBuffers, *_fs); Collection *ptr = nullptr; bool created = false; { @@ -295,7 +312,8 @@ DbResult ESPJsonDB::collection(const std::string &name) { created = inserted; ptr = it->second.get(); } - if (created) emitEvent(DBEventType::CollectionCreated); + if (created) + emitEvent(DBEventType::CollectionCreated); res.status = setLastError({DbStatusCode::Ok, ""}); res.value = ptr; return res; @@ -330,7 +348,8 @@ DbResult ESPJsonDB::create(const std::string &name, const JsonDocum return create(name, doc.as()); } -DbResult> ESPJsonDB::createMany(const std::string &name, JsonArrayConst arr) { +DbResult> +ESPJsonDB::createMany(const std::string &name, JsonArrayConst arr) { DbResult> res{}; auto cr = collection(name); if (!cr.status.ok()) { @@ -340,10 +359,12 @@ DbResult> ESPJsonDB::createMany(const std::string &name return cr.value->createMany(arr); } -DbResult> ESPJsonDB::createMany(const std::string &name, const JsonDocument &arrDoc) { +DbResult> +ESPJsonDB::createMany(const std::string &name, const JsonDocument &arrDoc) { if (!arrDoc.is()) { DbResult> res{}; - res.status = setLastError({DbStatusCode::InvalidArgument, "document must be an array of objects"}); + res.status = + setLastError({DbStatusCode::InvalidArgument, "document must be an array of objects"}); return res; } return createMany(name, arrDoc.as()); @@ -358,8 +379,8 @@ DbResult ESPJsonDB::findById(const std::string &name, const std::string return cr.value->findById(id); } -DbResult> ESPJsonDB::findMany(const std::string &name, - std::function pred) { +DbResult> +ESPJsonDB::findMany(const std::string &name, std::function pred) { DbResult> res{}; auto cr = collection(name); if (!cr.status.ok()) { @@ -369,7 +390,8 @@ DbResult> ESPJsonDB::findMany(const std::string &name, return cr.value->findMany(std::move(pred)); } -DbResult ESPJsonDB::findOne(const std::string &name, std::function pred) { +DbResult +ESPJsonDB::findOne(const std::string &name, std::function pred) { auto cr = collection(name); if (!cr.status.ok()) { // Return placeholder DocView; caller should check status before use @@ -387,10 +409,12 @@ DbResult ESPJsonDB::findOne(const std::string &name, const JsonDocument return cr.value->findOne(filter); } -DbStatus ESPJsonDB::updateOne(const std::string &name, - std::function pred, - std::function mutator, - bool create) { +DbStatus ESPJsonDB::updateOne( + const std::string &name, + std::function pred, + std::function mutator, + bool create +) { auto cr = collection(name); if (!cr.status.ok()) { return cr.status; @@ -398,10 +422,9 @@ DbStatus ESPJsonDB::updateOne(const std::string &name, return cr.value->updateOne(std::move(pred), std::move(mutator), create); } -DbStatus ESPJsonDB::updateOne(const std::string &name, - const JsonDocument &filter, - const JsonDocument &patch, - bool create) { +DbStatus ESPJsonDB::updateOne( + const std::string &name, const JsonDocument &filter, const JsonDocument &patch, bool create +) { auto cr = collection(name); if (!cr.status.ok()) { return cr.status; @@ -409,7 +432,9 @@ DbStatus ESPJsonDB::updateOne(const std::string &name, return cr.value->updateOne(filter, patch, create); } -DbStatus ESPJsonDB::updateById(const std::string &name, const std::string &id, std::function mutator) { +DbStatus ESPJsonDB::updateById( + const std::string &name, const std::string &id, std::function mutator +) { auto cr = collection(name); if (!cr.status.ok()) { return cr.status; @@ -425,9 +450,9 @@ DbStatus ESPJsonDB::removeById(const std::string &name, const std::string &id) { return cr.value->removeById(id); } -DbResult ESPJsonDB::updateMany(const std::string &collectionName, - const JsonDocument &patch, - const JsonDocument &filter) { +DbResult ESPJsonDB::updateMany( + const std::string &collectionName, const JsonDocument &patch, const JsonDocument &filter +) { DbResult res{}; auto cr = collection(collectionName); if (!cr.status.ok()) { @@ -514,13 +539,15 @@ DbStatus ESPJsonDB::runSyncPass() { if (!st.ok()) { return setLastError(st); } - if (changed) anyChanges = true; + if (changed) + anyChanges = true; } // Only refresh diagnostics and emit Sync if there were actual changes if (anyChanges) { emitEvent(DBEventType::Sync); } - if (!finalStatus.ok()) return finalStatus; + if (!finalStatus.ok()) + return finalStatus; return setLastError({DbStatusCode::Ok, ""}); } @@ -549,7 +576,8 @@ void ESPJsonDB::syncTaskLoop() { (void)runSyncPass(); uint32_t completed = _syncCompletedSeq.load(std::memory_order_acquire); while (completed < targetSeq && - !_syncCompletedSeq.compare_exchange_weak(completed, targetSeq, std::memory_order_acq_rel)) { + !_syncCompletedSeq + .compare_exchange_weak(completed, targetSeq, std::memory_order_acq_rel)) { } } _syncTaskExited.store(true, std::memory_order_release); @@ -557,7 +585,8 @@ void ESPJsonDB::syncTaskLoop() { } void ESPJsonDB::startSyncTaskUnlocked() { - if (_syncTask != nullptr) return; + if (_syncTask != nullptr) + return; _syncStopRequested.store(false, std::memory_order_release); _syncTaskExited.store(false, std::memory_order_release); _syncKickRequested.store(false, std::memory_order_release); @@ -572,16 +601,23 @@ void ESPJsonDB::startSyncTaskUnlocked() { void ESPJsonDB::stopSyncTaskUnlocked() { stopTask(_syncTask, _syncStopRequested, _syncTaskExited); _syncKickRequested.store(false, std::memory_order_release); - _syncCompletedSeq.store(_syncRequestSeq.load(std::memory_order_acquire), std::memory_order_release); + _syncCompletedSeq.store( + _syncRequestSeq.load(std::memory_order_acquire), + std::memory_order_release + ); } namespace { -static void listDirEntries(fs::FS &fsImpl, const std::string &dir, std::vector> &out) { +static void listDirEntries( + fs::FS &fsImpl, const std::string &dir, std::vector> &out +) { FrLock fs(g_fsMutex); - if (!fsImpl.exists(dir.c_str())) return; + if (!fsImpl.exists(dir.c_str())) + return; File d = fsImpl.open(dir.c_str()); if (!d || !d.isDirectory()) { - if (d) d.close(); + if (d) + d.close(); return; } for (File f = d.openNextFile(); f; f = d.openNextFile()) { @@ -600,7 +636,8 @@ static void removeTree(fs::FS &fsImpl, const std::string &path) { bool isDir = false; { FrLock fs(g_fsMutex); - if (!fsImpl.exists(path.c_str())) return; + if (!fsImpl.exists(path.c_str())) + return; File f = fsImpl.open(path.c_str()); if (f) { isDir = f.isDirectory(); @@ -635,9 +672,11 @@ static void removeTree(fs::FS &fsImpl, const std::string &path) { DbStatus ESPJsonDB::removeCollectionDir(const std::string &name) { std::string dir = _baseDir; - if (!dir.empty() && dir.back() != '/') dir += '/'; + if (!dir.empty() && dir.back() != '/') + dir += '/'; dir += name; - if (_fs) removeTree(*_fs, dir); + if (_fs) + removeTree(*_fs, dir); return setLastError({DbStatusCode::Ok, ""}); } @@ -648,7 +687,8 @@ void ESPJsonDB::emitEvent(DBEventType ev) { callbacks = _eventCbs; // copy snapshot } for (auto &fn : callbacks) { - if (fn) fn(ev); + if (fn) + fn(ev); } } @@ -659,14 +699,17 @@ void ESPJsonDB::emitError(const DbStatus &st) { callbacks = _errorCbs; // copy snapshot } for (auto &fn : callbacks) { - if (fn) fn(st); + if (fn) + fn(st); } } void ESPJsonDB::noteDocumentCreated(const std::string &collectionName, uint32_t count) { - if (collectionName.empty() || count == 0) return; + if (collectionName.empty() || count == 0) + return; FrLock lk(_mu); - if (!_diagCachePrimed) return; + if (!_diagCachePrimed) + return; uint32_t &docs = _diagCache.docsPerCollection[collectionName]; if (docs == 0) { ++_diagCache.collections; @@ -676,14 +719,18 @@ void ESPJsonDB::noteDocumentCreated(const std::string &collectionName, uint32_t } void ESPJsonDB::noteDocumentDeleted(const std::string &collectionName, uint32_t count) { - if (collectionName.empty() || count == 0) return; + if (collectionName.empty() || count == 0) + return; FrLock lk(_mu); - if (!_diagCachePrimed) return; + if (!_diagCachePrimed) + return; auto it = _diagCache.docsPerCollection.find(collectionName); - if (it == _diagCache.docsPerCollection.end()) return; + if (it == _diagCache.docsPerCollection.end()) + return; if (it->second <= count) { _diagCache.docsPerCollection.erase(it); - if (_diagCache.collections > 0) --_diagCache.collections; + if (_diagCache.collections > 0) + --_diagCache.collections; } else { it->second -= count; } @@ -692,7 +739,8 @@ void ESPJsonDB::noteDocumentDeleted(const std::string &collectionName, uint32_t DbStatus ESPJsonDB::preloadCollectionsFromFs() { auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); if (!_fs) { return setLastError({DbStatusCode::IoError, "filesystem not ready"}); } @@ -705,7 +753,8 @@ DbStatus ESPJsonDB::preloadCollectionsFromFs() { } File base = _fs->open(_baseDir.c_str()); if (!base || !base.isDirectory()) { - if (base) base.close(); + if (base) + base.close(); return setLastError({DbStatusCode::IoError, "open base dir failed"}); } for (File f = base.openNextFile(); f; f = base.openNextFile()) { @@ -717,8 +766,10 @@ DbStatus ESPJsonDB::preloadCollectionsFromFs() { f.close(); std::string name = raw.c_str(); auto slash = name.find_last_of('/'); - if (slash != std::string::npos) name = name.substr(slash + 1); - if (name.empty() || isReservedName(name)) continue; + if (slash != std::string::npos) + name = name.substr(slash + 1); + if (name.empty() || isReservedName(name)) + continue; names.push_back(name); } base.close(); @@ -730,13 +781,17 @@ DbStatus ESPJsonDB::preloadCollectionsFromFs() { Schema sc{}; { FrLock lk(_mu); - if (_cols.find(name) != _cols.end()) continue; + if (_cols.find(name) != _cols.end()) + continue; auto sit = _schemas.find(name); - if (sit != _schemas.end()) sc = sit->second; + if (sit != _schemas.end()) + sc = sit->second; } - auto col = std::make_unique(*this, name, sc, _baseDir, true, _cfg.usePSRAMBuffers, *_fs); + auto col = std::make_unique< + Collection>(*this, name, sc, _baseDir, true, _cfg.usePSRAMBuffers, *_fs); auto st = col->loadFromFs(_baseDir); - if (!st.ok()) return setLastError(st); + if (!st.ok()) + return setLastError(st); { FrLock lk(_mu); _cols.emplace(name, std::move(col)); @@ -763,7 +818,8 @@ JsonDocument ESPJsonDB::getDiag() { cached = _diagCache.docsPerCollection; // copy lastRefreshMs = _diagCache.lastRefreshMs; for (auto &kv : _cols) { - if (isReservedName(kv.first)) continue; + if (isReservedName(kv.first)) + continue; live[kv.first] = kv.second ? static_cast(kv.second->size()) : 0u; } cfgCopy = _cfg; @@ -779,16 +835,20 @@ JsonDocument ESPJsonDB::getDiag() { seen[kv.first] = true; } for (auto &kv : cached) { - if (isReservedName(kv.first)) continue; - if (seen.find(kv.first) != seen.end()) continue; + if (isReservedName(kv.first)) + continue; + if (seen.find(kv.first) != seen.end()) + continue; per[kv.first.c_str()] = kv.second; } // Collections = number of unique keys uint32_t collections = static_cast(seen.size()); for (auto &kv : cached) { - if (isReservedName(kv.first)) continue; - if (seen.find(kv.first) == seen.end()) ++collections; + if (isReservedName(kv.first)) + continue; + if (seen.find(kv.first) == seen.end()) + ++collections; } doc["collections"] = collections; doc["lastRefreshMs"] = lastRefreshMs; // for visibility (optional) @@ -824,7 +884,8 @@ DbStatus ESPJsonDB::dropAll() { // Clear in-memory state for (auto &kv : _cols) { - if (kv.second) kv.second->markAllRemoved(); + if (kv.second) + kv.second->markAllRemoved(); } _cols.clear(); _colsToDelete.clear(); @@ -854,11 +915,13 @@ std::vector ESPJsonDB::getAllCollectionName() { { FrLock lk(_mu); for (auto &kv : _cols) { - if (isReservedName(kv.first)) continue; + if (isReservedName(kv.first)) + continue; seen[kv.first] = true; } for (auto &kv : _diagCache.docsPerCollection) { - if (isReservedName(kv.first)) continue; + if (isReservedName(kv.first)) + continue; seen[kv.first] = true; } } @@ -874,7 +937,9 @@ DbStatus ESPJsonDB::changeConfig(const ESPJsonDBConfig &cfg) { return setLastError(ready); } if (!cfg.cacheEnabled) { - return setLastError({DbStatusCode::InvalidArgument, "cacheEnabled=false is no longer supported"}); + return setLastError( + {DbStatusCode::InvalidArgument, "cacheEnabled=false is no longer supported"} + ); } bool doColdSync = cfg.coldSync; // Stop existing task if running and apply new config @@ -889,11 +954,13 @@ DbStatus ESPJsonDB::changeConfig(const ESPJsonDBConfig &cfg) { _cfg = cfg; _cfg.cacheEnabled = true; for (auto &kv : _cols) { - if (kv.second) kv.second->setCacheEnabled(_cfg.cacheEnabled); + if (kv.second) + kv.second->setCacheEnabled(_cfg.cacheEnabled); } } auto fsStatus = ensureFsReady(); - if (!fsStatus.ok()) return fsStatus; + if (!fsStatus.ok()) + return fsStatus; if (doColdSync) { auto preloadStatus = preloadCollectionsFromFs(); if (!preloadStatus.ok()) { @@ -922,22 +989,26 @@ JsonDocument ESPJsonDB::getSnapshot() { std::vector> colDirs; listDirEntries(*_fs, _baseDir, colDirs); for (auto &cd : colDirs) { - if (!cd.second) continue; // not a directory + if (!cd.second) + continue; // not a directory const std::string &full = cd.first; auto p = full.find_last_of('/'); std::string colName = (p == std::string::npos) ? full : full.substr(p + 1); - if (isReservedName(colName)) continue; + if (isReservedName(colName)) + continue; // Iterate files in collection dir std::vector> files; listDirEntries(*_fs, full, files); JsonArray arr = colsObj[colName.c_str()].to(); for (auto &fe : files) { - if (fe.second) continue; // skip subdirectories + if (fe.second) + continue; // skip subdirectories const std::string &fpath = fe.first; // expect .mp auto dot = fpath.find_last_of('.'); - if (dot == std::string::npos || fpath.substr(dot) != ".mp") continue; + if (dot == std::string::npos || fpath.substr(dot) != ".mp") + continue; auto slash = fpath.find_last_of('/'); std::string fname = (slash == std::string::npos) ? fpath : fpath.substr(slash + 1); std::string id = fname.substr(0, fname.size() - 3); @@ -955,7 +1026,8 @@ JsonDocument ESPJsonDB::getSnapshot() { derr = DeserializationError::Code::InvalidInput; } } - if (derr) continue; // skip unreadable + if (derr) + continue; // skip unreadable JsonObject obj = arr.add(); obj.set(tmp.as()); obj["_id"] = id.c_str(); @@ -977,19 +1049,24 @@ DbStatus ESPJsonDB::restoreFromSnapshot(const JsonDocument &snapshot) { // Drop everything first auto st = dropAll(); - if (!st.ok()) return st; + if (!st.ok()) + return st; // For each collection, recreate documents for (auto kv : cols) { const char *colName = kv.key().c_str(); - if (!colName || !*colName) continue; - if (isReservedName(colName)) continue; + if (!colName || !*colName) + continue; + if (isReservedName(colName)) + continue; JsonArrayConst arr = kv.value().as(); - if (arr.isNull()) continue; + if (arr.isNull()) + continue; // Ensure directory exists std::string dir = _baseDir; - if (!dir.empty() && dir.back() != '/') dir += '/'; + if (!dir.empty() && dir.back() != '/') + dir += '/'; dir += colName; { FrLock fs(g_fsMutex); @@ -997,8 +1074,10 @@ DbStatus ESPJsonDB::restoreFromSnapshot(const JsonDocument &snapshot) { } for (JsonObjectConst obj : arr) { - const char *id = obj["_id"].is() ? obj["_id"].as() : nullptr; - if (!id || !*id) continue; + const char *id = + obj["_id"].is() ? obj["_id"].as() : nullptr; + if (!id || !*id) + continue; // Copy object without _id into a temp doc JsonDocument tmp; @@ -1010,7 +1089,8 @@ DbStatus ESPJsonDB::restoreFromSnapshot(const JsonDocument &snapshot) { JsonDbVector bytes{JsonDbAllocator(_cfg.usePSRAMBuffers)}; bytes.resize(sz); size_t written = serializeMsgPack(tmp, bytes.data(), bytes.size()); - if (written != sz) return setLastError({DbStatusCode::IoError, "serialize msgpack failed"}); + if (written != sz) + return setLastError({DbStatusCode::IoError, "serialize msgpack failed"}); // Write file atomically std::string finalPath = dir + "/" + std::string(id) + ".mp"; @@ -1018,7 +1098,8 @@ DbStatus ESPJsonDB::restoreFromSnapshot(const JsonDocument &snapshot) { { FrLock fs(g_fsMutex); File f = _fs->open(tmpPath.c_str(), FILE_WRITE); - if (!f) return setLastError({DbStatusCode::IoError, "open for write failed"}); + if (!f) + return setLastError({DbStatusCode::IoError, "open for write failed"}); WriteBufferingStream bufferedFile(f, 256); size_t w = bufferedFile.write(bytes.data(), bytes.size()); bufferedFile.flush(); @@ -1043,7 +1124,8 @@ DbStatus ESPJsonDB::restoreFromSnapshot(const JsonDocument &snapshot) { // Private: expensive FS scan; called on init and after successful sync void ESPJsonDB::refreshDiagFromFs() { - if (!_fs) return; + if (!_fs) + return; std::map perCol; uint32_t colCount = 0; { @@ -1061,7 +1143,8 @@ void ESPJsonDB::refreshDiagFromFs() { String colName = f.name(); std::string cname = colName.c_str(); auto slash = cname.find_last_of('/'); - if (slash != std::string::npos) cname = cname.substr(slash + 1); + if (slash != std::string::npos) + cname = cname.substr(slash + 1); if (isReservedName(cname)) { f.close(); continue; @@ -1070,7 +1153,8 @@ void ESPJsonDB::refreshDiagFromFs() { // Count .mp files in collection dir std::string dirPath = _baseDir; - if (!dirPath.empty() && dirPath.back() != '/') dirPath += '/'; + if (!dirPath.empty() && dirPath.back() != '/') + dirPath += '/'; dirPath += cname; File colDir = _fs->open(dirPath.c_str()); if (!colDir || !colDir.isDirectory()) { @@ -1086,7 +1170,8 @@ void ESPJsonDB::refreshDiagFromFs() { String fn = df.name(); df.close(); std::string n = fn.c_str(); - if (n.size() >= 3 && n.substr(n.size() - 3) == ".mp") ++cnt; + if (n.size() >= 3 && n.substr(n.size() - 3) == ".mp") + ++cnt; } colDir.close(); // Only include collections that currently have at least one document file diff --git a/src/esp_jsondb/db.h b/src/esp_jsondb/db.h index cd9fab6..65468a9 100644 --- a/src/esp_jsondb/db.h +++ b/src/esp_jsondb/db.h @@ -3,12 +3,12 @@ #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -24,7 +24,9 @@ class ESPJsonDB { ~ESPJsonDB(); DbStatus init(const char *baseDir = "/db", const ESPJsonDBConfig &cfg = {}); void deinit(); - bool isInitialized() const { return _initialized.load(std::memory_order_acquire); } + bool isInitialized() const { + return _initialized.load(std::memory_order_acquire); + } DbStatus registerSchema(const std::string &name, const Schema &s); DbStatus unRegisterSchema(const std::string &name); DbStatus dropCollection(const std::string &name); @@ -60,36 +62,49 @@ class ESPJsonDB { DbResult create(const std::string &collectionName, const JsonDocument &doc); // Convenience: bulk create documents in a collection - DbResult> createMany(const std::string &collectionName, JsonArrayConst arr); - DbResult> createMany(const std::string &collectionName, const JsonDocument &arrDoc); + DbResult> + createMany(const std::string &collectionName, JsonArrayConst arr); + DbResult> + createMany(const std::string &collectionName, const JsonDocument &arrDoc); // Convenience: find a document by _id in the given collection DbResult findById(const std::string &collectionName, const std::string &id); // Convenience: find documents matching predicate in the given collection - DbResult> findMany(const std::string &collectionName, - std::function pred); + DbResult> + findMany(const std::string &collectionName, std::function pred); // Convenience: find the first document matching predicate in the given collection - DbResult findOne(const std::string &collectionName, std::function pred); + DbResult + findOne(const std::string &collectionName, std::function pred); // Convenience: find the first document matching a JSON filter in the given collection DbResult findOne(const std::string &collectionName, const JsonDocument &filter); - // Convenience: update the first match (predicate + mutator). If create=true, creates new when none found - DbStatus updateOne(const std::string &collectionName, - std::function pred, - std::function mutator, - bool create = false); - - // Convenience: update the first match (JSON filter + JSON patch). If create=true, creates new when none found - DbStatus updateOne(const std::string &collectionName, - const JsonDocument &filter, - const JsonDocument &patch, - bool create = false); + // Convenience: update the first match (predicate + mutator). If create=true, creates new when + // none found + DbStatus updateOne( + const std::string &collectionName, + std::function pred, + std::function mutator, + bool create = false + ); + + // Convenience: update the first match (JSON filter + JSON patch). If create=true, creates new + // when none found + DbStatus updateOne( + const std::string &collectionName, + const JsonDocument &filter, + const JsonDocument &patch, + bool create = false + ); // Convenience: update a document by _id in the given collection - DbStatus updateById(const std::string &collectionName, const std::string &id, std::function mutator); + DbStatus updateById( + const std::string &collectionName, + const std::string &id, + std::function mutator + ); // Convenience: remove a document by _id in the given collection DbStatus removeById(const std::string &collectionName, const std::string &id); @@ -98,27 +113,41 @@ class ESPJsonDB { template DbResult removeMany(const std::string &collectionName, Pred &&p); - template , JsonDocument> && !std::is_same_v, JsonDocument>>> + template < + typename Pred, + typename Mut, + typename = std::enable_if_t< + !std::is_same_v, JsonDocument> && + !std::is_same_v, JsonDocument>>> DbResult updateMany(const std::string &collectionName, Pred &&p, Mut &&m); - template , JsonDocument>>> + template < + typename Mut, + typename = std::enable_if_t, JsonDocument>>> DbResult updateMany(const std::string &collectionName, Mut &&m); - template , JsonDocument>>> - DbResult updateMany(const std::string &collectionName, const JsonDocument &patch, Pred &&p); + template < + typename Pred, + typename = std::enable_if_t, JsonDocument>>> + DbResult + updateMany(const std::string &collectionName, const JsonDocument &patch, Pred &&p); - DbResult updateMany(const std::string &collectionName, const JsonDocument &patch, const JsonDocument &filter); + DbResult updateMany( + const std::string &collectionName, const JsonDocument &patch, const JsonDocument &filter + ); // Manual sync (safe to call from app) DbStatus syncNow(); // Retrieve last error or success status - DbStatus lastError() const { return _lastError; } + DbStatus lastError() const { + return _lastError; + } // Allow other components to update diagnostics/error state - DbStatus recordStatus(const DbStatus &st) { return setLastError(st); } + DbStatus recordStatus(const DbStatus &st) { + return setLastError(st); + } // Diagnostics: number of collections, doc counts, and config JsonDocument getDiag(); @@ -128,20 +157,30 @@ class ESPJsonDB { DbStatus restoreFromSnapshot(const JsonDocument &snapshot); // Generic file-bytes helpers under /_files. - DbStatus writeFileStream(const std::string &relativePath, - Stream &in, - size_t bytesToWrite, - const ESPJsonDBFileOptions &opts = {}); - DbStatus writeFileStream(const std::string &relativePath, - const DbFileUploadPullCb &pullCb, - const ESPJsonDBFileOptions &opts = {}); - DbStatus writeFileFromPath(const std::string &relativePath, - const std::string &sourceFsPath, - const ESPJsonDBFileOptions &opts = {}); - DbStatus writeFile(const std::string &relativePath, const uint8_t *data, size_t size, bool overwrite = true); - DbStatus writeTextFile(const std::string &relativePath, const std::string &text, bool overwrite = true); - - DbResult readFileStream(const std::string &relativePath, Stream &out, size_t chunkSize = 512); + DbStatus writeFileStream( + const std::string &relativePath, + Stream &in, + size_t bytesToWrite, + const ESPJsonDBFileOptions &opts = {} + ); + DbStatus writeFileStream( + const std::string &relativePath, + const DbFileUploadPullCb &pullCb, + const ESPJsonDBFileOptions &opts = {} + ); + DbStatus writeFileFromPath( + const std::string &relativePath, + const std::string &sourceFsPath, + const ESPJsonDBFileOptions &opts = {} + ); + DbStatus writeFile( + const std::string &relativePath, const uint8_t *data, size_t size, bool overwrite = true + ); + DbStatus + writeTextFile(const std::string &relativePath, const std::string &text, bool overwrite = true); + + DbResult + readFileStream(const std::string &relativePath, Stream &out, size_t chunkSize = 512); DbResult> readFile(const std::string &relativePath); DbResult readTextFile(const std::string &relativePath); @@ -151,10 +190,12 @@ class ESPJsonDB { // Non-blocking chunked file upload worker API. // The pull callback runs on a background task and must fill up to `requested` bytes. - DbResult writeFileStreamAsync(const std::string &relativePath, - const DbFileUploadPullCb &pullCb, - const ESPJsonDBFileOptions &opts = {}, - const DbFileUploadDoneCb &doneCb = {}); + DbResult writeFileStreamAsync( + const std::string &relativePath, + const DbFileUploadPullCb &pullCb, + const ESPJsonDBFileOptions &opts = {}, + const DbFileUploadDoneCb &doneCb = {} + ); DbStatus cancelFileUpload(uint32_t uploadId); DbResult getFileUploadState(uint32_t uploadId); @@ -177,7 +218,7 @@ class ESPJsonDB { std::vector> _eventCbs; std::vector> _errorCbs; fs::FS *_fs = &LittleFS; // active filesystem - FrMutex _mu; // guards _cols, _schemas, _colsToDelete + FrMutex _mu; // guards _cols, _schemas, _colsToDelete // Tracks most recent status for diagnostics/debugging DbStatus _lastError{DbStatusCode::Ok, ""}; @@ -188,7 +229,7 @@ class ESPJsonDB { uint32_t lastRefreshMs = 0; // millis when refreshed from FS }; - DiagCache _diagCache; // cached diagnostics; read without touching FS + DiagCache _diagCache; // cached diagnostics; read without touching FS bool _diagCachePrimed = false; // true once runtime counters are initialized std::atomic _initialized{false}; @@ -202,7 +243,8 @@ class ESPJsonDB { // Update last error/status helper DbStatus setLastError(const DbStatus &st) { _lastError = st; - if (!st.ok()) emitError(st); + if (!st.ok()) + emitError(st); return st; } @@ -236,7 +278,9 @@ class ESPJsonDB { bool isUploadTerminal(DbFileUploadState state) const; void trackTerminalUploadLocked(const std::shared_ptr &job); bool createTask(TaskFunction_t entry, const char *name, TaskHandle_t &outHandle); - void stopTask(TaskHandle_t &taskHandle, std::atomic &stopRequested, std::atomic &taskExited); + void stopTask( + TaskHandle_t &taskHandle, std::atomic &stopRequested, std::atomic &taskExited + ); static uint32_t stackBytesToWords(uint32_t stackBytes); // Refresh diag cache from filesystem (expensive; used only for explicit full refresh paths) @@ -295,7 +339,8 @@ DbResult ESPJsonDB::updateMany(const std::string &collectionName, Mut && } template -DbResult ESPJsonDB::updateMany(const std::string &collectionName, const JsonDocument &patch, Pred &&p) { +DbResult +ESPJsonDB::updateMany(const std::string &collectionName, const JsonDocument &patch, Pred &&p) { DbResult res{}; auto cr = collection(collectionName); if (!cr.status.ok()) { diff --git a/src/esp_jsondb/db_files.cpp b/src/esp_jsondb/db_files.cpp index 80e2362..376328d 100644 --- a/src/esp_jsondb/db_files.cpp +++ b/src/esp_jsondb/db_files.cpp @@ -17,12 +17,14 @@ std::string parentDirOf(const std::string &path) { return path.substr(0, pos); } -DbStatus writeFromPullCb(fs::FS &filesystem, - const std::string &finalPath, - bool usePSRAMBuffers, - const ESPJsonDBFileOptions &opts, - const DbFileUploadPullCb &pullCb, - size_t &totalWritten) { +DbStatus writeFromPullCb( + fs::FS &filesystem, + const std::string &finalPath, + bool usePSRAMBuffers, + const ESPJsonDBFileOptions &opts, + const DbFileUploadPullCb &pullCb, + size_t &totalWritten +) { totalWritten = 0; if (!pullCb) { return {DbStatusCode::InvalidArgument, "upload callback is required"}; @@ -63,7 +65,8 @@ DbStatus writeFromPullCb(fs::FS &filesystem, size_t produced = 0; bool eof = false; auto st = pullCb(chunkSize, buffer.data(), produced, eof); - if (!st.ok()) return fail(st); + if (!st.ok()) + return fail(st); if (produced > chunkSize) { return fail({DbStatusCode::InvalidArgument, "upload callback produced too many bytes"}); } @@ -74,16 +77,20 @@ DbStatus writeFromPullCb(fs::FS &filesystem, } totalWritten += written; } - if (eof) break; + if (eof) + break; if (produced == 0) { - return fail({DbStatusCode::InvalidArgument, "upload callback produced no bytes without eof"}); + return fail( + {DbStatusCode::InvalidArgument, "upload callback produced no bytes without eof"} + ); } } buffered.flush(); file.close(); - if (opts.overwrite && filesystem.exists(finalPath.c_str()) && !filesystem.remove(finalPath.c_str())) { + if (opts.overwrite && filesystem.exists(finalPath.c_str()) && + !filesystem.remove(finalPath.c_str())) { filesystem.remove(tmpPath.c_str()); return {DbStatusCode::IoError, "remove old file failed"}; } @@ -97,7 +104,8 @@ DbStatus writeFromPullCb(fs::FS &filesystem, } // namespace -DbStatus ESPJsonDB::normalizeFilePath(const std::string &rawRelativePath, std::string &normalized) const { +DbStatus +ESPJsonDB::normalizeFilePath(const std::string &rawRelativePath, std::string &normalized) const { normalized.clear(); if (rawRelativePath.empty()) { return {DbStatusCode::InvalidArgument, "file path is empty"}; @@ -128,16 +136,19 @@ DbStatus ESPJsonDB::normalizeFilePath(const std::string &rawRelativePath, std::s }; for (char c : rawRelativePath) { - if (c == '\\') c = '/'; + if (c == '\\') + c = '/'; if (c == '/') { auto st = flushSegment(); - if (!st.ok()) return st; + if (!st.ok()) + return st; continue; } segment.push_back(c); } auto st = flushSegment(); - if (!st.ok()) return st; + if (!st.ok()) + return st; if (normalized.empty()) { return {DbStatusCode::InvalidArgument, "file path resolves to empty"}; @@ -146,23 +157,26 @@ DbStatus ESPJsonDB::normalizeFilePath(const std::string &rawRelativePath, std::s return {DbStatusCode::Ok, ""}; } -DbStatus ESPJsonDB::writeFileStream(const std::string &relativePath, - Stream &in, - size_t bytesToWrite, - const ESPJsonDBFileOptions &opts) { +DbStatus ESPJsonDB::writeFileStream( + const std::string &relativePath, + Stream &in, + size_t bytesToWrite, + const ESPJsonDBFileOptions &opts +) { auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); std::string normalized; auto nst = normalizeFilePath(relativePath, normalized); - if (!nst.ok()) return setLastError(nst); + if (!nst.ok()) + return setLastError(nst); const std::string finalPath = joinPath(fileRootDir(), normalized); size_t remaining = bytesToWrite; - DbFileUploadPullCb pullCb = [&in, &remaining](size_t requested, - uint8_t *buffer, - size_t &produced, - bool &eof) -> DbStatus { + DbFileUploadPullCb pullCb = + [&in, + &remaining](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { if (!buffer) { return {DbStatusCode::InvalidArgument, "buffer is null"}; } @@ -184,7 +198,8 @@ DbStatus ESPJsonDB::writeFileStream(const std::string &relativePath, size_t totalWritten = 0; auto st = writeFromPullCb(*_fs, finalPath, _cfg.usePSRAMBuffers, opts, pullCb, totalWritten); - if (!st.ok()) return setLastError(st); + if (!st.ok()) + return setLastError(st); if (totalWritten != bytesToWrite) { return setLastError({DbStatusCode::IoError, "written size mismatch"}); } @@ -192,15 +207,19 @@ DbStatus ESPJsonDB::writeFileStream(const std::string &relativePath, return setLastError({DbStatusCode::Ok, ""}); } -DbStatus ESPJsonDB::writeFileStream(const std::string &relativePath, - const DbFileUploadPullCb &pullCb, - const ESPJsonDBFileOptions &opts) { +DbStatus ESPJsonDB::writeFileStream( + const std::string &relativePath, + const DbFileUploadPullCb &pullCb, + const ESPJsonDBFileOptions &opts +) { auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); std::string normalized; auto nst = normalizeFilePath(relativePath, normalized); - if (!nst.ok()) return setLastError(nst); + if (!nst.ok()) + return setLastError(nst); const std::string finalPath = joinPath(fileRootDir(), normalized); size_t totalWritten = 0; @@ -208,14 +227,17 @@ DbStatus ESPJsonDB::writeFileStream(const std::string &relativePath, return setLastError(st); } -DbStatus ESPJsonDB::writeFileFromPath(const std::string &relativePath, - const std::string &sourceFsPath, - const ESPJsonDBFileOptions &opts) { +DbStatus ESPJsonDB::writeFileFromPath( + const std::string &relativePath, + const std::string &sourceFsPath, + const ESPJsonDBFileOptions &opts +) { if (sourceFsPath.empty()) { return setLastError({DbStatusCode::InvalidArgument, "source file path is empty"}); } auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); File source; { @@ -232,20 +254,21 @@ DbStatus ESPJsonDB::writeFileFromPath(const std::string &relativePath, return st; } -DbStatus ESPJsonDB::writeFile(const std::string &relativePath, - const uint8_t *data, - size_t size, - bool overwrite) { +DbStatus ESPJsonDB::writeFile( + const std::string &relativePath, const uint8_t *data, size_t size, bool overwrite +) { if (size > 0 && data == nullptr) { return setLastError({DbStatusCode::InvalidArgument, "file data is null"}); } auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); std::string normalized; auto nst = normalizeFilePath(relativePath, normalized); - if (!nst.ok()) return setLastError(nst); + if (!nst.ok()) + return setLastError(nst); const std::string finalPath = joinPath(fileRootDir(), normalized); const std::string parentDir = parentDirOf(finalPath); @@ -291,16 +314,18 @@ DbStatus ESPJsonDB::writeFile(const std::string &relativePath, return setLastError({DbStatusCode::Ok, ""}); } -DbStatus ESPJsonDB::writeTextFile(const std::string &relativePath, - const std::string &text, - bool overwrite) { - return writeFile(relativePath, - reinterpret_cast(text.data()), - text.size(), - overwrite); +DbStatus +ESPJsonDB::writeTextFile(const std::string &relativePath, const std::string &text, bool overwrite) { + return writeFile( + relativePath, + reinterpret_cast(text.data()), + text.size(), + overwrite + ); } -DbResult ESPJsonDB::readFileStream(const std::string &relativePath, Stream &out, size_t chunkSize) { +DbResult +ESPJsonDB::readFileStream(const std::string &relativePath, Stream &out, size_t chunkSize) { DbResult res{}; auto ready = ensureReady(); if (!ready.ok()) { @@ -315,7 +340,8 @@ DbResult ESPJsonDB::readFileStream(const std::string &relativePath, Stre return res; } - if (chunkSize < 32) chunkSize = 32; + if (chunkSize < 32) + chunkSize = 32; JsonDbVector buffer{JsonDbAllocator(_cfg.usePSRAMBuffers)}; buffer.resize(chunkSize); const std::string path = joinPath(fileRootDir(), normalized); @@ -330,7 +356,8 @@ DbResult ESPJsonDB::readFileStream(const std::string &relativePath, Stre size_t total = 0; while (true) { size_t readBytes = f.read(buffer.data(), buffer.size()); - if (readBytes == 0) break; + if (readBytes == 0) + break; size_t written = out.write(buffer.data(), readBytes); if (written != readBytes) { f.close(); @@ -401,11 +428,13 @@ DbResult ESPJsonDB::readTextFile(const std::string &relativePath) { DbStatus ESPJsonDB::removeFile(const std::string &relativePath) { auto ready = ensureReady(); - if (!ready.ok()) return setLastError(ready); + if (!ready.ok()) + return setLastError(ready); std::string normalized; auto nst = normalizeFilePath(relativePath, normalized); - if (!nst.ok()) return setLastError(nst); + if (!nst.ok()) + return setLastError(nst); const std::string path = joinPath(fileRootDir(), normalized); FrLock fs(g_fsMutex); diff --git a/src/esp_jsondb/db_files_async.cpp b/src/esp_jsondb/db_files_async.cpp index 0642036..78ad789 100644 --- a/src/esp_jsondb/db_files_async.cpp +++ b/src/esp_jsondb/db_files_async.cpp @@ -18,13 +18,13 @@ std::string parentDirForAsyncUpload(const std::string &path) { } // namespace bool ESPJsonDB::isUploadTerminal(DbFileUploadState state) const { - return state == DbFileUploadState::Completed || - state == DbFileUploadState::Failed || - state == DbFileUploadState::Cancelled; + return state == DbFileUploadState::Completed || state == DbFileUploadState::Failed || + state == DbFileUploadState::Cancelled; } void ESPJsonDB::trackTerminalUploadLocked(const std::shared_ptr &job) { - if (!job || !isUploadTerminal(job->state) || job->terminalTracked) return; + if (!job || !isUploadTerminal(job->state) || job->terminalTracked) + return; job->terminalTracked = true; _terminalUploadOrder.push_back(job->id); @@ -34,17 +34,20 @@ void ESPJsonDB::trackTerminalUploadLocked(const std::shared_ptr & _terminalUploadOrder.erase(_terminalUploadOrder.begin()); auto it = _uploadJobs.find(expiredId); - if (it == _uploadJobs.end()) continue; + if (it == _uploadJobs.end()) + continue; if (!it->second || isUploadTerminal(it->second->state)) { _uploadJobs.erase(it); } } } -DbResult ESPJsonDB::writeFileStreamAsync(const std::string &relativePath, - const DbFileUploadPullCb &pullCb, - const ESPJsonDBFileOptions &opts, - const DbFileUploadDoneCb &doneCb) { +DbResult ESPJsonDB::writeFileStreamAsync( + const std::string &relativePath, + const DbFileUploadPullCb &pullCb, + const ESPJsonDBFileOptions &opts, + const DbFileUploadDoneCb &doneCb +) { DbResult res{}; auto ready = ensureReady(); if (!ready.ok()) { @@ -67,7 +70,8 @@ DbResult ESPJsonDB::writeFileStreamAsync(const std::string &relativePa job->relativePath = relativePath; job->normalizedPath = normalized; job->opts = opts; - if (job->opts.chunkSize < 32) job->opts.chunkSize = 32; + if (job->opts.chunkSize < 32) + job->opts.chunkSize = 32; job->pullCb = pullCb; job->doneCb = doneCb; @@ -78,7 +82,10 @@ DbResult ESPJsonDB::writeFileStreamAsync(const std::string &relativePa _uploadQueue.push_back(job->id); startFileUploadTaskUnlocked(); if (_fileUploadTask == nullptr) { - _uploadQueue.erase(std::remove(_uploadQueue.begin(), _uploadQueue.end(), job->id), _uploadQueue.end()); + _uploadQueue.erase( + std::remove(_uploadQueue.begin(), _uploadQueue.end(), job->id), + _uploadQueue.end() + ); _uploadJobs.erase(job->id); res.status = setLastError({DbStatusCode::Busy, "upload worker start failed"}); return res; @@ -112,7 +119,10 @@ DbStatus ESPJsonDB::cancelFileUpload(uint32_t uploadId) { job->cancelRequested = true; if (job->state == DbFileUploadState::Queued) { - _uploadQueue.erase(std::remove(_uploadQueue.begin(), _uploadQueue.end(), uploadId), _uploadQueue.end()); + _uploadQueue.erase( + std::remove(_uploadQueue.begin(), _uploadQueue.end(), uploadId), + _uploadQueue.end() + ); job->state = DbFileUploadState::Cancelled; job->finalStatus = doneStatus; trackTerminalUploadLocked(job); @@ -156,7 +166,8 @@ void ESPJsonDB::fileUploadTaskThunk(void *arg) { } void ESPJsonDB::startFileUploadTaskUnlocked() { - if (_fileUploadTask != nullptr) return; + if (_fileUploadTask != nullptr) + return; _fileUploadStopRequested.store(false, std::memory_order_release); _fileUploadTaskExited.store(false, std::memory_order_release); TaskHandle_t handle = nullptr; @@ -171,7 +182,8 @@ void ESPJsonDB::stopFileUploadTaskUnlocked(bool cancelPending) { if (cancelPending) { for (auto &kv : _uploadJobs) { auto &job = kv.second; - if (!job || isUploadTerminal(job->state)) continue; + if (!job || isUploadTerminal(job->state)) + continue; job->cancelRequested = true; job->state = DbFileUploadState::Cancelled; job->finalStatus = {DbStatusCode::Busy, "upload cancelled"}; @@ -183,7 +195,8 @@ void ESPJsonDB::stopFileUploadTaskUnlocked(bool cancelPending) { } } -DbStatus ESPJsonDB::runFileUploadJob(const std::shared_ptr &job, size_t &bytesWritten) { +DbStatus +ESPJsonDB::runFileUploadJob(const std::shared_ptr &job, size_t &bytesWritten) { bytesWritten = 0; if (!job || !job->pullCb) { return {DbStatusCode::InvalidArgument, "upload callback is required"}; @@ -276,7 +289,8 @@ DbStatus ESPJsonDB::runFileUploadJob(const std::shared_ptr &job, FrLock fs(g_fsMutex); f.flush(); f.close(); - if (job->opts.overwrite && _fs->exists(finalPath.c_str()) && !_fs->remove(finalPath.c_str())) { + if (job->opts.overwrite && _fs->exists(finalPath.c_str()) && + !_fs->remove(finalPath.c_str())) { if (_fs->exists(tmpPath.c_str())) { _fs->remove(tmpPath.c_str()); } diff --git a/src/esp_jsondb/document/document.cpp b/src/esp_jsondb/document/document.cpp index 163f6ab..89309c9 100644 --- a/src/esp_jsondb/document/document.cpp +++ b/src/esp_jsondb/document/document.cpp @@ -4,22 +4,22 @@ #include "../utils/time_utils.h" #include -DocView::DocView(std::shared_ptr rec, - const Schema *schema, - FrMutex *mu, - ESPJsonDB *db, - std::function&)> commitSink, - bool usePSRAMBuffers) - : _rec(std::move(rec)), - _schema(schema), - _mu(mu), - _db(db), - _commitSink(std::move(commitSink)), - _usePSRAMBuffers(usePSRAMBuffers) +DocView::DocView( + std::shared_ptr rec, + const Schema *schema, + FrMutex *mu, + ESPJsonDB *db, + std::function &)> commitSink, + bool usePSRAMBuffers +) + : _rec(std::move(rec)), _schema(schema), _mu(mu), _db(db), _commitSink(std::move(commitSink)), + _usePSRAMBuffers(usePSRAMBuffers) #if ESP_JSONDB_HAS_JSONDOC_ALLOCATOR - , _docAllocator(usePSRAMBuffers) + , + _docAllocator(usePSRAMBuffers) #endif -{} +{ +} DocView::~DocView() { // no auto-commit by default; discard decoded state @@ -27,8 +27,10 @@ DocView::~DocView() { DbStatus DocView::decode() { std::unique_ptr guard; - if (_mu) guard = std::make_unique(*_mu); - if (_doc) return recordStatus({DbStatusCode::Ok, ""}); + if (_mu) + guard = std::make_unique(*_mu); + if (_doc) + return recordStatus({DbStatusCode::Ok, ""}); #if ESP_JSONDB_HAS_JSONDOC_ALLOCATOR _docAllocator.setUsePSRAMBuffers(_usePSRAMBuffers); _doc = std::make_unique(&_docAllocator); @@ -64,14 +66,16 @@ struct CompareToBufferPrint : public Print { size_t size; size_t index; bool equal; - CompareToBufferPrint(const uint8_t *r, size_t s) : ref(r), size(s), index(0), equal(true) {} + CompareToBufferPrint(const uint8_t *r, size_t s) : ref(r), size(s), index(0), equal(true) { + } size_t write(uint8_t b) override { if (index >= size) { equal = false; ++index; // still advance to reflect extra bytes return 1; } - if (ref[index] != b) equal = false; + if (ref[index] != b) + equal = false; ++index; return 1; } @@ -88,11 +92,15 @@ DbStatus DocView::recordStatus(const DbStatus &st) const { } DbStatus DocView::encode() { - std::unique_ptr guard; - if (_mu) guard = std::make_unique(*_mu); - if (!_doc) return recordStatus({DbStatusCode::InvalidArgument, "no decoded doc"}); - if (!_rec) return recordStatus({DbStatusCode::InvalidArgument, "no backing record"}); - if (_rec->meta.removed) return recordStatus({DbStatusCode::NotFound, "document removed"}); + std::unique_ptr guard; + if (_mu) + guard = std::make_unique(*_mu); + if (!_doc) + return recordStatus({DbStatusCode::InvalidArgument, "no decoded doc"}); + if (!_rec) + return recordStatus({DbStatusCode::InvalidArgument, "no backing record"}); + if (_rec->meta.removed) + return recordStatus({DbStatusCode::NotFound, "document removed"}); // First, measure the size of the new serialization size_t sz = measureMsgPack(_doc->as()); @@ -113,30 +121,34 @@ DbStatus DocView::encode() { // Allocate and write new bytes _rec->msgpack.resize(sz); - size_t written = serializeMsgPack(_doc->as(), _rec->msgpack.data(), _rec->msgpack.size()); + size_t written = + serializeMsgPack(_doc->as(), _rec->msgpack.data(), _rec->msgpack.size()); if (written != sz) { return recordStatus({DbStatusCode::IoError, "serialize msgpack size mismatch"}); } - _rec->meta.updatedAt = nowUtcMs(); - _rec->meta.dirty = true; - _dirtyLocally = false; - return recordStatus({DbStatusCode::Ok, ""}); + _rec->meta.updatedAt = nowUtcMs(); + _rec->meta.dirty = true; + _dirtyLocally = false; + return recordStatus({DbStatusCode::Ok, ""}); } JsonVariant DocView::operator[](const char *key) { - if (!decode().ok()) return JsonVariant(); + if (!decode().ok()) + return JsonVariant(); _dirtyLocally = true; return (*_doc)[key]; } JsonVariant DocView::operator[](const String &key) { - if (!decode().ok()) return JsonVariant(); + if (!decode().ok()) + return JsonVariant(); _dirtyLocally = true; return (*_doc)[key]; } JsonVariant DocView::operator[](int index) { - if (!decode().ok()) return JsonVariant(); + if (!decode().ok()) + return JsonVariant(); _dirtyLocally = true; return (*_doc)[index]; } @@ -144,7 +156,8 @@ JsonVariant DocView::operator[](int index) { JsonVariantConst DocView::operator[](const char *key) const { if (!_doc) { auto self = const_cast(this); - if (!self->decode().ok()) return JsonVariantConst(); + if (!self->decode().ok()) + return JsonVariantConst(); } return _doc->as()[key]; } @@ -152,7 +165,8 @@ JsonVariantConst DocView::operator[](const char *key) const { JsonVariantConst DocView::operator[](const String &key) const { if (!_doc) { auto self = const_cast(this); - if (!self->decode().ok()) return JsonVariantConst(); + if (!self->decode().ok()) + return JsonVariantConst(); } return _doc->as()[key]; } @@ -160,13 +174,15 @@ JsonVariantConst DocView::operator[](const String &key) const { JsonVariantConst DocView::operator[](int index) const { if (!_doc) { auto self = const_cast(this); - if (!self->decode().ok()) return JsonVariantConst(); + if (!self->decode().ok()) + return JsonVariantConst(); } return _doc->as()[index]; } JsonObject DocView::asObject() { - if (!decode().ok()) return JsonObject(); + if (!decode().ok()) + return JsonObject(); return _doc->as(); } @@ -174,15 +190,18 @@ JsonObjectConst DocView::asObjectConst() const { if (!_doc) { // Need to const_cast to decode lazily auto self = const_cast(this); - if (!self->decode().ok()) return JsonObjectConst(); + if (!self->decode().ok()) + return JsonObjectConst(); } return _doc->as(); } DbStatus DocView::commit() { - if (!_doc) return recordStatus({DbStatusCode::Ok, "no changes"}); + if (!_doc) + return recordStatus({DbStatusCode::Ok, "no changes"}); auto st = encode(); - if (!st.ok()) return st; + if (!st.ok()) + return st; if (_commitSink && _rec) { st = _commitSink(_rec); } @@ -197,7 +216,8 @@ void DocView::discard() { DocRef DocView::getRef(const char *field) const { if (!_doc) { auto self = const_cast(this); - if (!self->decode().ok()) return {}; + if (!self->decode().ok()) + return {}; } return docRefFromJson(_doc->as()[field]); } @@ -217,7 +237,8 @@ DocView DocView::populate(const char *field, uint8_t maxDepth) const { return DocView(nullptr, nullptr, nullptr, _db, nullptr, _usePSRAMBuffers); } auto fr = _db->findById(ref.collection, ref.id); - if (!fr.status.ok()) return DocView(nullptr, nullptr, nullptr, _db, nullptr, _usePSRAMBuffers); + if (!fr.status.ok()) + return DocView(nullptr, nullptr, nullptr, _db, nullptr, _usePSRAMBuffers); if (maxDepth > 1) { for (auto kv : fr.value.asObjectConst()) { auto nested = docRefFromJson(kv.value()); diff --git a/src/esp_jsondb/document/document.h b/src/esp_jsondb/document/document.h index 7ffc226..15e33ae 100644 --- a/src/esp_jsondb/document/document.h +++ b/src/esp_jsondb/document/document.h @@ -5,9 +5,9 @@ #include #include +#include #include #include -#include #include #include "../utils/dbTypes.h" @@ -27,7 +27,8 @@ class ESPJsonDB; #if ESP_JSONDB_HAS_JSONDOC_ALLOCATOR class JsonDbDocAllocator : public ArduinoJson::Allocator { public: - explicit JsonDbDocAllocator(bool usePSRAMBuffers = false) : _usePSRAMBuffers(usePSRAMBuffers) {} + explicit JsonDbDocAllocator(bool usePSRAMBuffers = false) : _usePSRAMBuffers(usePSRAMBuffers) { + } void *allocate(size_t size) override { return jsondb_allocator_detail::allocate(size, _usePSRAMBuffers); @@ -57,22 +58,23 @@ class JsonDbDocAllocator : public ArduinoJson::Allocator { * The database does not manage or check time synchronization. */ struct DocumentMeta { - uint32_t createdAt = 0; // UTC milliseconds - uint32_t updatedAt = 0; // UTC milliseconds - std::string id; // 24-hex ObjectId - bool dirty = false; // needs flush to FS - bool removed = false; // logically deleted; DocView::commit should fail + uint32_t createdAt = 0; // UTC milliseconds + uint32_t updatedAt = 0; // UTC milliseconds + std::string id; // 24-hex ObjectId + bool dirty = false; // needs flush to FS + bool removed = false; // logically deleted; DocView::commit should fail }; // Internal storage unit (owned by Collection) struct DocumentRecord { explicit DocumentRecord(bool usePSRAMBuffers = false) - : msgpack(JsonDbAllocator(usePSRAMBuffers)) {} + : msgpack(JsonDbAllocator(usePSRAMBuffers)) { + } DocumentMeta meta; JsonDbVector msgpack; // authoritative source - // Optional decoded cache; created on demand and freed when view destroyed - // Decoding/encoding uses ArduinoJson. + // Optional decoded cache; created on demand and freed when view + // destroyed Decoding/encoding uses ArduinoJson. }; // A short-lived, RAII view for convenient operator[] access @@ -80,12 +82,14 @@ struct DocumentRecord { // - On commit(): reserialize to MessagePack and mark dirty class DocView { public: - DocView(std::shared_ptr rec, - const Schema *schema = nullptr, - FrMutex *mu = nullptr, - ESPJsonDB *db = nullptr, - std::function&)> commitSink = nullptr, - bool usePSRAMBuffers = false); + DocView( + std::shared_ptr rec, + const Schema *schema = nullptr, + FrMutex *mu = nullptr, + ESPJsonDB *db = nullptr, + std::function &)> commitSink = nullptr, + bool usePSRAMBuffers = false + ); ~DocView(); // optional auto-commit if enabled // non-copyable, movable @@ -107,8 +111,7 @@ class DocView { JsonObjectConst asObjectConst() const; // Convenience: read a field or return a default if absent/invalid - template - T getOr(const char *field, T def) const; + template T getOr(const char *field, T def) const; // DocRef helpers DocRef getRef(const char *field) const; @@ -116,7 +119,7 @@ class DocView { // persist changes back to record (MsgPack) DbStatus commit(); // serialize -> msgpack; set dirty+updatedAt only if bytes changed - void discard(); // drop changes, keep msgpack + void discard(); // drop changes, keep msgpack const DocumentMeta &meta() const { static const DocumentMeta kEmptyMeta{}; @@ -130,7 +133,7 @@ class DocView { bool _dirtyLocally = false; FrMutex *_mu = nullptr; // optional: used when called without external lock ESPJsonDB *_db = nullptr; - std::function&)> _commitSink; + std::function &)> _commitSink; bool _usePSRAMBuffers = false; #if ESP_JSONDB_HAS_JSONDOC_ALLOCATOR JsonDbDocAllocator _docAllocator; @@ -140,15 +143,17 @@ class DocView { DbStatus recordStatus(const DbStatus &st) const; }; -template -T DocView::getOr(const char *field, T def) const { +template T DocView::getOr(const char *field, T def) const { if (!_doc) { // Need to const_cast to decode lazily auto self = const_cast(this); - if (!self->decode().ok()) return def; + if (!self->decode().ok()) + return def; } JsonVariantConst v = _doc->as()[field]; - if (v.isNull()) return def; - if (!v.template is()) return def; + if (v.isNull()) + return def; + if (!v.template is()) + return def; return v.as(); } diff --git a/src/esp_jsondb/utils/dbTypes.h b/src/esp_jsondb/utils/dbTypes.h index abbacd3..0c1d5b7 100644 --- a/src/esp_jsondb/utils/dbTypes.h +++ b/src/esp_jsondb/utils/dbTypes.h @@ -5,8 +5,8 @@ #include #include -#include #include +#include #include #include @@ -30,7 +30,7 @@ struct ESPJsonDBConfig { bool autosync = true; bool coldSync = false; bool cacheEnabled = true; // must remain true; false is rejected at runtime - fs::FS *fs = nullptr; // optional external filesystem handle + fs::FS *fs = nullptr; // optional external filesystem handle bool initFileSystem = true; bool formatOnFail = true; uint8_t maxOpenFiles = 10; @@ -55,67 +55,65 @@ enum class DBEventType : uint8_t { // Human-readable descriptions for DBEventType values. // Embedded-friendly: constexpr data and inline accessor (no dynamic alloc, no exceptions) static constexpr const char *kDBEventTypeDescriptions[] = { - "Sync completed", - "Collection created", - "Collection dropped", - "Document created", - "Document updated", - "Document deleted"}; + "Sync completed", + "Collection created", + "Collection dropped", + "Document created", + "Document updated", + "Document deleted" +}; inline const char *dbEventTypeToString(DBEventType ev) { const auto idx = static_cast(ev); - const auto count = static_cast(sizeof(kDBEventTypeDescriptions) / sizeof(kDBEventTypeDescriptions[0])); + const auto count = static_cast( + sizeof(kDBEventTypeDescriptions) / sizeof(kDBEventTypeDescriptions[0]) + ); return (idx < count) ? kDBEventTypeDescriptions[idx] : "Unknown"; } // Human-readable descriptions for DbStatusCode values. // Embedded-friendly: constexpr data and inline accessor (no dynamic alloc, no exceptions) static constexpr const char *kDbStatusCodeDescriptions[] = { - "Ok", - "Not found", - "Already exists", - "Invalid argument", - "Validation failed", - "I/O error", - "Corrupted", - "Busy", - "Unknown", + "Ok", + "Not found", + "Already exists", + "Invalid argument", + "Validation failed", + "I/O error", + "Corrupted", + "Busy", + "Unknown", }; inline const char *dbStatusCodeToString(DbStatusCode code) { const auto idx = static_cast(code); - const auto count = static_cast(sizeof(kDbStatusCodeDescriptions) / sizeof(kDbStatusCodeDescriptions[0])); + const auto count = static_cast( + sizeof(kDbStatusCodeDescriptions) / sizeof(kDbStatusCodeDescriptions[0]) + ); return (idx < count) ? kDbStatusCodeDescriptions[idx] : "Unknown"; } struct DbStatus { - DbStatusCode code = DbStatusCode::Ok; - const char *message = ""; - // Explicit default and convenience constructor to allow brace assignments - DbStatus() = default; - DbStatus(DbStatusCode c, const char *msg) : code(c), message(msg) {} - bool ok() const { return code == DbStatusCode::Ok; } + DbStatusCode code = DbStatusCode::Ok; + const char *message = ""; + // Explicit default and convenience constructor to allow brace assignments + DbStatus() = default; + DbStatus(DbStatusCode c, const char *msg) : code(c), message(msg) { + } + bool ok() const { + return code == DbStatusCode::Ok; + } }; -enum class DbFileUploadState : uint8_t { - Queued = 0, - Running, - Completed, - Failed, - Cancelled -}; +enum class DbFileUploadState : uint8_t { Queued = 0, Running, Completed, Failed, Cancelled }; -using DbFileUploadPullCb = std::function; +using DbFileUploadPullCb = + std::function; -using DbFileUploadDoneCb = std::function; +using DbFileUploadDoneCb = + std::function; -template -struct DbResult { +template struct DbResult { DbStatus status; T value; }; diff --git a/src/esp_jsondb/utils/fr_mutex.h b/src/esp_jsondb/utils/fr_mutex.h index 8097e76..3d9e423 100644 --- a/src/esp_jsondb/utils/fr_mutex.h +++ b/src/esp_jsondb/utils/fr_mutex.h @@ -5,9 +5,12 @@ struct FrMutex { SemaphoreHandle_t h{nullptr}; - FrMutex() { h = xSemaphoreCreateMutex(); } + FrMutex() { + h = xSemaphoreCreateMutex(); + } ~FrMutex() { - if (h) vSemaphoreDelete(h); + if (h) + vSemaphoreDelete(h); } // non-copyable FrMutex(const FrMutex &) = delete; @@ -16,8 +19,12 @@ struct FrMutex { struct FrLock { FrMutex &m; - explicit FrLock(FrMutex &mtx) : m(mtx) { xSemaphoreTake(m.h, portMAX_DELAY); } - ~FrLock() { xSemaphoreGive(m.h); } + explicit FrLock(FrMutex &mtx) : m(mtx) { + xSemaphoreTake(m.h, portMAX_DELAY); + } + ~FrLock() { + xSemaphoreGive(m.h); + } // non-copyable FrLock(const FrLock &) = delete; FrLock &operator=(const FrLock &) = delete; diff --git a/src/esp_jsondb/utils/fs_utils.h b/src/esp_jsondb/utils/fs_utils.h index 3962a57..35465d8 100644 --- a/src/esp_jsondb/utils/fs_utils.h +++ b/src/esp_jsondb/utils/fs_utils.h @@ -7,22 +7,28 @@ std::string joinPath(const std::string &a, const std::string &b); inline bool fsEnsureDir(fs::FS &fs, const std::string &path) { - if (path.empty() || path == "/") return true; - if (fs.exists(path.c_str())) return true; + if (path.empty() || path == "/") + return true; + if (fs.exists(path.c_str())) + return true; size_t slash = path.rfind('/'); if (slash != std::string::npos && slash > 0) { std::string parent = path.substr(0, slash); - if (!fsEnsureDir(fs, parent)) return false; + if (!fsEnsureDir(fs, parent)) + return false; } return fs.mkdir(path.c_str()); } inline std::string joinPath(const std::string &a, const std::string &b) { - if (!b.empty() && b.front() == '/') return b; - if (a.empty()) return b; + if (!b.empty() && b.front() == '/') + return b; + if (a.empty()) + return b; if (a.back() == '/') { return b.empty() ? a : a + b; } - if (b.empty()) return a; + if (b.empty()) + return a; return a + "/" + b; } diff --git a/src/esp_jsondb/utils/jsondb_allocator.h b/src/esp_jsondb/utils/jsondb_allocator.h index 44abab2..c54eb28 100644 --- a/src/esp_jsondb/utils/jsondb_allocator.h +++ b/src/esp_jsondb/utils/jsondb_allocator.h @@ -46,19 +46,22 @@ inline void *reallocate(void *ptr, std::size_t bytes, bool usePSRAMBuffers) noex } // namespace jsondb_allocator_detail -template -class JsonDbAllocator { +template class JsonDbAllocator { public: using value_type = T; JsonDbAllocator() noexcept = default; - explicit JsonDbAllocator(bool usePSRAMBuffers) noexcept : _usePSRAMBuffers(usePSRAMBuffers) {} + explicit JsonDbAllocator(bool usePSRAMBuffers) noexcept : _usePSRAMBuffers(usePSRAMBuffers) { + } template - JsonDbAllocator(const JsonDbAllocator &other) noexcept : _usePSRAMBuffers(other.usePSRAMBuffers()) {} + JsonDbAllocator(const JsonDbAllocator &other) noexcept + : _usePSRAMBuffers(other.usePSRAMBuffers()) { + } T *allocate(std::size_t n) { - if (n == 0) return nullptr; + if (n == 0) + return nullptr; if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) throw std::bad_alloc(); @@ -86,22 +89,18 @@ class JsonDbAllocator { return _usePSRAMBuffers; } - template - bool operator==(const JsonDbAllocator &other) const noexcept { + template bool operator==(const JsonDbAllocator &other) const noexcept { return _usePSRAMBuffers == other.usePSRAMBuffers(); } - template - bool operator!=(const JsonDbAllocator &other) const noexcept { + template bool operator!=(const JsonDbAllocator &other) const noexcept { return !(*this == other); } private: - template - friend class JsonDbAllocator; + template friend class JsonDbAllocator; bool _usePSRAMBuffers = false; }; -template -using JsonDbVector = std::vector>; +template using JsonDbVector = std::vector>; diff --git a/src/esp_jsondb/utils/objectId.cpp b/src/esp_jsondb/utils/objectId.cpp index 3bcfe16..d5e9216 100644 --- a/src/esp_jsondb/utils/objectId.cpp +++ b/src/esp_jsondb/utils/objectId.cpp @@ -3,7 +3,8 @@ namespace { static uint32_t readEpochSeconds() { time_t now = time(nullptr); - if (now < 0) now = 0; + if (now < 0) + now = 0; return static_cast(now); } @@ -15,10 +16,14 @@ static void writeU32BE(uint8_t *out, uint32_t v) { } static uint8_t hexNibble(char c, bool *ok) { - if (c >= '0' && c <= '9') return static_cast(c - '0'); - if (c >= 'a' && c <= 'f') return static_cast(10 + (c - 'a')); - if (c >= 'A' && c <= 'F') return static_cast(10 + (c - 'A')); - if (ok) *ok = false; + if (c >= '0' && c <= '9') + return static_cast(c - '0'); + if (c >= 'a' && c <= 'f') + return static_cast(10 + (c - 'a')); + if (c >= 'A' && c <= 'F') + return static_cast(10 + (c - 'A')); + if (ok) + *ok = false; return 0; } } // namespace @@ -64,10 +69,12 @@ std::string ObjectId::toHex() const { } ObjectId ObjectId::fromHex(const std::string &hex, bool *ok) { - if (ok) *ok = true; + if (ok) + *ok = true; ObjectId out; if (hex.size() != 24) { - if (ok) *ok = false; + if (ok) + *ok = false; return out; } for (size_t i = 0; i < 12; ++i) { @@ -82,6 +89,7 @@ uint32_t ObjectId::nextCounter() { static uint32_t c = 0; // Simple increment; wrap at 24 bits c = (c + 1) & 0xFFFFFFu; - if (c == 0) c = 1; // avoid 0 + if (c == 0) + c = 1; // avoid 0 return c; } diff --git a/src/esp_jsondb/utils/objectId.h b/src/esp_jsondb/utils/objectId.h index 7820225..df49f3b 100644 --- a/src/esp_jsondb/utils/objectId.h +++ b/src/esp_jsondb/utils/objectId.h @@ -11,9 +11,9 @@ #include /* - ObjectId-style IDs (12-byte → 24-hex) - Layout: 4 bytes seconds since epoch, 5 bytes device/random, 3 bytes counter. - Nice compromise between uniqueness and cost. + ObjectId-style IDs (12-byte → 24-hex) + Layout: 4 bytes seconds since epoch, 5 bytes device/random, 3 bytes counter. + Nice compromise between uniqueness and cost. */ class ObjectId { public: diff --git a/src/esp_jsondb/utils/refs.h b/src/esp_jsondb/utils/refs.h index f5a6009..b3989e9 100644 --- a/src/esp_jsondb/utils/refs.h +++ b/src/esp_jsondb/utils/refs.h @@ -8,16 +8,21 @@ struct DocRef { std::string collection; std::string id; // target _id - bool valid() const { return !collection.empty() && !id.empty(); } + bool valid() const { + return !collection.empty() && !id.empty(); + } }; inline DocRef docRefFromJson(JsonVariantConst v) { DocRef r{}; - if (!v.is()) return r; + if (!v.is()) + return r; JsonObjectConst obj = v.as(); const char *col = obj["collection"].as(); const char *id = obj["_id"].as(); - if (col) r.collection = col; - if (id) r.id = id; + if (col) + r.collection = col; + if (id) + r.id = id; return r; } diff --git a/src/esp_jsondb/utils/schema.h b/src/esp_jsondb/utils/schema.h index c388c41..176e9ae 100644 --- a/src/esp_jsondb/utils/schema.h +++ b/src/esp_jsondb/utils/schema.h @@ -43,7 +43,9 @@ struct Schema { Schema() = default; - inline bool hasValidate() const { return validate != nullptr || preSave != nullptr || !fields.empty(); } + inline bool hasValidate() const { + return validate != nullptr || preSave != nullptr || !fields.empty(); + } inline void applyDefaults(JsonObject obj) const { for (const auto &f : fields) { @@ -60,7 +62,8 @@ struct Schema { obj[f.name] = atof(f.defaultValue); break; case FieldType::Bool: - obj[f.name] = (strcmp(f.defaultValue, "true") == 0 || strcmp(f.defaultValue, "1") == 0); + obj[f.name] = + (strcmp(f.defaultValue, "true") == 0 || strcmp(f.defaultValue, "1") == 0); break; case FieldType::Object: obj[f.name].to(); @@ -79,22 +82,28 @@ struct Schema { if (!v.isNull()) { switch (f.type) { case FieldType::String: - if (!v.is() && !v.is() && !v.is()) return false; + if (!v.is() && !v.is() && !v.is()) + return false; break; case FieldType::Int: - if (!v.is()) return false; + if (!v.is()) + return false; break; case FieldType::Float: - if (!v.is()) return false; + if (!v.is()) + return false; break; case FieldType::Bool: - if (!v.is()) return false; + if (!v.is()) + return false; break; case FieldType::Object: - if (!v.is()) return false; + if (!v.is()) + return false; break; case FieldType::Array: - if (!v.is()) return false; + if (!v.is()) + return false; break; } } @@ -104,19 +113,25 @@ struct Schema { inline ValidationError runPreSave(JsonObject &o) const { applyDefaults(o); - if (!validateTypes(o)) return {false, "schema: invalid type"}; - if (preSave) return preSave(o); - if (validate) return validate(o); + if (!validateTypes(o)) + return {false, "schema: invalid type"}; + if (preSave) + return preSave(o); + if (validate) + return validate(o); return {true, ""}; } inline ValidationError runValidate(const JsonObjectConst &o) const { - if (!validateTypes(o)) return {false, "schema: invalid type"}; - if (validate) return validate(o); + if (!validateTypes(o)) + return {false, "schema: invalid type"}; + if (validate) + return validate(o); return {true, ""}; } inline void runPostLoad(JsonObject &o) const { - if (postLoad) postLoad(o); + if (postLoad) + postLoad(o); } }; diff --git a/src/esp_jsondb/utils/time_utils.h b/src/esp_jsondb/utils/time_utils.h index 59e1d66..7bb1c0c 100644 --- a/src/esp_jsondb/utils/time_utils.h +++ b/src/esp_jsondb/utils/time_utils.h @@ -5,7 +5,8 @@ inline uint32_t nowUtcMs() { time_t s = time(nullptr); - if (s < 0) s = 0; + if (s < 0) + s = 0; uint64_t ms = static_cast(s) * 1000ULL; return static_cast(ms); } diff --git a/test/bulkTests.cpp b/test/bulkTests.cpp index 942cf44..494ac71 100644 --- a/test/bulkTests.cpp +++ b/test/bulkTests.cpp @@ -1,57 +1,57 @@ #include "dbTest.h" -void DbTester::updateManyFilter(){ - // Create multiple documents so we can update them. - multiDocCreate(20); +void DbTester::updateManyFilter() { + // Create multiple documents so we can update them. + multiDocCreate(20); - // Update role - JsonDocument updatedDoc; + // Update role + JsonDocument updatedDoc; updatedDoc["role"] = "admin"; - // Filter by role + // Filter by role JsonDocument filterDoc; filterDoc["role"] = "user"; - // Promote each user to admin. + // Promote each user to admin. auto result = db.updateMany("users", updatedDoc, filterDoc); - if( result.status.ok() ){ - ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); - }else{ - ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); - } + if (result.status.ok()) { + ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); + } else { + ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); + } - db.dropCollection("users"); + db.dropCollection("users"); } -void DbTester::updateManyLambdaFilter(){ - // Create multiple documents so we can update them. - multiDocCreate(20); +void DbTester::updateManyLambdaFilter() { + // Create multiple documents so we can update them. + multiDocCreate(20); - // Update role - JsonDocument updatedDoc; + // Update role + JsonDocument updatedDoc; updatedDoc["role"] = "admin"; - // Promote each user to admin. - auto result = db.updateMany("users", updatedDoc, [](const DocView &doc) { + // Promote each user to admin. + auto result = db.updateMany("users", updatedDoc, [](const DocView &doc) { return doc["role"].as() == "user"; }); - if( result.status.ok() ){ - ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); - }else{ - ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); - } + if (result.status.ok()) { + ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); + } else { + ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); + } - db.dropCollection("users"); + db.dropCollection("users"); } -void DbTester::updateManyCombined(){ - // Create multiple documents so we can update them. - multiDocCreate(20); +void DbTester::updateManyCombined() { + // Create multiple documents so we can update them. + multiDocCreate(20); - // Promote each user to admin. - auto result = db.updateMany("users", [](DocView &doc) { + // Promote each user to admin. + auto result = db.updateMany("users", [](DocView &doc) { if (doc["role"].as() == "user") { doc["role"].set("admin"); return true; @@ -59,29 +59,29 @@ void DbTester::updateManyCombined(){ return false; }); - if( result.status.ok() ){ - ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); - }else{ - ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); - } + if (result.status.ok()) { + ESP_LOGI(DB_TESTER_TAG, "Updated %d users", result.value); + } else { + ESP_LOGE(DB_TESTER_TAG, "Failed to update users. Error: %s", result.status.message); + } - db.dropCollection("users"); + db.dropCollection("users"); } -void DbTester::findMany(){ - // Create multiple documents so we can find them. - multiDocCreate(20); +void DbTester::findMany() { + // Create multiple documents so we can find them. + multiDocCreate(20); - // Search for admins - auto result = db.findMany("users", [](const DocView &doc) { - return doc["role"].as() == "admin"; - }); + // Search for admins + auto result = db.findMany("users", [](const DocView &doc) { + return doc["role"].as() == "admin"; + }); - if( result.status.ok() ){ - ESP_LOGI(DB_TESTER_TAG, "Found %d users", result.value.size()); - }else{ - ESP_LOGE(DB_TESTER_TAG, "Failed to find users. Error: %s", result.status.message); - } + if (result.status.ok()) { + ESP_LOGI(DB_TESTER_TAG, "Found %d users", result.value.size()); + } else { + ESP_LOGE(DB_TESTER_TAG, "Failed to find users. Error: %s", result.status.message); + } - db.dropCollection("users"); + db.dropCollection("users"); } diff --git a/test/collectionTests.cpp b/test/collectionTests.cpp index eba1153..804f99d 100644 --- a/test/collectionTests.cpp +++ b/test/collectionTests.cpp @@ -1,60 +1,46 @@ #include "dbTest.h" -void DbTester::simpleCollectionCreate(){ - auto result = db.collection("sensors"); - if( !result.status.ok() ){ - ESP_LOGE( - DB_TESTER_TAG, - "Failed to create 'sensors' collection. Error: %s", - result.status.message - ); - }else{ - ESP_LOGI(DB_TESTER_TAG, "Created 'sensors' collection"); - } +void DbTester::simpleCollectionCreate() { + auto result = db.collection("sensors"); + if (!result.status.ok()) { + ESP_LOGE( + DB_TESTER_TAG, + "Failed to create 'sensors' collection. Error: %s", + result.status.message + ); + } else { + ESP_LOGI(DB_TESTER_TAG, "Created 'sensors' collection"); + } } -void DbTester::simpleCollectionRemove(){ - auto result = db.dropCollection("sensors"); - if( !result.ok() ){ - ESP_LOGE( - DB_TESTER_TAG, - "Failed to drop 'sensors' collection. Error: %s", - result.message - ); - }else{ - ESP_LOGI(DB_TESTER_TAG, "Dropped 'sensors' collection"); - } +void DbTester::simpleCollectionRemove() { + auto result = db.dropCollection("sensors"); + if (!result.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to drop 'sensors' collection. Error: %s", result.message); + } else { + ESP_LOGI(DB_TESTER_TAG, "Dropped 'sensors' collection"); + } } -void DbTester::multiCollectionCreate(int collNum){ - int created = 0; - for (int i = 0; i < collNum; i++) { - std::string collectionName = "test_" + std::to_string(i); - auto result = db.collection(collectionName); - if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to create '%s' collection", - collectionName.c_str()); - continue; - } - created++; - } - ESP_LOGI( - DB_TESTER_TAG, - "Created %d collection", - created); +void DbTester::multiCollectionCreate(int collNum) { + int created = 0; + for (int i = 0; i < collNum; i++) { + std::string collectionName = "test_" + std::to_string(i); + auto result = db.collection(collectionName); + if (!result.status.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to create '%s' collection", collectionName.c_str()); + continue; + } + created++; + } + ESP_LOGI(DB_TESTER_TAG, "Created %d collection", created); } -void DbTester::allCollectionDrop(){ - auto result = db.dropAll(); - if( !result.ok() ){ - ESP_LOGE( - DB_TESTER_TAG, - "Failed to drop all collections. Error: %s", - result.message - ); - }else{ - ESP_LOGI(DB_TESTER_TAG, "Dropped all collections"); - } +void DbTester::allCollectionDrop() { + auto result = db.dropAll(); + if (!result.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to drop all collections. Error: %s", result.message); + } else { + ESP_LOGI(DB_TESTER_TAG, "Dropped all collections"); + } } diff --git a/test/dbTest.h b/test/dbTest.h index bcafe2d..d78a9b1 100644 --- a/test/dbTest.h +++ b/test/dbTest.h @@ -1,50 +1,50 @@ +#include "../src/esp_jsondb/db.h" #include #include -#include "../src/esp_jsondb/db.h" #define DB_TESTER_TAG "DB_Tester" class DbTester { - public: - void init(); - private: - ESPJsonDB db; - void run(); - // Event handlers - void dbEventHandler(DBEventType evt); - void dbErrorHandler(const DbStatus &st); + public: + void init(); - // Document tests - std::string lastNewDocId; - void simpleDocCreate(); - void simpleDocRemove(); - void multiDocCreate(int docNum); - void multiDocRemove(); - void refPopulateTest(); - void fileStorageTest(); - void asyncFileUploadTest(); - void asyncFileUploadRetentionBoundTest(); - // Collection tests - void simpleCollectionCreate(); - void simpleCollectionRemove(); - void multiCollectionCreate(int collNum); - void allCollectionDrop(); - // Bulk tests - void updateManyFilter(); - void updateManyLambdaFilter(); - void updateManyCombined(); - void findMany(); - // Schema tests - Schema userSchema; - void schemaFailDocCreate(); - void schemaSuccessDocCreate(); - void schemaFailWithTypesDocCreate(); - void schemaSuccessWithTypesDocCreate(); - void schemaFailDocUpdate(); - // Utils - void printDBDiag(); - void teardownLifecycle(); + private: + ESPJsonDB db; + void run(); + // Event handlers + void dbEventHandler(DBEventType evt); + void dbErrorHandler(const DbStatus &st); + // Document tests + std::string lastNewDocId; + void simpleDocCreate(); + void simpleDocRemove(); + void multiDocCreate(int docNum); + void multiDocRemove(); + void refPopulateTest(); + void fileStorageTest(); + void asyncFileUploadTest(); + void asyncFileUploadRetentionBoundTest(); + // Collection tests + void simpleCollectionCreate(); + void simpleCollectionRemove(); + void multiCollectionCreate(int collNum); + void allCollectionDrop(); + // Bulk tests + void updateManyFilter(); + void updateManyLambdaFilter(); + void updateManyCombined(); + void findMany(); + // Schema tests + Schema userSchema; + void schemaFailDocCreate(); + void schemaSuccessDocCreate(); + void schemaFailWithTypesDocCreate(); + void schemaSuccessWithTypesDocCreate(); + void schemaFailDocUpdate(); + // Utils + void printDBDiag(); + void teardownLifecycle(); }; inline DbTester dbTester; diff --git a/test/documentTests.cpp b/test/documentTests.cpp index 6aabea5..9bd112c 100644 --- a/test/documentTests.cpp +++ b/test/documentTests.cpp @@ -7,28 +7,25 @@ void DbTester::simpleDocCreate() { newUser["username"] = "esp-jsondb"; auto result = db.create("users", newUser.as()); if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - result.status.message - ); - }else{ - ESP_LOGI(DB_TESTER_TAG, "New user created"); + ESP_LOGE(DB_TESTER_TAG, "Failed to add new user to DB. Error: %s", result.status.message); + } else { + ESP_LOGI(DB_TESTER_TAG, "New user created"); lastNewDocId = result.value; - } + } } -void DbTester::simpleDocRemove(){ - if( lastNewDocId.empty() ) return; +void DbTester::simpleDocRemove() { + if (lastNewDocId.empty()) + return; auto result = db.removeById("users", lastNewDocId); - if( !result.ok() ){ + if (!result.ok()) { ESP_LOGE(DB_TESTER_TAG, "Failed to remove user from DB. Error: %s", result.message); - }else{ + } else { ESP_LOGI(DB_TESTER_TAG, "User removed"); } } -void DbTester::multiDocCreate(int docNum){ +void DbTester::multiDocCreate(int docNum) { int created = 0; for (int index = 0; index < docNum; index++) { JsonDocument newUser; @@ -38,34 +35,30 @@ void DbTester::multiDocCreate(int docNum){ auto result = db.create("users", newUser.as()); if (result.status.ok()) { created++; - }else{ + } else { ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user (%s) to DB. Error: %s", - newUser["email"], - result.status.message + DB_TESTER_TAG, + "Failed to add new user (%s) to DB. Error: %s", + newUser["email"], + result.status.message ); } } - ESP_LOGI( - DB_TESTER_TAG, - "Created %d document", - created - ); + ESP_LOGI(DB_TESTER_TAG, "Created %d document", created); } -void DbTester::multiDocRemove(){ +void DbTester::multiDocRemove() { // Remove all admins auto result = db.removeMany("users", [](const DocView &doc) { return doc["role"].as() == "admin"; }); if (!result.status.ok()) { ESP_LOGE( - DB_TESTER_TAG, - "Failed to remove users with admin role. Error: %s", - result.status.message + DB_TESTER_TAG, + "Failed to remove users with admin role. Error: %s", + result.status.message ); - }else{ + } else { ESP_LOGI(DB_TESTER_TAG, "Removed %d users", result.value); } } diff --git a/test/fileTests.cpp b/test/fileTests.cpp index d66e1b8..8dc90aa 100644 --- a/test/fileTests.cpp +++ b/test/fileTests.cpp @@ -63,7 +63,11 @@ void DbTester::fileStorageTest() { return; } - auto copiedFromPath = db.writeFileFromPath("bin/copied_from_path.bin", "/test_db/_files/bin/source.bin", streamOpts); + auto copiedFromPath = db.writeFileFromPath( + "bin/copied_from_path.bin", + "/test_db/_files/bin/source.bin", + streamOpts + ); if (!copiedFromPath.ok()) { ESP_LOGE(DB_TESTER_TAG, "writeFileFromPath failed: %s", copiedFromPath.message); return; @@ -83,8 +87,10 @@ void DbTester::fileStorageTest() { syncCtx.data = binaryPayload.data(); syncCtx.size = binaryPayload.size(); - auto pullCb = [&syncCtx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; + auto pullCb = + [&syncCtx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; if (syncCtx.offset >= syncCtx.size) { produced = 0; eof = true; @@ -111,20 +117,22 @@ void DbTester::fileStorageTest() { return; } - auto sourceNotFound = db.writeFileFromPath("bin/not_created.bin", "/test_db/_files/bin/missing.bin", streamOpts); + auto sourceNotFound = + db.writeFileFromPath("bin/not_created.bin", "/test_db/_files/bin/missing.bin", streamOpts); if (sourceNotFound.ok() || sourceNotFound.code != DbStatusCode::NotFound) { ESP_LOGE(DB_TESTER_TAG, "writeFileFromPath missing-source check failed"); return; } auto invalidProducer = db.writeFileStream( - "bin/invalid_callback.bin", - [](size_t requested, uint8_t *, size_t &produced, bool &eof) -> DbStatus { - produced = requested + 1; - eof = false; - return {DbStatusCode::Ok, ""}; - }, - streamOpts); + "bin/invalid_callback.bin", + [](size_t requested, uint8_t *, size_t &produced, bool &eof) -> DbStatus { + produced = requested + 1; + eof = false; + return {DbStatusCode::Ok, ""}; + }, + streamOpts + ); if (invalidProducer.ok() || invalidProducer.code != DbStatusCode::InvalidArgument) { ESP_LOGE(DB_TESTER_TAG, "writeFileStream invalid producer check failed"); return; @@ -176,8 +184,10 @@ void DbTester::asyncFileUploadTest() { volatile bool doneOk = false; volatile size_t doneBytes = 0; - DbFileUploadPullCb pullCb = [&ctx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; + DbFileUploadPullCb pullCb = + [&ctx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; if (ctx.offset >= ctx.size) { produced = 0; eof = true; @@ -192,11 +202,12 @@ void DbTester::asyncFileUploadTest() { return {DbStatusCode::Ok, ""}; }; - DbFileUploadDoneCb doneCb = [&done, &doneOk, &doneBytes](uint32_t, const DbStatus &st, size_t bytesWritten) { - doneOk = st.ok(); - doneBytes = bytesWritten; - done = true; - }; + DbFileUploadDoneCb doneCb = + [&done, &doneOk, &doneBytes](uint32_t, const DbStatus &st, size_t bytesWritten) { + doneOk = st.ok(); + doneBytes = bytesWritten; + done = true; + }; ESPJsonDBFileOptions opts; opts.overwrite = true; @@ -263,8 +274,10 @@ void DbTester::asyncFileUploadRetentionBoundTest() { volatile bool doneOk = false; volatile size_t doneBytes = 0; - DbFileUploadPullCb pullCb = [&ctx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { - if (!buffer) return {DbStatusCode::InvalidArgument, "buffer is null"}; + DbFileUploadPullCb pullCb = + [&ctx](size_t requested, uint8_t *buffer, size_t &produced, bool &eof) -> DbStatus { + if (!buffer) + return {DbStatusCode::InvalidArgument, "buffer is null"}; if (ctx.offset >= ctx.size) { produced = 0; eof = true; @@ -279,18 +292,22 @@ void DbTester::asyncFileUploadRetentionBoundTest() { return {DbStatusCode::Ok, ""}; }; - DbFileUploadDoneCb doneCb = [&done, &doneOk, &doneBytes](uint32_t, const DbStatus &st, size_t bytesWritten) { - doneOk = st.ok(); - doneBytes = bytesWritten; - done = true; - }; + DbFileUploadDoneCb doneCb = + [&done, &doneOk, &doneBytes](uint32_t, const DbStatus &st, size_t bytesWritten) { + doneOk = st.ok(); + doneBytes = bytesWritten; + done = true; + }; const std::string path = "async/retention_" + std::to_string(i) + ".bin"; auto asyncRes = db.writeFileStreamAsync(path, pullCb, opts, doneCb); if (!asyncRes.status.ok()) { - ESP_LOGE(DB_TESTER_TAG, "Retention test upload start failed at %u: %s", - static_cast(i), - asyncRes.status.message); + ESP_LOGE( + DB_TESTER_TAG, + "Retention test upload start failed at %u: %s", + static_cast(i), + asyncRes.status.message + ); return; } uploadIds.push_back(asyncRes.value); @@ -300,13 +317,21 @@ void DbTester::asyncFileUploadRetentionBoundTest() { delay(5); } if (!done || !doneOk || doneBytes != payload.size()) { - ESP_LOGE(DB_TESTER_TAG, "Retention test upload completion mismatch at %u", static_cast(i)); + ESP_LOGE( + DB_TESTER_TAG, + "Retention test upload completion mismatch at %u", + static_cast(i) + ); return; } auto latestState = db.getFileUploadState(asyncRes.value); if (!latestState.status.ok() || latestState.value != DbFileUploadState::Completed) { - ESP_LOGE(DB_TESTER_TAG, "Retention test latest upload state mismatch at %u", static_cast(i)); + ESP_LOGE( + DB_TESTER_TAG, + "Retention test latest upload state mismatch at %u", + static_cast(i) + ); return; } diff --git a/test/refTests.cpp b/test/refTests.cpp index b067593..63ec433 100644 --- a/test/refTests.cpp +++ b/test/refTests.cpp @@ -3,47 +3,51 @@ void DbTester::refPopulateTest() { // create author - JsonDocument authorDoc; - authorDoc["name"] = "John Doe"; - auto authorCreateRes = db.create("authors", authorDoc.as()); - if (!authorCreateRes.status.ok()) { - ESP_LOGE(DB_TESTER_TAG, "Failed to create author. Error: %s", authorCreateRes.status.message); - return; - } - std::string authorId = authorCreateRes.value; + JsonDocument authorDoc; + authorDoc["name"] = "John Doe"; + auto authorCreateRes = db.create("authors", authorDoc.as()); + if (!authorCreateRes.status.ok()) { + ESP_LOGE( + DB_TESTER_TAG, + "Failed to create author. Error: %s", + authorCreateRes.status.message + ); + return; + } + std::string authorId = authorCreateRes.value; // create book referencing author - DocRef authorRef{"authors", authorId}; - JsonDocument book; - book["title"] = "Example Book"; - JsonObject authorRefObj = book["author"].to(); - authorRefObj["collection"] = authorRef.collection; - authorRefObj["_id"] = authorRef.id; - auto bookCreateRes = db.create("books", book.as()); - if (!bookCreateRes.status.ok()) { - ESP_LOGE(DB_TESTER_TAG, "Failed to create book. Error: %s", bookCreateRes.status.message); - db.dropCollection("authors"); - return; - } - std::string bookId = bookCreateRes.value; + DocRef authorRef{"authors", authorId}; + JsonDocument book; + book["title"] = "Example Book"; + JsonObject authorRefObj = book["author"].to(); + authorRefObj["collection"] = authorRef.collection; + authorRefObj["_id"] = authorRef.id; + auto bookCreateRes = db.create("books", book.as()); + if (!bookCreateRes.status.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to create book. Error: %s", bookCreateRes.status.message); + db.dropCollection("authors"); + return; + } + std::string bookId = bookCreateRes.value; // fetch book and populate author reference - auto bookFindRes = db.findById("books", bookId); - if (!bookFindRes.status.ok()) { - ESP_LOGE(DB_TESTER_TAG, "Failed to find book. Error: %s", bookFindRes.status.message); - } else { - auto populatedAuthor = bookFindRes.value.populate("author"); - if (!db.lastError().ok()) { - ESP_LOGE(DB_TESTER_TAG, "Populate failed: %s", db.lastError().message); - } else { - const char *authorName = populatedAuthor["name"].as(); - if (authorName && std::string(authorName) == "John Doe") { - ESP_LOGI(DB_TESTER_TAG, "Reference populated successfully"); - } else { - ESP_LOGE(DB_TESTER_TAG, "Reference populated but data mismatch"); - } - } - } + auto bookFindRes = db.findById("books", bookId); + if (!bookFindRes.status.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to find book. Error: %s", bookFindRes.status.message); + } else { + auto populatedAuthor = bookFindRes.value.populate("author"); + if (!db.lastError().ok()) { + ESP_LOGE(DB_TESTER_TAG, "Populate failed: %s", db.lastError().message); + } else { + const char *authorName = populatedAuthor["name"].as(); + if (authorName && std::string(authorName) == "John Doe") { + ESP_LOGI(DB_TESTER_TAG, "Reference populated successfully"); + } else { + ESP_LOGE(DB_TESTER_TAG, "Reference populated but data mismatch"); + } + } + } // cleanup collections auto st = db.dropCollection("books"); diff --git a/test/schemaTests.cpp b/test/schemaTests.cpp index 9929f13..798cdec 100644 --- a/test/schemaTests.cpp +++ b/test/schemaTests.cpp @@ -24,10 +24,7 @@ void DbTester::schemaFailDocCreate() { newUser["thing"] = "notAPassword"; auto result = db.create("users", newUser.as()); if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - result.status.message); + ESP_LOGE(DB_TESTER_TAG, "Failed to add new user to DB. Error: %s", result.status.message); } else { ESP_LOGI(DB_TESTER_TAG, "New user created"); } @@ -43,10 +40,7 @@ void DbTester::schemaSuccessDocCreate() { newUser["password"] = "aSecureHashedPassword"; auto result = db.create("users", newUser.as()); if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - result.status.message); + ESP_LOGE(DB_TESTER_TAG, "Failed to add new user to DB. Error: %s", result.status.message); } else { ESP_LOGI(DB_TESTER_TAG, "New user created"); } @@ -54,13 +48,13 @@ void DbTester::schemaSuccessDocCreate() { void DbTester::schemaFailWithTypesDocCreate() { userSchema.fields = { - // key - type - default value - {"email", FieldType::String, "a@b.c"}, - {"username", FieldType::String}, - {"role", FieldType::String, "user"}, - {"password", FieldType::String}, - {"age", FieldType::Int}, - {"height", FieldType::Int}, + // key - type - default value + {"email", FieldType::String, "a@b.c"}, + {"username", FieldType::String}, + {"role", FieldType::String, "user"}, + {"password", FieldType::String}, + {"age", FieldType::Int}, + {"height", FieldType::Int}, }; JsonDocument newUser; @@ -69,10 +63,7 @@ void DbTester::schemaFailWithTypesDocCreate() { newUser["age"] = "cya"; auto result = db.create("users", newUser.as()); if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - result.status.message); + ESP_LOGE(DB_TESTER_TAG, "Failed to add new user to DB. Error: %s", result.status.message); } else { ESP_LOGI(DB_TESTER_TAG, "New user created"); } @@ -85,10 +76,7 @@ void DbTester::schemaSuccessWithTypesDocCreate() { newUser["age"] = 18; auto result = db.create("users", newUser.as()); if (!result.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - result.status.message); + ESP_LOGE(DB_TESTER_TAG, "Failed to add new user to DB. Error: %s", result.status.message); } else { ESP_LOGI(DB_TESTER_TAG, "New user created"); } @@ -99,8 +87,8 @@ void DbTester::schemaFailDocUpdate() { // Start from a clean collection and register a strict schema db.dropCollection("users"); userSchema.fields = { - {"username", FieldType::String}, - {"password", FieldType::String}, + {"username", FieldType::String}, + {"password", FieldType::String}, }; userSchema.validate = usersValidate; db.registerSchema("users", userSchema); @@ -112,35 +100,29 @@ void DbTester::schemaFailDocUpdate() { auto createRes = db.create("users", newUser.as()); if (!createRes.status.ok()) { ESP_LOGE( - DB_TESTER_TAG, - "Failed to add new user to DB. Error: %s", - createRes.status.message); + DB_TESTER_TAG, + "Failed to add new user to DB. Error: %s", + createRes.status.message + ); } else { ESP_LOGI(DB_TESTER_TAG, "New user created"); } - const std::string userId = createRes.value; + const std::string userId = createRes.value; // Attempt to update with invalid data (wrong type for password) - DbStatus updateStatus = db.updateById("users", userId, [](DocView &doc) { - doc["password"].set(123); - }); - if (!updateStatus.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to update user document. Error: %s", - updateStatus.message); - } else { - ESP_LOGI(DB_TESTER_TAG, "User document updated"); - } + DbStatus updateStatus = + db.updateById("users", userId, [](DocView &doc) { doc["password"].set(123); }); + if (!updateStatus.ok()) { + ESP_LOGE(DB_TESTER_TAG, "Failed to update user document. Error: %s", updateStatus.message); + } else { + ESP_LOGI(DB_TESTER_TAG, "User document updated"); + } // Verify the original document remains unchanged - auto findRes = db.findById("users", userId); + auto findRes = db.findById("users", userId); if (!findRes.status.ok()) { - ESP_LOGE( - DB_TESTER_TAG, - "Failed to find user document. Error: %s", - findRes.status.message); + ESP_LOGE(DB_TESTER_TAG, "Failed to find user document. Error: %s", findRes.status.message); } else { ESP_LOGI(DB_TESTER_TAG, "User document found"); }