test_timestamp_precision.cpp 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /**
  2. * Unit tests for per-collection timestamp precision (v1.6.0 Task 7).
  3. *
  4. * Exercises MemoryStore + CollectionConfigManager directly — no gRPC server.
  5. *
  6. * Test cases:
  7. * 1. ms is the default precision
  8. * 2. ns yields unique timestamps in a tight 1000-doc insert loop
  9. * 3. ms stamps fit in the ms range (< 10^15) over 1000 docs
  10. * 4. setConfig() is idempotent
  11. * 5. configFor() reflects setConfig() immediately (no reload required)
  12. * 6. Unknown collections return the default config
  13. */
  14. #include "../service/src/memory_store.hpp"
  15. #include "../service/src/config/collection_config_manager.hpp"
  16. #include "../service/src/document.hpp"
  17. #include <cassert>
  18. #include <cstdint>
  19. #include <iostream>
  20. #include <nlohmann/json.hpp>
  21. #include <optional>
  22. #include <set>
  23. #include <string>
  24. using smartbotic::database::CollectionCfg;
  25. using smartbotic::database::CollectionConfigManager;
  26. using smartbotic::database::CollectionOptions;
  27. using smartbotic::database::Document;
  28. using smartbotic::database::MemoryStore;
  29. namespace {
  30. int g_pass = 0;
  31. int g_fail = 0;
  32. void check(bool cond, const char* msg) {
  33. if (cond) {
  34. ++g_pass;
  35. } else {
  36. ++g_fail;
  37. std::cerr << "FAIL: " << msg << "\n";
  38. }
  39. }
  40. // Anything below 10^15 fits comfortably in the ms-since-epoch range (≈ 2001..),
  41. // anything above is unambiguously ns-since-epoch in the current era.
  42. constexpr uint64_t NS_THRESHOLD = 1'000'000'000'000'000ULL; // 10^15
  43. MemoryStore::Config defaultConfig() {
  44. MemoryStore::Config cfg;
  45. cfg.nodeId = "test";
  46. cfg.maxMemoryBytes = 256ULL * 1024 * 1024;
  47. return cfg;
  48. }
  49. Document makeDoc(const std::string& id = "") {
  50. Document d;
  51. if (!id.empty()) d.id = id;
  52. d.set_data(nlohmann::json{{"value", "x"}});
  53. return d;
  54. }
  55. // Mirrors the partial-update shape of DatabaseGrpcImpl::ConfigureCollection
  56. // (service/src/database_grpc_impl.cpp): start from the collection's current
  57. // config and overlay only the fields the caller actually set. An empty
  58. // precision string and a nullopt relations_enforced both mean "leave
  59. // unchanged" here, exactly as they do on the wire.
  60. void configureCollection(CollectionConfigManager& mgr,
  61. const std::string& collection,
  62. const std::string& precision,
  63. std::optional<bool> relationsEnforced) {
  64. CollectionCfg cfg = mgr.configFor(collection);
  65. if (!precision.empty()) {
  66. cfg.timestampPrecision = precision;
  67. }
  68. if (relationsEnforced.has_value()) {
  69. cfg.relationsEnforced = relationsEnforced.value();
  70. }
  71. std::string err;
  72. bool ok = mgr.setConfig(collection, cfg, err);
  73. check(ok, "setConfig should succeed for a partial-update configure call");
  74. }
  75. } // anonymous namespace
  76. void test_default_is_ns() {
  77. // v2.2 — default flipped to "ns" so rapid-write collections get
  78. // strict-ordering timestamps without an explicit configureCollection().
  79. MemoryStore store(defaultConfig());
  80. store.start();
  81. CollectionConfigManager mgr(store);
  82. store.setConfigManager(&mgr);
  83. store.createCollection("docs_default", CollectionOptions{});
  84. auto id = store.insert("docs_default", makeDoc());
  85. auto doc = store.get("docs_default", id);
  86. assert(doc.has_value());
  87. assert(doc->createdAt > 0);
  88. assert(doc->createdAt >= NS_THRESHOLD); // ns range
  89. assert(doc->updatedAt > 0);
  90. assert(doc->updatedAt >= NS_THRESHOLD);
  91. store.stop();
  92. std::cout << "PASS: default precision is ns (createdAt=" << doc->createdAt << ")\n";
  93. }
  94. void test_ns_yields_unique_timestamps_in_tight_loop() {
  95. MemoryStore store(defaultConfig());
  96. store.start();
  97. CollectionConfigManager mgr(store);
  98. store.setConfigManager(&mgr);
  99. store.createCollection("docs_ns", CollectionOptions{});
  100. CollectionCfg ns_cfg;
  101. ns_cfg.timestampPrecision = "ns";
  102. std::string err;
  103. bool ok = mgr.setConfig("docs_ns", ns_cfg, err);
  104. assert(ok);
  105. std::set<uint64_t> seen;
  106. constexpr int N = 1000;
  107. for (int i = 0; i < N; ++i) {
  108. auto id = store.insert("docs_ns", makeDoc());
  109. auto doc = store.get("docs_ns", id);
  110. assert(doc.has_value());
  111. assert(doc->createdAt >= NS_THRESHOLD); // ns range
  112. auto [it, inserted] = seen.insert(doc->createdAt);
  113. assert(inserted); // every timestamp must be unique
  114. }
  115. assert(seen.size() == static_cast<size_t>(N));
  116. store.stop();
  117. std::cout << "PASS: ns precision produces " << N
  118. << " unique timestamps in tight loop\n";
  119. }
  120. void test_ms_baseline_fits_in_ms_range() {
  121. MemoryStore store(defaultConfig());
  122. store.start();
  123. CollectionConfigManager mgr(store);
  124. store.setConfigManager(&mgr);
  125. store.createCollection("docs_ms", CollectionOptions{});
  126. CollectionCfg ms_cfg;
  127. ms_cfg.timestampPrecision = "ms";
  128. std::string err;
  129. bool ok = mgr.setConfig("docs_ms", ms_cfg, err);
  130. assert(ok);
  131. constexpr int N = 1000;
  132. for (int i = 0; i < N; ++i) {
  133. auto id = store.insert("docs_ms", makeDoc());
  134. auto doc = store.get("docs_ms", id);
  135. assert(doc.has_value());
  136. assert(doc->createdAt < NS_THRESHOLD); // ms range
  137. }
  138. // We deliberately do NOT assert on tie-production here — it's machine-
  139. // dependent. The invariant that ms-stamped docs stay in the ms range is
  140. // what the default-preservation story needs.
  141. store.stop();
  142. std::cout << "PASS: ms precision stamps fit in ms range (" << N << " docs)\n";
  143. }
  144. void test_idempotent_configure() {
  145. MemoryStore store(defaultConfig());
  146. store.start();
  147. CollectionConfigManager mgr(store);
  148. store.setConfigManager(&mgr);
  149. CollectionCfg cfg;
  150. cfg.timestampPrecision = "ns";
  151. std::string err;
  152. assert(mgr.setConfig("docs_idem", cfg, err));
  153. assert(mgr.configFor("docs_idem").timestampPrecision == "ns");
  154. assert(mgr.hasExplicitConfig("docs_idem"));
  155. // Second call with the same value — should succeed and leave state intact.
  156. assert(mgr.setConfig("docs_idem", cfg, err));
  157. assert(mgr.configFor("docs_idem").timestampPrecision == "ns");
  158. assert(mgr.hasExplicitConfig("docs_idem"));
  159. store.stop();
  160. std::cout << "PASS: setConfig is idempotent\n";
  161. }
  162. void test_cache_reflects_configure_immediately() {
  163. MemoryStore store(defaultConfig());
  164. store.start();
  165. CollectionConfigManager mgr(store);
  166. store.setConfigManager(&mgr);
  167. // Before configure: default ns (v2.2), not explicit.
  168. assert(mgr.configFor("docs_cache").timestampPrecision == "ns");
  169. assert(!mgr.hasExplicitConfig("docs_cache"));
  170. CollectionCfg cfg;
  171. cfg.timestampPrecision = "ms";
  172. std::string err;
  173. assert(mgr.setConfig("docs_cache", cfg, err));
  174. // After configure: ms, immediately — no reloadFromStore() required.
  175. assert(mgr.configFor("docs_cache").timestampPrecision == "ms");
  176. assert(mgr.hasExplicitConfig("docs_cache"));
  177. store.stop();
  178. std::cout << "PASS: cache reflects setConfig immediately\n";
  179. }
  180. void test_default_for_unknown_collection() {
  181. MemoryStore store(defaultConfig());
  182. store.start();
  183. CollectionConfigManager mgr(store);
  184. store.setConfigManager(&mgr);
  185. auto cfg = mgr.configFor("never_configured_collection");
  186. // v2.2 — default flipped from "ms" to "ns" so rapid-write collections
  187. // get nanosecond stamps without an explicit configureCollection() call.
  188. assert(cfg.timestampPrecision == "ns");
  189. assert(!mgr.hasExplicitConfig("never_configured_collection"));
  190. store.stop();
  191. std::cout << "PASS: default config returned for unknown collection\n";
  192. }
  193. // v2.11.0 T8 — relations_enforced is a partial update, same shape as
  194. // versioning_enabled (v2.4.5). Declaring a relation names its child and
  195. // parent explicitly, so the declaration IS the opt-in: the default must be
  196. // enforced, and an unrelated configure call (e.g. precision-only) must never
  197. // silently disable it.
  198. void test_relations_enforced_is_a_partial_update() {
  199. MemoryStore store(defaultConfig());
  200. store.start();
  201. CollectionConfigManager mgr(store);
  202. store.setConfigManager(&mgr);
  203. CollectionCfg cfg = mgr.configFor("c");
  204. check(cfg.relationsEnforced,
  205. "defaults to enforced - declaring a relation IS the opt-in, so a "
  206. "declared constraint must not silently do nothing");
  207. // Change ONLY precision. A plain proto3 bool defaults to false and would
  208. // silently disable enforcement here - the exact trap v2.4.5 hit with
  209. // versioning_enabled, which is why the field is `optional`.
  210. configureCollection(mgr, "c", /*precision=*/"ms", /*relationsEnforced=*/std::nullopt);
  211. check(mgr.configFor("c").relationsEnforced,
  212. "a precision-only call leaves enforcement alone");
  213. configureCollection(mgr, "c", "", /*relationsEnforced=*/false);
  214. check(!mgr.configFor("c").relationsEnforced, "and it can be turned off");
  215. check(mgr.configFor("c").timestampPrecision == "ms",
  216. "without resetting precision");
  217. store.stop();
  218. std::cout << "PASS: relations_enforced is a partial update\n";
  219. }
  220. int main() {
  221. test_default_is_ns();
  222. test_ns_yields_unique_timestamps_in_tight_loop();
  223. test_ms_baseline_fits_in_ms_range();
  224. test_idempotent_configure();
  225. test_cache_reflects_configure_immediately();
  226. test_default_for_unknown_collection();
  227. test_relations_enforced_is_a_partial_update();
  228. if (g_fail > 0) {
  229. std::cerr << "\n" << g_fail << " check(s) FAILED (" << g_pass << " passed)\n";
  230. return 1;
  231. }
  232. std::cout << "\nAll timestamp precision tests PASSED! (" << g_pass << " checks)\n";
  233. return 0;
  234. }