Skip to content

Commit 23a445c

Browse files
committed
Arrays, although not unwound in history now save only changed elements putting a placeholder of MinKey in for unchanged values - works when saving and fetching - this means a top level array that isn't modified much won't explode the history - nested arrays still might, but that's a whole other level of challenge.
1 parent 4beb7e4 commit 23a445c

6 files changed

Lines changed: 251 additions & 97 deletions

File tree

memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepositoryImpl.java

Lines changed: 178 additions & 82 deletions
Large diffs are not rendered by default.

memex/src/main/java/com/johnlpage/memex/generics/repository/OptimizedMongoLoadRepositoryImpl.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,61 @@ private void useSmartUpdate(
333333
// If changed record the old value otherwise record nothing
334334
Document coerceEmptyToNull =
335335
new Document("$ifNull", Arrays.asList("$" + entry.getKey(), null));
336+
337+
/*EXPERIMENTAL */
338+
// Check if the NEW value being set is an array - if so, do element-wise diff
339+
340+
Document previousValueExpr;
341+
if (entry.getValue() instanceof List) {
342+
// Build an element-wise comparison:
343+
// For each index in the OLD array, if the element equals the corresponding
344+
// new element, store MinKey; otherwise store the old element.
345+
// Also handles the case where array lengths differ.
346+
//
347+
// We use $map over the old array with index, comparing each element
348+
// to the new array at the same index.
349+
350+
// $range(0, $size(oldArray))
351+
Document oldArrayRef = new Document("$ifNull",
352+
Arrays.asList("$" + entry.getKey(), new ArrayList<>()));
353+
Document oldSize = new Document("$size", oldArrayRef);
354+
355+
// The new value as a literal (in case it contains $ strings)
356+
Object newValueLiteral = entry.getValue();
357+
358+
// $map over indices of the old array
359+
// For each index i:
360+
// if oldArray[i] == newArray[i] -> MinKey
361+
// else -> oldArray[i]
362+
Document oldElem = new Document("$arrayElemAt",
363+
Arrays.asList(oldArrayRef, "$$idx"));
364+
Document newElem = new Document("$arrayElemAt",
365+
Arrays.asList(new Document("$literal", newValueLiteral), "$$idx"));
366+
367+
Document elemEqual = new Document("$eq", Arrays.asList(oldElem, newElem));
368+
// MinKey as a constant - use $literal with a MinKey BSON value
369+
org.bson.types.MinKey minKey = new org.bson.types.MinKey();
370+
Document conditionalElem = new Document("$cond",
371+
Arrays.asList(elemEqual, minKey, oldElem));
372+
373+
Document mappedArray = new Document("$map", new Document()
374+
.append("input", new Document("$range",
375+
Arrays.asList(0, oldSize)))
376+
.append("as", "idx")
377+
.append("in", conditionalElem));
378+
379+
previousValueExpr = mappedArray;
380+
} else {
381+
previousValueExpr = coerceEmptyToNull;
382+
}
383+
/*EXPERIMENTAL */
384+
385+
/* WAS
386+
Document conditionalOnChange =
387+
new Document("$cond", Arrays.asList(valueChanged, coerceEmptyToNull, "$$REMOVE"));*/
388+
336389
Document conditionalOnChange =
337-
new Document("$cond", Arrays.asList(valueChanged, coerceEmptyToNull, "$$REMOVE"));
390+
new Document("$cond", Arrays.asList(valueChanged, previousValueExpr, "$$REMOVE"));
338391

339392
previousValues.append(PREVIOUS_VALS + "." + entry.getKey(), conditionalOnChange);
340393
}

memex/src/test/java/com/johnlpage/memex/cucumber/steps/RestApiSteps.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ public void responseShouldContain(String expectedSubstring) {
8989
response.then().body(containsString(expectedSubstring));
9090
}
9191

92+
@Then("the response should contain {string}: [{string}, {string}, {string}]")
93+
public void the_response_should_contain(String field, String val1, String val2, String val3) {
94+
List<String> expectedValues = List.of(val1, val2, val3);
95+
96+
// Extract actual values from your response
97+
List<String> actualValues = response.jsonPath().getList(field);
98+
99+
assertEquals(expectedValues, actualValues);
100+
}
101+
102+
92103
@Then("the response should contain {string} with {int} items")
93104
public void responseShouldContainContentWithItems(String key, int expectedCount) {
94105
assertNotNull(response.getBody(), "Response body should not be null");

memex/src/test/resources/features/inspections.rest.asof.feature

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ Feature: Vehicle Inspection REST API - Point-in-Time History (As Of)
77
Scenario: Successfully retrieve vehicle inspection history as of a specific date
88
Given the following vehicle inspections exist:
99
| vehicleinspection |
10-
| {"testid": 10001, "vehicle": {"make": "Ford", "model": "Focus"}} |
10+
| {"testid": 10001, "vehicle": {"make": "Ford", "model": "Focus"},"faileditems": ["Handbrake", "Lights","Wipers"]} |
1111
And I wait for 1 second
1212
And I capture the current timestamp to "<timestamp>" with "yyyyMMddHHmmss" pattern
1313
And I wait for 1 second
14-
And I send a POST request to "/api/inspections?updateStrategy=UPDATEWITHHISTORY&futz=true" with the payload:
14+
And I send a POST request to "/api/inspections?updateStrategy=UPDATEWITHHISTORY" with the payload:
1515
"""
1616
[
1717
{
@@ -23,7 +23,7 @@ Feature: Vehicle Inspection REST API - Point-in-Time History (As Of)
2323
"testmileage": 60000,
2424
"postcode": "SW1A 0AB",
2525
"fuel": "Diesel",
26-
"capacity": 80,
26+
"capacity": 90,
2727
"firstusedate": "2019-03-20T00:00:00Z",
2828
"faileditems": ["Brakes", "Lights"],
2929
"vehicle": {
@@ -41,4 +41,6 @@ Feature: Vehicle Inspection REST API - Point-in-Time History (As Of)
4141
And the "Transfer-Encoding" header should be "chunked"
4242
And the "Content-Type" header should be "application/json"
4343
And the response should contain "testid": 10001
44-
And the response should contain "combined.vehicle.model": "Focus"
44+
And the response should contain "vehicle.model": "Focus"
45+
And the response should contain "faileditems": ["Handbrake", "Lights", "Wipers"]
46+

memex/templates/controller/Controller.java.template

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,10 @@ public class __className__Controller {
5454
private final ObjectMapper objectMapper;
5555
private final __className__Repository repository;
5656

57-
/**
58-
* Create or update a single record
59-
*/
60-
@PostMapping("/__singularApiPath__")
61-
public void createOne(@RequestBody __className__ record) {
62-
LOG.info("Saving __className__: {}", record);
63-
repository.save(record);
64-
}
57+
6558

6659
/**
67-
* Bulk load from JSON stream.
60+
* Bulk load from JSON stream. Could take a single item too
6861
* This could read from a file, Kafka queue, or any stream of JSON data.
6962
*/
7063
@PostMapping("/__apiPath__")

memex/templates/scripts/generate-entity.groovy

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ println "Generated: ${generatedCount}, Skipped: ${skippedCount}"
228228
println "========================================="
229229
println ""
230230
println "API Endpoints created:"
231-
println " POST /api/${singularApiPath} - Create single record"
232231
println " POST /api/${apiPath} - Bulk load from JSON stream"
233232
println " GET /api/${apiPath}/id/{id} - Get by ID"
234233
println " GET /api/${apiPath}/byExample - Example query with pagination"

0 commit comments

Comments
 (0)