Repository: weaviate/weaviate
Pull request #4058 (2024-01-22 17:51:14)

Idempotent batch
### What's being changed:
Addresses https://github.com/weaviate/weaviate/issues/3949

**Notes**:
- `Shard::putObjectLSM(...)` and `Shard::determineInsertStatus(...)` methods were modified to determine whether incoming object requires:
    - full insert/update (objects, inverted indexes, vector index)
    - partial update (objects, inverted indexes (only relevant to changed properties))
    - no actions
Full insert if performed if object with same uuid does not exist yet.
Full update is performed if vectors or any of geo property between incoming and existing objects differ.
Partial update is performed if vectors and geo props are not changed, but any of properties or additional properties has changed. In that case only objects bucket and indexes of changed properties are updated.
No action is performed if vectors, additional properties and properties (including geo) are not changed.
- types of properties of incoming objects have been improved. Change was applied to array data types: instead of `[]interface{}` now `[]string`, `[]bool`, etc are used.
Simultaneously bug was identified and fixed: properties of type `DateArray` were casted to `[]string` instead of `[]time.Time`. As a result date values were unintentionally part of data being vectorized. **As of now dates will not be vectorized, which will result in different vector generated for the same object**.

**Performance**:

Modified ann benchmark script was used to verify performance of introduced changes in code and behaviour (https://github.com/weaviate/weaviate-chaos-engineering/compare/main...idempotent_batch_performance).
Script measures execution time of each of the following steps:

1. Initial ingestion - 100k objects with properties of each supported type (propsA, vectorA; full insert expected)
2. Ingestion of objects with original properties (propsA, vectorA; no action is expected)
3. Ingestion of objects with modified properties, except geo (propsB, vectorA; partial update is expected)
4. Ingestion of objects with modified properties, except geo (propsB, vectorA; no action is expected)
5. Ingestion of objects with original properties (propsA, vectorA; partial update is expected)

Script was tested on 3 different weaviate versions:
- `stable/1.23` (2362e17) - no optimizations 
- `master` (b7634fe) - skip reindex (https://github.com/weaviate/weaviate/issues/3948) applied
- `idempotent_batch` - skip reindex (https://github.com/weaviate/weaviate/issues/3948) + idempotent batch (https://github.com/weaviate/weaviate/issues/3949) applied

**Results** for objects with all types of properties (given in h:mm:ss; 2 runs):
|                  	| 1st batch (initial)<br>propsA geoA vecA 	| 2nd batch<br>propsA geoA vecA    	| 3rd batch<br>propsB geoA vecA    	| 4th batch<br>propsB geoA vecA    	| 5th batch<br>propsA geoA vecA    	|
|------------------	|-----------------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|
| stable           	| 0:00:30.180680<br>0:00:29.744101        	| 0:00:44.098360<br>0:00:43.767819 	| 0:01:04.596541<br>0:01:08.006057 	| 0:01:27.781050<br>0:01:28.503192 	| 0:02:09.266730<br>0:02:07.775083 	|
| master           	| 0:00:30.133406<br>0:00:28.950004        	| 0:00:43.106158<br>0:00:43.207250 	| 0:01:04.896243<br>0:01:04.092511 	| 0:01:27.050309<br>0:01:33.732612 	| 0:02:11.796772<br>0:02:06.355330 	|
| idempotent batch 	| 0:00:29.745860<br>0:00:29.142219        	| 0:00:20.802320<br>0:00:20.683051 	| 0:00:21.682167<br>0:00:21.653536 	| 0:00:20.579698<br>0:00:20.820579 	| 0:00:21.328010<br>0:00:21.547770 	|

**Results** for objects will all types of properties except **geo**  (given in h:mm:ss; 2 runs):
|                  	| 1st batch (initial)<br>propsA vecA 	| 2nd batch<br>propsA vecA         	| 3rd batch<br>propsB vecA         	| 4th batch<br>propsB vecA         	| 5th batch<br>propsA vecA         	|
|------------------	|------------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|
| stable           	| 0:00:29.110165<br>0:00:26.159162   	| 0:00:36.519708<br>0:00:35.443693 	| 0:00:50.148659<br>0:00:48.457433 	| 0:01:04.994465<br>0:01:02.787999 	| 0:01:21.374194<br>0:01:17.352514 	|
| master           	| 0:00:26.090419<br>0:00:25.515179   	| 0:00:19.860155<br>0:00:19.670457 	| 0:00:20.415592<br>0:00:20.516546 	| 0:00:20.041709<br>0:00:20.267077 	| 0:00:20.323540<br>0:00:20.311261 	|
| idempotent batch 	| 0:00:28.566477<br>0:00:25.487944   	| 0:00:19.242562<br>0:00:19.187122 	| 0:00:20.340286<br>0:00:20.595007 	| 0:00:19.464919<br>0:00:19.392559 	| 0:00:20.307666<br>0:00:20.280267 	|

**Results** for objects will all types of properties except **geo**/**object**/**object[]**  (given in h:mm:ss; 2 runs):
|                  	| 1st batch (initial)<br>propsA vecA 	| 2nd batch<br>propsA vecA         	| 3rd batch<br>propsB vecA         	| 4th batch<br>propsB vecA         	| 5th batch<br>propsA vecA         	|
|------------------	|------------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|----------------------------------	|
| stable           	| 0:00:17.522387<br>0:00:17.311325   	| 0:00:28.581429<br>0:00:28.357614 	| 0:00:43.538898<br>0:00:42.953510 	| 0:00:59.816922<br>0:00:58.314892 	| 0:01:17.125187<br>0:01:14.295222 	|
| master           	| 0:00:17.556203<br>0:00:17.055600   	| 0:00:08.914176<br>0:00:08.879083 	| 0:00:09.508935<br>0:00:09.565270 	| 0:00:09.087848<br>0:00:09.188158 	| 0:00:09.426510<br>0:00:09.612367 	|
| idempotent batch 	| 0:00:17.421162<br>0:00:17.235840   	| 0:00:08.722172<br>0:00:08.536178 	| 0:00:09.917492<br>0:00:09.681047 	| 0:00:08.814499<br>0:00:08.734398 	| 0:00:09.669369<br>0:00:09.530104 	|

### Review checklist

- [ ] Documentation has been updated, if necessary. Link to changed documentation:
- [x] Chaos pipeline run or not necessary. Link to pipeline: https://github.com/weaviate/weaviate-chaos-engineering/pull/171
- [x] All new code is covered by tests where it is reasonable.
- [ ] Performance tests have been run or not necessary.


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[2 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[2 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[1 New issue](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[2 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[3 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[3 New issues](https://sonarcloud.io/project/issues?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=weaviate_weaviate&pullRequest=4058&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[10.5% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[9.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[9.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[23.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[23.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[23.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[26.2% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058) **Quality Gate failed**  
Failed conditions

[26.9% Duplication on New Code](https://sonarcloud.io/component_measures?id=weaviate_weaviate&pullRequest=4058&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=weaviate_weaviate&pullRequest=4058)



Patch:
diff --git a/adapters/repos/db/shard_geo_props.go b/adapters/repos/db/shard_geo_props.go
index 2d55d5748f..4d59a674bf 100644
--- a/adapters/repos/db/shard_geo_props.go
+++ b/adapters/repos/db/shard_geo_props.go
@@ -129,6 +129,11 @@ func (s *Shard) updateGeoIndex(propName string, index propertyspecific.Index,
 		return storagestate.ErrStatusReadOnly
 	}
 
+	// geo props were not changed
+	if status.docIDPreserved || status.skipUpsert {
+		return nil
+	}
+
 	if status.docIDChanged {
 		if err := s.deleteFromGeoIndex(index, status.oldDocID); err != nil {
 			return errors.Wrap(err, "delete old doc id from geo index")
diff --git a/adapters/repos/db/shard_write_batch_objects.go b/adapters/repos/db/shard_write_batch_objects.go
index 973284d4f8..c2f3ff9e49 100644
--- a/adapters/repos/db/shard_write_batch_objects.go
+++ b/adapters/repos/db/shard_write_batch_objects.go
@@ -254,12 +254,12 @@ func (ob *objectsBatcher) storeAdditionalStorageWithWorkers(ctx context.Context)
 	ob.batchStartTime = time.Now()
 
 	for i, object := range ob.objects {
-		if ob.shouldSkipInAdditionalStorage(i) {
+		status := ob.statuses[object.ID()]
+		if ob.shouldSkipInAdditionalStorage(i, status) {
 			continue
 		}
 
 		ob.wg.Add(1)
-		status := ob.statuses[object.ID()]
 		ob.shard.addJobToQueue(job{
 			object:  object,
 			status:  status,
@@ -281,14 +281,13 @@ func (ob *objectsBatcher) storeAdditionalStorageWithAsyncQueue(ctx context.Conte
 
 	shouldGeoIndex := ob.shard.hasGeoIndex()
 	vectors := make([]vectorDescriptor, 0, len(ob.objects))
-	for i := range ob.objects {
-		object := ob.objects[i]
-		if ob.shouldSkipInAdditionalStorage(i) {
+	for i, object := range ob.objects {
+		status := ob.statuses[object.ID()]
+
+		if ob.shouldSkipInAdditionalStorage(i, status) {
 			continue
 		}
 
-		status := ob.statuses[object.ID()]
-
 		if shouldGeoIndex {
 			if err := ob.shard.updatePropertySpecificIndices(object, status); err != nil {
 				ob.setErrorAtIndex(errors.Wrap(err, "update prop-specific indices"), i)
@@ -296,6 +295,8 @@ func (ob *objectsBatcher) storeAdditionalStorageWithAsyncQueue(ctx context.Conte
 			}
 		}
 
+		// skip vector update, as vector was not changed
+		// https://github.com/weaviate/weaviate/issues/3948
 		if status.docIDPreserved {
 			continue
 		}
@@ -318,12 +319,18 @@ func (ob *objectsBatcher) storeAdditionalStorageWithAsyncQueue(ctx context.Conte
 	}
 }
 
-func (ob *objectsBatcher) shouldSkipInAdditionalStorage(i int) bool {
+func (ob *objectsBatcher) shouldSkipInAdditionalStorage(i int, status objectInsertStatus) bool {
 	if ok := ob.hasErrorAtIndex(i); ok {
 		// had an error prior, ignore
 		return true
 	}
 
+	// object was not changed, skip further updates
+	// https://github.com/weaviate/weaviate/issues/3949
+	if status.skipUpsert {
+		return true
+	}
+
 	// no need to lock the mutex for a duplicate check, as we only ever write
 	// during init() in there - not concurrently
 	if _, ok := ob.duplicates[i]; ok {
diff --git a/adapters/repos/db/shard_write_merge.go b/adapters/repos/db/shard_write_merge.go
index adadc9d3e7..d6cc30e6d8 100644
--- a/adapters/repos/db/shard_write_merge.go
+++ b/adapters/repos/db/shard_write_merge.go
@@ -45,16 +45,22 @@ func (s *Shard) MergeObject(ctx context.Context, merge objects.MergeDocument) er
 }
 
 func (s *Shard) merge(ctx context.Context, idBytes []byte, doc objects.MergeDocument) error {
-	next, status, err := s.mergeObjectInStorage(doc, idBytes)
+	obj, status, err := s.mergeObjectInStorage(doc, idBytes)
 	if err != nil {
 		return err
 	}
 
-	if err := s.updateVectorIndex(next.Vector, status); err != nil {
+	// object was not changed, no further updates are required
+	// https://github.com/weaviate/weaviate/issues/3949
+	if status.skipUpsert {
+		return nil
+	}
+
+	if err := s.updateVectorIndex(obj.Vector, status); err != nil {
 		return errors.Wrap(err, "update vector index")
 	}
 
-	if err := s.updatePropertySpecificIndices(next, status); err != nil {
+	if err := s.updatePropertySpecificIndices(obj, status); err != nil {
 		return errors.Wrap(err, "update property-specific indices")
 	}
 
@@ -70,45 +76,59 @@ func (s *Shard) mergeObjectInStorage(merge objects.MergeDocument,
 ) (*storobj.Object, objectInsertStatus, error) {
 	bucket := s.store.Bucket(helpers.ObjectsBucketLSM)
 
+	var prevObj, obj *storobj.Object
+	var status objectInsertStatus
+
 	// see comment in shard_write_put.go::putObjectLSM
 	lock := &s.docIdLock[s.uuidToIdLockPoolId(idBytes)]
-	lock.Lock()
-	previous, err := bucket.Get(idBytes)
-	if err != nil {
-		lock.Unlock()
-		return nil, objectInsertStatus{}, errors.Wrap(err, "get bucket")
-	}
 
-	nextObj, _, err := s.mergeObjectData(previous, merge)
-	if err != nil {
-		lock.Unlock()
-		return nil, objectInsertStatus{}, errors.Wrap(err, "merge object data")
-	}
+	// wrapped in function to handle lock/unlock
+	if err := func() error {
+		lock.Lock()
+		defer lock.Unlock()
 
-	status, err := s.determineInsertStatus(previous, nextObj)
-	if err != nil {
-		lock.Unlock()
-		return nil, status, errors.Wrap(err, "check insert/update status")
-	}
+		var err error
+		prevObj, err = fetchObject(bucket, idBytes)
+		if err != nil {
+			return errors.Wrap(err, "get bucket")
+		}
 
-	nextObj.DocID = status.docID
-	nextBytes, err := nextObj.MarshalBinary()
-	if err != nil {
-		lock.Unlock()
-		return nil, status, errors.Wrapf(err, "marshal object %s to binary", nextObj.ID())
-	}
+		obj, _, err = s.mergeObjectData(prevObj, merge)
+		if err != nil {
+			return errors.Wrap(err, "merge object data")
+		}
+
+		status, err = s.determineInsertStatus(prevObj, obj)
+		if err != nil {
+			return errors.Wrap(err, "check insert/update status")
+		}
 
-	if err := s.upsertObjectDataLSM(bucket, idBytes, nextBytes, status.docID); err != nil {
-		lock.Unlock()
-		return nil, status, errors.Wrap(err, "upsert object data")
+		obj.DocID = status.docID
+		if status.skipUpsert {
+			return nil
+		}
+
+		objBytes, err := obj.MarshalBinary()
+		if err != nil {
+			return errors.Wrapf(err, "marshal object %s to binary", obj.ID())
+		}
+
+		if err := s.upsertObjectDataLSM(bucket, idBytes, objBytes, status.docID); err != nil {
+			return errors.Wrap(err, "upsert object data")
+		}
+
+		return nil
+	}(); err != nil {
+		return nil, objectInsertStatus{}, err
+	} else if status.skipUpsert {
+		return obj, status, nil
 	}
-	lock.Unlock()
 
-	if err := s.updateInvertedIndexLSM(nextObj, status, previous); err != nil {
+	if err := s.updateInvertedIndexLSM(obj, status, prevObj); err != nil {
 		return nil, status, errors.Wrap(err, "update inverted indices")
 	}
 
-	return nextObj, status, nil
+	return obj, status, nil
 }
 
 // mutableMergeObjectLSM is a special version of mergeObjectInTx where no doc
@@ -141,32 +161,32 @@ func (s *Shard) mutableMergeObjectLSM(merge objects.MergeDocument,
 	lock.Lock()
 	defer lock.Unlock()
 
-	previous, err := bucket.Get(idBytes)
+	prevObj, err := fetchObject(bucket, idBytes)
 	if err != nil {
 		return out, err
 	}
 
-	nextObj, previousObj, err := s.mergeObjectData(previous, merge)
+	obj, notEmptyPrevObj, err := s.mergeObjectData(prevObj, merge)
 	if err != nil {
 		return out, errors.Wrap(err, "merge object data")
 	}
 
-	out.next = nextObj
-	out.previous = previousObj
+	out.next = obj
+	out.previous = notEmptyPrevObj
 
-	status, err := s.determineMutableInsertStatus(previous, nextObj)
+	status, err := s.determineMutableInsertStatus(prevObj, obj)
 	if err != nil {
 		return out, errors.Wrap(err, "check insert/update status")
 	}
 	out.status = status
 
-	nextObj.DocID = status.docID // is not changed
-	nextBytes, err := nextObj.MarshalBinary()
+	obj.DocID = status.docID // is not changed
+	objBytes, err := obj.MarshalBinary()
 	if err != nil {
-		return out, errors.Wrapf(err, "marshal object %s to binary", nextObj.ID())
+		return out, errors.Wrapf(err, "marshal object %s to binary", obj.ID())
 	}
 
-	if err := s.upsertObjectDataLSM(bucket, idBytes, nextBytes, status.docID); err != nil {
+	if err := s.upsertObjectDataLSM(bucket, idBytes, objBytes, status.docID); err != nil {
 		return out, errors.Wrap(err, "upsert object data")
 	}
 
@@ -182,26 +202,18 @@ type mutableMergeResult struct {
 	status   objectInsertStatus
 }
 
-func (s *Shard) mergeObjectData(previous []byte,
+func (s *Shard) mergeObjectData(prevObj *storobj.Object,
 	merge objects.MergeDocument,
 ) (*storobj.Object, *storobj.Object, error) {
-	var previousObj *storobj.Object
-	if len(previous) == 0 {
+	if prevObj == nil {
 		// DocID must be overwritten after status check, simply set to initial
 		// value
-		previousObj = storobj.New(0)
-		previousObj.SetClass(merge.Class)
-		previousObj.SetID(merge.ID)
-	} else {
-		p, err := storobj.FromBinary(previous)
-		if err != nil {
-			return nil, nil, errors.Wrap(err, "unmarshal previous")
-		}
-
-		previousObj = p
+		prevObj = storobj.New(0)
+		prevObj.SetClass(merge.Class)
+		prevObj.SetID(merge.ID)
 	}
 
-	return mergeProps(previousObj, merge), previousObj, nil
+	return mergeProps(prevObj, merge), prevObj, nil
 }
 
 func mergeProps(previous *storobj.Object,
diff --git a/adapters/repos/db/shard_write_put.go b/adapters/repos/db/shard_write_put.go
index 3a09c537ce..ee9b52bfbb 100644
--- a/adapters/repos/db/shard_write_put.go
+++ b/adapters/repos/db/shard_write_put.go
@@ -16,6 +16,7 @@ import (
 	"context"
 	"encoding/binary"
 	"fmt"
+	"reflect"
 	"time"
 
 	"github.com/google/uuid"
@@ -24,6 +25,7 @@ import (
 	"github.com/weaviate/weaviate/adapters/repos/db/inverted"
 	"github.com/weaviate/weaviate/adapters/repos/db/lsmkv"
 	"github.com/weaviate/weaviate/adapters/repos/db/vector/common"
+	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/storagestate"
 	"github.com/weaviate/weaviate/entities/storobj"
 )
@@ -53,6 +55,12 @@ func (s *Shard) putOne(ctx context.Context, uuid []byte, object *storobj.Object)
 		return errors.Wrap(err, "store object in LSM store")
 	}
 
+	// object was not changed, no further updates are required
+	// https://github.com/weaviate/weaviate/issues/3949
+	if status.skipUpsert {
+		return nil
+	}
+
 	if err := s.updateVectorIndex(object.Vector, status); err != nil {
 		return errors.Wrap(err, "update vector index")
 	}
@@ -78,9 +86,10 @@ func (s *Shard) putOne(ctx context.Context, uuid []byte, object *storobj.Object)
 func (s *Shard) updateVectorIndexIgnoreDelete(vector []float32,
 	status objectInsertStatus,
 ) error {
-	// vector was not changed, object was updated without changing docID
+	// vector was not changed, object was not changed or changed without changing vector
 	// https://github.com/weaviate/weaviate/issues/3948
-	if status.docIDPreserved {
+	// https://github.com/weaviate/weaviate/issues/3949
+	if status.docIDPreserved || status.skipUpsert {
 		return nil
 	}
 
@@ -133,72 +142,109 @@ func (s *Shard) updateVectorIndex(vector []float32,
 	return nil
 }
 
-// TODO AL_skip_vector_reindex: adjust to batch?
-func (s *Shard) putObjectLSM(object *storobj.Object, idBytes []byte,
+func fetchObject(bucket *lsmkv.Bucket, idBytes []byte) (*storobj.Object, error) {
+	objBytes, err := bucket.Get(idBytes)
+	if err != nil {
+		return nil, err
+	}
+	if len(objBytes) == 0 {
+		return nil, nil
+	}
+
+	obj, err := storobj.FromBinary(objBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	return obj, nil
+}
+
+func (s *Shard) putObjectLSM(obj *storobj.Object, idBytes []byte,
 ) (objectInsertStatus, error) {
 	before := time.Now()
 	defer s.metrics.PutObject(before)
 
 	bucket := s.store.Bucket(helpers.ObjectsBucketLSM)
+	var prevObj *storobj.Object
+	var status objectInsertStatus
 
-	// First the object bucket is checked if already an object with the same uuid is present, to determine if it is new
-	// or an update. Afterwards the bucket is updates. To avoid races, only one goroutine can do this at once.
+	// First the object bucket is checked if an object with the same uuid is alreadypresent,
+	// to determine if it is insert or an update.
+	// Afterwards the bucket is updated. To avoid races, only one goroutine can do this at once.
 	lock := &s.docIdLock[s.uuidToIdLockPoolId(idBytes)]
-	lock.Lock()
-	previous_object_bytes, err := bucket.Get(idBytes)
-	if err != nil {
-		lock.Unlock()
-		return objectInsertStatus{}, err
-	}
 
-	status, err := s.determineInsertStatus(previous_object_bytes, object)
-	if err != nil {
-		lock.Unlock()
-		return status, errors.Wrap(err, "check insert/update status")
-	}
-	s.metrics.PutObjectDetermineStatus(before)
+	// wrapped in function to handle lock/unlock
+	if err := func() error {
+		lock.Lock()
+		defer lock.Unlock()
 
-	object.DocID = status.docID
-	data, err := object.MarshalBinary()
-	if err != nil {
-		lock.Unlock()
-		return status, errors.Wrapf(err, "marshal object %s to binary", object.ID())
-	}
+		var err error
 
-	before = time.Now()
-	if err := s.upsertObjectDataLSM(bucket, idBytes, data, status.docID); err != nil {
-		lock.Unlock()
-		return status, errors.Wrap(err, "upsert object data")
+		before = time.Now()
+		prevObj, err = fetchObject(bucket, idBytes)
+		if err != nil {
+			return err
+		}
+
+		status, err = s.determineInsertStatus(prevObj, obj)
+		if err != nil {
+			return err
+		}
+		s.metrics.PutObjectDetermineStatus(before)
+
+		obj.DocID = status.docID
+		if status.skipUpsert {
+			return nil
+		}
+
+		objBinary, err := obj.MarshalBinary()
+		if err != nil {
+			return errors.Wrapf(err, "marshal object %s to binary", obj.ID())
+		}
+
+		before = time.Now()
+		if err := s.upsertObjectDataLSM(bucket, idBytes, objBinary, status.docID); err != nil {
+			return errors.Wrap(err, "upsert object data")
+		}
+		s.metrics.PutObjectUpsertObject(before)
+
+		return nil
+	}(); err != nil {
+		return objectInsertStatus{}, err
+	} else if status.skipUpsert {
+		return status, nil
 	}
-	lock.Unlock()
-	s.metrics.PutObjectUpsertObject(before)
 
 	before = time.Now()
-	if err := s.updateInvertedIndexLSM(object, status, previous_object_bytes); err != nil {
-		return status, errors.Wrap(err, "update inverted indices")
+	if err := s.updateInvertedIndexLSM(obj, status, prevObj); err != nil {
+		return objectInsertStatus{}, errors.Wrap(err, "update inverted indices")
 	}
-
 	s.metrics.PutObjectUpdateInverted(before)
 
 	return status, nil
 }
 
 type objectInsertStatus struct {
-	docID          uint64
-	docIDChanged   bool
-	oldDocID       uint64
-	docIDPreserved bool // docID preserved (docID was not changed, although object did change)
+	docID        uint64
+	docIDChanged bool
+	oldDocID     uint64
+	// docID was not changed, although object itself did. DocID can be preserved if
+	// object's vector remain the same, allowing to omit vector index update which is time
+	// consuming operation. New object is saved and inverted indexes updated if required.
+	docIDPreserved bool
+	// object was not changed, all properties and additional properties are the same as in
+	// the one already stored. No object update, inverted indexes update and vector index
+	// update is required.
+	skipUpsert bool
 }
 
 // to be called with the current contents of a row, if the row is empty (i.e.
 // didn't exist before), we will get a new docID from the central counter.
 // Otherwise, we will reuse the previous docID and mark this as an update
-func (s *Shard) determineInsertStatus(previous []byte,
-	next *storobj.Object,
-) (objectInsertStatus, error) {
+func (s *Shard) determineInsertStatus(prevObj, nextObj *storobj.Object) (objectInsertStatus, error) {
 	var out objectInsertStatus
 
-	if previous == nil {
+	if prevObj == nil {
 		docID, err := s.counter.GetAndInc()
 		if err != nil {
 			return out, errors.Wrap(err, "initial doc id: get new doc id from counter")
@@ -207,41 +253,27 @@ func (s *Shard) determineInsertStatus(previous []byte,
 		return out, nil
 	}
 
-	docID, err := storobj.DocIDFromBinary(previous)
-	if err != nil {
-		return out, errors.Wrap(err, "get previous doc id from object binary")
-	}
-	out.oldDocID = docID
+	out.oldDocID = prevObj.DocID
 
-	// if vector was not changed, replace object and update indexed properties
-	// without changing docID
+	// If object was not changed (props and additional props of prev and next objects are the same)
+	// skip updates of object, inverted indexes and vector index.
+	// https://github.com/weaviate/weaviate/issues/3949
+	//
+	// If object was changed (props or additional props of prev and next objects differ)
+	// update objects and inverted indexes, skip update of vector index.
 	// https://github.com/weaviate/weaviate/issues/3948
 	//
-	// Due to geo index does not support delete+insert of geo value for the same docID
-	// (as tombstones are used for hnsw geo index),
-	// preserving docID will not be supported for classes with geo properties for now.
-	// Feature can be improved in the future, by preserving docIDs for objects
-	// not updating values of geo properties.
-	if !s.hasGeoIndex() {
-		if l := len(next.Vector); l > 0 {
-			buffer := make([]float32, l)
-			prevVector, err := storobj.VectorFromBinary(previous, buffer)
-			if err != nil {
-				return out, fmt.Errorf("get previous vector from object binary: %w", err)
-			}
-			if common.VectorsEqual(prevVector, next.Vector) {
-				out.docID = docID
-				out.docIDPreserved = true
-				return out, nil
-			}
-		}
+	// Due to geo index's (using HNSW vector index) requirement new docID for delete+insert
+	// (delete initially adds tombstone, which "overwrite" following insert of the same docID)
+	// any update of geo property needs new docID for updating geo index.
+	if preserve, skip := compareObjsForInsertStatus(prevObj, nextObj); preserve || skip {
+		out.docID = prevObj.DocID
+		out.docIDPreserved = preserve
+		out.skipUpsert = skip
+		return out, nil
 	}
 
-	// with docIDs now being immutable (see
-	// https://github.com/weaviate/weaviate/issues/1282) there is no
-	// more check if we need to increase a docID. Any update will mean a doc ID
-	// needs to be updated.
-	docID, err = s.counter.GetAndInc()
+	docID, err := s.counter.GetAndInc()
 	if err != nil {
 		return out, errors.Wrap(err, "doc id update: get new doc id from counter")
 	}
@@ -255,9 +287,7 @@ func (s *Shard) determineInsertStatus(previous []byte,
 // where it does not alter the doc id if one already exists. Calling this
 // method only makes sense under very special conditions, such as those
 // outlined in mutableMergeObjectInTx
-func (s *Shard) determineMutableInsertStatus(previous []byte,
-	next *storobj.Object,
-) (objectInsertStatus, error) {
+func (s *Shard) determineMutableInsertStatus(previous, next *storobj.Object) (objectInsertStatus, error) {
 	var out objectInsertStatus
 
 	if previous == nil {
@@ -269,11 +299,7 @@ func (s *Shard) determineMutableInsertStatus(previous []byte,
 		return out, nil
 	}
 
-	docID, err := storobj.DocIDFromBinary(previous)
-	if err != nil {
-		return out, errors.Wrap(err, "get previous doc id from object binary")
-	}
-	out.docID = docID
+	out.docID = previous.DocID
 
 	// we are planning on mutating and thus not altering the doc id
 	return out, nil
@@ -290,23 +316,17 @@ func (s *Shard) upsertObjectDataLSM(bucket *lsmkv.Bucket, id []byte, data []byte
 }
 
 func (s *Shard) updateInvertedIndexLSM(object *storobj.Object,
-	status objectInsertStatus, previous []byte,
+	status objectInsertStatus, prevObject *storobj.Object,
 ) error {
 	props, nilprops, err := s.AnalyzeObject(object)
 	if err != nil {
 		return errors.Wrap(err, "analyze next object")
 	}
 
-	var prevObject *storobj.Object
 	var prevProps []inverted.Property
 	var prevNilprops []inverted.NilProperty
 
-	if previous != nil {
-		prevObject, err = storobj.FromBinary(previous)
-		if err != nil {
-			return fmt.Errorf("unmarshal previous object: %w", err)
-		}
-
+	if prevObject != nil {
 		prevProps, prevNilprops, err = s.AnalyzeObject(prevObject)
 		if err != nil {
 			return fmt.Errorf("analyze previous object: %w", err)
@@ -344,7 +364,7 @@ func (s *Shard) updateInvertedIndexLSM(object *storobj.Object,
 		nilpropsToDel = prevNilprops
 	}
 
-	if previous != nil {
+	if prevObject != nil {
 		// TODO: metrics
 		if err := s.deleteFromInvertedIndicesLSM(propsToDel, nilpropsToDel, status.oldDocID); err != nil {
 			return fmt.Errorf("delete inverted indices props: %w", err)
@@ -370,3 +390,174 @@ func (s *Shard) updateInvertedIndexLSM(object *storobj.Object,
 
 	return nil
 }
+
+func compareObjsForInsertStatus(prevObj, nextObj *storobj.Object) (preserve, skip bool) {
+	prevProps, ok := prevObj.Object.Properties.(map[string]interface{})
+	if !ok {
+		return false, false
+	}
+	nextProps, ok := nextObj.Object.Properties.(map[string]interface{})
+	if !ok {
+		return false, false
+	}
+	if !geoPropsEqual(prevProps, nextProps) {
+		return false, false
+	}
+	if !common.VectorsEqual(prevObj.Vector, nextObj.Vector) {
+		return false, false
+	}
+	if !addPropsEqual(prevObj.Object.Additional, nextObj.Object.Additional) {
+		return true, false
+	}
+	if !propsEqual(prevProps, nextProps) {
+		return true, false
+	}
+	return false, true
+}
+
+func geoPropsEqual(prevProps, nextProps map[string]interface{}) bool {
+	geoPropsCompared := map[string]struct{}{}
+
+	for name, prevVal := range prevProps {
+		switch prevGeoVal := prevVal.(type) {
+		case *models.GeoCoordinates:
+			nextVal, ok := nextProps[name]
+			if !ok {
+				// matching prop does not exist in next
+				return false
+			}
+
+			switch nextGeoVal := nextVal.(type) {
+			case *models.GeoCoordinates:
+				if !reflect.DeepEqual(prevGeoVal, nextGeoVal) {
+					// matching geo props in prev and next differ
+					return false
+				}
+			default:
+				// matching prop in next is not geo
+				return false
+			}
+			geoPropsCompared[name] = struct{}{}
+		}
+	}
+
+	for name, nextVal := range nextProps {
+		switch nextVal.(type) {
+		case *models.GeoCoordinates:
+			if _, ok := geoPropsCompared[name]; !ok {
+				// matching geo prop does not exist in prev
+				return false
+			}
+		}
+	}
+
+	return true
+}
+
+func timeToString(t time.Time) string {
+	if b, err := t.MarshalText(); err == nil {
+		return string(b)
+	}
+	return ""
+}
+
+func uuidToString(u uuid.UUID) string {
+	if b, err := u.MarshalText(); err == nil {
+		return string(b)
+	}
+	return ""
+}
+
+func addPropsEqual(prevAddProps, nextAddProps models.AdditionalProperties) bool {
+	return reflect.DeepEqual(prevAddProps, nextAddProps)
+}
+
+func propsEqual(prevProps, nextProps map[string]interface{}) bool {
+	if len(prevProps) != len(nextProps) {
+		return false
+	}
+
+	for name := range nextProps {
+		if _, ok := prevProps[name]; !ok {
+			return false
+		}
+
+		switch nextVal := nextProps[name].(type) {
+		case time.Time:
+			if timeToString(nextVal) != prevProps[name] {
+				return false
+			}
+
+		case []time.Time:
+			prevVal, ok := prevProps[name].([]string)
+			if !ok {
+				return false
+			}
+			if len(nextVal) != len(prevVal) {
+				return false
+			}
+			for i := range nextVal {
+				if timeToString(nextVal[i]) != prevVal[i] {
+					return false
+				}
+			}
+
+		case uuid.UUID:
+			if uuidToString(nextVal) != prevProps[name] {
+				return false
+			}
+
+		case []uuid.UUID:
+			prevVal, ok := prevProps[name].([]string)
+			if !ok {
+				return false
+			}
+			if len(nextVal) != len(prevVal) {
+				return false
+			}
+			for i := range nextVal {
+				if uuidToString(nextVal[i]) != prevVal[i] {
+					return false
+				}
+			}
+
+		case map[string]interface{}: // data type "object"
+			prevVal, ok := prevProps[name].(map[string]interface{})
+			if !ok {
+				return false
+			}
+			if !propsEqual(prevVal, nextVal) {
+				return false
+			}
+
+		case []interface{}: // data type "objects"
+			prevVal, ok := prevProps[name].([]interface{})
+			if !ok {
+				return false
+			}
+			if len(nextVal) != len(prevVal) {
+				return false
+			}
+			for i := range nextVal {
+				nextValI, ok := nextVal[i].(map[string]interface{})
+				if !ok {
+					return false
+				}
+				prevValI, ok := prevVal[i].(map[string]interface{})
+				if !ok {
+					return false
+				}
+				if !propsEqual(prevValI, nextValI) {
+					return false
+				}
+			}
+
+		default:
+			if !reflect.DeepEqual(nextProps[name], prevProps[name]) {
+				return false
+			}
+		}
+	}
+
+	return true
+}
diff --git a/modules/multi2vec-bind/vectorizer/vectorizer.go b/modules/multi2vec-bind/vectorizer/vectorizer.go
index ffcfe47f04..63a60c213d 100644
--- a/modules/multi2vec-bind/vectorizer/vectorizer.go
+++ b/modules/multi2vec-bind/vectorizer/vectorizer.go
@@ -131,6 +131,11 @@ func (v *Vectorizer) object(ctx context.Context, id strfmt.UUID,
 	// vectorize image and text
 	var texts, images, audio, video, imu, thermal, depth []string
 	if schema != nil {
+		appendTexts := func(text, prop string) {
+			texts = append(texts, text)
+			vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
+		}
+
 		for prop, value := range schema.(map[string]interface{}) {
 			if ichek.ImageField(prop) {
 				valueString, ok := value.(string)
@@ -140,18 +145,17 @@ func (v *Vectorizer) object(ctx context.Context, id strfmt.UUID,
 				}
 			}
 			if ichek.TextField(prop) {
-				valueString, ok := value.(string)
-				if ok {
-					texts = append(texts, valueString)
-					vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
-				}
-				valueArr, ok := value.([]interface{})
-				if ok {
-					for _, value := range valueArr {
-						valueString, ok := value.(string)
-						if ok {
-							texts = append(texts, valueString)
-							vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
+				switch typed := value.(type) {
+				case string:
+					appendTexts(typed, prop)
+				case []string:
+					for _, valueString := range typed {
+						appendTexts(valueString, prop)
+					}
+				case []interface{}:
+					for i := range typed {
+						if valueString, ok := typed[i].(string); ok {
+							appendTexts(valueString, prop)
 						}
 					}
 				}
diff --git a/modules/multi2vec-clip/vectorizer/vectorizer.go b/modules/multi2vec-clip/vectorizer/vectorizer.go
index fbfd06925b..c46bc07959 100644
--- a/modules/multi2vec-clip/vectorizer/vectorizer.go
+++ b/modules/multi2vec-clip/vectorizer/vectorizer.go
@@ -78,6 +78,11 @@ func (v *Vectorizer) object(ctx context.Context, id strfmt.UUID,
 	texts := []string{}
 	images := []string{}
 	if schema != nil {
+		appendTexts := func(text, prop string) {
+			texts = append(texts, text)
+			vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
+		}
+
 		for prop, value := range schema.(map[string]interface{}) {
 			if ichek.ImageField(prop) {
 				valueString, ok := value.(string)
@@ -87,19 +92,18 @@ func (v *Vectorizer) object(ctx context.Context, id strfmt.UUID,
 				}
 			}
 			if ichek.TextField(prop) {
-				valueString, ok := value.(string)
-				if ok {
-					texts = append(texts, valueString)
-					vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
-				}
-			}
-			valueArr, ok := value.([]interface{})
-			if ok {
-				for _, value := range valueArr {
-					valueString, ok := value.(string)
-					if ok {
-						texts = append(texts, valueString)
-						vectorize = vectorize || (objDiff != nil && objDiff.IsChangedProp(prop))
+				switch typed := value.(type) {
+				case string:
+					appendTexts(typed, prop)
+				case []string:
+					for _, valueString := range typed {
+						appendTexts(valueString, prop)
+					}
+				case []interface{}:
+					for i := range typed {
+						if valueString, ok := typed[i].(string); ok {
+							appendTexts(valueString, prop)
+						}
 					}
 				}
 			}
diff --git a/usecases/objects/validation/properties_validation.go b/usecases/objects/validation/properties_validation.go
index e73295585c..51133612a0 100644
--- a/usecases/objects/validation/properties_validation.go
+++ b/usecases/objects/validation/properties_validation.go
@@ -258,12 +258,7 @@ func (v *Validator) extractAndValidateProperty(ctx context.Context, propertyName
 			return nil, fmt.Errorf("invalid text property '%s' on class '%s': %s", propertyName, className, err)
 		}
 	case schema.DataTypeUUID:
-		asStr, err := stringVal(pv)
-		if err != nil {
-			return nil, fmt.Errorf("invalid uuid property '%s' on class '%s': %s", propertyName, className, err)
-		}
-
-		data, err = uuid.Parse(asStr)
+		data, err = uuidVal(pv)
 		if err != nil {
 			return nil, fmt.Errorf("invalid uuid property '%s' on class '%s': %s", propertyName, className, err)
 		}
@@ -328,7 +323,7 @@ func (v *Validator) extractAndValidateProperty(ctx context.Context, propertyName
 			return nil, fmt.Errorf("invalid date array property '%s' on class '%s': %s", propertyName, className, err)
 		}
 	case schema.DataTypeUUIDArray:
-		data, err = ParseUUIDArray(pv)
+		data, err = uuidArrayVal(pv)
 		if err != nil {
 			return nil, fmt.Errorf("invalid uuid array property '%s' on class '%s': %s", propertyName, className, err)
 		}
@@ -400,80 +395,77 @@ func boolVal(val interface{}) (bool, error) {
 }
 
 func dateVal(val interface{}) (time.Time, error) {
-	var data time.Time
-	var err error
-	var ok bool
-
-	errorInvalidDate := "requires a string with a RFC3339 formatted date, but the given value is '%v'"
-
-	var dateString string
-	if dateString, ok = val.(string); !ok {
-		return time.Time{}, fmt.Errorf(errorInvalidDate, val)
+	if dateStr, ok := val.(string); ok {
+		if date, err := time.Parse(time.RFC3339, dateStr); err == nil {
+			return date, nil
+		}
 	}
 
-	// Parse the time as this has to be correct
-	data, err = time.Parse(time.RFC3339, dateString)
+	errorInvalidDate := "requires a string with a RFC3339 formatted date, but the given value is '%v'"
+	return time.Time{}, fmt.Errorf(errorInvalidDate, val)
+}
 
-	// Return if there is an error while parsing
-	if err != nil {
-		return time.Time{}, fmt.Errorf(errorInvalidDate, val)
+func uuidVal(val interface{}) (uuid.UUID, error) {
+	if uuidStr, ok := val.(string); ok {
+		if uuid, err := uuid.Parse(uuidStr); err == nil {
+			return uuid, nil
+		}
 	}
 
-	return data, nil
+	errorInvalidUuid := "requires a string of UUID format, but the given value is '%v'"
+	return uuid.UUID{}, fmt.Errorf(errorInvalidUuid, val)
 }
 
-func intVal(val interface{}) (interface{}, error) {
-	var data interface{}
-	var ok bool
-	var err error
-
+func intVal(val interface{}) (float64, error) {
 	errInvalidInteger := "requires an integer, the given value is '%v'"
 	errInvalidIntegerConvertion := "the JSON number '%v' could not be converted to an int"
 
-	// Return err when the input can not be casted to json.Number
-	if _, ok = val.(json.Number); !ok {
-		// If value is not a json.Number, it could be an int, which is fine
-		if data, ok = val.(int64); !ok {
-			// If value is not a json.Number, it could be an int, which is fine when the float does not contain a decimal
-			if data, ok = val.(float64); ok {
-				// Check whether the float is containing a decimal
-				if data != float64(int64(data.(float64))) {
-					return nil, fmt.Errorf(errInvalidInteger, val)
-				}
-			} else {
-				// If it is not a float, it is certainly not a integer, return the err
-				return nil, fmt.Errorf(errInvalidInteger, val)
-			}
+	switch typed := val.(type) {
+	case json.Number:
+		asInt, err := typed.Int64()
+		if err != nil {
+			// return err when the input can not be converted to an int
+			return 0, fmt.Errorf(errInvalidIntegerConvertion, val)
 		}
-	} else if data, err = val.(json.Number).Int64(); err != nil {
-		// Return err when the input can not be converted to an int
-		return nil, fmt.Errorf(errInvalidIntegerConvertion, val)
-	}
+		return float64(asInt), nil
 
-	return data, nil
-}
+	case int64:
+		return float64(typed), nil
 
-func numberVal(val interface{}) (interface{}, error) {
-	var data interface{}
-	var ok bool
-	var err error
+	case float64:
+		if typed != float64(int64(typed)) {
+			// return err when float contains a decimal
+			return 0, fmt.Errorf(errInvalidInteger, val)
+		}
+		return typed, nil
+
+	default:
+		return 0, fmt.Errorf(errInvalidInteger, val)
+	}
+}
 
+func numberVal(val interface{}) (float64, error) {
 	errInvalidFloat := "requires a float, the given value is '%v'"
 	errInvalidFloatConvertion := "the JSON number '%v' could not be converted to a float."
 
-	if _, ok = val.(json.Number); !ok {
-		if data, ok = val.(float64); !ok {
-			data64, ok := val.(int64)
-			if !ok {
-				return nil, fmt.Errorf(errInvalidFloat, val)
-			}
-			data = float64(data64)
+	switch typed := val.(type) {
+	case json.Number:
+		asFloat, err := typed.Float64()
+		if err != nil {
+			// return err when the input can not be converted to an int
+			return 0, fmt.Errorf(errInvalidFloatConvertion, val)
 		}
-	} else if data, err = val.(json.Number).Float64(); err != nil {
-		return nil, fmt.Errorf(errInvalidFloatConvertion, val)
-	}
+		return asFloat, nil
 
-	return data, nil
+	case int64:
+		return float64(typed), nil
+
+	case float64:
+		return typed, nil
+
+	default:
+		return 0, fmt.Errorf(errInvalidFloat, val)
+	}
 }
 
 func geoCoordinates(input interface{}) (*models.GeoCoordinates, error) {
@@ -639,81 +631,112 @@ func (v *Validator) validateVectorWeights(in interface{}) (map[string]string, er
 	return out, nil
 }
 
-func stringArrayVal(val interface{}, typeName string) ([]interface{}, error) {
+func stringArrayVal(val interface{}, typeName string) ([]string, error) {
 	typed, ok := val.([]interface{})
 	if !ok {
 		return nil, fmt.Errorf("not a %s array, but %T", typeName, val)
 	}
 
+	data := make([]string, len(typed))
 	for i := range typed {
-		if _, err := stringVal(typed[i]); err != nil {
+		sval, err := stringVal(typed[i])
+		if err != nil {
 			return nil, fmt.Errorf("invalid %s array value: %s", typeName, val)
 		}
+		data[i] = sval
 	}
 
-	return typed, nil
+	return data, nil
 }
 
-func intArrayVal(val interface{}) ([]interface{}, error) {
+func intArrayVal(val interface{}) ([]float64, error) {
 	typed, ok := val.([]interface{})
 	if !ok {
 		return nil, fmt.Errorf("not an integer array, but %T", val)
 	}
 
+	data := make([]float64, len(typed))
 	for i := range typed {
-		if _, err := intVal(typed[i]); err != nil {
+		ival, err := intVal(typed[i])
+		if err != nil {
 			return nil, fmt.Errorf("invalid integer array value: %s", val)
 		}
+		data[i] = ival
 	}
 
-	return typed, nil
+	return data, nil
 }
 
-func numberArrayVal(val interface{}) ([]interface{}, error) {
+func numberArrayVal(val interface{}) ([]float64, error) {
 	typed, ok := val.([]interface{})
 	if !ok {
 		return nil, fmt.Errorf("not an integer array, but %T", val)
 	}
 
+	data := make([]float64, len(typed))
 	for i := range typed {
-		data, err := numberVal(typed[i])
+		nval, err := numberVal(typed[i])
 		if err != nil {
 			return nil, fmt.Errorf("invalid integer array value: %s", val)
 		}
-		typed[i] = data
+		data[i] = nval
 	}
 
-	return typed, nil
+	return data, nil
 }
 
-func boolArrayVal(val interface{}) ([]interface{}, error) {
+func boolArrayVal(val interface{}) ([]bool, error) {
 	typed, ok := val.([]interface{})
 	if !ok {
 		return nil, fmt.Errorf("not a boolean array, but %T", val)
 	}
 
+	data := make([]bool, len(typed))
 	for i := range typed {
-		if _, err := boolVal(typed[i]); err != nil {
+		bval, err := boolVal(typed[i])
+		if err != nil {
 			return nil, fmt.Errorf("invalid boolean array value: %s", val)
 		}
+		data[i] = bval
 	}
 
-	return typed, nil
+	return data, nil
 }
 
-func dateArrayVal(val interface{}) ([]interface{}, error) {
+func dateArrayVal(val interface{}) ([]time.Time, error) {
 	typed, ok := val.([]interface{})
 	if !ok {
 		return nil, fmt.Errorf("not a date array, but %T", val)
 	}
 
+	data := make([]time.Time, len(typed))
 	for i := range typed {
-		if _, err := dateVal(typed[i]); err != nil {
+		dval, err := dateVal(typed[i])
+		if err != nil {
 			return nil, fmt.Errorf("invalid date array value: %s", val)
 		}
+		data[i] = dval
 	}
 
-	return typed, nil
+	return data, nil
+}
+
+func uuidArrayVal(val interface{}) ([]uuid.UUID, error) {
+	typed, ok := val.([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("not a uuid array, but %T", val)
+	}
+
+	data := make([]uuid.UUID, len(typed))
+	for i := range typed {
+		uval, err := uuidVal(typed[i])
+		if err != nil {
+			return nil, fmt.Errorf("invalid uuid array value: %s", val)
+		}
+		data[i] = uval
+	}
+
+	return data, nil
 }
 
 func ParseUUIDArray(in any) ([]uuid.UUID, error) {


Test patch:
diff --git a/adapters/repos/db/shard_skip_vector_reindex_integration_test.go b/adapters/repos/db/shard_skip_vector_reindex_integration_test.go
new file mode 100644
index 0000000000..dab8423810
--- /dev/null
+++ b/adapters/repos/db/shard_skip_vector_reindex_integration_test.go
@@ -0,0 +1,1256 @@
+//                           _       _
+// __      _____  __ ___   ___  __ _| |_ ___
+// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
+//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
+//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
+//
+//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
+//
+//  CONTACT: hello@weaviate.io
+//
+
+//go:build integrationTest
+// +build integrationTest
+
+package db
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/require"
+	"github.com/weaviate/weaviate/entities/additional"
+	"github.com/weaviate/weaviate/entities/filters"
+	"github.com/weaviate/weaviate/entities/models"
+	"github.com/weaviate/weaviate/entities/schema"
+	"github.com/weaviate/weaviate/entities/storobj"
+	"github.com/weaviate/weaviate/entities/vectorindex/common"
+	"github.com/weaviate/weaviate/entities/vectorindex/hnsw"
+	"github.com/weaviate/weaviate/usecases/objects"
+)
+
+func TestShard_SkipVectorReindex(t *testing.T) {
+	ctx := context.Background()
+
+	uuid_ := strfmt.UUID(uuid.NewString())
+	origCreateTimeUnix := int64(1704161045)
+	origUpdateTimeUnix := int64(1704161046)
+	updCreateTimeUnix := int64(1704161047)
+	updUpdateTimeUnix := int64(1704161048)
+	vector := []float32{1, 2, 3}
+	altVector := []float32{10, 0, -20}
+
+	class := &models.Class{
+		Class: "TestClass",
+		InvertedIndexConfig: &models.InvertedIndexConfig{
+			IndexTimestamps:     true,
+			IndexNullState:      true,
+			IndexPropertyLength: true,
+		},
+		Properties: []*models.Property{
+			{
+				Name:         "texts",
+				DataType:     schema.DataTypeTextArray.PropString(),
+				Tokenization: models.PropertyTokenizationWord,
+			},
+			{
+				Name:     "numbers",
+				DataType: schema.DataTypeNumberArray.PropString(),
+			},
+			{
+				Name:     "ints",
+				DataType: schema.DataTypeIntArray.PropString(),
+			},
+			{
+				Name:     "booleans",
+				DataType: schema.DataTypeBooleanArray.PropString(),
+			},
+			{
+				Name:     "dates",
+				DataType: schema.DataTypeDateArray.PropString(),
+			},
+			{
+				Name:     "uuids",
+				DataType: schema.DataTypeUUIDArray.PropString(),
+			},
+			{
+				Name:         "text",
+				DataType:     schema.DataTypeText.PropString(),
+				Tokenization: models.PropertyTokenizationWord,
+			},
+			{
+				Name:     "number",
+				DataType: schema.DataTypeNumber.PropString(),
+			},
+			{
+				Name:     "int",
+				DataType: schema.DataTypeInt.PropString(),
+			},
+			{
+				Name:     "boolean",
+				DataType: schema.DataTypeBoolean.PropString(),
+			},
+			{
+				Name:     "date",
+				DataType: schema.DataTypeDate.PropString(),
+			},
+			{
+				Name:     "uuid",
+				DataType: schema.DataTypeUUID.PropString(),
+			},
+			{
+				Name:     "geo",
+				DataType: schema.DataTypeGeoCoordinates.PropString(),
+			},
+		},
+	}
+
+	createOrigObj := func() *storobj.Object {
+		return &storobj.Object{
+			MarshallerVersion: 1,
+			Object: models.Object{
+				ID:    uuid_,
+				Class: class.Class,
+				Properties: map[string]interface{}{
+					"texts": []string{
+						"aaa",
+						"bbb",
+						"ccc",
+					},
+					"numbers": []interface{}{},
+					"ints": []float64{
+						101, 101, 101, 101, 101, 101,
+						102,
+						103,
+						104,
+					},
+					"booleans": []bool{
+						true, true, true,
+						false,
+					},
+					"dates": []time.Time{
+						mustParseTime("2001-06-01T12:00:00.000000Z"),
+						mustParseTime("2002-06-02T12:00:00.000000Z"),
+					},
+					// no uuids
+					"text": "ddd",
+					// no number
+					"int":     float64(201),
+					"boolean": false,
+					"date":    mustParseTime("2003-06-01T12:00:00.000000Z"),
+					// no uuid
+					"geo": &models.GeoCoordinates{
+						Latitude:  ptFloat32(1.1),
+						Longitude: ptFloat32(2.2),
+					},
+				},
+				CreationTimeUnix:   origCreateTimeUnix,
+				LastUpdateTimeUnix: origUpdateTimeUnix,
+			},
+			Vector: vector,
+		}
+	}
+	createUpdObj := func() *storobj.Object {
+		return &storobj.Object{
+			MarshallerVersion: 1,
+			Object: models.Object{
+				ID:    uuid_,
+				Class: class.Class,
+				Properties: map[string]interface{}{
+					"texts": []interface{}{},
+					// no numbers
+					"ints": []float64{
+						101, 101, 101, 101,
+						103,
+						104,
+						105,
+					},
+					"booleans": []bool{
+						true, true, true,
+						false,
+					},
+					// no dates
+					"uuids": []uuid.UUID{
+						uuid.MustParse("d726c960-aede-411c-85d3-2c77e9290a6e"),
+					},
+					"text": "",
+					// no number
+					"int":     float64(202),
+					"boolean": true,
+					// no date
+					"uuid": uuid.MustParse("7fabaf01-9e10-458a-acea-cc627376c506"),
+					"geo": &models.GeoCoordinates{
+						Latitude:  ptFloat32(1.1),
+						Longitude: ptFloat32(2.2),
+					},
+				},
+				CreationTimeUnix:   updCreateTimeUnix,
+				LastUpdateTimeUnix: updUpdateTimeUnix,
+			},
+			Vector: vector,
+		}
+	}
+	createMergeDoc := func() objects.MergeDocument {
+		return objects.MergeDocument{
+			ID:    uuid_,
+			Class: class.Class,
+			PrimitiveSchema: map[string]interface{}{
+				"texts": []interface{}{},
+				"ints": []interface{}{
+					float64(101), float64(101), float64(101), float64(101),
+					float64(103),
+					float64(104),
+					float64(105),
+				},
+				"uuids": []interface{}{
+					uuid.MustParse("d726c960-aede-411c-85d3-2c77e9290a6e"),
+				},
+				"text":    "",
+				"int":     float64(202),
+				"boolean": true,
+				"uuid":    uuid.MustParse("7fabaf01-9e10-458a-acea-cc627376c506"),
+			},
+			UpdateTime:         updUpdateTimeUnix,
+			PropertiesToDelete: []string{"numbers", "dates", "date"},
+			Vector:             vector,
+		}
+	}
+
+	filterId := filterEqual[string](string(uuid_), schema.DataTypeText, class.Class, "_id")
+
+	filterTextsEqAAA := filterEqual[string]("aaa", schema.DataTypeText, class.Class, "texts")
+	filterTextsLen3 := filterEqual[int](3, schema.DataTypeInt, class.Class, "len(texts)")
+	filterTextsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(texts)")
+	filterTextsNotNil := filterNil(false, class.Class, "texts")
+	filterTextsNil := filterNil(true, class.Class, "texts")
+
+	filterNumbersEq123 := filterEqual[float64](1.23, schema.DataTypeNumber, class.Class, "numbers")
+	filterNumbersLen1 := filterEqual[int](1, schema.DataTypeInt, class.Class, "len(numbers)")
+	filterNumbersLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(numbers)")
+	filterNumbersNotNil := filterNil(false, class.Class, "numbers")
+	filterNumbersNil := filterNil(true, class.Class, "numbers")
+
+	filterIntsEq102 := filterEqual[int](102, schema.DataTypeInt, class.Class, "ints")
+	filterIntsEq105 := filterEqual[int](105, schema.DataTypeInt, class.Class, "ints")
+	filterIntsLen9 := filterEqual[int](9, schema.DataTypeInt, class.Class, "len(ints)")
+	filterIntsLen7 := filterEqual[int](7, schema.DataTypeInt, class.Class, "len(ints)")
+	filterIntsNotNil := filterNil(false, class.Class, "ints")
+	filterIntsNil := filterNil(true, class.Class, "ints")
+
+	filterBoolsEqTrue := filterEqual[bool](true, schema.DataTypeBoolean, class.Class, "booleans")
+	filterBoolsEqFalse := filterEqual[bool](false, schema.DataTypeBoolean, class.Class, "booleans")
+	filterBoolsLen4 := filterEqual[int](4, schema.DataTypeInt, class.Class, "len(booleans)")
+	filterBoolsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(booleans)")
+	filterBoolsNotNil := filterNil(false, class.Class, "booleans")
+	filterBoolsNil := filterNil(true, class.Class, "booleans")
+
+	filterDatesEq2001 := filterEqual[string]("2001-06-01T12:00:00.000000Z", schema.DataTypeDate, class.Class, "dates")
+	filterDatesLen2 := filterEqual[int](2, schema.DataTypeInt, class.Class, "len(dates)")
+	filterDatesLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(dates)")
+	filterDatesNotNil := filterNil(false, class.Class, "dates")
+	filterDatesNil := filterNil(true, class.Class, "dates")
+
+	filterUuidsEqD726 := filterEqual[string]("d726c960-aede-411c-85d3-2c77e9290a6e", schema.DataTypeText, class.Class, "uuids")
+	filterUuidsLen1 := filterEqual[int](1, schema.DataTypeInt, class.Class, "len(uuids)")
+	filterUuidsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(uuids)")
+	filterUuidsNotNil := filterNil(false, class.Class, "uuids")
+	filterUuidsNil := filterNil(true, class.Class, "uuids")
+
+	filterTextEqDDD := filterEqual[string]("ddd", schema.DataTypeText, class.Class, "text")
+	filterTextLen3 := filterEqual[int](3, schema.DataTypeInt, class.Class, "len(text)")
+	filterTextLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(text)")
+	filterTextNotNil := filterNil(false, class.Class, "text")
+	filterTextNil := filterNil(true, class.Class, "text")
+
+	filterNumberEq123 := filterEqual[float64](1.23, schema.DataTypeNumber, class.Class, "number")
+	filterNumberNotNil := filterNil(false, class.Class, "number")
+	filterNumberNil := filterNil(true, class.Class, "number")
+
+	filterIntEq201 := filterEqual[int](201, schema.DataTypeInt, class.Class, "int")
+	filterIntEq202 := filterEqual[int](202, schema.DataTypeInt, class.Class, "int")
+	filterIntNotNil := filterNil(false, class.Class, "int")
+	filterIntNil := filterNil(true, class.Class, "int")
+
+	filterBoolEqFalse := filterEqual[bool](false, schema.DataTypeBoolean, class.Class, "boolean")
+	filterBoolEqTrue := filterEqual[bool](true, schema.DataTypeBoolean, class.Class, "boolean")
+	filterBoolNotNil := filterNil(false, class.Class, "boolean")
+	filterBoolNil := filterNil(true, class.Class, "boolean")
+
+	filterDateEq2003 := filterEqual[string]("2003-06-01T12:00:00.000000Z", schema.DataTypeDate, class.Class, "date")
+	filterDateNotNil := filterNil(false, class.Class, "date")
+	filterDateNil := filterNil(true, class.Class, "date")
+
+	filterUuidEq7FAB := filterEqual[string]("7fabaf01-9e10-458a-acea-cc627376c506", schema.DataTypeText, class.Class, "uuid")
+	filterUuidNotNil := filterNil(false, class.Class, "uuid")
+	filterUuidNil := filterNil(true, class.Class, "uuid")
+
+	search := func(t *testing.T, shard ShardLike, filter *filters.LocalFilter) []*storobj.Object {
+		searchLimit := 10
+		found, _, err := shard.ObjectSearch(ctx, searchLimit, filter,
+			nil, nil, nil, additional.Properties{})
+		require.NoError(t, err)
+		return found
+	}
+
+	verifySearchAfterAdd := func(shard ShardLike) func(t *testing.T) {
+		return func(t *testing.T) {
+			t.Run("to be found", func(t *testing.T) {
+				for name, filter := range map[string]*filters.LocalFilter{
+					"id": filterId,
+
+					"textsEqAAA":  filterTextsEqAAA,
+					"textsLen3":   filterTextsLen3,
+					"textsNotNil": filterTextsNotNil,
+
+					"numbersLen0": filterNumbersLen0,
+					"numbersNil":  filterNumbersNil,
+
+					"intsEq102":  filterIntsEq102,
+					"intsLen9":   filterIntsLen9,
+					"intsNotNil": filterIntsNotNil,
+
+					"boolsEqTrue":  filterBoolsEqTrue,
+					"boolsEqFalse": filterBoolsEqFalse,
+					"boolsLen4":    filterBoolsLen4,
+					"boolsNotNil":  filterBoolsNotNil,
+
+					"datesEq2001": filterDatesEq2001,
+					"datesLen2":   filterDatesLen2,
+					"datesNotNil": filterDatesNotNil,
+
+					"uuidsLen0": filterUuidsLen0,
+					"uuidsNil":  filterUuidsNil,
+
+					"textEqDDD":  filterTextEqDDD,
+					"textLen3":   filterTextLen3,
+					"textNotNil": filterTextNotNil,
+
+					"numberNil": filterNumberNil,
+
+					"intEq201":  filterIntEq201,
+					"intNotNil": filterIntNotNil,
+
+					"boolEqFalse": filterBoolEqFalse,
+					"boolNotNil":  filterBoolNotNil,
+
+					"dateEq2003": filterDateEq2003,
+					"dateNotNil": filterDateNotNil,
+
+					"uuidNil": filterUuidNil,
+				} {
+					t.Run(name, func(t *testing.T) {
+						found := search(t, shard, filter)
+						require.Len(t, found, 1)
+						require.Equal(t, uuid_, found[0].Object.ID)
+					})
+				}
+			})
+
+			t.Run("not to be found", func(t *testing.T) {
+				for name, filter := range map[string]*filters.LocalFilter{
+					"textsLen0": filterTextsLen0,
+					"textsNil":  filterTextsNil,
+
+					"numbersEq123":  filterNumbersEq123,
+					"numbersLen1":   filterNumbersLen1,
+					"numbersNotNil": filterNumbersNotNil,
+
+					"intsEq105": filterIntsEq105,
+					"intsLen7":  filterIntsLen7,
+					"intsNil":   filterIntsNil,
+
+					"boolsLen0": filterBoolsLen0,
+					"boolsNil":  filterBoolsNil,
+
+					"datesLen0": filterDatesLen0,
+					"datesNil":  filterDatesNil,
+
+					"uuidsEqD726": filterUuidsEqD726,
+					"uuidsLen1":   filterUuidsLen1,
+					"uuidsNotNil": filterUuidsNotNil,
+
+					"textLen0": filterTextLen0,
+					"textNil":  filterTextNil,
+
+					"numberEq123":  filterNumberEq123,
+					"numberNotNil": filterNumberNotNil,
+
+					"intEq202": filterIntEq202,
+					"intNil":   filterIntNil,
+
+					"boolEqTrue": filterBoolEqTrue,
+					"boolNil":    filterBoolNil,
+
+					"dateNil": filterDateNil,
+
+					"uuidEq7FAB": filterUuidEq7FAB,
+					"uuidNotNil": filterUuidNotNil,
+				} {
+					t.Run(name, func(t *testing.T) {
+						found := search(t, shard, filter)
+						require.Len(t, found, 0)
+					})
+				}
+			})
+		}
+	}
+	verifySearchAfterUpdate := func(shard ShardLike) func(t *testing.T) {
+		return func(t *testing.T) {
+			t.Run("to be found", func(t *testing.T) {
+				for name, filter := range map[string]*filters.LocalFilter{
+					"id": filterId,
+
+					"textsLen0": filterTextsLen0,
+					"textsNil":  filterTextsNil,
+
+					"numbersLen0": filterNumbersLen0,
+					"numbersNil":  filterNumbersNil,
+
+					"intsEq105":  filterIntsEq105,
+					"intsLen7":   filterIntsLen7,
+					"intsNotNil": filterIntsNotNil,
+
+					"boolsEqTrue":  filterBoolsEqTrue,
+					"boolsEqFalse": filterBoolsEqFalse,
+					"boolsLen4":    filterBoolsLen4,
+					"boolsNotNil":  filterBoolsNotNil,
+
+					"datesLen0": filterDatesLen0,
+					"datesNil":  filterDatesNil,
+
+					"uuidsEqD726": filterUuidsEqD726,
+					"uuidsLen1":   filterUuidsLen1,
+					"uuidsNotNil": filterUuidsNotNil,
+
+					"textLen0": filterTextLen0,
+					"textNil":  filterTextNil,
+
+					"numberNil": filterNumberNil,
+
+					"intEq202":  filterIntEq202,
+					"intNotNil": filterIntNotNil,
+
+					"boolEqTrue": filterBoolEqTrue,
+					"boolNotNil": filterBoolNotNil,
+
+					"dateNil": filterDateNil,
+
+					"uuidEq7FAB": filterUuidEq7FAB,
+					"uuidNotNil": filterUuidNotNil,
+				} {
+					t.Run(name, func(t *testing.T) {
+						found := search(t, shard, filter)
+						require.Len(t, found, 1)
+						require.Equal(t, uuid_, found[0].Object.ID)
+					})
+				}
+			})
+
+			t.Run("not to be found", func(t *testing.T) {
+				for name, filter := range map[string]*filters.LocalFilter{
+					"textsEqAAA":  filterTextsEqAAA,
+					"textsLen3":   filterTextsLen3,
+					"textsNotNil": filterTextsNotNil,
+
+					"numbersEq123":  filterNumbersEq123,
+					"numbersLen1":   filterNumbersLen1,
+					"numbersNotNil": filterNumbersNotNil,
+
+					"intsEq102": filterIntsEq102,
+					"intsLen9":  filterIntsLen9,
+					"intsNil":   filterIntsNil,
+
+					"boolsLen0": filterBoolsLen0,
+					"boolsNil":  filterBoolsNil,
+
+					"datesEq2001": filterDatesEq2001,
+					"datesLen2":   filterDatesLen2,
+					"datesNotNil": filterDatesNotNil,
+
+					"uuidsLen0": filterUuidsLen0,
+					"uuidsNil":  filterUuidsNil,
+
+					"textEqDDD":  filterTextEqDDD,
+					"textLen3":   filterTextLen3,
+					"textNotNil": filterTextNotNil,
+
+					"numberEq123":  filterNumberEq123,
+					"numberNotNil": filterNumberNotNil,
+
+					"intEq201": filterIntEq201,
+					"intNil":   filterIntNil,
+
+					"boolEqFalse": filterBoolEqFalse,
+					"boolNil":     filterBoolNil,
+
+					"dateEq2003": filterDateEq2003,
+					"dateNotNil": filterDateNotNil,
+
+					"uuidNil": filterUuidNil,
+				} {
+					t.Run(name, func(t *testing.T) {
+						found := search(t, shard, filter)
+						require.Len(t, found, 0)
+					})
+				}
+			})
+		}
+	}
+	verifyVectorSearch := func(shard ShardLike, vectorToBeFound, vectorNotToBeFound []float32) func(t *testing.T) {
+		vectorSearchLimit := -1 // negative to limit results by distance
+		vectorSearchDist := float32(1)
+
+		return func(t *testing.T) {
+			t.Run("to be found", func(t *testing.T) {
+				found, _, err := shard.ObjectVectorSearch(ctx, vectorToBeFound,
+					vectorSearchDist, vectorSearchLimit, nil, nil, nil, additional.Properties{})
+				require.NoError(t, err)
+				require.Len(t, found, 1)
+				require.Equal(t, uuid_, found[0].Object.ID)
+			})
+
+			t.Run("not to be found", func(t *testing.T) {
+				found, _, err := shard.ObjectVectorSearch(ctx, vectorNotToBeFound,
+					vectorSearchDist, vectorSearchLimit, nil, nil, nil, additional.Properties{})
+				require.NoError(t, err)
+				require.Len(t, found, 0)
+			})
+		}
+	}
+
+	createShard := func(t *testing.T) ShardLike {
+		vectorIndexConfig := hnsw.UserConfig{Distance: common.DefaultDistanceMetric}
+		shard, _ := testShardWithSettings(t, ctx, class, vectorIndexConfig, true, true)
+		return shard
+	}
+
+	t.Run("single object", func(t *testing.T) {
+		t.Run("sanity check - search after add", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("verify initial docID and timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(1)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, origUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after add", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("replace with different object, same vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				updObj := createUpdObj()
+
+				err := shard.PutObject(ctx, updObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify same docID, changed create & update timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(1)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("replace with different object, different vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				// overwrite vector in updated object
+				altUpdObj := createUpdObj()
+				altUpdObj.Vector = altVector
+
+				err := shard.PutObject(ctx, altUpdObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, altVector, vector))
+		})
+
+		t.Run("replace with different object, different geo", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				// overwrite geo in updated object
+				altUpdObj := createUpdObj()
+				altUpdObj.Object.Properties.(map[string]interface{})["geo"] = &models.GeoCoordinates{
+					Latitude:  ptFloat32(3.3),
+					Longitude: ptFloat32(4.4),
+				}
+
+				err := shard.PutObject(ctx, altUpdObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("merge with different object, same vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				mergeDoc := createMergeDoc()
+
+				err := shard.MergeObject(ctx, mergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify same docID, changed update timestamp", func(t *testing.T) {
+				expectedNextDocID := uint64(1)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("merge with different object, different vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				// overwrite vector in merge doc
+				altMergeDoc := createMergeDoc()
+				altMergeDoc.Vector = altVector
+
+				err := shard.MergeObject(ctx, altMergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed update timestamp", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, altVector, vector))
+		})
+
+		t.Run("merge with different object, different geo", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				// overwrite geo in merge doc
+				mergeDoc := createMergeDoc()
+				mergeDoc.PrimitiveSchema["geo"] = &models.GeoCoordinates{
+					Latitude:  ptFloat32(3.3),
+					Longitude: ptFloat32(4.4),
+				}
+
+				err := shard.MergeObject(ctx, mergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed update timestamp", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge", verifySearchAfterUpdate(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("replace with same object, same vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				// overwrite timestamps in original object
+				updObj := createOrigObj()
+				updObj.Object.CreationTimeUnix = updCreateTimeUnix
+				updObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+
+				err := shard.PutObject(ctx, updObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify same docID, same timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(1)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, origUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("replace with same object, different vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				// overwrite timestamps and vector in original object
+				altUpdObj := createOrigObj()
+				altUpdObj.Object.CreationTimeUnix = updCreateTimeUnix
+				altUpdObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+				altUpdObj.Vector = altVector
+
+				err := shard.PutObject(ctx, altUpdObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, altVector, vector))
+		})
+
+		t.Run("replace with same object, different geo", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("put object", func(t *testing.T) {
+				// overwrite timestamps and geo in original object
+				updObj := createOrigObj()
+				updObj.Object.CreationTimeUnix = updCreateTimeUnix
+				updObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+				updObj.Object.Properties.(map[string]interface{})["geo"] = &models.GeoCoordinates{
+					Latitude:  ptFloat32(3.3),
+					Longitude: ptFloat32(4.4),
+				}
+
+				err := shard.PutObject(ctx, updObj)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after put same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after put", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("merge with same object, same vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				// same values as in original object
+				mergeDoc := objects.MergeDocument{
+					ID:    uuid_,
+					Class: class.Class,
+					PrimitiveSchema: map[string]interface{}{
+						"int":  float64(201),
+						"text": "ddd",
+					},
+					UpdateTime: updUpdateTimeUnix,
+					Vector:     vector,
+				}
+
+				err := shard.MergeObject(ctx, mergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify same docID, same timestamps", func(t *testing.T) {
+				expectedNextDocID := uint64(1)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, origUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, vector, altVector))
+		})
+
+		t.Run("merge with same object, different vector", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				// same props as in original object, overwrite timestamp and vector
+				altMergeDoc := objects.MergeDocument{
+					ID:    uuid_,
+					Class: class.Class,
+					PrimitiveSchema: map[string]interface{}{
+						"int":  float64(201),
+						"text": "ddd",
+					},
+					UpdateTime: updUpdateTimeUnix,
+					Vector:     altVector,
+				}
+
+				err := shard.MergeObject(ctx, altMergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed update timestamp", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, altVector, vector))
+		})
+
+		t.Run("merge with same object, different geo", func(t *testing.T) {
+			shard := createShard(t)
+
+			t.Run("add object", func(t *testing.T) {
+				err := shard.PutObject(ctx, createOrigObj())
+				require.NoError(t, err)
+			})
+
+			t.Run("merge object", func(t *testing.T) {
+				// overwrite geo and timestamp
+				mergeDoc := objects.MergeDocument{
+					ID:    uuid_,
+					Class: class.Class,
+					PrimitiveSchema: map[string]interface{}{
+						"geo": &models.GeoCoordinates{
+							Latitude:  ptFloat32(3.3),
+							Longitude: ptFloat32(4.4),
+						},
+					},
+					UpdateTime: updUpdateTimeUnix,
+				}
+
+				err := shard.MergeObject(ctx, mergeDoc)
+				require.NoError(t, err)
+			})
+
+			t.Run("verify changed docID, changed update timestamp", func(t *testing.T) {
+				expectedNextDocID := uint64(2)
+				require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+				found := search(t, shard, filterId)
+				require.Len(t, found, 1)
+				require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+				require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+			})
+
+			t.Run("verify search after merge same as add", verifySearchAfterAdd(shard))
+			t.Run("verify vector search after merge", verifyVectorSearch(shard, vector, altVector))
+		})
+	})
+
+	t.Run("batch", func(t *testing.T) {
+		runBatch := func(t *testing.T) {
+			t.Run("sanity check - search after add", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify initial docID and timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(1)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, origUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after batch", verifySearchAfterAdd(shard))
+				t.Run("verify vector search after batch", verifyVectorSearch(shard, vector, altVector))
+			})
+
+			t.Run("replace with different object, same vector", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					updObj := createUpdObj()
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{updObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify same docID, changed create & update timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(1)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch", verifySearchAfterUpdate(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, vector, altVector))
+			})
+
+			t.Run("replace with different object, different vector", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					// overwrite vector in updated object
+					altUpdObj := createUpdObj()
+					altUpdObj.Vector = altVector
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{altUpdObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(2)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch", verifySearchAfterUpdate(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, altVector, vector))
+			})
+
+			t.Run("replace with different object, different geo", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					// overwrite geo in updated object
+					altUpdObj := createUpdObj()
+					altUpdObj.Object.Properties.(map[string]interface{})["geo"] = &models.GeoCoordinates{
+						Latitude:  ptFloat32(3.3),
+						Longitude: ptFloat32(4.4),
+					}
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{altUpdObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(2)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch", verifySearchAfterUpdate(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, vector, altVector))
+			})
+
+			t.Run("replace with same object, same vector", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					// overwrite timestamps in original object
+					updObj := createOrigObj()
+					updObj.Object.CreationTimeUnix = updCreateTimeUnix
+					updObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{updObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify same docID, same timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(1)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, origCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, origUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch same as 1st", verifySearchAfterAdd(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, vector, altVector))
+			})
+
+			t.Run("replace with same object, different vector", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					// overwrite timestamps and vector in original object
+					altUpdObj := createOrigObj()
+					altUpdObj.Object.CreationTimeUnix = updCreateTimeUnix
+					altUpdObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+					altUpdObj.Vector = altVector
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{altUpdObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(2)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch same as 1st", verifySearchAfterAdd(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, altVector, vector))
+			})
+
+			t.Run("replace with same object, different geo", func(t *testing.T) {
+				shard := createShard(t)
+
+				t.Run("add batch", func(t *testing.T) {
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{createOrigObj()})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("add 2nd batch", func(t *testing.T) {
+					// overwrite geo and timestamp
+					updObj := createOrigObj()
+					updObj.Object.CreationTimeUnix = updCreateTimeUnix
+					updObj.Object.LastUpdateTimeUnix = updUpdateTimeUnix
+					updObj.Object.Properties.(map[string]interface{})["geo"] = &models.GeoCoordinates{
+						Latitude:  ptFloat32(3.3),
+						Longitude: ptFloat32(4.4),
+					}
+
+					errs := shard.PutObjectBatch(ctx, []*storobj.Object{updObj})
+					for i := range errs {
+						require.NoError(t, errs[i])
+					}
+				})
+
+				t.Run("verify changed docID, changed create & update timestamps", func(t *testing.T) {
+					expectedNextDocID := uint64(2)
+					require.Equal(t, expectedNextDocID, shard.Counter().Get())
+
+					found := search(t, shard, filterId)
+					require.Len(t, found, 1)
+					require.Equal(t, updCreateTimeUnix, found[0].CreationTimeUnix())
+					require.Equal(t, updUpdateTimeUnix, found[0].LastUpdateTimeUnix())
+				})
+
+				t.Run("verify search after 2nd batch same as 1st", verifySearchAfterAdd(shard))
+				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, vector, altVector))
+			})
+		}
+
+		t.Run("sync", func(t *testing.T) {
+			currentIndexing := os.Getenv("ASYNC_INDEXING")
+			t.Setenv("ASYNC_INDEXING", "")
+			defer t.Setenv("ASYNC_INDEXING", currentIndexing)
+
+			runBatch(t)
+		})
+
+		t.Run("async", func(t *testing.T) {
+			currentIndexing := os.Getenv("ASYNC_INDEXING")
+			t.Setenv("ASYNC_INDEXING", "true")
+			defer t.Setenv("ASYNC_INDEXING", currentIndexing)
+
+			runBatch(t)
+		})
+	})
+}
+
+func filterEqual[T any](value T, dataType schema.DataType, className, propName string) *filters.LocalFilter {
+	return &filters.LocalFilter{
+		Root: &filters.Clause{
+			Operator: filters.OperatorEqual,
+			Value: &filters.Value{
+				Value: value,
+				Type:  dataType,
+			},
+			On: &filters.Path{
+				Class:    schema.ClassName(className),
+				Property: schema.PropertyName(propName),
+			},
+		},
+	}
+}
+
+func filterNil(value bool, className, propName string) *filters.LocalFilter {
+	return &filters.LocalFilter{
+		Root: &filters.Clause{
+			Operator: filters.OperatorIsNull,
+			Value: &filters.Value{
+				Value: value,
+				Type:  schema.DataTypeBoolean,
+			},
+			On: &filters.Path{
+				Class:    schema.ClassName(className),
+				Property: schema.PropertyName(propName),
+			},
+		},
+	}
+}
diff --git a/adapters/repos/db/shard_skip_vector_reindex_test.go b/adapters/repos/db/shard_skip_vector_reindex_test.go
index a16d720369..1fb2fe31e8 100644
--- a/adapters/repos/db/shard_skip_vector_reindex_test.go
+++ b/adapters/repos/db/shard_skip_vector_reindex_test.go
@@ -9,830 +9,698 @@
 //  CONTACT: hello@weaviate.io
 //
 
-//go:build integrationTest
-// +build integrationTest
-
 package db
 
 import (
-	"context"
-	"encoding/json"
+	"encoding/binary"
 	"fmt"
-	"os"
 	"testing"
 	"time"
 
-	"github.com/go-openapi/strfmt"
 	"github.com/google/uuid"
-	"github.com/stretchr/testify/require"
-	"github.com/weaviate/weaviate/entities/additional"
-	"github.com/weaviate/weaviate/entities/filters"
+	"github.com/stretchr/testify/assert"
 	"github.com/weaviate/weaviate/entities/models"
-	"github.com/weaviate/weaviate/entities/schema"
-	"github.com/weaviate/weaviate/entities/storobj"
-	"github.com/weaviate/weaviate/entities/vectorindex/common"
-	"github.com/weaviate/weaviate/entities/vectorindex/hnsw"
-	"github.com/weaviate/weaviate/usecases/objects"
 )
 
-func TestShard_SkipVectorReindex(t *testing.T) {
-	ctx := context.Background()
-	searchLimit := 10
-	vectorSearchLimit := -1 // negative to limit results by distance
-	vectorSearchDist := float32(1)
+func TestGeoPropsEqual(t *testing.T) {
+	type testCase struct {
+		prevProps     map[string]interface{}
+		nextProps     map[string]interface{}
+		expectedEqual bool
+	}
+
+	ptrFloat32 := func(f float32) *float32 {
+		return &f
+	}
 
-	class := &models.Class{
-		Class: "TestClass",
-		InvertedIndexConfig: &models.InvertedIndexConfig{
-			IndexTimestamps:     true,
-			IndexNullState:      true,
-			IndexPropertyLength: true,
+	testCases := []testCase{
+		{
+			prevProps:     map[string]interface{}{},
+			nextProps:     map[string]interface{}{},
+			expectedEqual: true,
 		},
-		Properties: []*models.Property{
-			{
-				Name:         "texts",
-				DataType:     schema.DataTypeTextArray.PropString(),
-				Tokenization: models.PropertyTokenizationWord,
+		{
+			prevProps: map[string]interface{}{
+				"notGeo": "abc",
 			},
-			{
-				Name:     "numbers",
-				DataType: schema.DataTypeNumberArray.PropString(),
+			nextProps: map[string]interface{}{
+				"notGeo": "def",
 			},
-			{
-				Name:     "ints",
-				DataType: schema.DataTypeIntArray.PropString(),
+			expectedEqual: true,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": nil,
 			},
-			{
-				Name:     "booleans",
-				DataType: schema.DataTypeBooleanArray.PropString(),
+			nextProps: map[string]interface{}{
+				"geo": nil,
 			},
-			{
-				Name:     "dates",
-				DataType: schema.DataTypeDateArray.PropString(),
+			expectedEqual: true,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
-			{
-				Name:     "uuids",
-				DataType: schema.DataTypeUUIDArray.PropString(),
+			nextProps: map[string]interface{}{
+				"geo": nil,
 			},
-			{
-				Name:         "text",
-				DataType:     schema.DataTypeText.PropString(),
-				Tokenization: models.PropertyTokenizationWord,
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
-			{
-				Name:     "number",
-				DataType: schema.DataTypeNumber.PropString(),
+			nextProps:     map[string]interface{}{},
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": nil,
 			},
-			{
-				Name:     "int",
-				DataType: schema.DataTypeInt.PropString(),
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
-			{
-				Name:     "boolean",
-				DataType: schema.DataTypeBoolean.PropString(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
-			{
-				Name:     "date",
-				DataType: schema.DataTypeDate.PropString(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
-			{
-				Name:     "uuid",
-				DataType: schema.DataTypeUUID.PropString(),
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(-1.23),
+					Longitude: ptrFloat32(2.34),
+				},
 			},
+			expectedEqual: false,
 		},
-	}
-	vectorIndexConfig := hnsw.UserConfig{Distance: common.DefaultDistanceMetric}
-
-	uuid := strfmt.UUID(uuid.NewString())
-	vector := []float32{1, 2, 3}
-	altVector := []float32{10, 0, -20}
-
-	origObj := &storobj.Object{
-		MarshallerVersion: 1,
-		Object: models.Object{
-			ID:    uuid,
-			Class: class.Class,
-			Properties: map[string]interface{}{
-				"texts": []interface{}{
-					"aaa",
-					"bbb",
-					"ccc",
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
 				},
-				"numbers": []interface{}{},
-				"ints": []interface{}{
-					asJsonNumber(101), asJsonNumber(101), asJsonNumber(101), asJsonNumber(101), asJsonNumber(101), asJsonNumber(101),
-					asJsonNumber(102),
-					asJsonNumber(103),
-					asJsonNumber(104),
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(-2.34),
 				},
-				"booleans": []interface{}{
-					true, true, true,
-					false,
+			},
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
 				},
-				"dates": []interface{}{
-					"2001-06-01T12:00:00.000000Z",
-					"2002-06-02T12:00:00.000000Z",
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(-1.23),
+					Longitude: ptrFloat32(-2.34),
 				},
-				// no uuids
-				"text": "ddd",
-				// no number
-				"int":     int64(201),
-				"boolean": false,
-				"date":    asTime("2003-06-01T12:00:00.000000Z"),
-				// no uuid
 			},
+			expectedEqual: false,
 		},
-		Vector: vector,
-	}
-
-	updObj := &storobj.Object{
-		MarshallerVersion: 1,
-		Object: models.Object{
-			ID:    uuid,
-			Class: class.Class,
-			Properties: map[string]interface{}{
-				"texts": []interface{}{},
-				// no numbers
-				"ints": []interface{}{
-					asJsonNumber(101), asJsonNumber(101), asJsonNumber(101), asJsonNumber(101),
-					asJsonNumber(103),
-					asJsonNumber(104),
-					asJsonNumber(105),
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
 				},
-				"booleans": []interface{}{
-					true, true, true,
-					false,
+			},
+			expectedEqual: true,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
 				},
-				// no dates
-				"uuids": []interface{}{
-					asUuid("d726c960-aede-411c-85d3-2c77e9290a6e"),
+			},
+			nextProps: map[string]interface{}{
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
 				},
-				"text": "",
-				// no number
-				"int":     int64(202),
-				"boolean": true,
-				// no date
-				"uuid": asUuid("7fabaf01-9e10-458a-acea-cc627376c506"),
 			},
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"notGeo": "string",
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"notGeo": "otherString",
+			},
+			expectedEqual: true,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(4.56),
+					Longitude: ptrFloat32(5.67),
+				},
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(4.56),
+					Longitude: ptrFloat32(5.67),
+				},
+			},
+			expectedEqual: true,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(4.56),
+					Longitude: ptrFloat32(5.67),
+				},
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+			},
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(4.56),
+					Longitude: ptrFloat32(5.67),
+				},
+			},
+			nextProps: map[string]interface{}{
+				"geo": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(1.23),
+					Longitude: ptrFloat32(2.34),
+				},
+				"geo2": &models.GeoCoordinates{
+					Latitude:  ptrFloat32(4.56),
+					Longitude: ptrFloat32(-5.67),
+				},
+			},
+			expectedEqual: false,
+		},
+		{
+			prevProps: map[string]interface{}{
+				"geoLike": map[string]interface{}{
+					"Latitude":  ptrFloat32(1.23),
+					"Longitude": ptrFloat32(2.34),
+				},
+			},
+			nextProps: map[string]interface{}{
+				"geoLike": map[string]interface{}{
+					"Latitude":  ptrFloat32(1.23),
+					"Longitude": ptrFloat32(2.34),
+				},
+			},
+			expectedEqual: true,
 		},
-		Vector: vector,
-	}
-
-	mergeDoc := objects.MergeDocument{
-		ID:    uuid,
-		Class: class.Class,
-		PrimitiveSchema: map[string]interface{}{
-			"texts": []interface{}{},
-			"ints": []interface{}{
-				asJsonNumber(101), asJsonNumber(101), asJsonNumber(101), asJsonNumber(101),
-				asJsonNumber(103),
-				asJsonNumber(104),
-				asJsonNumber(105),
-			},
-			"uuids": []interface{}{
-				asUuid("d726c960-aede-411c-85d3-2c77e9290a6e"),
-			},
-			"text":    "",
-			"int":     int64(202),
-			"boolean": true,
-			"uuid":    asUuid("7fabaf01-9e10-458a-acea-cc627376c506"),
-		},
-		PropertiesToDelete: []string{"numbers", "dates", "date"},
-		Vector:             vector,
 	}
 
-	filterTextsEqAAA := filterEqual[string]("aaa", schema.DataTypeText, class.Class, "texts")
-	filterTextsLen3 := filterEqual[int](3, schema.DataTypeInt, class.Class, "len(texts)")
-	filterTextsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(texts)")
-	filterTextsNotNil := filterNil(false, class.Class, "texts")
-	filterTextsNil := filterNil(true, class.Class, "texts")
-
-	filterNumbersEq123 := filterEqual[float64](1.23, schema.DataTypeNumber, class.Class, "numbers")
-	filterNumbersLen1 := filterEqual[int](1, schema.DataTypeInt, class.Class, "len(numbers)")
-	filterNumbersLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(numbers)")
-	filterNumbersNotNil := filterNil(false, class.Class, "numbers")
-	filterNumbersNil := filterNil(true, class.Class, "numbers")
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
+			eq := geoPropsEqual(tc.prevProps, tc.nextProps)
 
-	filterIntsEq102 := filterEqual[int](102, schema.DataTypeInt, class.Class, "ints")
-	filterIntsEq105 := filterEqual[int](105, schema.DataTypeInt, class.Class, "ints")
-	filterIntsLen9 := filterEqual[int](9, schema.DataTypeInt, class.Class, "len(ints)")
-	filterIntsLen7 := filterEqual[int](7, schema.DataTypeInt, class.Class, "len(ints)")
-	filterIntsNotNil := filterNil(false, class.Class, "ints")
-	filterIntsNil := filterNil(true, class.Class, "ints")
-
-	filterBoolsEqTrue := filterEqual[bool](true, schema.DataTypeBoolean, class.Class, "booleans")
-	filterBoolsEqFalse := filterEqual[bool](false, schema.DataTypeBoolean, class.Class, "booleans")
-	filterBoolsLen4 := filterEqual[int](4, schema.DataTypeInt, class.Class, "len(booleans)")
-	filterBoolsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(booleans)")
-	filterBoolsNotNil := filterNil(false, class.Class, "booleans")
-	filterBoolsNil := filterNil(true, class.Class, "booleans")
-
-	filterDatesEq2001 := filterEqual[string]("2001-06-01T12:00:00.000000Z", schema.DataTypeDate, class.Class, "dates")
-	filterDatesLen2 := filterEqual[int](2, schema.DataTypeInt, class.Class, "len(dates)")
-	filterDatesLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(dates)")
-	filterDatesNotNil := filterNil(false, class.Class, "dates")
-	filterDatesNil := filterNil(true, class.Class, "dates")
-
-	filterUuidsEqD726 := filterEqual[string]("d726c960-aede-411c-85d3-2c77e9290a6e", schema.DataTypeText, class.Class, "uuids")
-	filterUuidsLen1 := filterEqual[int](1, schema.DataTypeInt, class.Class, "len(uuids)")
-	filterUuidsLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(uuids)")
-	filterUuidsNotNil := filterNil(false, class.Class, "uuids")
-	filterUuidsNil := filterNil(true, class.Class, "uuids")
-
-	filterTextEqDDD := filterEqual[string]("ddd", schema.DataTypeText, class.Class, "text")
-	filterTextLen3 := filterEqual[int](3, schema.DataTypeInt, class.Class, "len(text)")
-	filterTextLen0 := filterEqual[int](0, schema.DataTypeInt, class.Class, "len(text)")
-	filterTextNotNil := filterNil(false, class.Class, "text")
-	filterTextNil := filterNil(true, class.Class, "text")
-
-	filterNumberEq123 := filterEqual[float64](1.23, schema.DataTypeNumber, class.Class, "number")
-	filterNumberNotNil := filterNil(false, class.Class, "number")
-	filterNumberNil := filterNil(true, class.Class, "number")
-
-	filterIntEq201 := filterEqual[int](201, schema.DataTypeInt, class.Class, "int")
-	filterIntEq202 := filterEqual[int](202, schema.DataTypeInt, class.Class, "int")
-	filterIntNotNil := filterNil(false, class.Class, "int")
-	filterIntNil := filterNil(true, class.Class, "int")
-
-	filterBoolEqFalse := filterEqual[bool](false, schema.DataTypeBoolean, class.Class, "boolean")
-	filterBoolEqTrue := filterEqual[bool](true, schema.DataTypeBoolean, class.Class, "boolean")
-	filterBoolNotNil := filterNil(false, class.Class, "boolean")
-	filterBoolNil := filterNil(true, class.Class, "boolean")
-
-	filterDateEq2003 := filterEqual[string]("2003-06-01T12:00:00.000000Z", schema.DataTypeDate, class.Class, "date")
-	filterDateNotNil := filterNil(false, class.Class, "date")
-	filterDateNil := filterNil(true, class.Class, "date")
-
-	filterUuidEq7FAB := filterEqual[string]("7fabaf01-9e10-458a-acea-cc627376c506", schema.DataTypeText, class.Class, "uuid")
-	filterUuidNotNil := filterNil(false, class.Class, "uuid")
-	filterUuidNil := filterNil(true, class.Class, "uuid")
-
-	verifySearchAfterAdd := func(shard ShardLike) func(t *testing.T) {
-		return func(t *testing.T) {
-			t.Run("to be found", func(t *testing.T) {
-				for name, filter := range map[string]*filters.LocalFilter{
-					"textsEqAAA":  filterTextsEqAAA,
-					"textsLen3":   filterTextsLen3,
-					"textsNotNil": filterTextsNotNil,
-
-					"numbersLen0": filterNumbersLen0,
-					"numbersNil":  filterNumbersNil,
-
-					"intsEq102":  filterIntsEq102,
-					"intsLen9":   filterIntsLen9,
-					"intsNotNil": filterIntsNotNil,
-
-					"boolsEqTrue":  filterBoolsEqTrue,
-					"boolsEqFalse": filterBoolsEqFalse,
-					"boolsLen4":    filterBoolsLen4,
-					"boolsNotNil":  filterBoolsNotNil,
-
-					"datesEq2001": filterDatesEq2001,
-					"datesLen2":   filterDatesLen2,
-					"datesNotNil": filterDatesNotNil,
-
-					"uuidsLen0": filterUuidsLen0,
-					"uuidsNil":  filterUuidsNil,
-
-					"textEqDDD":  filterTextEqDDD,
-					"textLen3":   filterTextLen3,
-					"textNotNil": filterTextNotNil,
-
-					"numberNil": filterNumberNil,
-
-					"intEq201":  filterIntEq201,
-					"intNotNil": filterIntNotNil,
-
-					"boolEqFalse": filterBoolEqFalse,
-					"boolNotNil":  filterBoolNotNil,
-
-					"dateEq2003": filterDateEq2003,
-					"dateNotNil": filterDateNotNil,
-
-					"uuidNil": filterUuidNil,
-				} {
-					t.Run(name, func(t *testing.T) {
-						found, _, err := shard.ObjectSearch(ctx, searchLimit, filter,
-							nil, nil, nil, additional.Properties{})
-						require.NoError(t, err)
-						require.Len(t, found, 1)
-						require.Equal(t, uuid, found[0].Object.ID)
-					})
-				}
-			})
-
-			t.Run("not to be found", func(t *testing.T) {
-				for name, filter := range map[string]*filters.LocalFilter{
-					"textsLen0": filterTextsLen0,
-					"textsNil":  filterTextsNil,
-
-					"numbersEq123":  filterNumbersEq123,
-					"numbersLen1":   filterNumbersLen1,
-					"numbersNotNil": filterNumbersNotNil,
-
-					"intsEq105": filterIntsEq105,
-					"intsLen7":  filterIntsLen7,
-					"intsNil":   filterIntsNil,
-
-					"boolsLen0": filterBoolsLen0,
-					"boolsNil":  filterBoolsNil,
-
-					"datesLen0": filterDatesLen0,
-					"datesNil":  filterDatesNil,
-
-					"uuidsEqD726": filterUuidsEqD726,
-					"uuidsLen1":   filterUuidsLen1,
-					"uuidsNotNil": filterUuidsNotNil,
-
-					"textLen0": filterTextLen0,
-					"textNil":  filterTextNil,
-
-					"numberEq123":  filterNumberEq123,
-					"numberNotNil": filterNumberNotNil,
-
-					"intEq202": filterIntEq202,
-					"intNil":   filterIntNil,
-
-					"boolEqTrue": filterBoolEqTrue,
-					"boolNil":    filterBoolNil,
-
-					"dateNil": filterDateNil,
-
-					"uuidEq7FAB": filterUuidEq7FAB,
-					"uuidNotNil": filterUuidNotNil,
-				} {
-					t.Run(name, func(t *testing.T) {
-						found, _, err := shard.ObjectSearch(ctx, searchLimit, filter,
-							nil, nil, nil, additional.Properties{})
-						require.NoError(t, err)
-						require.Len(t, found, 0)
-					})
-				}
-			})
-		}
+			if tc.expectedEqual {
+				assert.True(t, eq)
+			} else {
+				assert.False(t, eq)
+			}
+		})
 	}
+}
 
-	verifySearchAfterUpdate := func(shard ShardLike) func(t *testing.T) {
-		return func(t *testing.T) {
-			t.Run("to be found", func(t *testing.T) {
-				for name, filter := range map[string]*filters.LocalFilter{
-					"textsLen0": filterTextsLen0,
-					"textsNil":  filterTextsNil,
-
-					"numbersLen0": filterNumbersLen0,
-					"numbersNil":  filterNumbersNil,
-
-					"intsEq105":  filterIntsEq105,
-					"intsLen7":   filterIntsLen7,
-					"intsNotNil": filterIntsNotNil,
-
-					"boolsEqTrue":  filterBoolsEqTrue,
-					"boolsEqFalse": filterBoolsEqFalse,
-					"boolsLen4":    filterBoolsLen4,
-					"boolsNotNil":  filterBoolsNotNil,
-
-					"datesLen0": filterDatesLen0,
-					"datesNil":  filterDatesNil,
-
-					"uuidsEqD726": filterUuidsEqD726,
-					"uuidsLen1":   filterUuidsLen1,
-					"uuidsNotNil": filterUuidsNotNil,
-
-					"textLen0": filterTextLen0,
-					"textNil":  filterTextNil,
-
-					"numberNil": filterNumberNil,
-
-					"intEq202":  filterIntEq202,
-					"intNotNil": filterIntNotNil,
-
-					"boolEqTrue": filterBoolEqTrue,
-					"boolNotNil": filterBoolNotNil,
-
-					"dateNil": filterDateNil,
-
-					"uuidEq7FAB": filterUuidEq7FAB,
-					"uuidNotNil": filterUuidNotNil,
-				} {
-					t.Run(name, func(t *testing.T) {
-						found, _, err := shard.ObjectSearch(ctx, searchLimit, filter,
-							nil, nil, nil, additional.Properties{})
-						require.NoError(t, err)
-						require.Len(t, found, 1)
-						require.Equal(t, uuid, found[0].Object.ID)
-					})
-				}
-			})
-
-			t.Run("not to be found", func(t *testing.T) {
-				for name, filter := range map[string]*filters.LocalFilter{
-					"textsEqAAA":  filterTextsEqAAA,
-					"textsLen3":   filterTextsLen3,
-					"textsNotNil": filterTextsNotNil,
-
-					"numbersEq123":  filterNumbersEq123,
-					"numbersLen1":   filterNumbersLen1,
-					"numbersNotNil": filterNumbersNotNil,
-
-					"intsEq102": filterIntsEq102,
-					"intsLen9":  filterIntsLen9,
-					"intsNil":   filterIntsNil,
-
-					"boolsLen0": filterBoolsLen0,
-					"boolsNil":  filterBoolsNil,
-
-					"datesEq2001": filterDatesEq2001,
-					"datesLen2":   filterDatesLen2,
-					"datesNotNil": filterDatesNotNil,
-
-					"uuidsLen0": filterUuidsLen0,
-					"uuidsNil":  filterUuidsNil,
-
-					"textEqDDD":  filterTextEqDDD,
-					"textLen3":   filterTextLen3,
-					"textNotNil": filterTextNotNil,
-
-					"numberEq123":  filterNumberEq123,
-					"numberNotNil": filterNumberNotNil,
-
-					"intEq201": filterIntEq201,
-					"intNil":   filterIntNil,
-
-					"boolEqFalse": filterBoolEqFalse,
-					"boolNil":     filterBoolNil,
-
-					"dateEq2003": filterDateEq2003,
-					"dateNotNil": filterDateNotNil,
-
-					"uuidNil": filterUuidNil,
-				} {
-					t.Run(name, func(t *testing.T) {
-						found, _, err := shard.ObjectSearch(ctx, searchLimit, filter,
-							nil, nil, nil, additional.Properties{})
-						require.NoError(t, err)
-						require.Len(t, found, 0)
-					})
-				}
-			})
-		}
+func TestPropsEqual(t *testing.T) {
+	type testCase struct {
+		prevProps     map[string]interface{}
+		nextProps     map[string]interface{}
+		expectedEqual bool
 	}
 
-	verifyVectorSearch := func(shard ShardLike, vectorToBeFound, vectorNotToBeFound []float32) func(t *testing.T) {
-		return func(t *testing.T) {
-			t.Run("to be found", func(t *testing.T) {
-				found, _, err := shard.ObjectVectorSearch(ctx, vectorToBeFound,
-					vectorSearchDist, vectorSearchLimit, nil, nil, nil, additional.Properties{})
-				require.NoError(t, err)
-				require.Len(t, found, 1)
-				require.Equal(t, uuid, found[0].Object.ID)
-			})
+	_uuid := func(i int) uuid.UUID {
+		b := [16]byte{}
+		binary.BigEndian.PutUint64(b[:8], 0)
+		binary.BigEndian.PutUint64(b[8:], 1234567890+uint64(i))
+		return uuid.UUID(b)
+	}
+	_uuidAsText := func(i int) string {
+		u, _ := _uuid(i).MarshalText()
+		return string(u)
+	}
+	_date := func(i int) time.Time {
+		return time.Unix(int64(1704063600+i), 0)
+	}
+	_dateAsText := func(i int) string {
+		d, _ := _date(i).MarshalText()
+		return string(d)
+	}
+	_text := func(i int) string {
+		return fmt.Sprintf("text%d", i)
+	}
+	ptrFloat32 := func(f float32) *float32 {
+		return &f
+	}
 
-			t.Run("not to be found", func(t *testing.T) {
-				found, _, err := shard.ObjectVectorSearch(ctx, vectorNotToBeFound,
-					vectorSearchDist, vectorSearchLimit, nil, nil, nil, additional.Properties{})
-				require.NoError(t, err)
-				require.Len(t, found, 0)
-			})
+	createPrevProps := func(i int) map[string]interface{} {
+		f := float64(i)
+		return map[string]interface{}{
+			"int":      f,
+			"number":   f + 0.5,
+			"text":     _text(i),
+			"boolean":  i%2 == 0,
+			"uuid":     _uuidAsText(i),
+			"date":     _dateAsText(i),
+			"ints":     []float64{f + 1, f + 2, f + 3},
+			"numbers":  []float64{f + 1.5, f + 2.5, f + 3.5},
+			"texts":    []string{_text(i + 1), _text(i + 2), _text(i + 3)},
+			"booleans": []bool{i%2 != 0, i%2 == 0},
+			"uuids":    []string{_uuidAsText(i + 1), _uuidAsText(i + 2), _uuidAsText(i + 3)},
+			"dates":    []string{_dateAsText(i + 1), _dateAsText(i + 2), _dateAsText(i + 3)},
+			"phone": &models.PhoneNumber{
+				DefaultCountry: "pl",
+				Input:          fmt.Sprintf("%d", 100_000_000+i),
+			},
+			"geo": &models.GeoCoordinates{
+				Latitude:  ptrFloat32(45.67),
+				Longitude: ptrFloat32(-12.34),
+			},
+			"object": map[string]interface{}{
+				"n_int":     f + 10,
+				"n_number":  f + 10.5,
+				"n_text":    _text(i + 10),
+				"n_boolean": i%2 == 0,
+				"n_uuid":    _uuidAsText(i + 10),
+				"n_date":    _dateAsText(i + 10),
+				"n_object": map[string]interface{}{
+					"nn_int": f + 20,
+				},
+			},
+			"objects": []interface{}{
+				map[string]interface{}{
+					"n_ints":     []float64{f + 11, f + 12, f + 13},
+					"n_numbers":  []float64{f + 11.5, f + 12.5, f + 13.5},
+					"n_texts":    []string{_text(i + 11), _text(i + 12), _text(i + 13)},
+					"n_booleans": []bool{i%2 != 0, i%2 == 0},
+					"n_uuids":    []string{_uuidAsText(i + 11), _uuidAsText(i + 12), _uuidAsText(i + 13)},
+					"n_dates":    []string{_dateAsText(i + 11), _dateAsText(i + 12), _dateAsText(i + 13)},
+					"n_objects": []interface{}{
+						map[string]interface{}{
+							"nn_ints": []float64{f + 21, f + 22, f + 23},
+						},
+					},
+				},
+			},
 		}
 	}
-
-	createShard := func(t *testing.T) ShardLike {
-		shard, _ := testShardWithSettings(t, ctx, class, vectorIndexConfig, true, true)
-		return shard
+	createNextProps := func(i int) map[string]interface{} {
+		props := createPrevProps(i)
+		props["uuid"] = _uuid(i)
+		props["date"] = _date(i)
+		props["uuids"] = []uuid.UUID{_uuid(i + 1), _uuid(i + 2), _uuid(i + 3)}
+		props["dates"] = []time.Time{_date(i + 1), _date(i + 2), _date(i + 3)}
+
+		obj := props["object"].(map[string]interface{})
+		obj["n_uuid"] = _uuid(i + 10)
+		obj["n_date"] = _date(i + 10)
+
+		objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+		objs0["n_uuids"] = []uuid.UUID{_uuid(i + 11), _uuid(i + 12), _uuid(i + 13)}
+		objs0["n_dates"] = []time.Time{_date(i + 11), _date(i + 12), _date(i + 13)}
+
+		return props
 	}
 
-	t.Run("single object", func(t *testing.T) {
-		t.Run("sanity check - search after add", func(t *testing.T) {
-			shard := createShard(t)
-
-			t.Run("add object", func(t *testing.T) {
-				expectedNextDocID := uint64(1)
-
-				err := shard.PutObject(ctx, origObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("verify search after add", verifySearchAfterAdd(shard))
-			t.Run("verify vector search after add", verifyVectorSearch(shard, vector, altVector))
-		})
-
-		t.Run("replaces object, same vector", func(t *testing.T) {
-			shard := createShard(t)
-
-			t.Run("add object", func(t *testing.T) {
-				expectedNextDocID := uint64(1)
-
-				err := shard.PutObject(ctx, origObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("put object", func(t *testing.T) {
-				expectedNextDocID := uint64(1) // has not changed
-
-				err := shard.PutObject(ctx, updObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("verify search after put", verifySearchAfterUpdate(shard))
-			t.Run("verify vector search after put", verifyVectorSearch(shard, vector, altVector))
-		})
-
-		t.Run("replaces object, different vector", func(t *testing.T) {
-			shard := createShard(t)
-
-			t.Run("add object", func(t *testing.T) {
-				expectedNextDocID := uint64(1)
-
-				err := shard.PutObject(ctx, origObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("put object", func(t *testing.T) {
-				altUpdObj := &storobj.Object{
-					MarshallerVersion: updObj.MarshallerVersion,
-					Object:            updObj.Object,
-					Vector:            altVector,
+	prevProps := createPrevProps(1)
+	nextProps := createNextProps(1)
+	testCases := []testCase{
+		{
+			prevProps:     nil,
+			nextProps:     nil,
+			expectedEqual: true,
+		},
+		{
+			prevProps:     prevProps,
+			nextProps:     nil,
+			expectedEqual: false,
+		},
+		{
+			prevProps:     nil,
+			nextProps:     nextProps,
+			expectedEqual: false,
+		},
+		{
+			prevProps:     prevProps,
+			nextProps:     nextProps,
+			expectedEqual: true,
+		},
+		{
+			prevProps:     prevProps,
+			nextProps:     createNextProps(2),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["int"] = float64(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["number"] = float64(1000.5)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["text"] = _text(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["boolean"] = true
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["uuid"] = _uuid(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["date"] = _date(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["ints"] = []float64{1000, 1001}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["numbers"] = []float64{1000.5, 1001.5}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["texts"] = []string{_text(1000), _text(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["booleans"] = []bool{false, true}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["uuids"] = []uuid.UUID{_uuid(1000), _uuid(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["dates"] = []time.Time{_date(1000), _date(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["phone"] = &models.PhoneNumber{
+					DefaultCountry: "pl",
+					Input:          fmt.Sprintf("%d", 123_456_789),
 				}
-				expectedNextDocID := uint64(2) // has changed
-
-				err := shard.PutObject(ctx, altUpdObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("verify search after put", verifySearchAfterUpdate(shard))
-			t.Run("verify vector search after put", verifyVectorSearch(shard, altVector, vector))
-		})
-
-		t.Run("merges object, same vector", func(t *testing.T) {
-			shard := createShard(t)
-
-			t.Run("add object", func(t *testing.T) {
-				expectedNextDocID := uint64(1)
-
-				err := shard.PutObject(ctx, origObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("merge object", func(t *testing.T) {
-				expectedNextDocID := uint64(1) // has not changed
-
-				err := shard.MergeObject(ctx, mergeDoc)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("verify search after merge", verifySearchAfterUpdate(shard))
-			t.Run("verify vector search after merge", verifyVectorSearch(shard, vector, altVector))
-		})
-
-		t.Run("merges object, different vector", func(t *testing.T) {
-			shard := createShard(t)
-
-			t.Run("add object", func(t *testing.T) {
-				expectedNextDocID := uint64(1)
-
-				err := shard.PutObject(ctx, origObj)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("merge object", func(t *testing.T) {
-				altMergeDoc := objects.MergeDocument{
-					ID:                 mergeDoc.ID,
-					Class:              mergeDoc.Class,
-					PrimitiveSchema:    mergeDoc.PrimitiveSchema,
-					PropertiesToDelete: mergeDoc.PropertiesToDelete,
-					Vector:             altVector,
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				props["geo"] = &models.GeoCoordinates{
+					Latitude:  ptrFloat32(45.67),
+					Longitude: ptrFloat32(12.34),
 				}
-				expectedNextDocID := uint64(2) // has changed
-
-				err := shard.MergeObject(ctx, altMergeDoc)
-				require.NoError(t, err)
-				require.Equal(t, expectedNextDocID, shard.Counter().Get())
-			})
-
-			t.Run("verify search after merge", verifySearchAfterUpdate(shard))
-			t.Run("verify vector search after merge", verifyVectorSearch(shard, altVector, vector))
-		})
-	})
-
-	t.Run("batch", func(t *testing.T) {
-		runBatch := func(t *testing.T) {
-			t.Run("sanity check - search after add", func(t *testing.T) {
-				shard := createShard(t)
-
-				t.Run("add batch", func(t *testing.T) {
-					expectedNextDocID := uint64(1)
-
-					errs := shard.PutObjectBatch(ctx, []*storobj.Object{origObj})
-					for i := range errs {
-						require.NoError(t, errs[i])
-					}
-					require.Equal(t, expectedNextDocID, shard.Counter().Get())
-				})
-
-				t.Run("verify search after batch", verifySearchAfterAdd(shard))
-				t.Run("verify vector search after batch", verifyVectorSearch(shard, vector, altVector))
-			})
-
-			t.Run("replaces object, same vector", func(t *testing.T) {
-				shard := createShard(t)
-
-				t.Run("add batch", func(t *testing.T) {
-					expectedNextDocID := uint64(1)
-
-					errs := shard.PutObjectBatch(ctx, []*storobj.Object{origObj})
-					for i := range errs {
-						require.NoError(t, errs[i])
-					}
-					require.Equal(t, expectedNextDocID, shard.Counter().Get())
-				})
-
-				t.Run("add 2nd batch", func(t *testing.T) {
-					expectedNextDocID := uint64(1) // has not changed
-
-					errs := shard.PutObjectBatch(ctx, []*storobj.Object{updObj})
-					for i := range errs {
-						require.NoError(t, errs[i])
-					}
-					require.Equal(t, expectedNextDocID, shard.Counter().Get())
-				})
-
-				t.Run("verify search after 2nd batch", verifySearchAfterUpdate(shard))
-				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, vector, altVector))
-			})
-
-			t.Run("replaces object, different vector", func(t *testing.T) {
-				shard := createShard(t)
-
-				t.Run("add batch", func(t *testing.T) {
-					expectedNextDocID := uint64(1)
-
-					errs := shard.PutObjectBatch(ctx, []*storobj.Object{origObj})
-					for i := range errs {
-						require.NoError(t, errs[i])
-					}
-					require.Equal(t, expectedNextDocID, shard.Counter().Get())
-				})
-
-				t.Run("add 2nd batch", func(t *testing.T) {
-					altUpdObj := &storobj.Object{
-						MarshallerVersion: updObj.MarshallerVersion,
-						Object:            updObj.Object,
-						Vector:            altVector,
-					}
-					expectedNextDocID := uint64(2) // has changed
-
-					errs := shard.PutObjectBatch(ctx, []*storobj.Object{altUpdObj})
-					for i := range errs {
-						require.NoError(t, errs[i])
-					}
-					require.Equal(t, expectedNextDocID, shard.Counter().Get())
-				})
-
-				t.Run("verify search after 2nd batch", verifySearchAfterUpdate(shard))
-				t.Run("verify vector search after 2nd batch", verifyVectorSearch(shard, altVector, vector))
-			})
-		}
-
-		t.Run("sync", func(t *testing.T) {
-			currentIndexing := os.Getenv("ASYNC_INDEXING")
-			t.Setenv("ASYNC_INDEXING", "")
-			defer t.Setenv("ASYNC_INDEXING", currentIndexing)
-
-			runBatch(t)
-		})
-
-		// t.Run("async", func(t *testing.T) {
-		// 	asyncIndexing := os.Getenv("ASYNC_INDEXING")
-		// 	t.Setenv("ASYNC_INDEXING", "true")
-		// 	defer t.Setenv("ASYNC_INDEXING", asyncIndexing)
-
-		// 	runBatch(t)
-		// })
-	})
-}
-
-func filterEqual[T any](value T, dataType schema.DataType, className, propName string) *filters.LocalFilter {
-	return &filters.LocalFilter{
-		Root: &filters.Clause{
-			Operator: filters.OperatorEqual,
-			Value: &filters.Value{
-				Value: value,
-				Type:  dataType,
-			},
-			On: &filters.Path{
-				Class:    schema.ClassName(className),
-				Property: schema.PropertyName(propName),
-			},
+				return props
+			}(),
+			expectedEqual: false,
 		},
-	}
-}
-
-func filterNil(value bool, className, propName string) *filters.LocalFilter {
-	return &filters.LocalFilter{
-		Root: &filters.Clause{
-			Operator: filters.OperatorIsNull,
-			Value: &filters.Value{
-				Value: value,
-				Type:  schema.DataTypeBoolean,
-			},
-			On: &filters.Path{
-				Class:    schema.ClassName(className),
-				Property: schema.PropertyName(propName),
-			},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_int"] = float64(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_number"] = float64(1000.5)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_text"] = _text(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_boolean"] = true
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_uuid"] = _uuid(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				obj["n_date"] = _date(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				obj := props["object"].(map[string]interface{})
+				nobj := obj["n_object"].(map[string]interface{})
+				nobj["nn_int"] = float64(1000)
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_ints"] = []float64{1000, 1001}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_numbers"] = []float64{1000.5, 1001.5}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_texts"] = []string{_text(1000), _text(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_booleans"] = []bool{false, true}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_uuids"] = []uuid.UUID{_uuid(1000), _uuid(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				objs0["n_dates"] = []time.Time{_date(1000), _date(1001)}
+				return props
+			}(),
+			expectedEqual: false,
+		},
+		{
+			prevProps: prevProps,
+			nextProps: func() map[string]interface{} {
+				props := createNextProps(1)
+				objs0 := props["objects"].([]interface{})[0].(map[string]interface{})
+				nobjs0 := objs0["n_objects"].([]interface{})[0].(map[string]interface{})
+				nobjs0["nn_ints"] = []float64{1000, 1001}
+				return props
+			}(),
+			expectedEqual: false,
 		},
 	}
-}
 
-func asJsonNumber(input int) json.Number {
-	return json.Number(fmt.Sprint(input))
-}
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) {
+			eq := propsEqual(tc.prevProps, tc.nextProps)
 
-func asTime(input string) time.Time {
-	asTime, err := time.Parse(time.RFC3339Nano, input)
-	if err != nil {
-		panic(err)
-	}
-	return asTime
-}
-
-func asUuid(input string) uuid.UUID {
-	asUuid, err := uuid.Parse(input)
-	if err != nil {
-		panic(err)
+			if tc.expectedEqual {
+				assert.True(t, eq)
+			} else {
+				assert.False(t, eq)
+			}
+		})
 	}
-	return asUuid
 }
-
-// "textsEqAAA":  filterTextsEqAAA,
-// "textsLen3":   filterTextsLen3,
-// "textsNotNil": filterTextsNotNil,
-// "textsLen0":   filterTextsLen0,
-// "textsNil":    filterTextsNil,
-
-// "numbersLen0":   filterNumbersLen0,
-// "numbersNil":    filterNumbersNil,
-// "numbersEq123":  filterNumbersEq123,
-// "numbersLen1":   filterNumbersLen1,
-// "numbersNotNil": filterNumbersNotNil,
-
-// "intsEq102":  filterIntsEq102,
-// "intsLen9":   filterIntsLen9,
-// "intsNotNil": filterIntsNotNil,
-// "intsEq105":  filterIntsEq105,
-// "intsLen7":   filterIntsLen7,
-// "intsNil":    filterIntsNil,
-
-// "boolsEqTrue":  filterBoolsEqTrue,
-// "boolsEqFalse": filterBoolsEqFalse,
-// "boolsLen4":    filterBoolsLen4,
-// "boolsNotNil":  filterBoolsNotNil,
-// "boolsLen0":    filterBoolsLen0,
-// "boolsNil":     filterBoolsNil,
-
-// "datesEq2001": filterDatesEq2001,
-// "datesLen2":   filterDatesLen2,
-// "datesNotNil": filterDatesNotNil,
-// "datesLen0":   filterDatesLen0,
-// "datesNil":    filterDatesNil,
-
-// "uuidsLen0":   filterUuidsLen0,
-// "uuidsNil":    filterUuidsNil,
-// "uuidsEqD726": filterUuidsEqD726,
-// "uuidsLen1":   filterUuidsLen1,
-// "uuidsNotNil": filterUuidsNotNil,
-
-// "textEqDDD":  filterTextEqDDD,
-// "textLen3":   filterTextLen3,
-// "textNotNil": filterTextNotNil,
-// "textLen0":   filterTextLen0,
-// "textNil":    filterTextNil,
-
-// "numberNil":    filterNumberNil,
-// "numberEq123":  filterNumberEq123,
-// "numberNotNil": filterNumberNotNil,
-
-// "intEq201":  filterIntEq201,
-// "intNotNil": filterIntNotNil,
-// "intEq202":  filterIntEq202,
-// "intNil":    filterIntNil,
-
-// "boolEqFalse": filterBoolEqFalse,
-// "boolNotNil":  filterBoolNotNil,
-// "boolEqTrue":  filterBoolEqTrue,
-// "boolNil":     filterBoolNil,
-
-// "dateEq2003": filterDateEq2003,
-// "dateNotNil": filterDateNotNil,
-// "dateNil":    filterDateNil,
-
-// "uuidNil":    filterUuidNil,
-// "uuidEq7FAB": filterUuidEq7FAB,
-// "uuidNotNil": filterUuidNotNil,
diff --git a/test/acceptance/graphql_resolvers/local_aggregate_matrix_groupby_test.go b/test/acceptance/graphql_resolvers/local_aggregate_matrix_groupby_test.go
index cbda48a687..bb0877b5c3 100644
--- a/test/acceptance/graphql_resolvers/local_aggregate_matrix_groupby_test.go
+++ b/test/acceptance/graphql_resolvers/local_aggregate_matrix_groupby_test.go
@@ -27,86 +27,78 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 
 	t.Run("aggregate ArrayClass with group by texts", func(t *testing.T) {
 		expectedAllResultsAssertions := map[string][]assertFunc{
-			"Atxt": {
-				asserts.groupedBy("Atxt", "texts"),
+			"Alpha": {
+				asserts.groupedBy("Alpha", "texts"),
 				asserts.meta(4),
 				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+				asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
 				asserts.dateArray("dates", 10),
 			},
-			"Btxt": {
-				asserts.groupedBy("Btxt", "texts"),
+			"Bravo": {
+				asserts.groupedBy("Bravo", "texts"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{3, 3, 2, 1}),
 				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
 				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
 				asserts.dateArray("dates", 9),
 			},
-			"Ctxt": {
-				asserts.groupedBy("Ctxt", "texts"),
+			"Charlie": {
+				asserts.groupedBy("Charlie", "texts"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
-			"Dtxt": {
-				asserts.groupedBy("Dtxt", "texts"),
+			"Delta": {
+				asserts.groupedBy("Delta", "texts"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedResultsWithDataAssertions := map[string][]assertFunc{
-			"Atxt": {
-				asserts.groupedBy("Atxt", "texts"),
+			"Alpha": {
+				asserts.groupedBy("Alpha", "texts"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
-			"Btxt": {
-				asserts.groupedBy("Btxt", "texts"),
+			"Bravo": {
+				asserts.groupedBy("Bravo", "texts"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
-			"Ctxt": {
-				asserts.groupedBy("Ctxt", "texts"),
+			"Charlie": {
+				asserts.groupedBy("Charlie", "texts"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
-			"Dtxt": {
-				asserts.groupedBy("Dtxt", "texts"),
+			"Delta": {
+				asserts.groupedBy("Delta", "texts"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -157,40 +149,36 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("101", "ints"),
 				asserts.meta(4),
 				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+				asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
 				asserts.dateArray("dates", 10),
 			},
 			"102": {
 				asserts.groupedBy("102", "ints"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{3, 3, 2, 1}),
 				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
 				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
 				asserts.dateArray("dates", 9),
 			},
 			"103": {
 				asserts.groupedBy("103", "ints"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"104": {
 				asserts.groupedBy("104", "ints"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -199,40 +187,36 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("101", "ints"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"102": {
 				asserts.groupedBy("102", "ints"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"103": {
 				asserts.groupedBy("103", "ints"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"104": {
 				asserts.groupedBy("104", "ints"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -283,40 +267,36 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("1", "numbers"),
 				asserts.meta(4),
 				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+				asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
 				asserts.dateArray("dates", 10),
 			},
 			"2": {
 				asserts.groupedBy("2", "numbers"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{3, 3, 2, 1}),
 				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
 				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
 				asserts.dateArray("dates", 9),
 			},
 			"3": {
 				asserts.groupedBy("3", "numbers"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"4": {
 				asserts.groupedBy("4", "numbers"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -325,40 +305,36 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("1", "numbers"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"2": {
 				asserts.groupedBy("2", "numbers"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"3": {
 				asserts.groupedBy("3", "numbers"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"4": {
 				asserts.groupedBy("4", "numbers"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -404,213 +380,79 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 	})
 
 	t.Run("aggregate ArrayClass with group by dates", func(t *testing.T) {
-		expectedAllResultsAssertions := map[string][]assertFunc{
-			"2001-06-01T12:00:00Z": {
-				asserts.groupedBy("2001-06-01T12:00:00Z", "dates"),
-				asserts.meta(4),
-				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
-				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
-				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
-				asserts.dateArray("dates", 10),
-			},
-			"2002-06-02T12:00:00Z": {
-				asserts.groupedBy("2002-06-02T12:00:00Z", "dates"),
-				asserts.meta(3),
-				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
-				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
-				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
-				asserts.dateArray("dates", 9),
-			},
-			"2003-06-03T12:00:00Z": {
-				asserts.groupedBy("2003-06-03T12:00:00Z", "dates"),
-				asserts.meta(2),
-				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
-				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
-				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
-				asserts.dateArray("dates", 7),
-			},
-			"2004-06-04T12:00:00Z": {
-				asserts.groupedBy("2004-06-04T12:00:00Z", "dates"),
-				asserts.meta(1),
-				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
-				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
-				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
-				asserts.dateArray("dates", 4),
-			},
-		}
-		expectedResultsWithDataAssertions := map[string][]assertFunc{
-			"2001-06-01T12:00:00Z": {
-				asserts.groupedBy("2001-06-01T12:00:00Z", "dates"),
-				asserts.meta(2),
-				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
-				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
-				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
-				asserts.dateArray("dates", 7),
-			},
-			"2002-06-02T12:00:00Z": {
-				asserts.groupedBy("2002-06-02T12:00:00Z", "dates"),
-				asserts.meta(2),
-				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
-				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
-				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
-				asserts.dateArray("dates", 7),
-			},
-			"2003-06-03T12:00:00Z": {
-				asserts.groupedBy("2003-06-03T12:00:00Z", "dates"),
-				asserts.meta(2),
-				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
-				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
-				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
-				asserts.dateArray("dates", 7),
-			},
-			"2004-06-04T12:00:00Z": {
-				asserts.groupedBy("2004-06-04T12:00:00Z", "dates"),
-				asserts.meta(1),
-				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
-				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
-				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
-				asserts.dateArray("dates", 4),
-			},
-		}
-		expectedResultsWithoutDataAssertions := map[string][]assertFunc{}
-		expectedNoResultsAssertions := map[string][]assertFunc{}
-
-		testCases := []aggregateTestCase{
-			testCasesGen.WithoutFilters(expectedAllResultsAssertions),
-
-			testCasesGen.WithWhereFilter_AllResults(expectedAllResultsAssertions),
-			testCasesGen.WithWhereFilter_ResultsWithData(expectedResultsWithDataAssertions),
-			testCasesGen.WithWhereFilter_ResultsWithoutData(expectedResultsWithoutDataAssertions),
-			testCasesGen.WithWhereFilter_NoResults(expectedNoResultsAssertions),
-
-			testCasesGen.WithNearObjectFilter_AllResults(expectedAllResultsAssertions),
-			testCasesGen.WithNearObjectFilter_ResultsWithData(expectedResultsWithDataAssertions),
-			testCasesGen.WithNearObjectFilter_ResultsWithoutData(expectedResultsWithoutDataAssertions),
-
-			testCasesGen.WithWhereAndNearObjectFilters_AllResults(expectedAllResultsAssertions),
-			testCasesGen.WithWhereAndNearObjectFilters_ResultsWithData(expectedResultsWithDataAssertions),
-			testCasesGen.WithWhereAndNearObjectFilters_ResultsWithoutData(expectedResultsWithoutDataAssertions),
-			testCasesGen.WithWhereAndNearObjectFilters_NoResults(expectedNoResultsAssertions),
-		}
-
-		for _, tc := range testCases {
-			query := aggregateArrayClassQuery(tc.filters, "groupBy: [\"dates\"]")
-
-			t.Run(tc.name, func(t *testing.T) {
-				result := graphqlhelper.AssertGraphQL(t, helper.RootAuth, query)
-				extracted := extractArrayClassGroupByResult(result)
-
-				assert.Len(t, extracted, len(tc.groupedAssertions))
-				for groupedBy, groupAssertions := range tc.groupedAssertions {
-					group := findGroup(groupedBy, extracted)
-					require.NotNil(t, group, fmt.Sprintf("Group '%s' not found", groupedBy))
-
-					for _, assertion := range groupAssertions {
-						assertion(group)
-					}
-				}
-			})
-		}
-	})
-
-	t.Run("aggregate ArrayClass with group by dates as strings", func(t *testing.T) {
 		expectedAllResultsAssertions := map[string][]assertFunc{
 			"2021-06-01T22:18:59.640162Z": {
-				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "dates"),
 				asserts.meta(4),
 				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+				asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
 				asserts.dateArray("dates", 10),
 			},
 			"2022-06-02T22:18:59.640162Z": {
-				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "dates"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{3, 3, 2, 1}),
 				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
 				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
 				asserts.dateArray("dates", 9),
 			},
 			"2023-06-03T22:18:59.640162Z": {
-				asserts.groupedBy("2023-06-03T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2023-06-03T22:18:59.640162Z", "dates"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"2024-06-04T22:18:59.640162Z": {
-				asserts.groupedBy("2024-06-04T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2024-06-04T22:18:59.640162Z", "dates"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedResultsWithDataAssertions := map[string][]assertFunc{
 			"2021-06-01T22:18:59.640162Z": {
-				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "dates"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"2022-06-02T22:18:59.640162Z": {
-				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "dates"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"2023-06-03T22:18:59.640162Z": {
-				asserts.groupedBy("2023-06-03T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2023-06-03T22:18:59.640162Z", "dates"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"2024-06-04T22:18:59.640162Z": {
-				asserts.groupedBy("2024-06-04T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2024-06-04T22:18:59.640162Z", "dates"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{1, 1, 1, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{1, 1, 1, 1}),
 				asserts.numberArray("numbers", 4, 4, 1, 1, 10, 2.5, 2.5),
 				asserts.intArray("ints", 4, 104, 101, 101, 410, 102.5, 102.5),
-				asserts.dateArray("datesAsStrings", 4),
 				asserts.dateArray("dates", 4),
 			},
 		}
@@ -636,7 +478,7 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 		}
 
 		for _, tc := range testCases {
-			query := aggregateArrayClassQuery(tc.filters, "groupBy: [\"datesAsStrings\"]")
+			query := aggregateArrayClassQuery(tc.filters, "groupBy: [\"dates\"]")
 
 			t.Run(tc.name, func(t *testing.T) {
 				result := graphqlhelper.AssertGraphQL(t, helper.RootAuth, query)
@@ -661,20 +503,18 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("true", "booleans"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{3, 3, 2, 1}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{3, 3, 2, 1}),
 				asserts.numberArray("numbers", 9, 4, 1, 1, 19, 2, 2.111111111111111),
 				asserts.intArray("ints", 9, 104, 101, 101, 919, 102, 102.11111111111111),
-				asserts.dateArray("datesAsStrings", 9),
 				asserts.dateArray("dates", 9),
 			},
 			"false": {
 				asserts.groupedBy("false", "booleans"),
 				asserts.meta(4),
 				asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-				asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+				asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 				asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 				asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-				asserts.dateArray("datesAsStrings", 10),
 				asserts.dateArray("dates", 10),
 			},
 		}
@@ -683,20 +523,18 @@ func aggregateArrayClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("true", "booleans"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 			"false": {
 				asserts.groupedBy("false", "booleans"),
 				asserts.meta(2),
 				asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-				asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+				asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 				asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 				asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-				asserts.dateArray("datesAsStrings", 7),
 				asserts.dateArray("dates", 7),
 			},
 		}
@@ -748,43 +586,43 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 
 	t.Run("aggregate DuplicatesClass with group by texts", func(t *testing.T) {
 		expectedAllResultsAssertions := map[string][]assertFunc{
-			"Atxt": {
-				asserts.groupedBy("Atxt", "texts"),
+			"Alpha": {
+				asserts.groupedBy("Alpha", "texts"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
-			"Btxt": {
-				asserts.groupedBy("Btxt", "texts"),
+			"Bravo": {
+				asserts.groupedBy("Bravo", "texts"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 		}
 		expectedSomeResultsAssertions := map[string][]assertFunc{
-			"Atxt": {
-				asserts.groupedBy("Atxt", "texts"),
+			"Alpha": {
+				asserts.groupedBy("Alpha", "texts"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
-			"Btxt": {
-				asserts.groupedBy("Btxt", "texts"),
+			"Bravo": {
+				asserts.groupedBy("Bravo", "texts"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedNoResultsAssertsions := map[string][]assertFunc{}
@@ -823,19 +661,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("101", "ints"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 			"102": {
 				asserts.groupedBy("102", "ints"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 		}
 		expectedSomeResultsAssertions := map[string][]assertFunc{
@@ -843,19 +681,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("101", "ints"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 			"102": {
 				asserts.groupedBy("102", "ints"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedNoResultsAssertsions := map[string][]assertFunc{}
@@ -894,19 +732,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("1", "numbers"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 			"2": {
 				asserts.groupedBy("2", "numbers"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 		}
 		expectedSomeResultsAssertions := map[string][]assertFunc{
@@ -914,19 +752,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("1", "numbers"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 			"2": {
 				asserts.groupedBy("2", "numbers"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedNoResultsAssertsions := map[string][]assertFunc{}
@@ -962,42 +800,42 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 	t.Run("aggregate DuplicatesClass with group by dates as string", func(t *testing.T) {
 		expectedAllResultsAssertions := map[string][]assertFunc{
 			"2021-06-01T22:18:59.640162Z": {
-				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "dates"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 			"2022-06-02T22:18:59.640162Z": {
-				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "dates"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 		}
 		expectedSomeResultsAssertions := map[string][]assertFunc{
 			"2021-06-01T22:18:59.640162Z": {
-				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2021-06-01T22:18:59.640162Z", "dates"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 			"2022-06-02T22:18:59.640162Z": {
-				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "datesAsStrings"),
+				asserts.groupedBy("2022-06-02T22:18:59.640162Z", "dates"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedNoResultsAssertsions := map[string][]assertFunc{}
@@ -1011,7 +849,7 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 		}
 
 		for _, tc := range testCases {
-			query := aggregateDuplicatesClassQuery(tc.filters, "groupBy: [\"datesAsStrings\"]")
+			query := aggregateDuplicatesClassQuery(tc.filters, "groupBy: [\"dates\"]")
 
 			t.Run(tc.name, func(t *testing.T) {
 				result := graphqlhelper.AssertGraphQL(t, helper.RootAuth, query)
@@ -1036,19 +874,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("true", "booleans"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 			"false": {
 				asserts.groupedBy("false", "booleans"),
 				asserts.meta(3),
 				asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-				asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+				asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 				asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 				asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-				asserts.dateArray("datesAsStrings", 9),
+				asserts.dateArray("dates", 9),
 			},
 		}
 		expectedSomeResultsAssertions := map[string][]assertFunc{
@@ -1056,19 +894,19 @@ func aggregateDuplicatesClassWithGroupByTest(t *testing.T) {
 				asserts.groupedBy("true", "booleans"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 			"false": {
 				asserts.groupedBy("false", "booleans"),
 				asserts.meta(1),
 				asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-				asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+				asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 				asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 				asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-				asserts.dateArray("datesAsStrings", 4),
+				asserts.dateArray("dates", 4),
 			},
 		}
 		expectedNoResultsAssertsions := map[string][]assertFunc{}
diff --git a/test/acceptance/graphql_resolvers/local_aggregate_matrix_no_groupby_test.go b/test/acceptance/graphql_resolvers/local_aggregate_matrix_no_groupby_test.go
index 29eb502c5b..5355a4b7cf 100644
--- a/test/acceptance/graphql_resolvers/local_aggregate_matrix_no_groupby_test.go
+++ b/test/acceptance/graphql_resolvers/local_aggregate_matrix_no_groupby_test.go
@@ -26,19 +26,17 @@ func aggregateArrayClassWithoutGroupByTest(t *testing.T) {
 		expectedAllResultsAssertions := []assertFunc{
 			asserts.meta(7),
 			asserts.booleanArray("booleans", 10, 4, 6, 0.4, 0.6),
-			asserts.textArray("texts", 10, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{4, 3, 2, 1}),
+			asserts.textArray("texts", 10, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{4, 3, 2, 1}),
 			asserts.numberArray("numbers", 10, 4, 1, 1, 20, 2, 2),
 			asserts.intArray("ints", 10, 104, 101, 101, 1020, 102, 102),
-			asserts.dateArray("datesAsStrings", 10),
 			asserts.dateArray("dates", 10),
 		}
 		expectedResultsWithDataAssertions := []assertFunc{
 			asserts.meta(2),
 			asserts.booleanArray("booleans", 7, 2, 5, 0.2857142857142857, 0.7142857142857143),
-			asserts.textArray("texts", 7, []string{"Atxt", "Btxt", "Ctxt", "Dtxt"}, []int64{2, 2, 2, 1}),
+			asserts.textArray("texts", 7, []string{"Alpha", "Bravo", "Charlie", "Delta"}, []int64{2, 2, 2, 1}),
 			asserts.numberArray("numbers", 7, 4, 1, 1, 16, 2, 2.2857142857142856),
 			asserts.intArray("ints", 7, 104, 101, 101, 716, 102, 102.28571428571429),
-			asserts.dateArray("datesAsStrings", 7),
 			asserts.dateArray("dates", 7),
 		}
 		expectedResultsWithoutDataAssertions := []assertFunc{
@@ -47,7 +45,6 @@ func aggregateArrayClassWithoutGroupByTest(t *testing.T) {
 			asserts.textArray0("texts"),
 			asserts.numberArray0("numbers"),
 			asserts.intArray0("ints"),
-			asserts.dateArray0("datesAsStrings"),
 			asserts.dateArray0("dates"),
 		}
 		expectedNoResultsAssertions := []assertFunc{
@@ -56,7 +53,6 @@ func aggregateArrayClassWithoutGroupByTest(t *testing.T) {
 			asserts.textArray0("texts"),
 			asserts.numberArray0("numbers"),
 			asserts.intArray0("ints"),
-			asserts.dateArray0("datesAsStrings"),
 			asserts.dateArray0("dates"),
 		}
 
@@ -103,18 +99,18 @@ func aggregateDuplicatesClassWithoutGroupByTest(t *testing.T) {
 		expectedAllResultsAssertions := []assertFunc{
 			asserts.meta(3),
 			asserts.booleanArray("booleans", 9, 3, 6, 0.3333333333333333, 0.6666666666666666),
-			asserts.textArray("texts", 9, []string{"Atxt", "Btxt"}, []int64{6, 3}),
+			asserts.textArray("texts", 9, []string{"Alpha", "Bravo"}, []int64{6, 3}),
 			asserts.numberArray("numbers", 9, 2, 1, 1, 12, 1, 1.3333333333333333),
 			asserts.intArray("ints", 9, 102, 101, 101, 912, 101, 101.33333333333333),
-			asserts.dateArray("datesAsStrings", 9),
+			asserts.dateArray("dates", 9),
 		}
 		expectedSomeResultsAssertions := []assertFunc{
 			asserts.meta(1),
 			asserts.booleanArray("booleans", 4, 1, 3, 0.25, 0.75),
-			asserts.textArray("texts", 4, []string{"Atxt", "Btxt"}, []int64{3, 1}),
+			asserts.textArray("texts", 4, []string{"Alpha", "Bravo"}, []int64{3, 1}),
 			asserts.numberArray("numbers", 4, 2, 1, 1, 5, 1, 1.25),
 			asserts.intArray("ints", 4, 102, 101, 101, 405, 101, 101.25),
-			asserts.dateArray("datesAsStrings", 4),
+			asserts.dateArray("dates", 4),
 		}
 		expectedNoResultsAssertions := []assertFunc{
 			asserts.meta(0),
@@ -122,7 +118,7 @@ func aggregateDuplicatesClassWithoutGroupByTest(t *testing.T) {
 			asserts.textArray0("texts"),
 			asserts.numberArray0("numbers"),
 			asserts.intArray0("ints"),
-			asserts.dateArray0("datesAsStrings"),
+			asserts.dateArray0("dates"),
 		}
 
 		testCases := []aggregateTestCase{
diff --git a/test/acceptance/graphql_resolvers/local_aggregate_matrix_setup_test.go b/test/acceptance/graphql_resolvers/local_aggregate_matrix_setup_test.go
index 2aa705416c..0c9159f251 100644
--- a/test/acceptance/graphql_resolvers/local_aggregate_matrix_setup_test.go
+++ b/test/acceptance/graphql_resolvers/local_aggregate_matrix_setup_test.go
@@ -80,10 +80,6 @@ func arrayClassSchema() *models.Class {
 				Name:     "booleans",
 				DataType: []string{"boolean[]"},
 			},
-			{
-				Name:     "datesAsStrings",
-				DataType: []string{"date[]"},
-			},
 			{
 				Name:     "dates",
 				DataType: []string{"date[]"},
@@ -109,22 +105,16 @@ func objectArrayClass4el() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID1_4el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Btxt", "Ctxt", "Dtxt"},
+			"texts":    []string{"Alpha", "Bravo", "Charlie", "Delta"},
 			"numbers":  []float64{1.0, 2.0, 3.0, 4.0},
 			"ints":     []int{101, 102, 103, 104},
 			"booleans": []bool{true, true, true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2022-06-02T22:18:59.640162Z",
 				"2023-06-03T22:18:59.640162Z",
 				"2024-06-04T22:18:59.640162Z",
 			},
-			"dates": []time.Time{
-				time.Date(2001, 6, 1, 12, 0, 0, 0, time.UTC),
-				time.Date(2002, 6, 2, 12, 0, 0, 0, time.UTC),
-				time.Date(2003, 6, 3, 12, 0, 0, 0, time.UTC),
-				time.Date(2004, 6, 4, 12, 0, 0, 0, time.UTC),
-			},
 		},
 	}
 }
@@ -134,20 +124,15 @@ func objectArrayClass3el() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID2_3el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Btxt", "Ctxt"},
+			"texts":    []string{"Alpha", "Bravo", "Charlie"},
 			"numbers":  []float64{1.0, 2.0, 3.0},
 			"ints":     []int{101, 102, 103},
 			"booleans": []bool{true, true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2022-06-02T22:18:59.640162Z",
 				"2023-06-03T22:18:59.640162Z",
 			},
-			"dates": []time.Time{
-				time.Date(2001, 6, 1, 12, 0, 0, 0, time.UTC),
-				time.Date(2002, 6, 2, 12, 0, 0, 0, time.UTC),
-				time.Date(2003, 6, 3, 12, 0, 0, 0, time.UTC),
-			},
 		},
 	}
 }
@@ -157,18 +142,14 @@ func objectArrayClass2el() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID3_2el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Btxt"},
+			"texts":    []string{"Alpha", "Bravo"},
 			"numbers":  []float64{1.0, 2.0},
 			"ints":     []int{101, 102},
 			"booleans": []bool{true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2022-06-02T22:18:59.640162Z",
 			},
-			"dates": []time.Time{
-				time.Date(2001, 6, 1, 12, 0, 0, 0, time.UTC),
-				time.Date(2002, 6, 2, 12, 0, 0, 0, time.UTC),
-			},
 		},
 	}
 }
@@ -178,16 +159,13 @@ func objectArrayClass1el() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID4_1el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt"},
+			"texts":    []string{"Alpha"},
 			"numbers":  []float64{1.0},
 			"ints":     []int{101},
 			"booleans": []bool{false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 			},
-			"dates": []time.Time{
-				time.Date(2001, 6, 1, 12, 0, 0, 0, time.UTC),
-			},
 		},
 	}
 }
@@ -197,12 +175,11 @@ func objectArrayClass0el() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID5_0el,
 		Properties: map[string]interface{}{
-			"texts":          []string{},
-			"numbers":        []float64{},
-			"ints":           []int{},
-			"booleans":       []bool{},
-			"datesAsStrings": []string{},
-			"dates":          []time.Time{},
+			"texts":    []string{},
+			"numbers":  []float64{},
+			"ints":     []int{},
+			"booleans": []bool{},
+			"dates":    []time.Time{},
 		},
 	}
 }
@@ -212,12 +189,11 @@ func objectArrayClassNils() *models.Object {
 		Class: arrayClassName,
 		ID:    objectArrayClassID6_nils,
 		Properties: map[string]interface{}{
-			"texts":          nil,
-			"numbers":        nil,
-			"ints":           nil,
-			"booleans":       nil,
-			"datesAsStrings": nil,
-			"dates":          nil,
+			"texts":    nil,
+			"numbers":  nil,
+			"ints":     nil,
+			"booleans": nil,
+			"dates":    nil,
 		},
 	}
 }
@@ -274,9 +250,6 @@ func aggregateArrayClassQuery(filters, groupBy string) string {
 						mode
 						sum
 					}
-					datesAsStrings{
-						count
-					}
 					dates{
 						count
 					}
@@ -493,7 +466,7 @@ func duplicatesClassSchema() *models.Class {
 				DataType: []string{"boolean[]"},
 			},
 			{
-				Name:     "datesAsStrings",
+				Name:     "dates",
 				DataType: []string{"date[]"},
 			},
 		},
@@ -513,11 +486,11 @@ func objectDuplicatesClass4el() *models.Object {
 		Class: duplicatesClassName,
 		ID:    objectDuplicatesClassID1_4el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Atxt", "Atxt", "Btxt"},
+			"texts":    []string{"Alpha", "Alpha", "Alpha", "Bravo"},
 			"numbers":  []float64{1.0, 1.0, 1.0, 2.0},
 			"ints":     []int{101, 101, 101, 102},
 			"booleans": []bool{true, true, true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2021-06-01T22:18:59.640162Z",
 				"2021-06-01T22:18:59.640162Z",
@@ -532,11 +505,11 @@ func objectDuplicatesClass3el() *models.Object {
 		Class: duplicatesClassName,
 		ID:    objectDuplicatesClassID2_3el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Atxt", "Btxt"},
+			"texts":    []string{"Alpha", "Alpha", "Bravo"},
 			"numbers":  []float64{1.0, 1.0, 2.0},
 			"ints":     []int{101, 101, 102},
 			"booleans": []bool{true, true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2021-06-01T22:18:59.640162Z",
 				"2022-06-02T22:18:59.640162Z",
@@ -550,11 +523,11 @@ func objectDuplicatesClass2el() *models.Object {
 		Class: duplicatesClassName,
 		ID:    objectDuplicatesClassID3_2el,
 		Properties: map[string]interface{}{
-			"texts":    []string{"Atxt", "Btxt"},
+			"texts":    []string{"Alpha", "Bravo"},
 			"numbers":  []float64{1.0, 2.0},
 			"ints":     []int{101, 102},
 			"booleans": []bool{true, false},
-			"datesAsStrings": []string{
+			"dates": []string{
 				"2021-06-01T22:18:59.640162Z",
 				"2022-06-02T22:18:59.640162Z",
 			},
@@ -607,7 +580,7 @@ func aggregateDuplicatesClassQuery(filters, groupBy string) string {
 						mode
 						sum
 					}
-					datesAsStrings{
+					dates{
 						count
 					}
 					%s
@@ -707,7 +680,7 @@ func (tc *aggregateArrayClassTestCases) WithNearObjectFilter_AllResults(groupedA
 		filters: fmt.Sprintf(`
 			nearObject: {
 				id: "%s"
-				certainty: 0.1
+				certainty: 0.7
 			}`, objectArrayClassID1_4el),
 		groupedAssertions: groupedAssertions,
 	}
@@ -719,7 +692,7 @@ func (tc *aggregateArrayClassTestCases) WithNearObjectFilter_ResultsWithData(gro
 		filters: fmt.Sprintf(`
 			nearObject: {
 				id: "%s"
-				certainty: 0.988
+				certainty: 0.97
 			}`, objectArrayClassID1_4el),
 		groupedAssertions: groupedAssertions,
 	}
@@ -748,7 +721,7 @@ func (tc *aggregateArrayClassTestCases) WithWhereAndNearObjectFilters_AllResults
 			}
 			nearObject: {
 				id: "%s"
-				certainty: 0.1
+				certainty: 0.7
 			}`, objectArrayClassID1_4el),
 		groupedAssertions: groupedAssertions,
 	}
@@ -765,7 +738,7 @@ func (tc *aggregateArrayClassTestCases) WithWhereAndNearObjectFilters_ResultsWit
 			}
 			nearObject: {
 				id: "%s"
-				certainty: 0.98
+				certainty: 0.97
 			}`, objectArrayClassID1_4el[:35]+"?", objectArrayClassID1_4el),
 		groupedAssertions: groupedAssertions,
 	}
@@ -799,7 +772,7 @@ func (tc *aggregateArrayClassTestCases) WithWhereAndNearObjectFilters_NoResults(
 			}
 			nearObject: {
 				id: "%s"
-				certainty: 0.1
+				certainty: 0.8
 			}`, notExistingObjectId, objectArrayClassID1_4el),
 		groupedAssertions: groupedAssertions,
 	}
diff --git a/usecases/objects/merge_test.go b/usecases/objects/merge_test.go
index 9a8b9cd296..a9f3f3455d 100644
--- a/usecases/objects/merge_test.go
+++ b/usecases/objects/merge_test.go
@@ -315,7 +315,7 @@ func Test_MergeObject(t *testing.T) {
 				PrimitiveSchema: map[string]interface{}{
 					"name":      "My little pony zoo with extra sparkles",
 					"area":      3.222,
-					"employees": int64(70),
+					"employees": float64(70),
 					"located": &models.GeoCoordinates{
 						Latitude:  ptFloat32(30.2),
 						Longitude: ptFloat32(60.2),
diff --git a/usecases/objects/validation/properties_validation_test.go b/usecases/objects/validation/properties_validation_test.go
index 25e155ad9f..280cdc0fd3 100644
--- a/usecases/objects/validation/properties_validation_test.go
+++ b/usecases/objects/validation/properties_validation_test.go
@@ -13,14 +13,17 @@ package validation
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"reflect"
 	"testing"
+	"time"
 
 	"github.com/go-openapi/strfmt"
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/weaviate/weaviate/entities/additional"
-
 	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/schema"
 	"github.com/weaviate/weaviate/usecases/config"
@@ -217,3 +220,484 @@ func extractBeacon(t *testing.T, props models.PropertySchema) strfmt.URI {
 	gotBeacon := journalProp.(models.MultipleRef)[0].Beacon
 	return gotBeacon
 }
+
+func TestValidator_ValuesCasting(t *testing.T) {
+	t.Run("int(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue float64
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         json.Number("123"),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+			{
+				value:         int64(123),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+			{
+				value:         float64(123),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+
+			{
+				value:         json.Number("123.5"),
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         float64(123.5),
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         "123.5",
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         "something",
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         true,
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("int #%d", i), func(t *testing.T) {
+				value, err := intVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("ints (single) #%d", i), func(t *testing.T) {
+				value, err := intArrayVal(tc.value)
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("ints (array) #%d", i), func(t *testing.T) {
+				value, err := intArrayVal([]interface{}{tc.value})
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []float64{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+
+	t.Run("number(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue float64
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         json.Number("123"),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+			{
+				value:         int64(123),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+			{
+				value:         float64(123),
+				expectedValue: float64(123),
+				expectedErr:   false,
+			},
+			{
+				value:         json.Number("123.5"),
+				expectedValue: float64(123.5),
+				expectedErr:   false,
+			},
+			{
+				value:         float64(123.5),
+				expectedValue: float64(123.5),
+				expectedErr:   false,
+			},
+
+			{
+				value:         "123.5",
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         "something",
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+			{
+				value:         true,
+				expectedValue: float64(0),
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("number #%d", i), func(t *testing.T) {
+				value, err := numberVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("numbers (single) #%d", i), func(t *testing.T) {
+				value, err := numberArrayVal(tc.value)
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("numbers (array) #%d", i), func(t *testing.T) {
+				value, err := numberArrayVal([]interface{}{tc.value})
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []float64{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+
+	t.Run("string(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue string
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         "123.5",
+				expectedValue: "123.5",
+				expectedErr:   false,
+			},
+			{
+				value:         "something",
+				expectedValue: "something",
+				expectedErr:   false,
+			},
+
+			{
+				value:         json.Number("123"),
+				expectedValue: "",
+				expectedErr:   true,
+			},
+			{
+				value:         int64(123),
+				expectedValue: "",
+				expectedErr:   true,
+			},
+			{
+				value:         float64(123),
+				expectedValue: "",
+				expectedErr:   true,
+			},
+			{
+				value:         []byte("something"),
+				expectedValue: "",
+				expectedErr:   true,
+			},
+			{
+				value:         true,
+				expectedValue: "",
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("string #%d", i), func(t *testing.T) {
+				value, err := stringVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("strings (single) #%d", i), func(t *testing.T) {
+				value, err := stringArrayVal(tc.value, "text")
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("strings (array) #%d", i), func(t *testing.T) {
+				value, err := stringArrayVal([]interface{}{tc.value}, "text")
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []string{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+
+	t.Run("bool(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue bool
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         true,
+				expectedValue: true,
+				expectedErr:   false,
+			},
+			{
+				value:         false,
+				expectedValue: false,
+				expectedErr:   false,
+			},
+
+			{
+				value:         float64(1),
+				expectedValue: false,
+				expectedErr:   true,
+			},
+			{
+				value:         int64(1),
+				expectedValue: false,
+				expectedErr:   true,
+			},
+			{
+				value:         "1",
+				expectedValue: false,
+				expectedErr:   true,
+			},
+			{
+				value:         "true",
+				expectedValue: false,
+				expectedErr:   true,
+			},
+			{
+				value:         "something",
+				expectedValue: false,
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("bool #%d", i), func(t *testing.T) {
+				value, err := boolVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("bools (single) #%d", i), func(t *testing.T) {
+				value, err := boolArrayVal(tc.value)
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("bools (array) #%d", i), func(t *testing.T) {
+				value, err := boolArrayVal([]interface{}{tc.value})
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []bool{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+
+	t.Run("uuid(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue uuid.UUID
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         "e780b0a4-8d0e-4c09-898e-19d6b81e1e63",
+				expectedValue: uuid.MustParse("e780b0a4-8d0e-4c09-898e-19d6b81e1e63"),
+				expectedErr:   false,
+			},
+			{
+				value:         "e780b0a48d0e4c09898e19d6b81e1e63",
+				expectedValue: uuid.MustParse("e780b0a4-8d0e-4c09-898e-19d6b81e1e63"),
+				expectedErr:   false,
+			},
+
+			{
+				value:         float64(123),
+				expectedValue: [16]byte{},
+				expectedErr:   true,
+			},
+			{
+				value:         int64(123),
+				expectedValue: [16]byte{},
+				expectedErr:   true,
+			},
+			{
+				value:         "123",
+				expectedValue: [16]byte{},
+				expectedErr:   true,
+			},
+			{
+				value:         "something",
+				expectedValue: [16]byte{},
+				expectedErr:   true,
+			},
+			{
+				value:         true,
+				expectedValue: [16]byte{},
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("uuid #%d", i), func(t *testing.T) {
+				value, err := uuidVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("uuids (single) #%d", i), func(t *testing.T) {
+				value, err := uuidArrayVal(tc.value)
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("uuids (array) #%d", i), func(t *testing.T) {
+				value, err := uuidArrayVal([]interface{}{tc.value})
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []uuid.UUID{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+
+	t.Run("date(s)", func(t *testing.T) {
+		type testCase struct {
+			value         interface{}
+			expectedValue time.Time
+			expectedErr   bool
+		}
+
+		testCases := []testCase{
+			{
+				value:         "2024-01-02T03:04:05.00Z",
+				expectedValue: time.Unix(1704164645, 0).UTC(),
+				expectedErr:   false,
+			},
+
+			{
+				value:         float64(123),
+				expectedValue: time.Time{},
+				expectedErr:   true,
+			},
+			{
+				value:         int64(123),
+				expectedValue: time.Time{},
+				expectedErr:   true,
+			},
+			{
+				value:         "123",
+				expectedValue: time.Time{},
+				expectedErr:   true,
+			},
+			{
+				value:         "something",
+				expectedValue: time.Time{},
+				expectedErr:   true,
+			},
+			{
+				value:         true,
+				expectedValue: time.Time{},
+				expectedErr:   true,
+			},
+		}
+
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("date #%d", i), func(t *testing.T) {
+				value, err := dateVal(tc.value)
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+				} else {
+					assert.NoError(t, err)
+				}
+				assert.Equal(t, tc.expectedValue, value)
+			})
+		}
+		for i, tc := range testCases {
+			t.Run(fmt.Sprintf("dates (single) #%d", i), func(t *testing.T) {
+				value, err := dateArrayVal(tc.value)
+
+				assert.Error(t, err)
+				assert.Nil(t, value)
+			})
+			t.Run(fmt.Sprintf("dates (array) #%d", i), func(t *testing.T) {
+				value, err := dateArrayVal([]interface{}{tc.value})
+
+				if tc.expectedErr {
+					assert.Error(t, err)
+					assert.Nil(t, value)
+				} else {
+					assert.NoError(t, err)
+					assert.Equal(t, []time.Time{tc.expectedValue}, value)
+				}
+			})
+		}
+	})
+}