document.hpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. #pragma once
  2. #include <string>
  3. #include <string_view>
  4. #include <vector>
  5. #include <chrono>
  6. #include <optional>
  7. #include <nlohmann/json.hpp>
  8. #include "doc_binary.hpp"
  9. namespace smartbotic::database {
  10. /**
  11. * Represents a document stored in a collection.
  12. * Documents are JSON objects with metadata for versioning, TTL, and encryption.
  13. * Named Document to avoid conflict with proto-generated Document message.
  14. */
  15. struct Document {
  16. std::string id; // Unique document ID within collection
  17. std::string collection; // Collection name
  18. // v1.11+ — Binary in-memory storage. The canonical state lives in
  19. // data_binary (yyjson mut_doc-backed). data_view is a lazily-materialised
  20. // nlohmann::json cache for full-tree access; invalidated on set_data().
  21. // See docs/superpowers/specs/2026-05-15-binary-doc-format-decision.md.
  22. smartbotic::db::doc_binary::Doc data_binary;
  23. mutable std::optional<nlohmann::json> data_view;
  24. uint64_t version = 1; // Monotonic version for optimistic locking
  25. uint64_t createdAt = 0; // Creation timestamp (ms since epoch)
  26. uint64_t updatedAt = 0; // Last modification timestamp (ms since epoch)
  27. uint64_t expiresAt = 0; // TTL expiration (0 = no expiration)
  28. std::string nodeId; // Origin node for replication
  29. bool encrypted = false; // Whether sensitive fields are encrypted
  30. std::vector<std::string> encryptedFields; // List of encrypted field paths
  31. std::string createdBy; // User ID who created the document
  32. std::string updatedBy; // User ID who last updated the document
  33. uint64_t lastAccessedAt = 0; // Last read access timestamp (for LRU eviction)
  34. // Lazy full-tree accessor. Materialises on first call, returns cached
  35. // reference thereafter. Cost: one decode() pass plus the cache memory.
  36. // Prefer field() for single-field reads.
  37. [[nodiscard]] const nlohmann::json& data() const {
  38. if (!data_view) {
  39. data_view = smartbotic::db::doc_binary::decode(data_binary);
  40. }
  41. return *data_view;
  42. }
  43. // Replace the binary payload from an nlohmann::json tree. Invalidates the
  44. // cached view so subsequent data() materialises fresh from data_binary.
  45. void set_data(const nlohmann::json& j) {
  46. data_binary = smartbotic::db::doc_binary::encode(j);
  47. data_view.reset();
  48. }
  49. // Fast-path single-field read. No full-tree materialise. Returns nullopt
  50. // if the field is absent or the root is not an object.
  51. [[nodiscard]] std::optional<nlohmann::json> field(std::string_view name) const {
  52. return smartbotic::db::doc_binary::get_field(data_binary, name);
  53. }
  54. // Check if document has expired
  55. [[nodiscard]] bool isExpired() const {
  56. if (expiresAt == 0) return false;
  57. auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
  58. std::chrono::system_clock::now().time_since_epoch()
  59. ).count();
  60. return static_cast<uint64_t>(now) >= expiresAt;
  61. }
  62. // Set TTL in seconds from now
  63. void setTtlSeconds(uint32_t seconds) {
  64. if (seconds == 0) {
  65. expiresAt = 0;
  66. } else {
  67. auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
  68. std::chrono::system_clock::now().time_since_epoch()
  69. ).count();
  70. expiresAt = static_cast<uint64_t>(now) + (seconds * 1000ULL);
  71. }
  72. }
  73. // Get remaining TTL in seconds (0 if expired or no TTL)
  74. [[nodiscard]] uint32_t getRemainingTtlSeconds() const {
  75. if (expiresAt == 0) return 0;
  76. auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
  77. std::chrono::system_clock::now().time_since_epoch()
  78. ).count();
  79. if (static_cast<uint64_t>(now) >= expiresAt) return 0;
  80. return static_cast<uint32_t>((expiresAt - static_cast<uint64_t>(now)) / 1000);
  81. }
  82. // JSON serialization — unchanged on disk; decodes the binary back to
  83. // nlohmann::json for the "data" field. WAL/snapshot serializers call this.
  84. [[nodiscard]] nlohmann::json toJson() const {
  85. nlohmann::json j;
  86. j["id"] = id;
  87. j["collection"] = collection;
  88. j["data"] = smartbotic::db::doc_binary::decode(data_binary);
  89. j["version"] = version;
  90. j["createdAt"] = createdAt;
  91. j["updatedAt"] = updatedAt;
  92. j["expiresAt"] = expiresAt;
  93. j["nodeId"] = nodeId;
  94. j["encrypted"] = encrypted;
  95. j["encryptedFields"] = encryptedFields;
  96. j["createdBy"] = createdBy;
  97. j["updatedBy"] = updatedBy;
  98. j["lastAccessedAt"] = lastAccessedAt;
  99. return j;
  100. }
  101. // JSON deserialization — unchanged on disk; encodes the "data" field
  102. // into the binary representation. WAL replay / snapshot load call this.
  103. static Document fromJson(const nlohmann::json& j) {
  104. Document doc;
  105. doc.id = j.value("id", "");
  106. doc.collection = j.value("collection", "");
  107. doc.set_data(j.value("data", nlohmann::json::object()));
  108. doc.version = j.value("version", 1ULL);
  109. doc.createdAt = j.value("createdAt", 0ULL);
  110. doc.updatedAt = j.value("updatedAt", 0ULL);
  111. doc.expiresAt = j.value("expiresAt", 0ULL);
  112. doc.nodeId = j.value("nodeId", "");
  113. doc.encrypted = j.value("encrypted", false);
  114. doc.encryptedFields = j.value("encryptedFields", std::vector<std::string>{});
  115. doc.createdBy = j.value("createdBy", "");
  116. doc.updatedBy = j.value("updatedBy", "");
  117. // Backward-compatible: default to updatedAt (or createdAt) for existing documents
  118. if (j.contains("lastAccessedAt")) {
  119. doc.lastAccessedAt = j.value("lastAccessedAt", 0ULL);
  120. } else {
  121. doc.lastAccessedAt = doc.updatedAt > 0 ? doc.updatedAt : doc.createdAt;
  122. }
  123. return doc;
  124. }
  125. };
  126. /**
  127. * Represents a historical snapshot of a document at a specific version.
  128. * Stored in version history when a document is updated or deleted.
  129. */
  130. struct DocumentVersion {
  131. uint64_t version = 0; // Version number
  132. // v1.11+ — Binary in-memory storage. See Document::data_binary for the
  133. // canonical-state/lazy-view contract; same lifecycle applies here.
  134. smartbotic::db::doc_binary::Doc data_binary;
  135. mutable std::optional<nlohmann::json> data_view;
  136. uint64_t timestamp = 0; // updatedAt when this version was saved
  137. std::string updatedBy; // Who made this change
  138. bool encrypted = false; // Whether fields were encrypted
  139. std::vector<std::string> encryptedFields; // Encrypted field paths at time of save
  140. uint64_t createdAt = 0; // Original document createdAt (for restore after delete)
  141. std::string createdBy; // Original document createdBy (for restore after delete)
  142. // Lazy full-tree accessor. Materialises on first call, returns cached
  143. // reference thereafter. Cost: one decode() pass plus the cache memory.
  144. // Prefer field() for single-field reads.
  145. [[nodiscard]] const nlohmann::json& data() const {
  146. if (!data_view) {
  147. data_view = smartbotic::db::doc_binary::decode(data_binary);
  148. }
  149. return *data_view;
  150. }
  151. // Replace the binary payload from an nlohmann::json tree. Invalidates the
  152. // cached view so subsequent data() materialises fresh from data_binary.
  153. void set_data(const nlohmann::json& j) {
  154. data_binary = smartbotic::db::doc_binary::encode(j);
  155. data_view.reset();
  156. }
  157. // Fast-path single-field read. No full-tree materialise. Returns nullopt
  158. // if the field is absent or the root is not an object.
  159. [[nodiscard]] std::optional<nlohmann::json> field(std::string_view name) const {
  160. return smartbotic::db::doc_binary::get_field(data_binary, name);
  161. }
  162. [[nodiscard]] nlohmann::json toJson() const {
  163. nlohmann::json j;
  164. j["version"] = version;
  165. j["data"] = smartbotic::db::doc_binary::decode(data_binary);
  166. j["timestamp"] = timestamp;
  167. j["updatedBy"] = updatedBy;
  168. j["encrypted"] = encrypted;
  169. j["encryptedFields"] = encryptedFields;
  170. j["createdAt"] = createdAt;
  171. j["createdBy"] = createdBy;
  172. return j;
  173. }
  174. static DocumentVersion fromJson(const nlohmann::json& j) {
  175. DocumentVersion v;
  176. v.version = j.value("version", 0ULL);
  177. v.set_data(j.value("data", nlohmann::json::object()));
  178. v.timestamp = j.value("timestamp", 0ULL);
  179. v.updatedBy = j.value("updatedBy", "");
  180. v.encrypted = j.value("encrypted", false);
  181. v.encryptedFields = j.value("encryptedFields", std::vector<std::string>{});
  182. v.createdAt = j.value("createdAt", 0ULL);
  183. v.createdBy = j.value("createdBy", "");
  184. return v;
  185. }
  186. };
  187. /**
  188. * Per-collection eviction priority. Added in v1.7.0 to let operators
  189. * bias the eviction selector: Low collections are evicted first, High
  190. * collections last. Normal is the default and preserves v1.6.x behavior.
  191. */
  192. enum class MemoryPriority {
  193. Low = 0,
  194. Normal = 1, // default — preserves existing behavior
  195. High = 2
  196. };
  197. inline std::string memoryPriorityToString(MemoryPriority p) {
  198. switch (p) {
  199. case MemoryPriority::Low: return "low";
  200. case MemoryPriority::Normal: return "normal";
  201. case MemoryPriority::High: return "high";
  202. }
  203. return "normal";
  204. }
  205. inline MemoryPriority memoryPriorityFromString(const std::string& s) {
  206. if (s == "low") return MemoryPriority::Low;
  207. if (s == "high") return MemoryPriority::High;
  208. return MemoryPriority::Normal; // default for unknown/"normal"
  209. }
  210. /**
  211. * Options for creating a collection.
  212. * Named CollectionOptions to avoid conflict with proto-generated type.
  213. */
  214. struct CollectionOptions {
  215. bool autoCreateId = true; // Auto-generate IDs if not provided
  216. uint32_t defaultTtlSeconds = 0; // Default TTL for documents (0 = none)
  217. bool encrypted = false; // Encrypt all documents by default
  218. std::vector<std::string> sensitiveFields; // Fields to always encrypt
  219. uint32_t maxVersions = 0; // Max version history per document (0 = unlimited)
  220. bool pinned = false; // If true, never evict documents from this collection
  221. uint32_t vectorDimension = 0; // Vector dimension for vector search (0 = disabled)
  222. MemoryPriority memoryPriority = MemoryPriority::Normal; // v1.7.0 eviction bias
  223. [[nodiscard]] nlohmann::json toJson() const {
  224. nlohmann::json j;
  225. j["autoCreateId"] = autoCreateId;
  226. j["defaultTtlSeconds"] = defaultTtlSeconds;
  227. j["encrypted"] = encrypted;
  228. j["sensitiveFields"] = sensitiveFields;
  229. j["maxVersions"] = maxVersions;
  230. j["pinned"] = pinned;
  231. j["vector_dimension"] = vectorDimension;
  232. j["memory_priority"] = memoryPriorityToString(memoryPriority);
  233. return j;
  234. }
  235. static CollectionOptions fromJson(const nlohmann::json& j) {
  236. CollectionOptions opts;
  237. opts.autoCreateId = j.value("autoCreateId", true);
  238. opts.defaultTtlSeconds = j.value("defaultTtlSeconds", 0U);
  239. opts.encrypted = j.value("encrypted", false);
  240. opts.sensitiveFields = j.value("sensitiveFields", std::vector<std::string>{});
  241. opts.maxVersions = j.value("maxVersions", 0U);
  242. opts.pinned = j.value("pinned", false); // Backward-compatible default
  243. if (j.contains("vector_dimension") && j["vector_dimension"].is_number())
  244. opts.vectorDimension = j["vector_dimension"].get<uint32_t>();
  245. if (j.contains("memory_priority") && j["memory_priority"].is_string())
  246. opts.memoryPriority = memoryPriorityFromString(j["memory_priority"].get<std::string>());
  247. return opts;
  248. }
  249. };
  250. /**
  251. * Collection metadata.
  252. */
  253. struct CollectionInfo {
  254. std::string name;
  255. uint64_t documentCount = 0;
  256. uint64_t sizeBytes = 0;
  257. CollectionOptions options;
  258. uint64_t createdAt = 0;
  259. uint64_t updatedAt = 0;
  260. [[nodiscard]] nlohmann::json toJson() const {
  261. nlohmann::json j;
  262. j["name"] = name;
  263. j["documentCount"] = documentCount;
  264. j["sizeBytes"] = sizeBytes;
  265. j["options"] = options.toJson();
  266. j["createdAt"] = createdAt;
  267. j["updatedAt"] = updatedAt;
  268. return j;
  269. }
  270. };
  271. /**
  272. * Filter operator for queries.
  273. */
  274. enum class FilterOp {
  275. EQ, // Equal
  276. NE, // Not equal
  277. GT, // Greater than
  278. GTE, // Greater than or equal
  279. LT, // Less than
  280. LTE, // Less than or equal
  281. IN, // Value in array
  282. CONTAINS, // Array contains value
  283. EXISTS, // Field exists
  284. REGEX, // Regex match
  285. SEARCH // Full-text search across ID and string fields
  286. };
  287. /**
  288. * Query filter.
  289. */
  290. struct Filter {
  291. std::string field; // JSON path (e.g., "status" or "user.email")
  292. FilterOp op = FilterOp::EQ;
  293. nlohmann::json value;
  294. [[nodiscard]] nlohmann::json toJson() const {
  295. nlohmann::json j;
  296. j["field"] = field;
  297. j["op"] = static_cast<int>(op);
  298. j["value"] = value;
  299. return j;
  300. }
  301. static Filter fromJson(const nlohmann::json& j) {
  302. Filter f;
  303. f.field = j.value("field", "");
  304. f.op = static_cast<FilterOp>(j.value("op", 0));
  305. f.value = j.value("value", nlohmann::json());
  306. return f;
  307. }
  308. };
  309. /**
  310. * Sort specification.
  311. */
  312. struct Sort {
  313. std::string field;
  314. bool descending = false;
  315. [[nodiscard]] nlohmann::json toJson() const {
  316. nlohmann::json j;
  317. j["field"] = field;
  318. j["descending"] = descending;
  319. return j;
  320. }
  321. static Sort fromJson(const nlohmann::json& j) {
  322. Sort s;
  323. s.field = j.value("field", "");
  324. s.descending = j.value("descending", false);
  325. return s;
  326. }
  327. };
  328. /**
  329. * Query specification.
  330. */
  331. struct Query {
  332. std::vector<Filter> filters;
  333. std::optional<Sort> sort;
  334. uint32_t limit = 100;
  335. uint32_t offset = 0;
  336. std::vector<std::string> projection; // Fields to include (empty = all)
  337. [[nodiscard]] nlohmann::json toJson() const {
  338. nlohmann::json j;
  339. j["filters"] = nlohmann::json::array();
  340. for (const auto& f : filters) {
  341. j["filters"].push_back(f.toJson());
  342. }
  343. if (sort) {
  344. j["sort"] = sort->toJson();
  345. }
  346. j["limit"] = limit;
  347. j["offset"] = offset;
  348. j["projection"] = projection;
  349. return j;
  350. }
  351. static Query fromJson(const nlohmann::json& j) {
  352. Query q;
  353. if (j.contains("filters") && j["filters"].is_array()) {
  354. for (const auto& f : j["filters"]) {
  355. q.filters.push_back(Filter::fromJson(f));
  356. }
  357. }
  358. if (j.contains("sort") && !j["sort"].is_null()) {
  359. q.sort = Sort::fromJson(j["sort"]);
  360. }
  361. q.limit = j.value("limit", 100U);
  362. q.offset = j.value("offset", 0U);
  363. q.projection = j.value("projection", std::vector<std::string>{});
  364. return q;
  365. }
  366. };
  367. /**
  368. * Result of a find query.
  369. */
  370. struct QueryResult {
  371. std::vector<Document> documents;
  372. uint64_t totalCount = 0;
  373. bool hasMore = false;
  374. // WAL fallback metrics (for queries that include evicted documents)
  375. bool usedWalFallback = false; // True if WAL was scanned for evicted docs
  376. uint32_t memoryMatchCount = 0; // Documents matched from memory
  377. uint32_t walMatchCount = 0; // Documents matched from WAL
  378. uint64_t memorySearchMicros = 0; // Time spent searching memory
  379. uint64_t walSearchMicros = 0; // Time spent loading/searching WAL
  380. };
  381. /**
  382. * Storage event types.
  383. *
  384. * Integer ordering matters: database_grpc_impl.cpp converts to pb::EventType
  385. * with a `+1` offset (pb::EVENT_UNKNOWN == 0 is the wire-level default).
  386. * When adding entries, also add them to proto EventType in matching order.
  387. */
  388. enum class EventType {
  389. INSERT, // pb::EVENT_INSERT
  390. UPDATE, // pb::EVENT_UPDATE
  391. DELETE, // pb::EVENT_DELETE
  392. EXPIRE, // pb::EVENT_EXPIRE
  393. INVALIDATE, // pb::EVENT_INVALIDATE
  394. // v1.7.0 — memory-pressure observability events (T10).
  395. // System-level: collection="" and documentId="". Payload in `data` carries
  396. // pressure level, percent, and (for bursts) doc count and bytes freed.
  397. MEMORY_PRESSURE_HIGH, // pb::EVENT_MEMORY_PRESSURE_HIGH
  398. MEMORY_EVICTION_BURST // pb::EVENT_MEMORY_EVICTION_BURST
  399. };
  400. /**
  401. * Storage event for pub/sub.
  402. */
  403. struct DatabaseEvent {
  404. EventType type;
  405. std::string collection;
  406. std::string documentId;
  407. uint64_t timestamp = 0;
  408. std::string nodeId;
  409. std::optional<nlohmann::json> data; // Document data (if included)
  410. [[nodiscard]] nlohmann::json toJson() const {
  411. nlohmann::json j;
  412. j["type"] = static_cast<int>(type);
  413. j["collection"] = collection;
  414. j["documentId"] = documentId;
  415. j["timestamp"] = timestamp;
  416. j["nodeId"] = nodeId;
  417. if (data) {
  418. j["data"] = *data;
  419. }
  420. return j;
  421. }
  422. };
  423. } // namespace smartbotic::database