diff --git a/src/Soil-Core-Tests/SoilBackupTest.class.st b/src/Soil-Core-Tests/SoilBackupTest.class.st index 86be5603..54da88fd 100644 --- a/src/Soil-Core-Tests/SoilBackupTest.class.st +++ b/src/Soil-Core-Tests/SoilBackupTest.class.st @@ -39,13 +39,10 @@ SoilBackupTest >> testBackupWithIndex [ dict := SoilSkipListDictionary new keySize: 8; maxLevel: 16. - - tx := soil newTransaction. - tx root: dict. - dict at: #foo put: (SoilTestNestedObject new label: #indexed). object := SoilTestClusterRoot new nested: dict. + tx := soil newTransaction. tx root: dict. tx commit. soil backupTo: self backupPath. @@ -63,18 +60,15 @@ SoilBackupTest >> testBackupWithIndexRemoval [ | tx backup tx2 dict object | "removed keys in indexes get objectId 0:0. On backup time we only need to copy the non-removed" - dict := SoilSkipListDictionary new keySize: 8; maxLevel: 16. - tx := soil newTransaction. - tx root: dict. - dict at: #foo put: (SoilTestNestedObject new label: #indexed). dict at: #bar put: (SoilTestNestedObject new label: #bar). object := SoilTestClusterRoot new nested: dict. - + tx := soil newTransaction. + tx root: dict. tx commit. tx2 := soil newTransaction. tx2 root removeKey: #bar. diff --git a/src/Soil-Core-Tests/SoilIndexedDictionaryTest.class.st b/src/Soil-Core-Tests/SoilIndexedDictionaryTest.class.st index 913cda4e..011ccbb2 100644 --- a/src/Soil-Core-Tests/SoilIndexedDictionaryTest.class.st +++ b/src/Soil-Core-Tests/SoilIndexedDictionaryTest.class.st @@ -74,6 +74,16 @@ SoilIndexedDictionaryTest >> testAddAndRemoveExistingList [ ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testAddAndRemoveOnNewList [ + self + shouldnt: [ dict at: #foo put: #bar ] + raise: Error. + self assert: (dict at: #foo) equals: #bar. + dict removeKey: #foo. + self assert: dict size equals: 0 +] + { #category : #tests } SoilIndexedDictionaryTest >> testAddToExistingEmptyList [ | tx tx2 tx3 tx4 | @@ -129,6 +139,31 @@ SoilIndexedDictionaryTest >> testAddToExistingNonEmptyList [ self assert: (tx4 root at: #foo) equals: #bar. ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testAddToNewList [ + self + shouldnt: [ dict at: #foo put: #bar ] + raise: Error. + self assert: (dict at: #foo) equals: #bar +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testAt [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + self assert: (dict at: #foo2) equals: #bar2. + self should: [dict at: #ff] raise: KeyNotFound +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testAtIndex [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + self assert: (dict atIndex: 1) equals: #bar2 +] + { #category : #tests } SoilIndexedDictionaryTest >> testAtIndexWithTransaction [ | tx tx2 | @@ -148,8 +183,9 @@ SoilIndexedDictionaryTest >> testAtIndexWithTransaction [ SoilIndexedDictionaryTest >> testConcurrentAddKey [ | tx1 tx2 tx3 | tx1 := soil newTransaction. - tx1 root: dict. - dict at: #one put: #onevalue. + tx1 root: (dict + at: #one put: #onevalue; + yourself). tx1 commit. tx2 := soil newTransaction. "After creating tx2 we open a concurrent transaction and add a key to @@ -166,12 +202,12 @@ SoilIndexedDictionaryTest >> testConcurrentAddKey [ SoilIndexedDictionaryTest >> testConcurrentDo [ | tx1 tx2 tx3 col | tx1 := soil newTransaction. - tx1 root: dict. - dict + tx1 root: (dict at: #one put: #onevalue; at: #two put: #twovalue; at: #three put: #threevalue; - at: #four put: #fourvalue. + at: #four put: #fourvalue; + yourself). tx1 commit. tx2 := soil newTransaction. "After creating tx2 we open a concurrent transaction and add a key to @@ -191,10 +227,9 @@ SoilIndexedDictionaryTest >> testConcurrentDo [ SoilIndexedDictionaryTest >> testConcurrentIsEmpty [ | tx1 tx2 tx3 | tx1 := soil newTransaction. - tx1 root: dict. - dict - at: #one - put: #onevalue. + tx1 root: (dict + at: #one put: #onevalue; + yourself). tx1 commit. tx2 := soil newTransaction. "After creating tx2 we open a concurrent transaction and add a key to @@ -213,10 +248,9 @@ SoilIndexedDictionaryTest >> testConcurrentIsEmpty [ SoilIndexedDictionaryTest >> testConcurrentRemoveKey [ | tx1 tx2 tx3 | tx1 := soil newTransaction. - tx1 root: dict. - dict - at: #one - put: #onevalue. + tx1 root: (dict + at: #one put: #onevalue; + yourself). tx1 commit. tx2 := soil newTransaction. "After creating tx2 we open a concurrent transaction and remove a key to @@ -230,6 +264,19 @@ SoilIndexedDictionaryTest >> testConcurrentRemoveKey [ ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testDo [ + | counter | + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + counter := 0. + dict do: [ :each | + self assert: (each beginsWith: 'bar'). + counter := counter + 1]. + self assert: counter equals: 2 +] + { #category : #tests } SoilIndexedDictionaryTest >> testDoWithTransAction [ | tx tx1 tx2 counter | @@ -268,6 +315,33 @@ SoilIndexedDictionaryTest >> testDoWithTransAction [ ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testFirst [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + "first in key order" + self assert: dict first equals: #bar. + self assert: (dict first: 1) first equals: #bar. +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testFirstAssociation [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + "firstAssocation in key order" + self assert: dict firstAssociation equals: #foo->#bar. +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testFirstAssociationWithSingleRemovedItem [ + + dict at: #foo put: #bar. + dict removeKey: #foo. + self assert: dict firstAssociation equals: nil +] + { #category : #tests } SoilIndexedDictionaryTest >> testFirstAssociationWithTransaction [ | tx tx2 | @@ -284,6 +358,14 @@ SoilIndexedDictionaryTest >> testFirstAssociationWithTransaction [ ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testFirstWithSingleRemovedItem [ + + dict at: #foo put: #bar. + dict removeKey: #foo. + self assert: dict first equals: nil +] + { #category : #tests } SoilIndexedDictionaryTest >> testFirstWithTransaction [ | tx tx2 | @@ -385,6 +467,31 @@ SoilIndexedDictionaryTest >> testIsEmpty [ self deny: tx1 root isEmpty. ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testLast [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + "last in key order" + self assert: dict last equals: #bar2 +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testLastAssociation [ + dict at: #foo2 put: #bar2. + dict at: #foo put: #bar. + + "last association in key order" + self assert: dict lastAssociation equals: #foo2->#bar2 +] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testLastAssociationWithSingleRemovedItem [ + dict at: #foo put: #bar. + dict removeKey: #foo. + self assert: dict lastAssociation equals: nil +] + { #category : #tests } SoilIndexedDictionaryTest >> testLastAssociationWithTransaction [ | tx tx2 | @@ -401,6 +508,13 @@ SoilIndexedDictionaryTest >> testLastAssociationWithTransaction [ self assert: tx2 root lastAssociation equals: 2->#two ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testLastWithSingleRemovedItem [ + dict at: #foo put: #bar. + dict removeKey: #foo. + self assert: dict last equals: nil +] + { #category : #tests } SoilIndexedDictionaryTest >> testLastWithTransaction [ | tx tx2 | @@ -433,6 +547,14 @@ SoilIndexedDictionaryTest >> testLastWithTransactionRemoveLast [ self assert: tx2 root last equals: #one ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testNextAfter [ + dict at: 1 put: #bar. + dict at: 2 put: #bar2. + + self assert:( dict nextAfter: 1) equals: #bar2 +] + { #category : #tests } SoilIndexedDictionaryTest >> testNextAfterWithTransaction [ | tx tx2 | @@ -448,6 +570,17 @@ SoilIndexedDictionaryTest >> testNextAfterWithTransaction [ self assert: (tx2 root nextAfter: 1) value equals: #two ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testRemoveKey [ + dict at: #foo put: #bar. + dict at: #foo2 put: #bar2. + + dict removeKey: #foo. + self assert: dict size equals: 1. + + self should: [ dict removeKey: #blah ] raise: KeyNotFound +] + { #category : #tests } SoilIndexedDictionaryTest >> testRemoveKeyIfAbsentWithTransaction [ @@ -503,6 +636,14 @@ SoilIndexedDictionaryTest >> testRemoveKeyWithTwoTransactions [ self should: [tx commit] raise: SoilObjectHasConcurrentChange ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testSecond [ + dict at: #foo put: #bar. + dict at: #foo2 put: #bar2. + + self assert: dict second equals: #bar2 +] + { #category : #tests } SoilIndexedDictionaryTest >> testSecondWithTransaction [ | tx tx2 | @@ -518,6 +659,14 @@ SoilIndexedDictionaryTest >> testSecondWithTransaction [ self assert: tx2 root second equals: #two ] +{ #category : #tests } +SoilIndexedDictionaryTest >> testSize [ + dict at: #foo put: #bar. + dict at: #foo2 put: #bar2. + + self assert: dict size equals: 2 +] + { #category : #tests } SoilIndexedDictionaryTest >> testSizeWithTransaction [ | tx tx2 | @@ -532,3 +681,12 @@ SoilIndexedDictionaryTest >> testSizeWithTransaction [ "and test last" self assert: tx2 root size equals: 2 ] + +{ #category : #tests } +SoilIndexedDictionaryTest >> testValues [ + dict at: #foo put: #bar. + dict at: #foo2 put: #bar2. + + self assert: (dict values includes: 'bar'). + self assert: (dict values includes: 'bar2') +] diff --git a/src/Soil-Core/SoilIndexedDictionary.class.st b/src/Soil-Core/SoilIndexedDictionary.class.st index 397e41ce..2dd0e269 100644 --- a/src/Soil-Core/SoilIndexedDictionary.class.st +++ b/src/Soil-Core/SoilIndexedDictionary.class.st @@ -34,37 +34,44 @@ SoilIndexedDictionary >> at: key [ { #category : #accessing } SoilIndexedDictionary >> at: key ifAbsent: aBlock [ | objectId | - - objectId := (self basicAt: key ifAbsent: [ ^ aBlock value ]) asSoilObjectId. - ^ transaction proxyForObjectId: objectId - + ^ transaction + ifNotNil: [ + objectId := (self basicAt: key ifAbsent: [ ^ aBlock value ]) asSoilObjectId. + transaction proxyForObjectId: objectId ] + ifNil: [ newValues at: key ifAbsent: aBlock ] ] { #category : #accessing } SoilIndexedDictionary >> at: key put: anObject [ - - | objectId iterator | - objectId := transaction makeRoot: anObject. - "binKey := (key asSkipListKeyOfSize: index keySize) asInteger." - iterator := self index newIterator. - (iterator at: key put: objectId) ifNotNil: [ :value | - oldValues - at: key - ifAbsentPut: objectId ]. - "if there has been a prior removal of the key this new addition invalidates it" - removedValues removeKey: key ifAbsent: nil. - newValues at: key put: objectId + ^ transaction + ifNotNil: [ + | objectId iterator | + objectId := transaction makeRoot: anObject. + "binKey := (key asSkipListKeyOfSize: index keySize) asInteger." + iterator := self index newIterator. + (iterator at: key put: objectId) ifNotNil: [ :value | + oldValues + at: key + ifAbsentPut: objectId ]. + "if there has been a prior removal of the key this new + addition invalidates it" + removedValues removeKey: key ifAbsent: nil. + newValues at: key put: objectId. ] + ifNil: [ + newValues at: key put: anObject ] ] { #category : #accessing } SoilIndexedDictionary >> atIndex: anInteger [ - - ^ (self index atIndex: anInteger) ifNotNil: [ :bytes | - transaction - objectId: bytes asSoilObjectId - ifVisible: [ :objectId | - objectId asSoilObjectProxy transaction: transaction ] - ifHidden: nil ] + ^ transaction + ifNotNil: [ + (self index atIndex: anInteger) + ifNotNil: [ :bytes | + transaction + objectId: bytes asSoilObjectId + ifVisible: [:objectId | (objectId asSoilObjectProxy) transaction: transaction ] + ifHidden: nil ] ] + ifNil: [ (newValues associations at: anInteger) value ] ] { #category : #accessing } @@ -89,35 +96,46 @@ SoilIndexedDictionary >> createIndex [ { #category : #enumerating } SoilIndexedDictionary >> do: aBlock [ - | iterator assoc | - iterator := self index newIterator. - [ (assoc := iterator nextAssociation) notNil ] whileTrue: [ - (self - restoreValue: assoc value - forKey: assoc key - iterator: iterator) ifNotNil: [ :objectId | - aBlock value: (transaction proxyForObjectId: objectId)]] + transaction + ifNotNil: [ + | iterator assoc | + iterator := self index newIterator. + [ (assoc := iterator nextAssociation) notNil ] whileTrue: [ + (self + restoreValue: assoc value + forKey: assoc key + iterator: iterator) ifNotNil: [ :objectId | + aBlock value: (transaction proxyForObjectId: objectId) ] ] ] + ifNil: [ + newValues valuesDo: [ :each | aBlock value: each ] ] ] { #category : #accessing } SoilIndexedDictionary >> first [ - ^ self proxyFromByteArray: self index newIterator first - + ^ transaction + ifNotNil: [ self proxyFromByteArray: self index newIterator first ] + ifNil: [ + self newValuesSortedByKeyOrder ifNotEmpty: [:nv | nv first value ] ifEmpty: nil] ] { #category : #accessing } SoilIndexedDictionary >> first: anInteger [ - - ^ (self index first: anInteger) - collect: [ :each | self proxyFromByteArray: each ] + ^ transaction + ifNotNil: [ + (self index first: anInteger) + collect: [ :each | self proxyFromByteArray: each ] ] + ifNil: [ (self newValuesSortedByKeyOrder first: anInteger) collect: #value ] ] { #category : #accessing } SoilIndexedDictionary >> firstAssociation [ - ^ index newIterator firstAssociation ifNotNil: [ :assoc | - assoc key -> (transaction objectWithId: assoc value asSoilObjectId) ] - + ^ transaction + ifNotNil: [ + index newIterator firstAssociation ifNotNil: [ :assoc | + assoc key -> (transaction objectWithId: assoc value asSoilObjectId) ] ] + ifNil: [ + self newValuesSortedByKeyOrder ifNotEmpty: [:nv | nv first ] ifEmpty: nil] ] { #category : #testing } @@ -174,12 +192,13 @@ SoilIndexedDictionary >> isEmpty [ | iterator| iterator := self index newIterator. iterator currentPage: self index firstPage. - ^ self index isEmpty or: [ + ^ newValues isEmpty and: [ self index isEmpty + or: [ "all items might be removed and not restorable" (self index firstPage items allSatisfy: [ :each | (self restoreValue: each value forKey: each key - iterator: iterator)isNil] )] + iterator: iterator)isNil] )]] ] { #category : #testing } @@ -194,14 +213,21 @@ SoilIndexedDictionary >> keySize: anInteger [ { #category : #accessing } SoilIndexedDictionary >> last [ - ^ self proxyFromByteArray: self index newIterator last + ^ transaction + ifNotNil: [ self proxyFromByteArray: self index newIterator last ] + ifNil: [ + self newValuesSortedByKeyOrder ifNotEmpty: [:nv | nv last value ] ifEmpty: nil ] ] { #category : #accessing } SoilIndexedDictionary >> lastAssociation [ - ^ index newIterator lastAssociation ifNotNil: [ :assoc | - assoc key -> (transaction objectWithId: assoc value asSoilObjectId) ] + ^ transaction + ifNotNil: [ + index newIterator lastAssociation ifNotNil: [ :assoc | + assoc key -> (transaction objectWithId: assoc value asSoilObjectId) ] ] + ifNil: [ + self newValuesSortedByKeyOrder ifNotEmpty: [:nv | nv last ] ifEmpty: nil ] ] { #category : #private } @@ -217,9 +243,21 @@ SoilIndexedDictionary >> maxLevel: anInteger [ ] +{ #category : #accessing } +SoilIndexedDictionary >> newValuesSortedByKeyOrder [ + + ^ newValues associations sort: [ :a :b | + (a key asSkipListKeyOfSize: self index keySize) asInteger + < (b key asSkipListKeyOfSize: self index keySize) asInteger ] +] + { #category : #accessing } SoilIndexedDictionary >> nextAfter: key [ | iterator | + transaction ifNil: [ + | newValueSorted | + newValueSorted := self newValuesSortedByKeyOrder. + ^ (newValueSorted after: (newValues associationAt: key)) value ]. iterator := self index newIterator find: key asInteger; @@ -229,6 +267,13 @@ SoilIndexedDictionary >> nextAfter: key [ assoc key -> (transaction objectWithId: assoc value asSoilObjectId) ] ] +{ #category : #private } +SoilIndexedDictionary >> prepareNewValues [ + newValues copy keysAndValuesDo: [ :key :object | + object isObjectId ifFalse: [ + newValues at: key put: (transaction makeRoot: object) ] ] +] + { #category : #printing } SoilIndexedDictionary >> printOn: aStream [ super printOn: aStream. @@ -251,14 +296,21 @@ SoilIndexedDictionary >> removeKey: key [ { #category : #removing } SoilIndexedDictionary >> removeKey: key ifAbsent: aBlock [ | iterator v | - "remove from newValues as there could be a new at:put: on that key but removing the key will remove the value again" - newValues removeKey: key ifAbsent: nil. - iterator := self index newIterator. - v := self basicAt: key ifAbsent: [^ aBlock value]. - removedValues - at: key - put: v asSoilObjectId. - ^ iterator at: key put: (SoilObjectId segment: 0 index: 0) + ^ transaction + ifNotNil: [ + "remove from newValues as there could be a new at:put: on that + key but removing the key will remove the value again" + newValues removeKey: key ifAbsent: nil. + iterator := self index newIterator. + v := self basicAt: key ifAbsent: [^ aBlock value]. + removedValues + at: key + put: v asSoilObjectId. + iterator at: key put: (SoilObjectId segment: 0 index: 0) ] + ifNil: [ + removedValues + at: key + put: (newValues removeKey: key ifAbsent: [ ^ aBlock value ]) ] ] { #category : #private } @@ -281,18 +333,23 @@ SoilIndexedDictionary >> restoreValue: value forKey: key iterator: iterator [ { #category : #accessing } SoilIndexedDictionary >> second [ - ^ self proxyFromByteArray: (index newIterator first; next) + ^ transaction + ifNotNil: [ self proxyFromByteArray: (index newIterator first; next) ] + ifNil: [ self newValuesSortedByKeyOrder second value ] ] { #category : #accessing } SoilIndexedDictionary >> size [ - ^ self index size + ^ transaction + ifNotNil: [ self index size ] + ifNil: [ newValues size ] ] { #category : #serializing } SoilIndexedDictionary >> soilBasicSerialize: aSerializer [ transaction ifNil: [ - transaction := aSerializer transaction]. + transaction := aSerializer transaction. + self prepareNewValues ]. super soilBasicSerialize: aSerializer. aSerializer registerIndexId: id. ] @@ -301,6 +358,14 @@ SoilIndexedDictionary >> soilBasicSerialize: aSerializer [ SoilIndexedDictionary >> soilClusterRootIn: aTransaction [ transaction ifNotNil: [ ^ self ]. transaction := aTransaction. + newValues copy keysAndValuesDo: [ :key :object | | obj | + obj := object isObjectId + ifTrue: [ object ] + ifFalse: [ + newValues + at: key + put: (transaction makeRoot: object) ]. + self index newIterator at: key put: obj ]. transaction markDirty: self ]