From 1d2de854d1cc61750ec3b61015f78831bcf7653e Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Sat, 24 Dec 2022 09:53:29 -0500 Subject: [PATCH] refactor!(collections): don't panic when initializing collections (#14323) --- collections/collections_test.go | 5 +- collections/go.mod | 6 +- collections/go.sum | 36 +++++++--- collections/item.go | 4 +- collections/item_test.go | 9 ++- collections/iter.go | 6 +- collections/iter_test.go | 27 +++++--- collections/keyset.go | 2 +- collections/keyset_test.go | 6 +- collections/map.go | 11 ++- collections/map_test.go | 6 +- collections/schema.go | 118 +++++++++++++++++++++++++------- collections/schema_test.go | 72 +++++++++++++------ collections/sequence.go | 2 +- collections/sequence_test.go | 5 +- 15 files changed, 227 insertions(+), 88 deletions(-) diff --git a/collections/collections_test.go b/collections/collections_test.go index 4602059ab3..475b06ac4e 100644 --- a/collections/collections_test.go +++ b/collections/collections_test.go @@ -2,11 +2,12 @@ package collections import ( "context" - "cosmossdk.io/core/store" - db "github.com/tendermint/tm-db" "math" "testing" + "cosmossdk.io/core/store" + db "github.com/tendermint/tm-db" + "github.com/stretchr/testify/require" ) diff --git a/collections/go.mod b/collections/go.mod index c571de307b..b2bebcfe97 100644 --- a/collections/go.mod +++ b/collections/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( cosmossdk.io/core v0.3.4 + github.com/hashicorp/go-multierror v1.1.1 // TODO: remove with go 1.20 release github.com/stretchr/testify v1.8.1 github.com/tendermint/tm-db v0.6.7 ) @@ -17,13 +18,16 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/klauspost/compress v1.15.12 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect + github.com/onsi/gomega v1.20.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect diff --git a/collections/go.sum b/collections/go.sum index a9dd03c42e..b1b8612aa5 100644 --- a/collections/go.sum +++ b/collections/go.sum @@ -33,8 +33,9 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -59,6 +60,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -75,15 +81,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -105,6 +114,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -115,19 +125,25 @@ github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -140,7 +156,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -149,8 +167,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -171,7 +192,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/collections/item.go b/collections/item.go index 4fc0ecaf40..8cef33e121 100644 --- a/collections/item.go +++ b/collections/item.go @@ -12,12 +12,12 @@ type Item[V any] Map[noKey, V] // Name and prefix must be unique within the schema and name must match the format specified by NameRegex, or // else this method will panic. func NewItem[V any]( - schema Schema, + schema *SchemaBuilder, prefix Prefix, name string, valueCodec ValueCodec[V], ) Item[V] { - item := (Item[V])(newMap[noKey, V](schema, prefix, name, noKey{}, valueCodec)) + item := (Item[V])(newMap[noKey](schema, prefix, name, noKey{}, valueCodec)) schema.addCollection(item) return item } diff --git a/collections/item_test.go b/collections/item_test.go index 8d439f3de8..2af8c5d515 100644 --- a/collections/item_test.go +++ b/collections/item_test.go @@ -8,10 +8,13 @@ import ( func TestItem(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - item := NewItem(schema, NewPrefix("item"), "item", Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + item := NewItem(schemaBuilder, NewPrefix("item"), "item", Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) + // set - err := item.Set(ctx, 1000) + err = item.Set(ctx, 1000) require.NoError(t, err) // get diff --git a/collections/iter.go b/collections/iter.go index 3a4a2a5214..c99d3b9bc1 100644 --- a/collections/iter.go +++ b/collections/iter.go @@ -104,8 +104,10 @@ func (r *Range[K]) Descending() *Range[K] { } // test sentinel error -var errRange = errors.New("collections: range error") -var errOrder = errors.New("collections: invalid order") +var ( + errRange = errors.New("collections: range error") + errOrder = errors.New("collections: invalid order") +) func (r *Range[K]) RangeValues() (prefix *K, start *Bound[K], end *Bound[K], order Order, err error) { if r.prefix != nil && (r.end != nil || r.start != nil) { diff --git a/collections/iter_test.go b/collections/iter_test.go index 3434090a5d..a7aaeae48d 100644 --- a/collections/iter_test.go +++ b/collections/iter_test.go @@ -2,14 +2,17 @@ package collections import ( "fmt" - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestIteratorBasic(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - m := NewMap(schema, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + m := NewMap(schemaBuilder, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) for i := uint64(1); i <= 2; i++ { require.NoError(t, m.Set(ctx, fmt.Sprintf("%d", i), i)) @@ -55,8 +58,10 @@ func TestIteratorBasic(t *testing.T) { func TestIteratorKeyValues(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - m := NewMap(schema, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + m := NewMap(schemaBuilder, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) for i := uint64(0); i <= 5; i++ { require.NoError(t, m.Set(ctx, fmt.Sprintf("%d", i), i)) @@ -102,8 +107,10 @@ func TestIteratorKeyValues(t *testing.T) { func TestIteratorPrefixing(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - m := NewMap(schema, NewPrefix("cool"), "cool", StringKey, Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + m := NewMap(schemaBuilder, NewPrefix("cool"), "cool", StringKey, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) require.NoError(t, m.Set(ctx, "A1", 11)) require.NoError(t, m.Set(ctx, "A2", 12)) @@ -118,8 +125,10 @@ func TestIteratorPrefixing(t *testing.T) { func TestIteratorRanging(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - m := NewMap(schema, NewPrefix("cool"), "cool", Uint64Key, Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + m := NewMap(schemaBuilder, NewPrefix("cool"), "cool", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) for i := uint64(0); i <= 7; i++ { require.NoError(t, m.Set(ctx, i, i)) diff --git a/collections/keyset.go b/collections/keyset.go index 0e3e90b306..40d096e082 100644 --- a/collections/keyset.go +++ b/collections/keyset.go @@ -12,7 +12,7 @@ import ( type KeySet[K any] Map[K, noValue] // NewKeySet returns a KeySet given a Schema, Prefix a human name for the collection and a KeyCodec for the key K. -func NewKeySet[K any](schema Schema, prefix Prefix, name string, keyCodec KeyCodec[K]) KeySet[K] { +func NewKeySet[K any](schema *SchemaBuilder, prefix Prefix, name string, keyCodec KeyCodec[K]) KeySet[K] { return (KeySet[K])(NewMap(schema, prefix, name, keyCodec, noValueCodec)) } diff --git a/collections/keyset_test.go b/collections/keyset_test.go index 5fe3ec8078..817ff0b5aa 100644 --- a/collections/keyset_test.go +++ b/collections/keyset_test.go @@ -1,13 +1,14 @@ package collections import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestKeySet(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) + schema := NewSchemaBuilder(sk) ks := NewKeySet(schema, NewPrefix("keyset"), "keyset", StringKey) // set @@ -65,5 +66,4 @@ func Test_noValue(t *testing.T) { _, err = noValueCodec.Decode([]byte("bad")) require.ErrorIs(t, err, ErrEncoding) - } diff --git a/collections/map.go b/collections/map.go index 16d2fb9b77..298baa4cdd 100644 --- a/collections/map.go +++ b/collections/map.go @@ -24,25 +24,25 @@ type Map[K, V any] struct { // Name and prefix must be unique within the schema and name must match the format specified by NameRegex, or // else this method will panic. func NewMap[K, V any]( - schema Schema, + schemaBuilder *SchemaBuilder, prefix Prefix, name string, keyCodec KeyCodec[K], valueCodec ValueCodec[V], ) Map[K, V] { - m := newMap(schema, prefix, name, keyCodec, valueCodec) - schema.addCollection(m) + m := newMap(schemaBuilder, prefix, name, keyCodec, valueCodec) + schemaBuilder.addCollection(m) return m } func newMap[K, V any]( - schema Schema, prefix Prefix, name string, + schemaBuilder *SchemaBuilder, prefix Prefix, name string, keyCodec KeyCodec[K], valueCodec ValueCodec[V], ) Map[K, V] { return Map[K, V]{ kc: keyCodec, vc: valueCodec, - sa: schema.storeAccessor, + sa: schemaBuilder.schema.storeAccessor, prefix: prefix.Bytes(), name: name, } @@ -60,7 +60,6 @@ func (m Map[K, V]) getPrefix() []byte { // Errors with ErrEncoding if key or value encoding fails. func (m Map[K, V]) Set(ctx context.Context, key K, value V) error { bytesKey, err := encodeKeyWithPrefix(m.prefix, m.kc, key) - if err != nil { return err } diff --git a/collections/map_test.go b/collections/map_test.go index 2db6e71555..a01879cf38 100644 --- a/collections/map_test.go +++ b/collections/map_test.go @@ -8,8 +8,10 @@ import ( func TestMap(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) - m := NewMap(schema, NewPrefix("hi"), "m", Uint64Key, Uint64Value) + schemaBuilder := NewSchemaBuilder(sk) + m := NewMap(schemaBuilder, NewPrefix("hi"), "m", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) // test not has has, err := m.Has(ctx, 1) diff --git a/collections/schema.go b/collections/schema.go index e813b1382a..49c9553bce 100644 --- a/collections/schema.go +++ b/collections/schema.go @@ -2,11 +2,102 @@ package collections import ( "context" - "cosmossdk.io/core/store" "fmt" "regexp" + "strings" + + "cosmossdk.io/core/store" + + "github.com/hashicorp/go-multierror" ) +// SchemaBuilder is used for building schemas. The Build method should always +// be called after all collections have been initialized. Initializing new +// collections with the builder after initialization will result in panics. +type SchemaBuilder struct { + schema *Schema + err *multierror.Error +} + +// NewSchemaBuilder creates a new schema builder from the provided store key. +// Callers should always call the SchemaBuilder.Build method when they are +// done adding collections to the schema. +func NewSchemaBuilder(service store.KVStoreService) *SchemaBuilder { + return &SchemaBuilder{ + schema: &Schema{ + storeAccessor: service.OpenKVStore, + collectionsByName: map[string]collection{}, + collectionsByPrefix: map[string]collection{}, + }, + } +} + +// Build should be called after all collections that are part of the schema +// have been initialized in order to get a reference to the Schema. It is +// important to check the returned error for any initialization errors. +// The SchemaBuilder CANNOT be used after Build is called - doing so will +// result in panics. +func (s *SchemaBuilder) Build() (Schema, error) { + if s.err != nil { + return Schema{}, s.err + } + + // check for any overlapping prefixes + for prefix := range s.schema.collectionsByPrefix { + for prefix2 := range s.schema.collectionsByPrefix { + // don't compare the prefix to itself + if prefix == prefix2 { + continue + } + + // if one prefix is the prefix of the other we have an overlap and + // this schema is corrupt + if strings.HasPrefix(prefix, prefix2) { + return Schema{}, fmt.Errorf("schema has overlapping prefixes 0x%x and 0x%x", prefix, prefix2) + } + } + } + + if s.schema == nil { + // explicit panic to avoid nil pointer dereference + panic("schema is nil") + } + + schema := *s.schema + + s.schema = nil // this makes the builder unusable + + return schema, nil +} + +func (s *SchemaBuilder) addCollection(collection collection) { + prefix := collection.getPrefix() + name := collection.getName() + + if _, ok := s.schema.collectionsByPrefix[string(prefix)]; ok { + s.err = multierror.Append(s.err, fmt.Errorf("prefix %v already taken within schema", prefix)) + return + } + + if _, ok := s.schema.collectionsByName[name]; ok { + s.err = multierror.Append(s.err, fmt.Errorf("name %s already taken within schema", name)) + return + } + + if !nameRegex.MatchString(name) { + s.err = multierror.Append(s.err, fmt.Errorf("name must match regex %s, got %s", NameRegex, name)) + return + } + + s.schema.collectionsByPrefix[string(prefix)] = collection + s.schema.collectionsByName[name] = collection +} + +// NameRegex is the regular expression that all valid collection names must match. +const NameRegex = "[A-Za-z][A-Za-z0-9_]*" + +var nameRegex = regexp.MustCompile("^" + NameRegex + "$") + // Schema specifies a group of collections stored within the storage specified // by a single store key. All the collections within the schema must have a // unique binary prefix and human-readable name. Schema will eventually include @@ -47,28 +138,3 @@ func NewSchemaFromAccessor(accessor func(context.Context) store.KVStore) Schema collectionsByPrefix: map[string]collection{}, } } - -func (s Schema) addCollection(collection collection) { - prefix := collection.getPrefix() - name := collection.getName() - - if _, ok := s.collectionsByPrefix[string(prefix)]; ok { - panic(fmt.Errorf("prefix %v already taken within schema", prefix)) - } - - if _, ok := s.collectionsByName[name]; ok { - panic(fmt.Errorf("name %s already taken within schema", name)) - } - - if !nameRegex.MatchString(name) { - panic(fmt.Errorf("name must match regex %s, got %s", NameRegex, name)) - } - - s.collectionsByPrefix[string(prefix)] = collection - s.collectionsByName[name] = collection -} - -// NameRegex is the regular expression that all valid collection names must match. -const NameRegex = "[A-Za-z][A-Za-z0-9_]*" - -var nameRegex = regexp.MustCompile("^" + NameRegex + "$") diff --git a/collections/schema_test.go b/collections/schema_test.go index 554316ea96..89db0c2977 100644 --- a/collections/schema_test.go +++ b/collections/schema_test.go @@ -1,8 +1,9 @@ package collections import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestNameRegex(t *testing.T) { @@ -14,27 +15,58 @@ func TestNameRegex(t *testing.T) { require.NotRegexp(t, nameRegex, "abc-xyz") } -func TestAddCollection(t *testing.T) { - require.NotPanics(t, func() { - schema := NewSchema(nil) - NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) - NewMap(schema, NewPrefix(2), "def", Uint64Key, Uint64Value) - }) +func TestGoodSchema(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schemaBuilder, NewPrefix(2), "def", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) +} - require.PanicsWithError(t, "name must match regex [A-Za-z][A-Za-z0-9_]*, got 123", func() { - schema := NewSchema(nil) - NewMap(schema, NewPrefix(1), "123", Uint64Key, Uint64Value) - }) +func TestBadName(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix(1), "123", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.ErrorContains(t, err, "name must match regex") +} - require.PanicsWithError(t, "prefix [1] already taken within schema", func() { - schema := NewSchema(nil) - NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) - NewMap(schema, NewPrefix(1), "def", Uint64Key, Uint64Value) - }) +func TestDuplicatePrefix(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schemaBuilder, NewPrefix(1), "def", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.ErrorContains(t, err, "prefix [1] already taken") +} - require.PanicsWithError(t, "name abc already taken within schema", func() { - schema := NewSchema(nil) - NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) - NewMap(schema, NewPrefix(2), "abc", Uint64Key, Uint64Value) +func TestDuplicateName(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schemaBuilder, NewPrefix(2), "abc", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.ErrorContains(t, err, "name abc already taken") +} + +func TestOverlappingPrefixes(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix("ab"), "ab", Uint64Key, Uint64Value) + NewMap(schemaBuilder, NewPrefix("abc"), "abc", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.ErrorContains(t, err, "overlapping prefixes") +} + +func TestSchemaBuilderCantBeUsedAfterBuild(t *testing.T) { + sk, _ := deps() + schemaBuilder := NewSchemaBuilder(sk) + NewMap(schemaBuilder, NewPrefix(1), "abc", Uint64Key, Uint64Value) + _, err := schemaBuilder.Build() + require.NoError(t, err) + // can't use schema builder safely after calling build + require.Panics(t, func() { + NewMap(schemaBuilder, NewPrefix(2), "def", Uint64Key, Uint64Value) }) } diff --git a/collections/sequence.go b/collections/sequence.go index 61be844fa4..4b4b86beaa 100644 --- a/collections/sequence.go +++ b/collections/sequence.go @@ -13,7 +13,7 @@ type Sequence Item[uint64] // NewSequence instantiates a new sequence given // a Schema, a Prefix and humanised name for the sequence. -func NewSequence(schema Schema, prefix Prefix, name string) Sequence { +func NewSequence(schema *SchemaBuilder, prefix Prefix, name string) Sequence { return (Sequence)(NewItem(schema, prefix, name, Uint64Value)) } diff --git a/collections/sequence_test.go b/collections/sequence_test.go index 082e300066..b8202380b9 100644 --- a/collections/sequence_test.go +++ b/collections/sequence_test.go @@ -1,13 +1,14 @@ package collections import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestSequence(t *testing.T) { sk, ctx := deps() - schema := NewSchema(sk) + schema := NewSchemaBuilder(sk) seq := NewSequence(schema, NewPrefix(0), "sequence") // initially the first available number is DefaultSequenceStart n, err := seq.Peek(ctx)