Bläddra i källkod

feat(config): client API for collection config + timestamp migration

- Client::CollectionConfig struct + configureCollection/getCollectionConfig/
  hasCollectionConfig methods
- Client::TimestampMigrationResult + migrateCollectionTimestamps method with
  10-minute deadline (can process large collections)
fszontagh 3 månader sedan
förälder
incheckning
0f75dc9a90
2 ändrade filer med 158 tillägg och 0 borttagningar
  1. 53 0
      client/include/smartbotic/database/client.hpp
  2. 105 0
      client/src/client.cpp

+ 53 - 0
client/include/smartbotic/database/client.hpp

@@ -287,6 +287,59 @@ public:
      */
     [[nodiscard]] std::optional<CollectionInfo> getCollectionInfo(const std::string& name);
 
+    // ===== Collection Configuration =====
+
+    /**
+     * Per-collection configuration.
+     */
+    struct CollectionConfig {
+        // "ms" (default) — document _created_at/_updated_at in milliseconds since epoch
+        // "ns"            — nanoseconds since epoch (use for collections with rapid writes)
+        std::string timestampPrecision = "ms";
+    };
+
+    /**
+     * Set or replace the configuration for a collection. Idempotent.
+     * Default (no explicit config) is ms precision.
+     *
+     * Note: this does NOT migrate existing timestamps. Newly-inserted docs use
+     * the new precision; old docs keep whatever they had. Run
+     * migrateCollectionTimestamps() during maintenance if you need conversion.
+     */
+    bool configureCollection(const std::string& collection, const CollectionConfig& cfg);
+
+    /**
+     * Read the current config for a collection. Returns default if none set.
+     */
+    [[nodiscard]] CollectionConfig getCollectionConfig(const std::string& collection);
+
+    /**
+     * Check if a collection has an explicit config (vs. default).
+     */
+    [[nodiscard]] bool hasCollectionConfig(const std::string& collection);
+
+    /**
+     * Result of a timestamp migration.
+     */
+    struct TimestampMigrationResult {
+        bool success = false;
+        std::string error;
+        uint64_t rowsMigrated = 0;
+        uint64_t rowsSkipped = 0;  // already in target precision
+    };
+
+    /**
+     * Convert _created_at/_updated_at across all docs in a collection between
+     * ms and ns precision. Idempotent and resumable — already-converted rows
+     * are detected via threshold (10^15) and skipped.
+     *
+     * Call during planned maintenance AFTER configureCollection() to bring
+     * existing data in line with the new precision.
+     */
+    TimestampMigrationResult migrateCollectionTimestamps(const std::string& collection,
+                                                          const std::string& fromPrecision,
+                                                          const std::string& toPrecision);
+
     // ===== View Management =====
 
     struct Sort {

+ 105 - 0
client/src/client.cpp

@@ -628,6 +628,91 @@ public:
         return result;
     }
 
+    // ===== Collection Configuration =====
+
+    bool configureCollection(const std::string& collection, const Client::CollectionConfig& cfg) {
+        smartbotic::databasepb::ConfigureCollectionRequest request;
+        request.set_collection(collection);
+        request.mutable_config()->set_timestamp_precision(cfg.timestampPrecision);
+
+        smartbotic::databasepb::ConfigureCollectionResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->ConfigureCollection(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::configureCollection failed: {}", status.error_message());
+            return false;
+        }
+        if (!response.success()) {
+            spdlog::error("Client::configureCollection rejected: {}", response.error());
+            return false;
+        }
+        return true;
+    }
+
+    Client::CollectionConfig getCollectionConfig(const std::string& collection) {
+        smartbotic::databasepb::GetCollectionConfigRequest request;
+        request.set_collection(collection);
+
+        smartbotic::databasepb::GetCollectionConfigResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        Client::CollectionConfig out;
+        auto status = stub_->GetCollectionConfig(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::getCollectionConfig failed: {}", status.error_message());
+            return out;
+        }
+        out.timestampPrecision = response.config().timestamp_precision();
+        if (out.timestampPrecision.empty()) out.timestampPrecision = "ms";
+        return out;
+    }
+
+    bool hasCollectionConfig(const std::string& collection) {
+        smartbotic::databasepb::GetCollectionConfigRequest request;
+        request.set_collection(collection);
+
+        smartbotic::databasepb::GetCollectionConfigResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->GetCollectionConfig(&context, request, &response);
+        if (!status.ok()) return false;
+        return response.found();
+    }
+
+    Client::TimestampMigrationResult migrateCollectionTimestamps(
+        const std::string& collection,
+        const std::string& fromPrecision,
+        const std::string& toPrecision
+    ) {
+        smartbotic::databasepb::MigrateCollectionTimestampsRequest request;
+        request.set_collection(collection);
+        request.set_from_precision(fromPrecision);
+        request.set_to_precision(toPrecision);
+
+        smartbotic::databasepb::MigrateCollectionTimestampsResponse response;
+        grpc::ClientContext context;
+        // Longer deadline — can iterate many rows
+        auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(10);
+        context.set_deadline(deadline);
+
+        Client::TimestampMigrationResult out;
+        auto status = stub_->MigrateCollectionTimestamps(&context, request, &response);
+        if (!status.ok()) {
+            out.success = false;
+            out.error = status.error_message();
+            return out;
+        }
+        out.success = response.success();
+        out.error = response.error();
+        out.rowsMigrated = response.rows_migrated();
+        out.rowsSkipped = response.rows_skipped();
+        return out;
+    }
+
     // ===== View Management =====
 
     bool createView(const std::string& name,
@@ -1366,6 +1451,26 @@ std::optional<Client::CollectionInfo> Client::getCollectionInfo(const std::strin
     return impl_->getCollectionInfo(name);
 }
 
+bool Client::configureCollection(const std::string& collection, const CollectionConfig& cfg) {
+    return impl_->configureCollection(collection, cfg);
+}
+
+Client::CollectionConfig Client::getCollectionConfig(const std::string& collection) {
+    return impl_->getCollectionConfig(collection);
+}
+
+bool Client::hasCollectionConfig(const std::string& collection) {
+    return impl_->hasCollectionConfig(collection);
+}
+
+Client::TimestampMigrationResult Client::migrateCollectionTimestamps(
+    const std::string& collection,
+    const std::string& fromPrecision,
+    const std::string& toPrecision
+) {
+    return impl_->migrateCollectionTimestamps(collection, fromPrecision, toPrecision);
+}
+
 bool Client::createView(const std::string& name,
                         const std::string& collection,
                         const std::vector<std::string>& include,