From 7687c26a74c29a22f347fd93a817a781c7e468f4 Mon Sep 17 00:00:00 2001 From: Sean DeNigris Date: Sat, 13 Jan 2024 22:31:18 -0500 Subject: [PATCH] Cached Mementos - To Copy or Not To Copy? (#344) * Cached Mementos - To Copy or Not To Copy? - Hopefully finally resolves a buffet of related issues (and PRs) mentioned below - Add Lepiter page describing the situation - The problem is that mementos were copying described values in two places: - ```smalltalk MACachedMemento>>cookRawPull: aDictionary super cookRawPull: aDictionary. aDictionary keysAndValuesDo: [ :key :value | | isCollectionOfRelations | isCollectionOfRelations := value isCollection and: [ key isKindOf: MAToManyRelationDescription ]. isCollectionOfRelations ifTrue: [ aDictionary at: key put: value copy ] ].``` - There are several problems with the above: Firstly, it is applied to cached mementos. This overly broad, since the motivating problem affects the `original` dictionary - something only checked mementos have. Even worse, it applies to all pulls, not just those to `original` dictionaries. This lead to a host of problems described in more detail below. - ```smalltalk MACheckedMemento>>reset super reset. self setOriginal: (self pullRawTransforming: [ :e | e copy ]). ``` - While this second copying might sometimes be what we want, it clearly does not work in some scenarios, creating the false impression that the model object has changed elsewhere, which prevents committing the memento. - For example, {{gtMethod:MAMementoTest>>testSingletonValue}} was failing because the value was a class and the memento was storing a copying of it. Validation then failed because the installed class is not equivalent to a copy. - Let's take a step back and review the domain model. For checked mementos, there are three versions of model state: - 1. the model itself, which is the real *current* state - 2. the cache, which is the desired state which the memento will attempt to push all at once on commit - 3. the original, which is the memento's copy of the model state at the time the memento was created. This will be used before committing to make sure the model state hasn't changed elsewhere because otherwise committing might intentionally overwrite/destroy needed data. - The motivating problem with the *original*, which inspired all this copying, is that, if a field references a collection, and we hold onto that actual collection, and the elements in the collection are changed from the outside, our "original" will also change and validation will never fail and so offers no protection. - {{gtMethod:MACheckedMementoTest>>testValidateFailsOnReferencedCollectionChange}} tests for this. If we comment out the `#copy` in {{gtMethod:MACheckedMemento>>reset}} it will fail. - Interestingly, outside changes also "bleed" into the cache, but it seems not to matter. If a user was worried about outside changes, they would use an {{gtClass:MACheckedMemento}} which would flag the problem during validation, not an {{gtClass:MACachedMemento}}. Choosing that memento type means we are specifically *not* checking for whether the model has changed elsewhere. Since the cache is changing with the model, there will be nothing to commit unless we explicitly change the field in the memento, which seems like the expected behavior. - There are a bunch of seemingly related GH issues and PRs: - 2019-09-04 - [Mementos Should Ignore Default values](https://github.com/magritte-metamodel/magritte/issues/120) - claims to be fixed by the "...Copy Collections" fix below, but I don't see how - 2022-05-31 - [Cached Memento Should Copy ToMany Collections](https://github.com/magritte-metamodel/magritte/pull/281) - this first fix led to a lot of future pain. The primary mistake seems to have been that it was applied to all pulls by cached mementos, instead of just pulls to the original dictionary by checked mementos only. The issue talks about "the protection of the cache", but I think I meant "the protection of the original dictionary" because I don't see what damage could be caused by the cache changing underneath you. If you explicitly change a field, it will overwrite the cache, and if not it will be ignored because it is equivalent to the real live model state. - 2022-08-22 - [Checked Memento "Original" Should Copy Collections](https://github.com/magritte-metamodel/magritte/pull/295) - the description states "Otherwise changes to the model can bleed through to the "original" dictionary". However, the fix is applied to cached memento, not checked memento only - 2022-08-23 - [Bloc Form To-One Tokens Pointing to Copy](https://github.com/magritte-metamodel/magritte/pull/304) - tried to workaround ramifications of the overly broad copy problem fix - 2022-08-23 - [Mementos - Only Copy Checked Original](https://github.com/magritte-metamodel/magritte/pull/305/files) attempted to improve the situation by replacing the original behavior - when pulling copy every value in every memento type - with a more targeted approach - copy only when setting the original of a checked memento. However, this still copied *all* values, not just the problematic referenced collections. It also missed the fact that cached mementos were copying collections during all pulls via `#cookPullRaw:`, not just when pulling to the original dictionary. It also seemingly undid some of the fix above - 2023-11-18 Checked Memento raises errors for Pier components - there is a now-passing test for just this scenario - MAMementoTest>>testSingletonValue --- lepiter/d7hyk2iipxwmivsgijclbq2y0.bak | 747 ++++++++++++++++++ lepiter/d7hyk2iipxwmivsgijclbq2y0.lepiter | 747 ++++++++++++++++++ .../Magritte-Model/MACachedMemento.class.st | 14 +- .../Magritte-Model/MACheckedMemento.class.st | 27 +- .../MAElementDescription.class.st | 28 + source/Magritte-Model/MAMemento.class.st | 37 +- .../MAOptionDescription.class.st | 5 - .../MAReferenceDescription.class.st | 5 + .../Magritte-Model/MAStraightMemento.class.st | 3 +- .../MAToManyRelationDescription.class.st | 5 + .../MACheckedMementoTest.class.st | 23 + .../MADescriptionBuilderTest.class.st | 4 +- .../MAMementoTest.class.st | 62 +- .../MAMockAddress.class.st | 46 +- 14 files changed, 1708 insertions(+), 45 deletions(-) create mode 100644 lepiter/d7hyk2iipxwmivsgijclbq2y0.bak create mode 100644 lepiter/d7hyk2iipxwmivsgijclbq2y0.lepiter diff --git a/lepiter/d7hyk2iipxwmivsgijclbq2y0.bak b/lepiter/d7hyk2iipxwmivsgijclbq2y0.bak new file mode 100644 index 00000000..9f6a9cd2 --- /dev/null +++ b/lepiter/d7hyk2iipxwmivsgijclbq2y0.bak @@ -0,0 +1,747 @@ +{ + "__schema" : "4.1", + "__type" : "page", + "uid" : { + "__type" : "uuid", + "uuid" : "6870a1aa-38cb-0d00-8f66-51a200a61adf" + }, + "pageType" : { + "__type" : "namedPage", + "title" : "Cached Mementos - To Copy or Not To Copy?" + }, + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:02.295492-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:04.52591-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "DvCoM0DLDQCUvW8oDmVp6Q==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "```smalltalk\nMACachedMemento>>cookRawPull: aDictionary\r\r\tsuper cookRawPull: aDictionary.\r\taDictionary keysAndValuesDo: [ :key :value |\r\t\t| isCollectionOfRelations |\r\t\tisCollectionOfRelations := value isCollection and: [ key isKindOf: MAToManyRelationDescription ].\r\t\tisCollectionOfRelations ifTrue: [ \r\t\t\taDictionary at: key put: value copy ] ].```" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:10:23.303431-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:34.383327-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "mhAqGzvLDQCqHkyvBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "There are several problems with the above: Firstly, it is applied to cached mementos. This overly broad, since the motivating problem affects the `original` dictionary - something only checked mementos have. Even worse, it applies to all pulls, not just those to `original` dictionaries. This lead to a host of problems described in more detail below." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:15:57.283707-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:28:02.243445-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "oEG/qzjLDQCPaJEoAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "While this second copying might sometimes be what we want, it clearly does not work in some scenarios, creating the false impression that the model object has changed elsewhere, which prevents committing the memento." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:11:23.632865-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:30:58.326014-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "qvKWSDrLDQCWb4zOAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "For example, {{gtMethod:MAMementoTest>>testSingletonValue}} was failing because the value was a class and the memento was storing a copying of it. Validation then failed because the installed class is not equivalent to a copy." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:41:59.218048-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:08:54.366768-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "y3XYCDnLDQCYHE5pAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "```smalltalk\nMACheckedMemento>>reset\r\tsuper reset.\r\tself setOriginal: (self pullRawTransforming: [ :e | e copy ]).\n```" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:09:25.64184-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:06:52.577448-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "KuX6ajnLDQCjkkexAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "The problem is that mementos were copying described values in two places:" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:15:17.154524-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:14:38.476851-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "TIzufznLDQCoJxcnAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "1. the model itself, which is the real *current* state" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:22.674676-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:21:44.989027-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "T1DWgznLDQCq316JAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2. the cache, which is the desired state which the memento will attempt to push all at once on commit" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:48.400288-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:22:26.874825-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "s9tehTnLDQCspUBhAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "3. the original, which is the memento's copy of the model state at the time the memento was created. This will be used before committing to make sure the model state hasn't changed elsewhere because otherwise committing might intentionally overwrite/destroy needed data." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:03.343177-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:13:44.04558-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "BVmvgjnLDQCqbjclAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "Let's take a step back and review the domain model. For checked mementos, there are three versions of model state:" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:00:28.071116-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:23:14.918565-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "G8rBIDrLDQCGxhQxAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "{{gtMethod:MACheckedMementoTest>>testValidateFailsOnReferencedCollectionChange}} tests for this. If we comment out the `#copy` in {{gtMethod:MACheckedMemento>>reset}} it will fail." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:02:16.223282-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T22:10:17.264203-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "9CD2JzrLDQCJJWd0AKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "Interestingly, outside changes also \"bleed\" into the cache, but it seems not to matter. If a user was worried about outside changes, they would use an MACheckedMemento which would flag the problem during validation, not an {{gtClass:MACachedMemento}}. Choosing that memento type means we are specifically *not* checking for whether the model has changed elsewhere. Since the cache is changing with the model, there will be nothing to commit unless we explicitly change the field in the memento, which seems like the expected behavior." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:18:41.587253-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T22:05:33.783955-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "CPMdjDnLDQCzQ4jFAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "The motivating problem with the *original*, which inspired all this copying, is that, if a field references a collection, and we hold onto that actual collection, and the elements in the collection are changed from the outside, our \"original\" will also change and validation will never fail and so offers no protection. " + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:12:25.214197-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:05:47.598878-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "lPKudTnLDQCmPtikAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2019-09-04 - [Mementos Should Ignore Default values](https://github.com/magritte-metamodel/magritte/issues/120) - claims to be fixed by the \"...Copy Collections\" fix below, but I don't see how" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:05:49.415651-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:09:37.818001-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "Is4+CzvLDQCb+VbtBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-05-31 - [Cached Memento Should Copy ToMany Collections](https://github.com/magritte-metamodel/magritte/pull/281) - this first fix led to a lot of future pain. The primary mistake seems to have been that it was applied to all pulls by cached mementos, instead of just pulls to the original dictionary by checked mementos only. The issue talks about \"the protection of the cache\", but I think I meant \"the protection of the original dictionary\" because I don't see what damage could be caused by the cache changing underneath you. If you explicitly change a field, it will overwrite the cache, and if not it will be ignored because it is equivalent to the real live model state." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:59:28.896048-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:58:24.210511-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "7EZpRznLDQCcfRcPAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-22 - [Checked Memento \"Original\" Should Copy Collections](https://github.com/magritte-metamodel/magritte/pull/295) - the description states \"Otherwise changes to the model can bleed through to the \"original\" dictionary\". However, the fix is applied to cached memento, not checked memento only" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:00:27.959074-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:03:05.83124-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "bbQV+DrLDQCXycoMBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-23 - [Bloc Form To-One Tokens Pointing to Copy](https://github.com/magritte-metamodel/magritte/pull/304) - tried to workaround ramifications of the overly broad copy problem fix" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:48:17.909974-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:03:22.259765-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "Qh+SzDrLDQCLAM1vBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-23 - [Mementos - Only Copy Checked Original](https://github.com/magritte-metamodel/magritte/pull/305/files) attempted to improve the situation by replacing the original behavior - when pulling copy every value in every memento type - with a more targeted approach - copy only when setting the original of a checked memento. However, this still copied *all* values, not just the problematic referenced collections. It also missed the fact that cached mementos were copying collections during all pulls via `#cookPullRaw:`, not just when pulling to the original dictionary. It also seemingly undid some of the fix above" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:11:27.886984-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:23:56.642796-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "2TVEcjnLDQCkhAyxAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "There are a bunch of seemingly related GH issues and PRs:" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:15:38.552643-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:14:14.78749-05:00" + } + } +} \ No newline at end of file diff --git a/lepiter/d7hyk2iipxwmivsgijclbq2y0.lepiter b/lepiter/d7hyk2iipxwmivsgijclbq2y0.lepiter new file mode 100644 index 00000000..458a9814 --- /dev/null +++ b/lepiter/d7hyk2iipxwmivsgijclbq2y0.lepiter @@ -0,0 +1,747 @@ +{ + "__schema" : "4.1", + "__type" : "page", + "uid" : { + "__type" : "uuid", + "uuid" : "6870a1aa-38cb-0d00-8f66-51a200a61adf" + }, + "pageType" : { + "__type" : "namedPage", + "title" : "Cached Mementos - To Copy or Not To Copy?" + }, + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:02.295492-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:04.52591-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "DvCoM0DLDQCUvW8oDmVp6Q==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "```smalltalk\nMACachedMemento>>cookRawPull: aDictionary\r\r\tsuper cookRawPull: aDictionary.\r\taDictionary keysAndValuesDo: [ :key :value |\r\t\t| isCollectionOfRelations |\r\t\tisCollectionOfRelations := value isCollection and: [ key isKindOf: MAToManyRelationDescription ].\r\t\tisCollectionOfRelations ifTrue: [ \r\t\t\taDictionary at: key put: value copy ] ].```" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:10:23.303431-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:15:34.383327-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "mhAqGzvLDQCqHkyvBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "There are several problems with the above: Firstly, it is applied to cached mementos. This overly broad, since the motivating problem affects the `original` dictionary - something only checked mementos have. Even worse, it applies to all pulls, not just those to `original` dictionaries. This lead to a host of problems described in more detail below." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:15:57.283707-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:28:02.243445-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "oEG/qzjLDQCPaJEoAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "While this second copying might sometimes be what we want, it clearly does not work in some scenarios, creating the false impression that the model object has changed elsewhere, which prevents committing the memento." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:11:23.632865-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:30:58.326014-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "qvKWSDrLDQCWb4zOAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "For example, {{gtMethod:MAMementoTest>>testSingletonValue}} was failing because the value was a class and the memento was storing a copying of it. Validation then failed because the installed class is not equivalent to a copy." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:41:59.218048-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:08:54.366768-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "y3XYCDnLDQCYHE5pAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "```smalltalk\nMACheckedMemento>>reset\r\tsuper reset.\r\tself setOriginal: (self pullRawTransforming: [ :e | e copy ]).\n```" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:09:25.64184-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:06:52.577448-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "KuX6ajnLDQCjkkexAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "The problem is that mementos were copying described values in two places:" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:15:17.154524-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:14:38.476851-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "TIzufznLDQCoJxcnAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "1. the model itself, which is the real *current* state" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:22.674676-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:21:44.989027-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "T1DWgznLDQCq316JAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2. the cache, which is the desired state which the memento will attempt to push all at once on commit" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:48.400288-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:22:26.874825-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "s9tehTnLDQCspUBhAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "3. the original, which is the memento's copy of the model state at the time the memento was created. This will be used before committing to make sure the model state hasn't changed elsewhere because otherwise committing might intentionally overwrite/destroy needed data." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:16:03.343177-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:13:44.04558-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "BVmvgjnLDQCqbjclAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "Let's take a step back and review the domain model. For checked mementos, there are three versions of model state:" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:00:28.071116-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:23:14.918565-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "G8rBIDrLDQCGxhQxAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "{{gtMethod:MACheckedMementoTest>>testValidateFailsOnReferencedCollectionChange}} tests for this. If we comment out the `#copy` in {{gtMethod:MACheckedMemento>>reset}} it will fail." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:02:16.223282-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T22:10:42.463849-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "9CD2JzrLDQCJJWd0AKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "Interestingly, outside changes also \"bleed\" into the cache, but it seems not to matter. If a user was worried about outside changes, they would use an {{gtClass:MACheckedMemento}} which would flag the problem during validation, not an {{gtClass:MACachedMemento}}. Choosing that memento type means we are specifically *not* checking for whether the model has changed elsewhere. Since the cache is changing with the model, there will be nothing to commit unless we explicitly change the field in the memento, which seems like the expected behavior." + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:18:41.587253-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T22:05:33.783955-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "CPMdjDnLDQCzQ4jFAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "The motivating problem with the *original*, which inspired all this copying, is that, if a field references a collection, and we hold onto that actual collection, and the elements in the collection are changed from the outside, our \"original\" will also change and validation will never fail and so offers no protection. " + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:12:25.214197-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:05:47.598878-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "lPKudTnLDQCmPtikAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2019-09-04 - [Mementos Should Ignore Default values](https://github.com/magritte-metamodel/magritte/issues/120) - claims to be fixed by the \"...Copy Collections\" fix below, but I don't see how" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:05:49.415651-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:09:37.818001-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "Is4+CzvLDQCb+VbtBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-05-31 - [Cached Memento Should Copy ToMany Collections](https://github.com/magritte-metamodel/magritte/pull/281) - this first fix led to a lot of future pain. The primary mistake seems to have been that it was applied to all pulls by cached mementos, instead of just pulls to the original dictionary by checked mementos only. The issue talks about \"the protection of the cache\", but I think I meant \"the protection of the original dictionary\" because I don't see what damage could be caused by the cache changing underneath you. If you explicitly change a field, it will overwrite the cache, and if not it will be ignored because it is equivalent to the real live model state." + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:59:28.896048-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:58:24.210511-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "7EZpRznLDQCcfRcPAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-22 - [Checked Memento \"Original\" Should Copy Collections](https://github.com/magritte-metamodel/magritte/pull/295) - the description states \"Otherwise changes to the model can bleed through to the \"original\" dictionary\". However, the fix is applied to cached memento, not checked memento only" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:00:27.959074-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:03:05.83124-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "bbQV+DrLDQCXycoMBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-23 - [Bloc Form To-One Tokens Pointing to Copy](https://github.com/magritte-metamodel/magritte/pull/304) - tried to workaround ramifications of the overly broad copy problem fix" + }, + { + "__type" : "textSnippet", + "children" : { + "__type" : "snippets", + "items" : [ ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T14:48:17.909974-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T15:03:22.259765-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "Qh+SzDrLDQCLAM1vBYVcUQ==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "2022-08-23 - [Mementos - Only Copy Checked Original](https://github.com/magritte-metamodel/magritte/pull/305/files) attempted to improve the situation by replacing the original behavior - when pulling copy every value in every memento type - with a more targeted approach - copy only when setting the original of a checked memento. However, this still copied *all* values, not just the problematic referenced collections. It also missed the fact that cached mementos were copying collections during all pulls via `#cookPullRaw:`, not just when pulling to the original dictionary. It also seemingly undid some of the fix above" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T13:11:27.886984-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:23:56.642796-05:00" + } + }, + "uid" : { + "__type" : "uid", + "uidString" : "2TVEcjnLDQCkhAyxAKYa3w==" + }, + "paragraphStyle" : { + "__type" : "textStyle" + }, + "string" : "There are a bunch of seemingly related GH issues and PRs:" + } + ] + }, + "createEmail" : { + "__type" : "email", + "emailString" : "" + }, + "createTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T12:15:38.552643-05:00" + } + }, + "editEmail" : { + "__type" : "email", + "emailString" : "" + }, + "editTime" : { + "__type" : "time", + "time" : { + "__type" : "dateAndTime", + "dateAndTimeString" : "2024-01-13T21:14:14.78749-05:00" + } + } +} \ No newline at end of file diff --git a/source/Magritte-Model/MACachedMemento.class.st b/source/Magritte-Model/MACachedMemento.class.st index 61ee8c29..f654a41f 100644 --- a/source/Magritte-Model/MACachedMemento.class.st +++ b/source/Magritte-Model/MACachedMemento.class.st @@ -27,23 +27,11 @@ MACachedMemento >> commit [ self reset ] -{ #category : #private } -MACachedMemento >> cookRawPull: aDictionary [ - - super cookRawPull: aDictionary. - aDictionary keysAndValuesDo: [ :key :value | - | isCollectionOfRelations | - self flag: 'duplicate logic with cookRawPull:'. - isCollectionOfRelations := value isCollection and: [ key isKindOf: MAToManyRelationDescription ]. - isCollectionOfRelations ifTrue: [ - aDictionary at: key put: value copy ] ]. -] - { #category : #testing } MACachedMemento >> hasChanges [ "Answer ==true==, if the cached data is different to the data in the model." - ^ self isDifferent: self cache to: self pullRaw + ^ self has: self cache changedFrom: self pullRaw ] { #category : #private } diff --git a/source/Magritte-Model/MACheckedMemento.class.st b/source/Magritte-Model/MACheckedMemento.class.st index eef4d566..1472166d 100644 --- a/source/Magritte-Model/MACheckedMemento.class.st +++ b/source/Magritte-Model/MACheckedMemento.class.st @@ -19,7 +19,7 @@ MACheckedMemento >> hasConflict [ { #category : #testing } MACheckedMemento >> hasModelChangedElsewhere [ - ^ self isDifferent: self original to: self pullRaw + ^ self has: self original changedFrom: self pullRaw ] { #category : #accessing } @@ -29,10 +29,24 @@ MACheckedMemento >> original [ { #category : #actions } MACheckedMemento >> reset [ + + | dict | super reset. - self setOriginal: (self pullRawTransforming: [ :e | e copy ]). - "Implementation note: We copy the field values because checked mementos compare this to the current object to see if it has changed elsewhere. Unless we make a copy each time, this comparison would not be possible for complex objects, because any changes to them from outside will be reflected equally in this `original` dictionary. E.g. if `original at: #person == self model person` and outside someone does `self model person age: 25`, the check above would pass even though it should fail." + dict := self pullRawTransforming: [ :value :desc | + | isCollectionOfRelations | + isCollectionOfRelations := value isCollection and: [ (desc isKindOf: MAReferenceDescription) and: [ desc isMultiple ] ]. + isCollectionOfRelations + ifTrue: [ value copy ] + ifFalse: [ value ] ]. + + self setOriginal: dict. + + "Implementation note: We copy some field values because checked mementos compare them to the current object to see if it has changed elsewhere. If a field references a collection, and we hold onto that actual collection, and the elements in the collection are changed from the outside, our 'original' will also change and validation will never fail and so offers no protection. We limit the copying to just this case because copying can cause other unforeseen problems. + + One example is illustrated in MAMementoTest>>testSingletonValue. + + Another is that we had at one time done the copying in MACachedMemento>>#cookPullRaw:, but it proved too dangerous because the *cache* also contained copies, not just the *original* as here. For example (ignoring the question of whether this is good design), what if the model required that a field point to a *particular* collection? We might replace that particular collection with a copy, which then goes out of sync with the 'real' collection. Now, outside changes also 'bleed' into caches, but it seems not to matter. If a user was worried about outside changes, they would use an MACheckedMemento which would flag the problem during validation, not an MACachedMemento. Choosing a cached memento means we are specifically *not* checking for whether the model has changed elsewhere. Since the cache is changing with the model, there will be nothing to commit unless we explicitly change the field in the memento, which seems like the expected behavior." ] { #category : #initialization } @@ -43,10 +57,13 @@ MACheckedMemento >> setOriginal: aDictionary [ { #category : #'private-testing' } MACheckedMemento >> shouldPush: anObject using: aDescription [ - | originalValue cachedValue | + | originalValue cachedValue didChangeHere | originalValue := self original at: aDescription. cachedValue := self cache at: aDescription. - ^ (originalValue = cachedValue) not and: [ super shouldPush: anObject using: aDescription ] + + didChangeHere := aDescription shouldWrite: cachedValue over: originalValue. + + ^ didChangeHere and: [ super shouldPush: anObject using: aDescription ] ] { #category : #actions } diff --git a/source/Magritte-Model/MAElementDescription.class.st b/source/Magritte-Model/MAElementDescription.class.st index 469f9e9a..aee92508 100644 --- a/source/Magritte-Model/MAElementDescription.class.st +++ b/source/Magritte-Model/MAElementDescription.class.st @@ -121,6 +121,34 @@ MAElementDescription >> printFor: anObject on: aWriteStream [ (self read: anObject) ifNotNil: [ :value | aWriteStream nextPutAll: (self toString: value) ] ] +{ #category : #utility } +MAElementDescription >> shouldWrite: anObject over: anotherObject [ + + | defaultValues areBothDefaultValues isIgnorableDefault | + + self isVisible ifFalse: [ ^ false ]. + self isReadOnly ifTrue: [ ^ false ]. + + defaultValues := OrderedCollection + with: nil + with: self undefinedValue. + + isIgnorableDefault := self shouldCacheDefault not and: [ anObject = self default ]. + isIgnorableDefault ifTrue: [ defaultValues add: self default ]. + + areBothDefaultValues := { anObject. anotherObject } allSatisfy: [ :obj | defaultValues includes: obj ]. + + areBothDefaultValues ifTrue: [ ^ false ]. + (anObject == anotherObject or: [ anObject = anotherObject ]) + ifTrue: [ ^ false ]. + + ^ true + + "Implementation note: We compare via both #== and #= to cover two scenarios: + 1) a referenced object that changes on the outside. From our perspective, as long as we are still refering to the same object there is no relevant change. Unless we accept identity equality, any changes to complex objects from outside will bleed into this object. E.g. if outside someone does `self person age: 25`, this object will appear to be changed. This would wreak havoc with mementos, forms, etc. + 2) objects (e.g. strings and other value objects) that override #= to mean something other than identity comparison." +] + { #category : #printing } MAElementDescription >> storeOn: aStream [ aStream diff --git a/source/Magritte-Model/MAMemento.class.st b/source/Magritte-Model/MAMemento.class.st index abc26eff..a21e7c04 100644 --- a/source/Magritte-Model/MAMemento.class.st +++ b/source/Magritte-Model/MAMemento.class.st @@ -35,10 +35,13 @@ MAMemento >> commit [ { #category : #private } MAMemento >> cookRawPull: aDictionary [ + "The reason for the somewhat cryptic message name is that each subclass is free to perform any needed actions. Here we just replace undefinedValues with default values" - aDictionary keysAndValuesDo: [ :key :value | - value isNil - ifTrue: [ aDictionary at: key put: key default yourself ] ] + aDictionary keysAndValuesDo: [ :elemDescription :value | + value = elemDescription undefinedValue ifTrue: [ + aDictionary + at: elemDescription + put: elemDescription default ] ] ] { #category : #'reflective operations' } @@ -50,13 +53,14 @@ MAMemento >> doesNotUnderstand: aMessage [ ] { #category : #private } -MAMemento >> isDifferent: firstDictionary to: secondDictionary [ - | firstValue secondValue | - self magritteDescription do: [ :each | - (each isVisible and: [ each isReadOnly not ]) ifTrue: [ - firstValue := firstDictionary at: each ifAbsent: [ nil ]. - secondValue := secondDictionary at: each ifAbsent: [ nil ]. - firstValue = secondValue ifFalse: [ ^ true ] ] ]. +MAMemento >> has: firstDictionary changedFrom: secondDictionary [ + + self magritteDescription do: [ :desc | + | firstValue secondValue hasFieldChanged | + firstValue := firstDictionary at: desc ifAbsent: [ nil ]. + secondValue := secondDictionary at: desc ifAbsent: [ nil ]. + hasFieldChanged := desc shouldWrite: firstValue over: secondValue. + hasFieldChanged ifTrue: [ ^ true ] ]. ^ false ] @@ -97,11 +101,11 @@ MAMemento >> pullRaw [ MAMemento >> pullRawTransforming: aBlock [ | result | result := Dictionary new. - self magritteDescription do: [ :each | + self magritteDescription do: [ :elemDescription | | value transformedValue | - value := self model readUsing: each. - transformedValue := aBlock value: value. - result at: each put: transformedValue ]. + value := self model readUsing: elemDescription. + transformedValue := aBlock cull: value cull: elemDescription. + result at: elemDescription put: transformedValue ]. ^ result ] @@ -136,7 +140,10 @@ MAMemento >> setModel: aModel [ { #category : #'private-testing' } MAMemento >> shouldPush: anObject using: aDescription [ - ^ aDescription isVisible and: [ aDescription isReadOnly not ] + + | modelValue | + modelValue := self model readUsing: aDescription. + ^ aDescription shouldWrite: anObject over: modelValue. ] { #category : #actions } diff --git a/source/Magritte-Model/MAOptionDescription.class.st b/source/Magritte-Model/MAOptionDescription.class.st index 6774d841..6c729bbe 100644 --- a/source/Magritte-Model/MAOptionDescription.class.st +++ b/source/Magritte-Model/MAOptionDescription.class.st @@ -133,11 +133,6 @@ MAOptionDescription >> isExtensible [ ^ self extensible ] -{ #category : #testing } -MAOptionDescription >> isMultiple [ - ^false -] - { #category : #testing } MAOptionDescription >> isSorted [ ^ self sorted diff --git a/source/Magritte-Model/MAReferenceDescription.class.st b/source/Magritte-Model/MAReferenceDescription.class.st index 0e934eb3..f36e1c3a 100644 --- a/source/Magritte-Model/MAReferenceDescription.class.st +++ b/source/Magritte-Model/MAReferenceDescription.class.st @@ -112,6 +112,11 @@ MAReferenceDescription >> initializer: valuable [ self propertyAt: #initializer put: valuable ] +{ #category : #testing } +MAReferenceDescription >> isMultiple [ + ^false +] + { #category : #copying } MAReferenceDescription >> postCopy [ super postCopy. diff --git a/source/Magritte-Model/MAStraightMemento.class.st b/source/Magritte-Model/MAStraightMemento.class.st index cd7bb512..161dc8c1 100644 --- a/source/Magritte-Model/MAStraightMemento.class.st +++ b/source/Magritte-Model/MAStraightMemento.class.st @@ -4,7 +4,7 @@ I am a memento that forwards read- and write-access directly to the model. I can Class { #name : #MAStraightMemento, #superclass : #MAMemento, - #category : 'Magritte-Model-Memento' + #category : #'Magritte-Model-Memento' } { #category : #testing } @@ -20,5 +20,6 @@ MAStraightMemento >> readUsing: aDescription [ { #category : #private } MAStraightMemento >> write: anObject using: aDescription [ + (self shouldPush: anObject using: aDescription) ifFalse: [ ^ self ]. self model write: anObject using: aDescription ] diff --git a/source/Magritte-Model/MAToManyRelationDescription.class.st b/source/Magritte-Model/MAToManyRelationDescription.class.st index 573ce108..64f69be6 100644 --- a/source/Magritte-Model/MAToManyRelationDescription.class.st +++ b/source/Magritte-Model/MAToManyRelationDescription.class.st @@ -129,6 +129,11 @@ MAToManyRelationDescription >> isDefinitive [ ^ self definitive. ] +{ #category : #accessing } +MAToManyRelationDescription >> isMultiple [ + ^true +] + { #category : #testing } MAToManyRelationDescription >> isOrdered [ ^ self ordered diff --git a/source/Magritte-Tests-Model/MACheckedMementoTest.class.st b/source/Magritte-Tests-Model/MACheckedMementoTest.class.st index dcbc6465..aa017eed 100644 --- a/source/Magritte-Tests-Model/MACheckedMementoTest.class.st +++ b/source/Magritte-Tests-Model/MACheckedMementoTest.class.st @@ -80,3 +80,26 @@ MACheckedMementoTest >> testValidateConflictReset [ self memento reset. self shouldnt: [ self memento validate ] raise: MAValidationError ] + +{ #category : #'tests-actions' } +MACheckedMementoTest >> testValidateFailsOnReferencedCollectionChange [ + + | occupants obj | + occupants := #(Bill Fred) asOrderedCollection. + + obj := MAMockAddress new + occupants: occupants; + yourself. + + memento := self actualClass + model: obj + description: obj magritteDescription. + + "Field-referenced collection changes outside of memento" + obj occupants add: 'Jim'. + + "Now we change the same field via the memento" + memento write: #(Tom) using: obj occupantsDescription. + + self should: [ memento validate ] raise: MAConflictError. +] diff --git a/source/Magritte-Tests-Model/MADescriptionBuilderTest.class.st b/source/Magritte-Tests-Model/MADescriptionBuilderTest.class.st index e6b6a885..1e12a4a9 100644 --- a/source/Magritte-Tests-Model/MADescriptionBuilderTest.class.st +++ b/source/Magritte-Tests-Model/MADescriptionBuilderTest.class.st @@ -1,7 +1,7 @@ Class { #name : #MADescriptionBuilderTest, #superclass : #TestCase, - #category : 'Magritte-Tests-Model-Utility' + #category : #'Magritte-Tests-Model-Utility' } { #category : #'acessing-magritte' } @@ -85,7 +85,7 @@ MADescriptionBuilderTest >> testExtension [ MADescriptionBuilderTest >> testNilled [ | description | description := MAMockAddress new magritteDescription. - self assert: description size = 3 + self assert: description size = 5 ] { #category : #tests } diff --git a/source/Magritte-Tests-Model/MAMementoTest.class.st b/source/Magritte-Tests-Model/MAMementoTest.class.st index a84c489f..5c02f16d 100644 --- a/source/Magritte-Tests-Model/MAMementoTest.class.st +++ b/source/Magritte-Tests-Model/MAMementoTest.class.st @@ -6,7 +6,7 @@ Class { 'value', 'description' ], - #category : 'Magritte-Tests-Model-Memento' + #category : #'Magritte-Tests-Model-Memento' } { #category : #testing } @@ -94,6 +94,31 @@ MAMementoTest >> setUp [ memento := self mementoInstance ] +{ #category : #'tests-actions' } +MAMementoTest >> testCachedDefaultValues [ + + | obj plzDescription | + obj := MAMockAddress new. + + memento := self actualClass + model: obj + description: obj magritteDescription. + + memento := self actualClass + model: obj + description: obj magritteDescription. + + plzDescription := obj descriptionPlz + default: 1; + shouldCacheDefault: true; + yourself. + + memento push: { + plzDescription -> plzDescription default } asDictionary. + + self assert: obj plz equals: plzDescription default +] + { #category : #'tests-actions' } MAMementoTest >> testCommit [ self subclassResponsibility @@ -120,6 +145,41 @@ MAMementoTest >> testReset [ self subclassResponsibility ] +{ #category : #'tests-actions' } +MAMementoTest >> testSingletonValue [ + + | obj | + obj := MAMockAddress new + plzType: Number; + yourself. + + memento := obj mementoClass + model: obj + description: obj magritteDescription. + + self assert: (memento original at: obj plzTypeDescription) == obj plzType. +] + +{ #category : #'tests-actions' } +MAMementoTest >> testUncachedDefaultValues [ + + | obj plzDescription | + obj := MAMockAddress new. + + memento := self actualClass + model: obj + description: obj magritteDescription. + + plzDescription := obj descriptionPlz + default: 1; + yourself. + + memento push: { + plzDescription -> plzDescription default } asDictionary. + + self assert: obj plz equals: nil +] + { #category : #'tests-actions' } MAMementoTest >> testValidateIncluded [ self write: self includedInstance. diff --git a/source/Magritte-Tests-Model/MAMockAddress.class.st b/source/Magritte-Tests-Model/MAMockAddress.class.st index 496689c8..b6f2278b 100644 --- a/source/Magritte-Tests-Model/MAMockAddress.class.st +++ b/source/Magritte-Tests-Model/MAMockAddress.class.st @@ -4,9 +4,11 @@ Class { #instVars : [ 'place', 'street', - 'plz' + 'plz', + 'plzType', + 'occupants' ], - #category : 'Magritte-Tests-Model-Mocks' + #category : #'Magritte-Tests-Model-Mocks' } { #category : #comparing } @@ -76,6 +78,7 @@ MAMockAddress >> descriptionPlz [ ^ MANumberDescription new accessor: #plz; label: 'PLZ'; + default: 1; yourself ] @@ -84,7 +87,6 @@ MAMockAddress >> descriptionStreet [ ^ MAStringDescription new accessor: #street; - label: 'Street'; yourself ] @@ -93,6 +95,25 @@ MAMockAddress >> hash [ ^ self street hash ] +{ #category : #accessing } +MAMockAddress >> occupants [ + ^ occupants +] + +{ #category : #accessing } +MAMockAddress >> occupants: anObject [ + occupants := anObject +] + +{ #category : #'acessing-magritte' } +MAMockAddress >> occupantsDescription [ + + ^ MAToManyRelationDescription new + accessor: #occupants; + classes: String withAllSubclasses; + yourself +] + { #category : #'accessing-generated' } MAMockAddress >> place [ ^ place @@ -113,6 +134,25 @@ MAMockAddress >> plz: anObject [ plz := anObject ] +{ #category : #accessing } +MAMockAddress >> plzType [ + ^ plzType +] + +{ #category : #accessing } +MAMockAddress >> plzType: anObject [ + plzType := anObject +] + +{ #category : #'acessing-magritte' } +MAMockAddress >> plzTypeDescription [ + + ^ MASingleOptionDescription new + accessor: #plzType; + options: Number withAllSubclasses; + yourself +] + { #category : #'accessing-generated' } MAMockAddress >> street [ ^ street