| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- /**
- * Unit tests for per-collection timestamp precision (v1.6.0 Task 7).
- *
- * Exercises MemoryStore + CollectionConfigManager directly — no gRPC server.
- *
- * Test cases:
- * 1. ms is the default precision
- * 2. ns yields unique timestamps in a tight 1000-doc insert loop
- * 3. ms stamps fit in the ms range (< 10^15) over 1000 docs
- * 4. setConfig() is idempotent
- * 5. configFor() reflects setConfig() immediately (no reload required)
- * 6. Unknown collections return the default config
- */
- #include "../service/src/memory_store.hpp"
- #include "../service/src/config/collection_config_manager.hpp"
- #include "../service/src/document.hpp"
- #include <cassert>
- #include <cstdint>
- #include <iostream>
- #include <nlohmann/json.hpp>
- #include <optional>
- #include <set>
- #include <string>
- using smartbotic::database::CollectionCfg;
- using smartbotic::database::CollectionConfigManager;
- using smartbotic::database::CollectionOptions;
- using smartbotic::database::Document;
- using smartbotic::database::MemoryStore;
- namespace {
- int g_pass = 0;
- int g_fail = 0;
- void check(bool cond, const char* msg) {
- if (cond) {
- ++g_pass;
- } else {
- ++g_fail;
- std::cerr << "FAIL: " << msg << "\n";
- }
- }
- // Anything below 10^15 fits comfortably in the ms-since-epoch range (≈ 2001..),
- // anything above is unambiguously ns-since-epoch in the current era.
- constexpr uint64_t NS_THRESHOLD = 1'000'000'000'000'000ULL; // 10^15
- MemoryStore::Config defaultConfig() {
- MemoryStore::Config cfg;
- cfg.nodeId = "test";
- cfg.maxMemoryBytes = 256ULL * 1024 * 1024;
- return cfg;
- }
- Document makeDoc(const std::string& id = "") {
- Document d;
- if (!id.empty()) d.id = id;
- d.set_data(nlohmann::json{{"value", "x"}});
- return d;
- }
- // Mirrors the partial-update shape of DatabaseGrpcImpl::ConfigureCollection
- // (service/src/database_grpc_impl.cpp): start from the collection's current
- // config and overlay only the fields the caller actually set. An empty
- // precision string and a nullopt relations_enforced both mean "leave
- // unchanged" here, exactly as they do on the wire.
- void configureCollection(CollectionConfigManager& mgr,
- const std::string& collection,
- const std::string& precision,
- std::optional<bool> relationsEnforced) {
- CollectionCfg cfg = mgr.configFor(collection);
- if (!precision.empty()) {
- cfg.timestampPrecision = precision;
- }
- if (relationsEnforced.has_value()) {
- cfg.relationsEnforced = relationsEnforced.value();
- }
- std::string err;
- bool ok = mgr.setConfig(collection, cfg, err);
- check(ok, "setConfig should succeed for a partial-update configure call");
- }
- } // anonymous namespace
- void test_default_is_ns() {
- // v2.2 — default flipped to "ns" so rapid-write collections get
- // strict-ordering timestamps without an explicit configureCollection().
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- store.createCollection("docs_default", CollectionOptions{});
- auto id = store.insert("docs_default", makeDoc());
- auto doc = store.get("docs_default", id);
- assert(doc.has_value());
- assert(doc->createdAt > 0);
- assert(doc->createdAt >= NS_THRESHOLD); // ns range
- assert(doc->updatedAt > 0);
- assert(doc->updatedAt >= NS_THRESHOLD);
- store.stop();
- std::cout << "PASS: default precision is ns (createdAt=" << doc->createdAt << ")\n";
- }
- void test_ns_yields_unique_timestamps_in_tight_loop() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- store.createCollection("docs_ns", CollectionOptions{});
- CollectionCfg ns_cfg;
- ns_cfg.timestampPrecision = "ns";
- std::string err;
- bool ok = mgr.setConfig("docs_ns", ns_cfg, err);
- assert(ok);
- std::set<uint64_t> seen;
- constexpr int N = 1000;
- for (int i = 0; i < N; ++i) {
- auto id = store.insert("docs_ns", makeDoc());
- auto doc = store.get("docs_ns", id);
- assert(doc.has_value());
- assert(doc->createdAt >= NS_THRESHOLD); // ns range
- auto [it, inserted] = seen.insert(doc->createdAt);
- assert(inserted); // every timestamp must be unique
- }
- assert(seen.size() == static_cast<size_t>(N));
- store.stop();
- std::cout << "PASS: ns precision produces " << N
- << " unique timestamps in tight loop\n";
- }
- void test_ms_baseline_fits_in_ms_range() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- store.createCollection("docs_ms", CollectionOptions{});
- CollectionCfg ms_cfg;
- ms_cfg.timestampPrecision = "ms";
- std::string err;
- bool ok = mgr.setConfig("docs_ms", ms_cfg, err);
- assert(ok);
- constexpr int N = 1000;
- for (int i = 0; i < N; ++i) {
- auto id = store.insert("docs_ms", makeDoc());
- auto doc = store.get("docs_ms", id);
- assert(doc.has_value());
- assert(doc->createdAt < NS_THRESHOLD); // ms range
- }
- // We deliberately do NOT assert on tie-production here — it's machine-
- // dependent. The invariant that ms-stamped docs stay in the ms range is
- // what the default-preservation story needs.
- store.stop();
- std::cout << "PASS: ms precision stamps fit in ms range (" << N << " docs)\n";
- }
- void test_idempotent_configure() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- CollectionCfg cfg;
- cfg.timestampPrecision = "ns";
- std::string err;
- assert(mgr.setConfig("docs_idem", cfg, err));
- assert(mgr.configFor("docs_idem").timestampPrecision == "ns");
- assert(mgr.hasExplicitConfig("docs_idem"));
- // Second call with the same value — should succeed and leave state intact.
- assert(mgr.setConfig("docs_idem", cfg, err));
- assert(mgr.configFor("docs_idem").timestampPrecision == "ns");
- assert(mgr.hasExplicitConfig("docs_idem"));
- store.stop();
- std::cout << "PASS: setConfig is idempotent\n";
- }
- void test_cache_reflects_configure_immediately() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- // Before configure: default ns (v2.2), not explicit.
- assert(mgr.configFor("docs_cache").timestampPrecision == "ns");
- assert(!mgr.hasExplicitConfig("docs_cache"));
- CollectionCfg cfg;
- cfg.timestampPrecision = "ms";
- std::string err;
- assert(mgr.setConfig("docs_cache", cfg, err));
- // After configure: ms, immediately — no reloadFromStore() required.
- assert(mgr.configFor("docs_cache").timestampPrecision == "ms");
- assert(mgr.hasExplicitConfig("docs_cache"));
- store.stop();
- std::cout << "PASS: cache reflects setConfig immediately\n";
- }
- void test_default_for_unknown_collection() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- auto cfg = mgr.configFor("never_configured_collection");
- // v2.2 — default flipped from "ms" to "ns" so rapid-write collections
- // get nanosecond stamps without an explicit configureCollection() call.
- assert(cfg.timestampPrecision == "ns");
- assert(!mgr.hasExplicitConfig("never_configured_collection"));
- store.stop();
- std::cout << "PASS: default config returned for unknown collection\n";
- }
- // v2.11.0 T8 — relations_enforced is a partial update, same shape as
- // versioning_enabled (v2.4.5). Declaring a relation names its child and
- // parent explicitly, so the declaration IS the opt-in: the default must be
- // enforced, and an unrelated configure call (e.g. precision-only) must never
- // silently disable it.
- void test_relations_enforced_is_a_partial_update() {
- MemoryStore store(defaultConfig());
- store.start();
- CollectionConfigManager mgr(store);
- store.setConfigManager(&mgr);
- CollectionCfg cfg = mgr.configFor("c");
- check(cfg.relationsEnforced,
- "defaults to enforced - declaring a relation IS the opt-in, so a "
- "declared constraint must not silently do nothing");
- // Change ONLY precision. A plain proto3 bool defaults to false and would
- // silently disable enforcement here - the exact trap v2.4.5 hit with
- // versioning_enabled, which is why the field is `optional`.
- configureCollection(mgr, "c", /*precision=*/"ms", /*relationsEnforced=*/std::nullopt);
- check(mgr.configFor("c").relationsEnforced,
- "a precision-only call leaves enforcement alone");
- configureCollection(mgr, "c", "", /*relationsEnforced=*/false);
- check(!mgr.configFor("c").relationsEnforced, "and it can be turned off");
- check(mgr.configFor("c").timestampPrecision == "ms",
- "without resetting precision");
- store.stop();
- std::cout << "PASS: relations_enforced is a partial update\n";
- }
- int main() {
- test_default_is_ns();
- test_ns_yields_unique_timestamps_in_tight_loop();
- test_ms_baseline_fits_in_ms_range();
- test_idempotent_configure();
- test_cache_reflects_configure_immediately();
- test_default_for_unknown_collection();
- test_relations_enforced_is_a_partial_update();
- if (g_fail > 0) {
- std::cerr << "\n" << g_fail << " check(s) FAILED (" << g_pass << " passed)\n";
- return 1;
- }
- std::cout << "\nAll timestamp precision tests PASSED! (" << g_pass << " checks)\n";
- return 0;
- }
|