diff --git a/pjson_db_pmm.h b/pjson_db_pmm.h index e80f613..b328e9a 100644 --- a/pjson_db_pmm.h +++ b/pjson_db_pmm.h @@ -183,7 +183,7 @@ class pjson_db_pmm return _get_metrics( path ); node_error err = node_error::none; - node_id cur = _walk_path( path, deref_refs, &err ); + node_id cur = _walk_path_read( path, deref_refs, &err ); if ( cur == 0 ) return node_view_error( err ); @@ -586,17 +586,16 @@ class pjson_db_pmm } // ----------------------------------------------------------------------- - // Общий обход пути: _walk_path + // Обход пути: _walk_path_read / _walk_path_create (Этап 10.4) // ----------------------------------------------------------------------- - /// Обход пути по сегментам с двумя режимами: - /// CreateMode=false (read) — возвращает 0 при отсутствии узла; - /// CreateMode=true (create) — создаёт промежуточные узлы. + /// Обход пути по сегментам (read-only). + /// Возвращает 0 при отсутствии узла, не создаёт промежуточных узлов. /// @param path абсолютный путь вида /a/b/0/c /// @param deref_refs разыменовывать ли $ref-узлы при обходе - /// @param out_err (только read) указатель на код ошибки (может быть nullptr) + /// @param out_err указатель на код ошибки (может быть nullptr) /// @return node_id целевого узла или 0 при ошибке - template node_id _walk_path( const char* path, bool deref_refs, node_error* out_err ) const + node_id _walk_path_read( const char* path, bool deref_refs, node_error* out_err ) const { auto fail = [&]( node_error e ) -> node_id { @@ -646,7 +645,91 @@ class pjson_db_pmm node_view cur_v{ cur }; - [[maybe_unused]] bool is_last = ( *p == '\0' ); + // Единственный вызов tag() вместо отдельных is_object() + is_array() (Этап 10.3). + node_tag cur_tag = cur_v.tag(); + + // --- навигация по object --- + if ( cur_tag == node_tag::object ) + { + node_view child = cur_v.at( seg.c_str() ); + if ( child.valid() ) + cur = child.id; + else + return fail( node_error::not_found ); + } + // --- навигация по array --- + else if ( cur_tag == node_tag::array ) + { + char* end_ptr = nullptr; + uintptr_t idx = static_cast( std::strtoull( seg.c_str(), &end_ptr, 10 ) ); + if ( end_ptr == seg.c_str() || *end_ptr != '\0' ) + return fail( node_error::wrong_type ); + + node_view elem = cur_v.at( idx ); + if ( elem.valid() ) + cur = elem.id; + else + return fail( node_error::index_out_of_range ); + } + // --- ни object, ни array --- + else + { + return fail( node_error::wrong_type ); + } + } + + return cur; + } + + /// Обход пути по сегментам (create). + /// Создаёт промежуточные узлы при их отсутствии. + /// @param path абсолютный путь вида /a/b/0/c + /// @param deref_refs разыменовывать ли $ref-узлы при обходе + /// @return node_id целевого узла или 0 при ошибке + node_id _walk_path_create( const char* path, bool deref_refs ) + { + if ( path == nullptr ) + return 0; + + node_id cur = _find_root(); + if ( cur == 0 ) + return 0; + + const char* p = path; + if ( *p == '/' ) + ++p; + + while ( *p != '\0' ) + { + // --- разбор сегмента --- + const char* seg_start = p; + while ( *p != '\0' && *p != '/' ) + ++p; + uintptr_t seg_len = static_cast( p - seg_start ); + if ( *p == '/' ) + ++p; + + if ( seg_len == 0 ) + continue; + + std::string seg = pjson_decode_rfc6901_segment( seg_start, seg_len ); + + // --- разыменование $ref --- + if ( deref_refs ) + { + node_view cur_v{ cur }; + if ( cur_v.is_ref() ) + { + node_view resolved = cur_v.deref( true, 32 ); + if ( !resolved.valid() ) + return 0; + cur = resolved.id; + } + } + + node_view cur_v{ cur }; + + bool is_last = ( *p == '\0' ); // Единственный вызов tag() вместо отдельных is_object() + is_array() (Этап 10.3). node_tag cur_tag = cur_v.tag(); @@ -659,7 +742,7 @@ class pjson_db_pmm { cur = child.id; } - else if constexpr ( CreateMode ) + else { node_id slot = node_object_insert( cur, seg.c_str() ); if ( slot == 0 ) @@ -673,10 +756,6 @@ class pjson_db_pmm } cur = slot; } - else - { - return fail( node_error::not_found ); - } } // --- навигация по array --- else if ( cur_tag == node_tag::array ) @@ -684,14 +763,14 @@ class pjson_db_pmm char* end_ptr = nullptr; uintptr_t idx = static_cast( std::strtoull( seg.c_str(), &end_ptr, 10 ) ); if ( end_ptr == seg.c_str() || *end_ptr != '\0' ) - return fail( node_error::wrong_type ); + return 0; node_view elem = cur_v.at( idx ); if ( elem.valid() ) { cur = elem.id; } - else if constexpr ( CreateMode ) + else { uintptr_t cur_size = cur_v.size(); for ( uintptr_t i = cur_size; i <= idx; ++i ) @@ -710,22 +789,14 @@ class pjson_db_pmm } } } - else - { - return fail( node_error::index_out_of_range ); - } } // --- ни object, ни array --- - else if constexpr ( CreateMode ) + else { if ( is_last ) return cur; return 0; } - else - { - return fail( node_error::wrong_type ); - } } return cur; @@ -943,7 +1014,7 @@ class pjson_db_pmm return node_view{ tmp_off }; } - node_id _ensure_path( const char* path ) { return _walk_path( path, true, nullptr ); } + node_id _ensure_path( const char* path ) { return _walk_path_create( path, true ); } // ----------------------------------------------------------------------- // Вспомогательные методы: удаление узлов diff --git a/plan.md b/plan.md index 5b155b7..157d41a 100644 --- a/plan.md +++ b/plan.md @@ -21,7 +21,7 @@ | 7. Унификация итераторов | CRTP-база pjson_iterator_base + шаблонный pjson_range; ~22 строки удалено | ✅ | **Итого удалено:** ~5 файлов (~1900 строк), ~381 строка дублирования. -**Тесты:** 690 тестов, ~360 000 assertion. +**Тесты:** 700 тестов, ~360 000 assertion. --- @@ -108,13 +108,13 @@ --- -### Проблема 11: Отсутствие const-корректности в _walk_path +### ~~Проблема 11: Отсутствие const-корректности в _walk_path~~ ✅ -**Файл:** `pjson_db_pmm.h`, строка 593 +**Решено в Этапе 10.4:** Шаблонный `_walk_path() const` разделён на два метода с корректными квалификаторами: +- `_walk_path_read()` — `const`, только чтение, возвращает 0 при отсутствии узла; +- `_walk_path_create()` — без `const`, создаёт промежуточные узлы. -Метод `_walk_path()` объявлен `const`, но в режиме `CreateMode=true` модифицирует дерево узлов. Это возможно только потому, что PMM — глобальное состояние, и модификация происходит через глобальные функции, а не через членов класса. - -**Связь:** Это следствие Проблемы 3 (глобальное состояние). +Устранена ложная `const`-квалификация метода, который в режиме `CreateMode=true` модифицировал дерево узлов через глобальное состояние PMM. Добавлены 10 тестов. --- @@ -184,7 +184,7 @@ pvector был бы предпочтительнее **только** при ч | ~~3~~ | ~~Глобальное состояние PMM (Этап A)~~ | ~~pam_pmm.h~~ | ~~Высокая~~ | ✅ | | ~~7~~ | ~~Нет escaping '/' в путях~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ | ✅ | | ~~10~~ | ~~Многократный resolve в is_*()~~ | ~~pjson_node.h~~ | ~~Средняя~~ | ✅ | -| 11 | const-корректность _walk_path | pjson_db_pmm.h | Средняя | Корректность | +| ~~11~~ | ~~const-корректность _walk_path~~ | ~~pjson_db_pmm.h~~ | ~~Средняя~~ | ✅ | --- @@ -209,7 +209,7 @@ pvector был бы предпочтительнее **только** при ч 10.1 ✅ Инкапсуляция глобального состояния pam_pmm (Этап A: структура pam_pmm_state + синглтон) 10.2 ✅ Поддержка RFC 6901 (JSON Pointer) для путей 10.3 ✅ Оптимизация tag-проверок на горячих путях - 10.4 const-корректность с явной передачей состояния + 10.4 ✅ const-корректность _walk_path: разделение на _walk_path_read (const) и _walk_path_create ``` --- @@ -218,6 +218,7 @@ pvector был бы предпочтительнее **только** при ч | Дата | Изменение | |------|-----------| +| 2026-03-22 | Этап 10.4: const-корректность _walk_path — разделение на _walk_path_read (const) и _walk_path_create (Issue #208) | | 2026-03-22 | Этап 10.3: оптимизация tag-проверок на горячих путях — сокращение избыточных pmm_resolve (Issue #207) | | 2026-03-22 | Этап 10.2: поддержка RFC 6901 (JSON Pointer) для путей — escaping ~/slash в ключах (Issue #206) | | 2026-03-22 | Этап 10.1: инкапсуляция глобального состояния pam_pmm в структуру pam_pmm_state (Issue #205) | diff --git a/readme.md b/readme.md index 74c422c..00de5ba 100644 --- a/readme.md +++ b/readme.md @@ -139,7 +139,7 @@ int main() { | `pjson_db_pmm.h` | D | Менеджер персистной JSON-БД: path-адресация, `put`/`get`/`erase`, `$ref`, метрики, поиск, клонирование | | `deps/pmm/pmm.h` | A | [PersistMemoryManager](https://github.com/netkeep80/PersistMemoryManager) — бэкенд ПАП | | `main.cpp` | — | Демонстрационная программа | -| `tests/` | — | Тесты на Catch2 (690 тестов, ~360 000 assertion) | +| `tests/` | — | Тесты на Catch2 (700 тестов, ~360 000 assertion) | | `CMakeLists.txt` | — | Система сборки (CMake 3.16+, C++20) | --- @@ -455,6 +455,7 @@ db.put("/copy/name", "Bob"); - ~~**Нет escaping `/` в путях**~~ — **Исправлено** в Этапе 10.2: поддержка RFC 6901 (JSON Pointer) — `~1` для `/`, `~0` для `~` в сегментах путей - ~~**Утечка временных узлов метрик**~~ — **Исправлено** в Этапе 8.4: один pre-allocated узел переиспользуется для всех вызовов метрик - ~~**Многократный resolve в is_*() проверках**~~ — **Исправлено** в Этапе 10.3: `is_number()`, `deref()`, traversal и walk_path оптимизированы для единственного `pmm_resolve` вместо повторных вызовов +- ~~**const-некорректность _walk_path**~~ — **Исправлено** в Этапе 10.4: шаблонный `_walk_path() const` разделён на `_walk_path_read() const` (только чтение) и `_walk_path_create()` (мутирующий) - **Не потокобезопасно** — CacheManagerConfig (по умолчанию) использует NoLock; для многопоточности нужен PersistentDataConfig - **Строки не освобождаются** — словарь `pstringview_pmm` только растёт diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d4cc28..5fb44c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(TEST_SOURCES test_pjson_ref_stability.cpp test_pjson_rfc6901.cpp test_pjson_tag_opt.cpp + test_pjson_const_correctness.cpp ) add_executable(tests ${TEST_SOURCES}) diff --git a/tests/test_pjson_const_correctness.cpp b/tests/test_pjson_const_correctness.cpp new file mode 100644 index 0000000..fe0fed7 --- /dev/null +++ b/tests/test_pjson_const_correctness.cpp @@ -0,0 +1,177 @@ +#include + +#include +#include + +#include "pjson_db_pmm.h" + +using namespace pjson; + +// Вспомогательная функция: сбросить PMM перед каждым тестом. +namespace +{ +void reset_pam() +{ + pstringview_manager::reset(); + pam_pmm_reset(); +} +} // anonymous namespace + +// ============================================================================= +// Этап 10.4: const-корректность _walk_path — тесты +// ============================================================================= + +// --------------------------------------------------------------------------- +// const get(): read-only обход пути работает на const-ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: get works on const db reference", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/a/b", 42 ); + + const pjson_db_pmm& cdb = db; + node_view v = cdb.get( "/a/b" ); + REQUIRE( v.valid() ); + REQUIRE( v.is_integer() ); + REQUIRE( v.as_int() == 42 ); +} + +TEST_CASE( "const_correctness: get returns error on const db for missing path", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/x", 1 ); + + const pjson_db_pmm& cdb = db; + node_view v = cdb.get( "/nonexistent" ); + REQUIRE( v.is_error() ); + REQUIRE( v.error() == node_error::not_found ); +} + +// --------------------------------------------------------------------------- +// const exists(): работает на const-ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: exists works on const db reference", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/foo/bar", "hello" ); + + const pjson_db_pmm& cdb = db; + REQUIRE( cdb.exists( "/foo/bar" ) ); + REQUIRE_FALSE( cdb.exists( "/foo/missing" ) ); +} + +// --------------------------------------------------------------------------- +// const find(): работает на const-ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: find works on const db reference", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/k", 99 ); + + const pjson_db_pmm& cdb = db; + node_view v = cdb.find( "/k" ); + REQUIRE( v.valid() ); + REQUIRE( v.as_int() == 99 ); +} + +// --------------------------------------------------------------------------- +// const root() / root_id(): работают на const-ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: root and root_id work on const db reference", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/v", 1 ); + + const pjson_db_pmm& cdb = db; + REQUIRE( cdb.root_id() != 0 ); + REQUIRE( cdb.root().valid() ); +} + +// --------------------------------------------------------------------------- +// const metrics(): работает на const-ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: metrics works on const db reference", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/m", 1 ); + + const pjson_db_pmm& cdb = db; + node_view nc = cdb.get( "/$metrics/node_count_total" ); + REQUIRE( nc.valid() ); + REQUIRE( nc.is_uinteger() ); + REQUIRE( nc.as_uint() > 0 ); +} + +// --------------------------------------------------------------------------- +// non-const put: мутирующие методы работают на non-const ссылке +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: put creates intermediate nodes correctly", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + + REQUIRE( db.put( "/deep/nested/path", 123 ) ); + node_view v = db.get( "/deep/nested/path" ); + REQUIRE( v.valid() ); + REQUIRE( v.as_int() == 123 ); +} + +// --------------------------------------------------------------------------- +// _walk_path_read: навигация по массивам +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: const get navigates arrays correctly", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/arr/0", 10 ); + db.put( "/arr/1", 20 ); + db.put( "/arr/2", 30 ); + + const pjson_db_pmm& cdb = db; + REQUIRE( cdb.get( "/arr/0" ).as_int() == 10 ); + REQUIRE( cdb.get( "/arr/1" ).as_int() == 20 ); + REQUIRE( cdb.get( "/arr/2" ).as_int() == 30 ); + + // Out-of-range index + node_view oob = cdb.get( "/arr/5" ); + REQUIRE( oob.is_error() ); + REQUIRE( oob.error() == node_error::index_out_of_range ); +} + +// --------------------------------------------------------------------------- +// _walk_path_read: ошибка wrong_type при навигации через скалярный узел +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: const get returns wrong_type for scalar traversal", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + db.put( "/scalar", 42 ); + + const pjson_db_pmm& cdb = db; + node_view v = cdb.get( "/scalar/child" ); + REQUIRE( v.is_error() ); + REQUIRE( v.error() == node_error::wrong_type ); +} + +// --------------------------------------------------------------------------- +// Разделение read/create: create создаёт, read не создаёт +// --------------------------------------------------------------------------- +TEST_CASE( "const_correctness: get does not create intermediate nodes", "[pjson_db_pmm][const]" ) +{ + reset_pam(); + auto db = pjson_db_pmm::open( "" ); + + // Чтение несуществующего пути не должно создавать узлов + const pjson_db_pmm& cdb = db; + node_view v = cdb.get( "/should/not/exist" ); + REQUIRE( v.is_error() ); + + // Проверяем, что путь действительно не создан + REQUIRE_FALSE( cdb.exists( "/should" ) ); +}