// monorepo/cloud/maplefile-backend/internal/maplefile/repo/collection/collectionsync.go package collection import ( "context" "fmt" "time" "github.com/gocql/gocql" "go.uber.org/zap" dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection" ) // GetCollectionSyncData uses the general table when you need all collections regardless of access type func (impl *collectionRepositoryImpl) GetCollectionSyncData(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64) (*dom_collection.CollectionSyncResponse, error) { var query string var args []any // Key Insight: We can query all collections for a user efficiently because user_id is the partition key // We select access_type in the result set so we can filter or categorize after retrieval if cursor == nil { query = `SELECT collection_id, modified_at, access_type FROM collections_by_user_id_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? LIMIT ?` args = []any{userID, limit} } else { query = `SELECT collection_id, modified_at, access_type FROM collections_by_user_id_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? AND (modified_at, collection_id) > (?, ?) LIMIT ?` args = []any{userID, cursor.LastModified, cursor.LastID, limit} } iter := impl.Session.Query(query, args...).WithContext(ctx).Iter() var syncItems []dom_collection.CollectionSyncItem var lastModified time.Time var lastID gocql.UUID // Critical Fix: We must scan all three selected columns var collectionID gocql.UUID var modifiedAt time.Time var accessType string for iter.Scan(&collectionID, &modifiedAt, &accessType) { // Get minimal sync data for this collection syncItem, err := impl.getCollectionSyncItem(ctx, collectionID) if err != nil { impl.Logger.Warn("failed to get sync item for collection", zap.String("collection_id", collectionID.String()), zap.String("access_type", accessType), zap.Error(err)) continue } if syncItem != nil { syncItems = append(syncItems, *syncItem) lastModified = modifiedAt lastID = collectionID } } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to get collection sync data: %w", err) } // Prepare response response := &dom_collection.CollectionSyncResponse{ Collections: syncItems, HasMore: len(syncItems) == int(limit), } // Set next cursor if there are more results if response.HasMore { response.NextCursor = &dom_collection.CollectionSyncCursor{ LastModified: lastModified, LastID: lastID, } } return response, nil } // GetCollectionSyncData uses the access-type-specific table for optimal performance // This method demonstrates the power of compound partition keys in Cassandra func (impl *collectionRepositoryImpl) GetCollectionSyncDataByAccessType(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64, accessType string) (*dom_collection.CollectionSyncResponse, error) { var query string var args []any // Key Insight: With the compound partition key (user_id, access_type), this query is lightning fast // Cassandra can directly access the specific partition without any filtering or scanning if cursor == nil { query = `SELECT collection_id, modified_at FROM collections_by_user_id_and_access_type_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? AND access_type = 'owner' LIMIT ?` args = []any{userID, limit} } else { query = `SELECT collection_id, modified_at FROM collections_by_user_id_and_access_type_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? AND access_type = 'owner' AND (modified_at, collection_id) > (?, ?) LIMIT ?` args = []any{userID, cursor.LastModified, cursor.LastID, limit} } iter := impl.Session.Query(query, args...).WithContext(ctx).Iter() var syncItems []dom_collection.CollectionSyncItem var lastModified time.Time var lastID gocql.UUID var collectionID gocql.UUID var modifiedAt time.Time for iter.Scan(&collectionID, &modifiedAt) { // Get minimal sync data for this collection syncItem, err := impl.getCollectionSyncItem(ctx, collectionID) if err != nil { impl.Logger.Warn("failed to get sync item for collection", zap.String("collection_id", collectionID.String()), zap.Error(err)) continue } if syncItem != nil { syncItems = append(syncItems, *syncItem) lastModified = modifiedAt lastID = collectionID } } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to get collection sync data: %w", err) } // Prepare response response := &dom_collection.CollectionSyncResponse{ Collections: syncItems, HasMore: len(syncItems) == int(limit), } // Set next cursor if there are more results if response.HasMore { response.NextCursor = &dom_collection.CollectionSyncCursor{ LastModified: lastModified, LastID: lastID, } } return response, nil } // Helper method to get minimal sync data for a collection func (impl *collectionRepositoryImpl) getCollectionSyncItem(ctx context.Context, collectionID gocql.UUID) (*dom_collection.CollectionSyncItem, error) { var ( id gocql.UUID version, tombstoneVersion uint64 modifiedAt, tombstoneExpiry time.Time state string parentID gocql.UUID encryptedCustomIcon string ) query := `SELECT id, version, modified_at, state, parent_id, tombstone_version, tombstone_expiry, encrypted_custom_icon FROM collections_by_id WHERE id = ?` err := impl.Session.Query(query, collectionID).WithContext(ctx).Scan( &id, &version, &modifiedAt, &state, &parentID, &tombstoneVersion, &tombstoneExpiry, &encryptedCustomIcon) if err != nil { if err == gocql.ErrNotFound { return nil, nil } return nil, fmt.Errorf("failed to get collection sync item: %w", err) } syncItem := &dom_collection.CollectionSyncItem{ ID: id, Version: version, ModifiedAt: modifiedAt, State: state, TombstoneVersion: tombstoneVersion, TombstoneExpiry: tombstoneExpiry, EncryptedCustomIcon: encryptedCustomIcon, } // Only include ParentID if it's valid if impl.isValidUUID(parentID) { syncItem.ParentID = &parentID } return syncItem, nil }