Jelajahi Sumber

feat(config): MigrateCollectionTimestamps — idempotent ms↔ns migration

- MemoryStore::updateTimestamps — metadata-only mutation (no re-stamping,
  no version bump, no history entry)
- Handler iterates collection, detects already-migrated rows using 10^15
  threshold (ms < 10^15 < ns for any real-world timestamp)
- Returns rows_migrated + rows_skipped for resume visibility
- from==to is a no-op success
- Admin calls this during planned maintenance after configureCollection
fszontagh 3 bulan lalu
induk
melakukan
374f160bea

+ 81 - 7
service/src/database_grpc_impl.cpp

@@ -1420,15 +1420,89 @@ grpc::Status DatabaseGrpcImpl::GetCollectionConfig(
 
 grpc::Status DatabaseGrpcImpl::MigrateCollectionTimestamps(
     grpc::ServerContext* /*context*/,
-    const pb::MigrateCollectionTimestampsRequest* /*request*/,
+    const pb::MigrateCollectionTimestampsRequest* request,
     pb::MigrateCollectionTimestampsResponse* response
 ) {
-    // Implemented in v1.6.0 T5 (separate commit)
-    response->set_success(false);
-    response->set_error("not yet implemented — coming in next task");
-    response->set_rows_migrated(0);
-    response->set_rows_skipped(0);
-    return grpc::Status::OK;
+    try {
+        const std::string& collection = request->collection();
+        const std::string& from = request->from_precision();
+        const std::string& to = request->to_precision();
+
+        // No-op success when caller supplies identical precisions.
+        if (from == to) {
+            response->set_success(true);
+            response->set_rows_migrated(0);
+            response->set_rows_skipped(0);
+            return grpc::Status::OK;
+        }
+
+        bool msToNs;
+        if (from == "ms" && to == "ns") {
+            msToNs = true;
+        } else if (from == "ns" && to == "ms") {
+            msToNs = false;
+        } else {
+            response->set_success(false);
+            response->set_error("from_precision and to_precision must each be 'ms' or 'ns'");
+            response->set_rows_migrated(0);
+            response->set_rows_skipped(0);
+            return grpc::Status::OK;
+        }
+
+        // Idempotency heuristic: any timestamp >= 10^15 is treated as ns
+        // (10^15 ms = year 33658, no real data is that far in the future;
+        //  10^15 ns = 1970-01-12, no real data is that old).
+        constexpr uint64_t NS_THRESHOLD = 1'000'000'000'000'000ULL;  // 10^15
+
+        Query q;
+        QueryResult result = store_.find(collection, q);
+
+        uint64_t migrated = 0;
+        uint64_t skipped = 0;
+
+        for (const auto& doc : result.documents) {
+            const uint64_t c = doc.createdAt;
+            const uint64_t u = doc.updatedAt;
+
+            bool cInTarget;
+            bool uInTarget;
+            if (msToNs) {
+                cInTarget = (c >= NS_THRESHOLD);
+                uInTarget = (u >= NS_THRESHOLD);
+            } else {
+                cInTarget = (c < NS_THRESHOLD);
+                uInTarget = (u < NS_THRESHOLD);
+            }
+
+            if (cInTarget && uInTarget) {
+                ++skipped;
+                continue;
+            }
+
+            const uint64_t newC = cInTarget
+                ? c
+                : (msToNs ? c * 1'000'000ULL : c / 1'000'000ULL);
+            const uint64_t newU = uInTarget
+                ? u
+                : (msToNs ? u * 1'000'000ULL : u / 1'000'000ULL);
+
+            if (store_.updateTimestamps(collection, doc.id, newC, newU)) {
+                ++migrated;
+            }
+        }
+
+        response->set_success(true);
+        response->set_rows_migrated(migrated);
+        response->set_rows_skipped(skipped);
+        return grpc::Status::OK;
+
+    } catch (const std::exception& e) {
+        response->set_success(false);
+        response->set_error(e.what());
+        response->set_rows_migrated(0);
+        response->set_rows_skipped(0);
+        return grpc::Status::OK;
+    }
 }
 
 } // namespace smartbotic::database

+ 22 - 0
service/src/memory_store.cpp

@@ -734,6 +734,28 @@ uint64_t MemoryStore::patchDocument(const std::string& collection, const std::st
     return newVersion;
 }
 
+bool MemoryStore::updateTimestamps(const std::string& collection, const std::string& id,
+                                    uint64_t newCreatedAt, uint64_t newUpdatedAt) {
+    if (!collectionExists(collection)) {
+        return false;
+    }
+
+    CollectionData* coll = getOrCreateCollection(collection);
+
+    std::unique_lock<std::shared_mutex> lock(coll->mutex);
+    auto it = coll->documents.find(id);
+    if (it == coll->documents.end()) {
+        return false;
+    }
+
+    // Metadata-only mutation: do not bump version, do not save history.
+    // The user-visible document data is unchanged; only the internal
+    // timestamp representation is rewritten (ms <-> ns).
+    it->second.createdAt = newCreatedAt;
+    it->second.updatedAt = newUpdatedAt;
+    return true;
+}
+
 // ===== Query Operations =====
 
 QueryResult MemoryStore::find(const std::string& collection, const Query& query) const {

+ 13 - 0
service/src/memory_store.hpp

@@ -199,6 +199,19 @@ public:
     uint64_t patchDocument(const std::string& collection, const std::string& id,
                            const nlohmann::json& patch, const std::string& actor = "");
 
+    /**
+     * Directly update a document's createdAt / updatedAt metadata fields
+     * without triggering re-stamping or version-bumping. Metadata-only mutation,
+     * used exclusively by the timestamp precision migration helper.
+     *
+     * Note: no version history entry is written — the user-visible document
+     * data is unchanged; only internal metadata is rewritten.
+     *
+     * @return true if document was updated, false if collection or document not found
+     */
+    bool updateTimestamps(const std::string& collection, const std::string& id,
+                          uint64_t newCreatedAt, uint64_t newUpdatedAt);
+
     // ===== Query Operations =====
 
     /**