diff --git a/server/channels/store/sqlstore/property_value_store.go b/server/channels/store/sqlstore/property_value_store.go index a483c41acef..1343c875a19 100644 --- a/server/channels/store/sqlstore/property_value_store.go +++ b/server/channels/store/sqlstore/property_value_store.go @@ -133,6 +133,10 @@ func (s *SqlPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSea builder = builder.Where(sq.Eq{"FieldID": opts.FieldID}) } + if opts.SinceUpdateAt > 0 { + builder = builder.Where(sq.Gt{"UpdateAt": opts.SinceUpdateAt}) + } + var values []*model.PropertyValue if err := s.GetReplica().SelectBuilder(&values, builder); err != nil { return nil, errors.Wrap(err, "property_value_search_query") diff --git a/server/channels/store/storetest/property_value_store.go b/server/channels/store/storetest/property_value_store.go index 7d27eed2c36..33e78a021b0 100644 --- a/server/channels/store/storetest/property_value_store.go +++ b/server/channels/store/storetest/property_value_store.go @@ -25,6 +25,7 @@ func TestPropertyValueStore(t *testing.T, rctx request.CTX, ss store.Store, s Sq t.Run("UpsertPropertyValue", func(t *testing.T) { testUpsertPropertyValue(t, rctx, ss) }) t.Run("DeletePropertyValue", func(t *testing.T) { testDeletePropertyValue(t, rctx, ss) }) t.Run("SearchPropertyValues", func(t *testing.T) { testSearchPropertyValues(t, rctx, ss) }) + t.Run("SearchPropertyValuesSince", func(t *testing.T) { testSearchPropertyValuesSince(t, rctx, ss) }) t.Run("DeleteForField", func(t *testing.T) { testDeleteForField(t, rctx, ss) }) t.Run("DeleteForTarget", func(t *testing.T) { testDeleteForTarget(t, rctx, ss) }) } @@ -879,6 +880,40 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) { }, expectedIDs: []string{value1.ID, value2.ID}, }, + { + name: "filter by SinceUpdateAt timestamp - no results before", + opts: model.PropertyValueSearchOpts{ + SinceUpdateAt: value3.UpdateAt, // After all existing values + PerPage: 10, + }, + expectedIDs: []string{}, + }, + { + name: "filter by SinceUpdateAt timestamp - get values after specific time", + opts: model.PropertyValueSearchOpts{ + SinceUpdateAt: value1.UpdateAt, // After value1, should get value2 and value3 + PerPage: 10, + }, + expectedIDs: []string{value2.ID, value3.ID}, + }, + { + name: "filter by SinceUpdateAt timestamp with group filter", + opts: model.PropertyValueSearchOpts{ + GroupID: groupID, + SinceUpdateAt: value1.UpdateAt, // After value1, should only get value2 from same group + PerPage: 10, + }, + expectedIDs: []string{value2.ID}, + }, + { + name: "filter by SinceUpdateAt timestamp including deleted", + opts: model.PropertyValueSearchOpts{ + SinceUpdateAt: value3.UpdateAt, // After value3, should get value4 (deleted) + IncludeDeleted: true, + PerPage: 10, + }, + expectedIDs: []string{value4.ID}, + }, } for _, tc := range tests { @@ -899,6 +934,111 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) { } } +func testSearchPropertyValuesSince(t *testing.T, _ request.CTX, ss store.Store) { + // Create values with controlled timestamps for precise testing + groupID := model.NewId() + + // Create value 1 (will remain unchanged) + value1, err := ss.PropertyValue().Create(&model.PropertyValue{ + GroupID: groupID, + TargetID: model.NewId(), + TargetType: "test_type", + FieldID: model.NewId(), + Value: json.RawMessage(`"value1"`), + }) + require.NoError(t, err) + + time.Sleep(10 * time.Millisecond) // Ensure different timestamps + + // Create value 2 (will be updated later) + value2, err := ss.PropertyValue().Create(&model.PropertyValue{ + GroupID: groupID, + TargetID: model.NewId(), + TargetType: "test_type", + FieldID: model.NewId(), + Value: json.RawMessage(`"value2"`), + }) + require.NoError(t, err) + + time.Sleep(10 * time.Millisecond) + + // Create value 3 (will remain unchanged) + value3, err := ss.PropertyValue().Create(&model.PropertyValue{ + GroupID: groupID, + TargetID: model.NewId(), + TargetType: "test_type", + FieldID: model.NewId(), + Value: json.RawMessage(`"value3"`), + }) + require.NoError(t, err) + + // Update value2 to change its UpdateAt timestamp + time.Sleep(10 * time.Millisecond) + value2.Value = json.RawMessage(`"value2_updated"`) + updatedValues, err := ss.PropertyValue().Update("", []*model.PropertyValue{value2}) + require.NoError(t, err) + require.Len(t, updatedValues, 1) + updatedValue2 := updatedValues[0] + + t.Run("SinceUpdateAt filters correctly by UpdateAt", func(t *testing.T) { + // Get values updated after value1 (should get value2 and value3) + results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{ + GroupID: groupID, + SinceUpdateAt: value1.UpdateAt, + PerPage: 10, + }) + require.NoError(t, err) + require.Len(t, results, 2) + + resultIDs := make([]string, len(results)) + for i, result := range results { + resultIDs[i] = result.ID + } + require.ElementsMatch(t, []string{value2.ID, value3.ID}, resultIDs) + }) + + t.Run("SinceUpdateAt with boundary condition", func(t *testing.T) { + // Get values updated after value3's timestamp + // Should get both value2 (updated) and value3, so expect 2 results + results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{ + GroupID: groupID, + SinceUpdateAt: value3.UpdateAt - 1, // Slightly before value3's timestamp + PerPage: 10, + }) + require.NoError(t, err) + require.Len(t, results, 2) + + resultIDs := make([]string, len(results)) + for i, result := range results { + resultIDs[i] = result.ID + } + // Should get both value2 (updated with new timestamp) and value3 + require.ElementsMatch(t, []string{value2.ID, value3.ID}, resultIDs) + }) + + t.Run("SinceUpdateAt after all updates", func(t *testing.T) { + // Get values updated after the most recent update + results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{ + GroupID: groupID, + SinceUpdateAt: updatedValue2.UpdateAt, // After the update + PerPage: 10, + }) + require.NoError(t, err) + require.Len(t, results, 0) // Should be empty + }) + + t.Run("SinceUpdateAt with very recent timestamp", func(t *testing.T) { + // Get values updated since current time + results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{ + GroupID: groupID, + SinceUpdateAt: model.GetMillis(), + PerPage: 10, + }) + require.NoError(t, err) + require.Len(t, results, 0) + }) +} + func testCreatePropertyValueWithArray(t *testing.T, _ request.CTX, ss store.Store) { t.Run("should create a property value with array", func(t *testing.T) { newValue := &model.PropertyValue{ diff --git a/server/public/model/property_value.go b/server/public/model/property_value.go index 6c108ccaaae..8f1ba110198 100644 --- a/server/public/model/property_value.go +++ b/server/public/model/property_value.go @@ -108,6 +108,7 @@ type PropertyValueSearchOpts struct { TargetType string TargetIDs []string FieldID string + SinceUpdateAt int64 // UpdateAt after which to send the items IncludeDeleted bool Cursor PropertyValueSearchCursor PerPage int