diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7c0fce92..0797aea2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,7 +27,7 @@ jobs: run: go build -v ./cmd/main.go - name: Test - run: make test-unit + run: make test - name: Test for Data Race run: make test-race diff --git a/Makefile b/Makefile index e25afab2..0dabe089 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ run: make build && \ docker-compose up --build -test-unit: +test: env RACE=false OUT=internal/modules/admin/testdata make build-modules-test && \ env RACE=false OUT=echovault/testdata make build-modules-test && \ go clean -testcache && \ diff --git a/coverage/coverage.out b/coverage/coverage.out index d8957da8..0b038949 100644 --- a/coverage/coverage.out +++ b/coverage/coverage.out @@ -1,107 +1,4 @@ mode: set -github.com/echovault/echovault/internal/aof/preamble/store.go:45.62,46.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:46.36,48.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:51.71,52.36 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:52.36,54.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:57.88,58.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:58.36,60.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:63.95,64.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:64.36,66.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:69.65,70.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:70.36,72.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:75.86,81.52 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:81.52,84.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:85.60,85.61 0 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:88.2,88.33 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:88.33,90.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:93.2,93.46 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:93.46,95.17 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:95.17,97.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:98.3,99.17 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:99.17,101.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:102.3,102.15 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:105.2,105.19 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:108.52,115.16 5 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:115.16,117.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:120.2,120.44 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:120.44,122.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:124.2,124.46 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:124.46,126.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:128.2,128.44 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:128.44,130.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:133.2,133.39 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:133.39,135.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:137.2,137.12 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:140.45,141.21 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:141.21,143.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:146.2,146.47 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:146.47,148.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:150.2,151.16 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:151.16,153.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:155.2,155.17 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:155.17,157.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:159.2,161.49 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:161.49,163.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:165.2,165.56 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:165.56,167.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:169.2,169.12 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:172.43,176.2 3 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:179.110,181.26 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:181.26,182.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:182.36,183.12 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:185.3,185.43 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:185.43,187.4 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:189.2,189.35 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:189.35,191.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:192.2,192.14 1 1 -github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 -github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 -github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 -github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 -github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 -github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 -github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 -github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 -github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 -github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 -github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 -github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 -github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 -github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 -github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 -github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 -github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 -github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 -github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 github.com/echovault/echovault/internal/aof/log/store.go:46.60,47.34 1 1 github.com/echovault/echovault/internal/aof/log/store.go:47.34,49.3 1 1 github.com/echovault/echovault/internal/aof/log/store.go:52.61,53.34 1 1 @@ -157,6 +54,109 @@ github.com/echovault/echovault/internal/aof/log/store.go:191.2,191.47 1 0 github.com/echovault/echovault/internal/aof/log/store.go:191.47,193.3 1 0 github.com/echovault/echovault/internal/aof/log/store.go:194.2,194.12 1 0 github.com/echovault/echovault/internal/aof/log/store.go:197.41,201.2 3 1 +github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 +github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 +github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 +github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 +github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 +github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 +github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 +github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 +github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 +github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 +github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 +github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 +github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 +github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 +github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 +github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 +github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 +github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 +github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:45.62,46.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:46.36,48.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:51.71,52.36 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:52.36,54.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:57.88,58.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:58.36,60.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:63.95,64.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:64.36,66.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:69.65,70.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:70.36,72.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:75.86,81.52 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:81.52,84.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:85.60,85.61 0 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:88.2,88.33 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:88.33,90.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:93.2,93.46 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:93.46,95.17 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:95.17,97.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:98.3,99.17 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:99.17,101.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:102.3,102.15 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:105.2,105.19 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:108.52,115.16 5 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:115.16,117.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:120.2,120.44 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:120.44,122.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:124.2,124.46 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:124.46,126.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:128.2,128.44 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:128.44,130.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:133.2,133.39 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:133.39,135.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:137.2,137.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:140.45,141.21 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:141.21,143.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:146.2,146.47 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:146.47,148.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:150.2,151.16 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:151.16,153.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:155.2,155.17 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:155.17,157.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:159.2,161.49 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:161.49,163.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:165.2,165.56 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:165.56,167.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:169.2,169.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:172.43,176.2 3 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:179.110,181.26 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:181.26,182.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:182.36,183.12 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:185.3,185.43 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:185.43,187.4 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:189.2,189.35 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:189.35,191.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:192.2,192.14 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:35.29,42.2 3 1 github.com/echovault/echovault/internal/eviction/lfu.go:44.34,46.2 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:48.44,50.54 1 1 @@ -634,235 +634,288 @@ github.com/echovault/echovault/internal/modules/connection/commands.go:28.9,29.3 github.com/echovault/echovault/internal/modules/connection/commands.go:30.9,31.94 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:35.36,45.84 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:45.84,51.5 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:33.67,35.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:35.16,37.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:39.2,45.16 6 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:45.16,47.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:51.2,51.17 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:51.17,52.45 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:52.45,54.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:54.9,56.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:59.2,59.45 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:59.45,61.45 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:61.45,63.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:64.3,64.47 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:65.8,65.52 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:65.52,67.44 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:67.44,69.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:70.3,70.56 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:71.8,73.45 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:73.45,76.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:76.9,79.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:81.2,81.16 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:81.16,83.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:84.2,86.87 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:86.87,88.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:91.2,91.29 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:91.29,93.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:95.2,95.17 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:98.68,100.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:100.16,102.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:104.2,107.15 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:107.15,108.29 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:108.29,109.16 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:109.16,115.5 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:120.2,120.41 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:120.41,121.15 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:121.15,126.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:131.2,131.28 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:131.28,132.42 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:132.42,133.63 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:133.63,135.5 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:136.4,137.12 2 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:139.3,139.71 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:139.71,141.4 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:142.3,142.55 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:146.2,146.28 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:146.28,147.69 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:147.69,149.4 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:152.2,152.42 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:155.67,157.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:157.16,159.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:160.2,162.44 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:162.44,164.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:166.2,167.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:167.16,169.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:170.2,174.51 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:177.68,179.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:179.16,181.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:183.2,186.36 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:186.36,187.31 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:187.31,189.12 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:191.3,191.44 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:191.44,193.18 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:193.18,195.5 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:196.4,197.12 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:199.3,199.19 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:201.2,201.15 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:201.15,202.34 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:202.34,203.14 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:203.14,206.5 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:210.2,210.28 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:210.28,212.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:214.2,216.41 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:216.41,217.24 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:217.24,219.12 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:221.3,221.96 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:224.2,224.19 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:227.67,229.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:229.16,231.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:232.2,233.37 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:233.37,235.17 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:235.17,237.12 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:239.3,239.13 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:241.2,241.51 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:244.71,246.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:246.16,248.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:250.2,252.44 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:252.44,254.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:256.2,256.62 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:256.62,258.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:259.2,262.31 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:262.31,264.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:266.2,268.30 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:271.74,273.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:273.16,275.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:277.2,279.44 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:279.44,281.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:283.2,283.63 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:283.63,285.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:286.2,290.31 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:290.31,292.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:294.2,295.57 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:295.57,297.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:299.2,299.47 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:302.67,304.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:304.16,306.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:308.2,312.44 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:312.44,314.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:316.2,316.63 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:316.63,318.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:319.2,323.31 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:323.31,325.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:327.2,328.50 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:328.50,330.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:332.2,332.12 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:332.12,334.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:336.2,336.47 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:339.70,341.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:341.16,343.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:345.2,349.16 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:349.16,351.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:352.2,353.53 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:353.53,355.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:357.2,357.44 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:357.44,359.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:361.2,361.62 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:361.62,363.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:364.2,366.30 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:366.30,369.3 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:371.2,373.44 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:374.12,375.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:375.39,377.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:378.3,378.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:379.12,380.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:380.39,382.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:383.3,383.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:384.12,385.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:385.39,387.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:388.3,388.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:388.39,390.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:391.3,391.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:392.12,393.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:393.39,394.40 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:394.40,396.5 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:397.4,397.58 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:399.3,399.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:400.10,401.82 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:404.2,404.30 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:407.72,409.16 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:409.16,411.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:413.2,417.16 3 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:417.16,419.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:420.2,421.55 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:421.55,423.3 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:425.2,425.44 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:425.44,427.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:429.2,429.62 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:429.62,431.3 1 0 -github.com/echovault/echovault/internal/modules/generic/commands.go:432.2,434.30 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:434.30,437.3 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:439.2,441.44 2 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:442.12,443.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:443.39,445.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:446.3,446.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:447.12,448.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:448.39,450.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:451.3,451.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:452.12,453.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:453.39,455.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:456.3,456.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:456.39,458.4 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:459.3,459.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:460.12,461.39 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:461.39,462.40 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:462.40,464.5 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:465.4,465.58 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:467.3,467.57 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:468.10,469.82 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:472.2,472.30 1 1 -github.com/echovault/echovault/internal/modules/generic/commands.go:475.36,644.2 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:23.73,24.34 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:24.34,26.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:27.2,31.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:34.74,35.25 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:35.25,37.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:38.2,39.30 2 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:39.30,40.15 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:40.15,42.4 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:44.2,48.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:51.73,52.19 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:52.19,54.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:55.2,59.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:62.74,63.18 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:63.18,65.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:66.2,70.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:73.73,74.18 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:74.18,76.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:77.2,81.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:84.77,85.19 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:85.19,87.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:88.2,92.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:95.80,96.19 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:96.19,98.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:99.2,103.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:106.73,107.19 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:107.19,109.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:110.2,114.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:117.76,118.34 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:118.34,120.3 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:121.2,125.8 1 1 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:128.78,129.34 1 0 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:129.34,131.3 1 0 -github.com/echovault/echovault/internal/modules/generic/key_funcs.go:132.2,136.8 1 0 -github.com/echovault/echovault/internal/modules/generic/utils.go:32.100,33.19 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:33.19,35.3 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:36.2,36.33 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:37.13,39.55 2 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:41.12,42.27 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:42.27,44.4 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:45.3,46.55 2 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:48.12,49.27 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:49.27,51.4 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:52.3,53.55 2 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:55.12,56.19 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:56.19,58.4 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:59.3,59.30 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:59.30,61.4 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:62.3,64.17 3 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:64.17,66.4 1 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:67.3,68.55 2 1 -github.com/echovault/echovault/internal/modules/generic/utils.go:70.12,71.19 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:27.78,33.29 4 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:33.29,34.54 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:34.54,40.42 4 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:40.42,42.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:44.4,47.12 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:50.3,50.36 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:50.36,57.43 5 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:57.43,59.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:61.4,63.21 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:67.2,69.25 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:72.76,76.35 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:76.35,77.65 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:77.65,78.41 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:78.41,80.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:81.4,81.12 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:83.3,83.13 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:86.2,86.51 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:89.75,90.29 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:91.9,96.36 4 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:96.36,97.66 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:97.66,98.52 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:98.52,102.6 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:103.5,103.13 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:105.4,106.14 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:108.3,109.26 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:111.9,115.56 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:115.56,117.4 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:118.3,118.53 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:118.53,122.37 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:122.37,123.67 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:123.67,124.53 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:124.53,125.59 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:125.59,129.8 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:131.6,131.14 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:133.5,133.54 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:133.54,136.6 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:138.9,138.61 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:138.61,142.37 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:142.37,143.67 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:143.67,144.53 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:144.53,146.24 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:146.24,149.8 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:151.6,151.14 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:153.5,153.33 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:153.33,156.6 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:158.9,158.60 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:158.60,162.37 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:162.37,163.67 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:163.67,164.53 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:164.53,165.55 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:165.55,169.8 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:171.6,171.14 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:173.5,173.50 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:173.50,176.6 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:178.9,180.4 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:181.3,182.26 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:183.10,184.54 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:188.75,190.2 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:192.36,200.84 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:225.86,229.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:267.84,271.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:272.73,273.49 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:273.49,275.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:276.5,276.45 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:285.84,289.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:290.73,292.18 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:292.18,294.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:295.5,295.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:304.84,308.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:309.73,310.47 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:310.47,312.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:313.5,313.45 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:321.84,325.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:335.86,339.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:340.75,341.34 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:341.34,343.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:344.7,345.34 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:345.34,347.8 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:348.7,348.75 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:348.75,350.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:351.7,351.47 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:361.86,365.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:366.75,367.35 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:367.35,369.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:370.7,371.47 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:380.86,384.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:385.75,388.38 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:388.38,390.8 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:391.7,391.30 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:33.67,35.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:35.16,37.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:39.2,46.16 7 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:46.16,48.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:52.2,52.17 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:52.17,53.17 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:53.17,55.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:55.9,57.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:60.2,60.45 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:60.45,62.17 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:62.17,64.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:65.8,65.52 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:65.52,67.16 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:67.16,69.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:72.2,74.17 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:74.17,76.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:79.2,79.29 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:79.29,81.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:83.2,83.17 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:86.68,88.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:88.16,90.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:92.2,95.41 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:95.41,96.15 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:96.15,98.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:102.2,102.65 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:102.65,104.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:106.2,106.42 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:109.67,111.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:111.16,113.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:114.2,117.16 3 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:117.16,119.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:121.2,123.51 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:126.68,128.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:128.16,130.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:132.2,133.74 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:133.74,134.19 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:134.19,136.12 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:138.3,138.41 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:141.2,143.41 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:143.41,144.24 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:144.24,146.12 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:148.3,148.96 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:151.2,151.19 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:154.67,156.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:156.16,158.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:159.2,160.60 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:160.60,161.14 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:161.14,162.12 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:164.3,165.17 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:165.17,167.12 2 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:169.3,169.13 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:171.2,171.51 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:174.71,176.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:176.16,178.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:180.2,183.16 3 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:183.16,185.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:187.2,188.31 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:188.31,190.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:192.2,194.30 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:197.74,199.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:199.16,201.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:203.2,206.16 3 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:206.16,208.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:210.2,212.31 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:212.31,214.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:216.2,217.57 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:217.57,219.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:221.2,221.47 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:224.67,226.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:226.16,228.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:230.2,235.16 4 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:235.16,237.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:239.2,241.31 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:241.31,243.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:245.2,246.50 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:246.50,248.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:250.2,250.12 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:250.12,252.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:254.2,254.47 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:257.70,259.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:259.16,261.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:263.2,268.16 4 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:268.16,270.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:271.2,272.53 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:272.53,274.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:276.2,276.16 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:276.16,278.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:280.2,280.30 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:280.30,283.3 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:285.2,287.44 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:288.12,289.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:289.39,291.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:292.3,292.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:293.12,294.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:294.39,296.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:297.3,297.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:298.12,299.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:299.39,301.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:302.3,302.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:302.39,304.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:305.3,305.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:306.12,307.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:307.39,308.40 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:308.40,310.5 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:311.4,311.58 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:313.3,313.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:314.10,315.82 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:318.2,318.30 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:321.72,323.16 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:323.16,325.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:327.2,332.16 4 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:332.16,334.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:335.2,336.55 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:336.55,338.3 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:340.2,340.16 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:340.16,342.3 1 0 +github.com/echovault/echovault/internal/modules/generic/commands.go:344.2,344.30 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:344.30,347.3 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:349.2,351.44 2 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:352.12,353.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:353.39,355.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:356.3,356.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:357.12,358.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:358.39,360.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:361.3,361.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:362.12,363.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:363.39,365.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:366.3,366.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:366.39,368.4 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:369.3,369.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:370.12,371.39 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:371.39,372.40 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:372.40,374.5 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:375.4,375.58 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:377.3,377.57 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:378.10,379.82 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:382.2,382.30 1 1 +github.com/echovault/echovault/internal/modules/generic/commands.go:385.36,554.2 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:23.73,24.34 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:24.34,26.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:27.2,31.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:34.74,35.25 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:35.25,37.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:38.2,39.30 2 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:39.30,40.15 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:40.15,42.4 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:44.2,48.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:51.73,52.19 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:52.19,54.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:55.2,59.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:62.74,63.18 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:63.18,65.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:66.2,70.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:73.73,74.18 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:74.18,76.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:77.2,81.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:84.77,85.19 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:85.19,87.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:88.2,92.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:95.80,96.19 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:96.19,98.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:99.2,103.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:106.73,107.19 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:107.19,109.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:110.2,114.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:117.76,118.34 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:118.34,120.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:121.2,125.8 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:128.78,129.34 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:129.34,131.3 1 1 +github.com/echovault/echovault/internal/modules/generic/key_funcs.go:132.2,136.8 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:32.100,33.19 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:33.19,35.3 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:36.2,36.33 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:37.13,39.55 2 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:41.12,42.27 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:42.27,44.4 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:45.3,46.55 2 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:48.12,49.27 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:49.27,51.4 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:52.3,53.55 2 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:55.12,56.19 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:56.19,58.4 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:59.3,59.30 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:59.30,61.4 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:62.3,64.17 3 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:64.17,66.4 1 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:67.3,68.55 2 1 +github.com/echovault/echovault/internal/modules/generic/utils.go:70.12,71.19 1 1 github.com/echovault/echovault/internal/modules/generic/utils.go:71.19,73.4 1 1 github.com/echovault/echovault/internal/modules/generic/utils.go:74.3,74.30 1 1 github.com/echovault/echovault/internal/modules/generic/utils.go:74.30,76.4 1 1 @@ -884,1270 +937,214 @@ github.com/echovault/echovault/internal/modules/generic/utils.go:107.3,109.17 3 github.com/echovault/echovault/internal/modules/generic/utils.go:109.17,111.4 1 1 github.com/echovault/echovault/internal/modules/generic/utils.go:112.3,113.55 2 1 github.com/echovault/echovault/internal/modules/generic/utils.go:115.10,116.96 1 1 -github.com/echovault/echovault/echovault/api_acl.go:126.71,128.23 2 0 -github.com/echovault/echovault/echovault/api_acl.go:128.23,130.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:131.2,132.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:132.16,134.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:135.2,135.45 1 0 -github.com/echovault/echovault/echovault/api_acl.go:139.55,141.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:141.16,143.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:144.2,144.45 1 0 -github.com/echovault/echovault/echovault/api_acl.go:155.62,158.18 2 0 -github.com/echovault/echovault/echovault/api_acl.go:158.18,160.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:160.8,162.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:164.2,164.21 1 0 -github.com/echovault/echovault/echovault/api_acl.go:164.21,166.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:168.2,168.17 1 0 -github.com/echovault/echovault/echovault/api_acl.go:168.17,170.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:172.2,172.21 1 0 -github.com/echovault/echovault/echovault/api_acl.go:172.21,174.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:176.2,176.20 1 0 -github.com/echovault/echovault/echovault/api_acl.go:176.20,178.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:180.2,180.20 1 0 -github.com/echovault/echovault/echovault/api_acl.go:180.20,182.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:184.2,184.24 1 0 -github.com/echovault/echovault/echovault/api_acl.go:184.24,186.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:188.2,188.50 1 0 -github.com/echovault/echovault/echovault/api_acl.go:188.50,190.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:192.2,192.53 1 0 -github.com/echovault/echovault/echovault/api_acl.go:192.53,194.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:196.2,196.49 1 0 -github.com/echovault/echovault/echovault/api_acl.go:196.49,198.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:200.2,200.52 1 0 -github.com/echovault/echovault/echovault/api_acl.go:200.52,202.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:204.2,204.50 1 0 -github.com/echovault/echovault/echovault/api_acl.go:204.50,206.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:208.2,208.50 1 0 -github.com/echovault/echovault/echovault/api_acl.go:208.50,210.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:212.2,212.47 1 0 -github.com/echovault/echovault/echovault/api_acl.go:212.47,214.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:216.2,216.47 1 0 -github.com/echovault/echovault/echovault/api_acl.go:216.47,218.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:220.2,220.48 1 0 -github.com/echovault/echovault/echovault/api_acl.go:220.48,222.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:224.2,224.43 1 0 -github.com/echovault/echovault/echovault/api_acl.go:224.43,226.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:228.2,228.44 1 0 -github.com/echovault/echovault/echovault/api_acl.go:228.44,230.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:232.2,232.47 1 0 -github.com/echovault/echovault/echovault/api_acl.go:232.47,234.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:236.2,236.47 1 0 -github.com/echovault/echovault/echovault/api_acl.go:236.47,238.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:240.2,241.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:241.16,243.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:245.2,246.40 2 0 -github.com/echovault/echovault/echovault/api_acl.go:293.83,295.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:295.16,297.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:299.2,301.16 3 0 -github.com/echovault/echovault/echovault/api_acl.go:301.16,303.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:305.2,308.35 3 0 -github.com/echovault/echovault/echovault/api_acl.go:308.35,314.35 4 0 -github.com/echovault/echovault/echovault/api_acl.go:314.35,316.4 1 0 -github.com/echovault/echovault/echovault/api_acl.go:319.2,319.20 1 0 -github.com/echovault/echovault/echovault/api_acl.go:329.72,332.16 3 0 -github.com/echovault/echovault/echovault/api_acl.go:332.16,334.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:335.2,336.40 2 0 -github.com/echovault/echovault/echovault/api_acl.go:340.54,342.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:342.16,344.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:345.2,345.45 1 0 -github.com/echovault/echovault/echovault/api_acl.go:356.72,358.9 2 0 -github.com/echovault/echovault/echovault/api_acl.go:359.21,360.29 1 0 -github.com/echovault/echovault/echovault/api_acl.go:361.23,362.31 1 0 -github.com/echovault/echovault/echovault/api_acl.go:363.10,364.31 1 0 -github.com/echovault/echovault/echovault/api_acl.go:367.2,368.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:368.16,370.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:372.2,373.40 2 0 -github.com/echovault/echovault/echovault/api_acl.go:379.50,381.16 2 0 -github.com/echovault/echovault/echovault/api_acl.go:381.16,383.3 1 0 -github.com/echovault/echovault/echovault/api_acl.go:384.2,385.40 2 0 -github.com/echovault/echovault/echovault/api_admin.go:166.84,169.9 2 0 -github.com/echovault/echovault/echovault/api_admin.go:170.28,171.71 1 0 -github.com/echovault/echovault/echovault/api_admin.go:172.29,173.73 1 0 -github.com/echovault/echovault/echovault/api_admin.go:174.28,175.71 1 0 -github.com/echovault/echovault/echovault/api_admin.go:178.2,179.16 2 0 -github.com/echovault/echovault/echovault/api_admin.go:179.16,181.3 1 0 -github.com/echovault/echovault/echovault/api_admin.go:183.2,183.45 1 0 -github.com/echovault/echovault/echovault/api_admin.go:189.54,191.16 2 0 -github.com/echovault/echovault/echovault/api_admin.go:191.16,193.3 1 0 -github.com/echovault/echovault/echovault/api_admin.go:194.2,194.41 1 0 -github.com/echovault/echovault/echovault/api_admin.go:198.49,200.16 2 0 -github.com/echovault/echovault/echovault/api_admin.go:200.16,202.3 1 0 -github.com/echovault/echovault/echovault/api_admin.go:203.2,203.40 1 0 -github.com/echovault/echovault/echovault/api_admin.go:207.50,209.16 2 0 -github.com/echovault/echovault/echovault/api_admin.go:209.16,211.3 1 0 -github.com/echovault/echovault/echovault/api_admin.go:212.2,212.41 1 0 -github.com/echovault/echovault/echovault/api_admin.go:216.55,218.16 2 0 -github.com/echovault/echovault/echovault/api_admin.go:218.16,220.3 1 0 -github.com/echovault/echovault/echovault/api_admin.go:221.2,221.40 1 0 -github.com/echovault/echovault/echovault/api_admin.go:233.67,237.36 3 1 -github.com/echovault/echovault/echovault/api_admin.go:237.36,238.52 1 1 -github.com/echovault/echovault/echovault/api_admin.go:238.52,240.4 1 0 -github.com/echovault/echovault/echovault/api_admin.go:243.2,243.63 1 1 -github.com/echovault/echovault/echovault/api_admin.go:243.63,248.32 1 1 -github.com/echovault/echovault/echovault/api_admin.go:248.32,251.44 2 1 -github.com/echovault/echovault/echovault/api_admin.go:251.44,253.6 1 0 -github.com/echovault/echovault/echovault/api_admin.go:254.5,254.16 1 1 -github.com/echovault/echovault/echovault/api_admin.go:258.111,260.19 2 0 -github.com/echovault/echovault/echovault/api_admin.go:260.19,262.6 1 0 -github.com/echovault/echovault/echovault/api_admin.go:263.5,267.11 1 0 -github.com/echovault/echovault/echovault/api_admin.go:269.94,282.5 1 1 -github.com/echovault/echovault/echovault/api_admin.go:284.3,284.13 1 1 -github.com/echovault/echovault/echovault/api_admin.go:288.2,291.31 1 1 -github.com/echovault/echovault/echovault/api_admin.go:291.31,294.43 2 1 -github.com/echovault/echovault/echovault/api_admin.go:294.43,296.5 1 0 -github.com/echovault/echovault/echovault/api_admin.go:297.4,297.15 1 1 -github.com/echovault/echovault/echovault/api_admin.go:301.83,303.4 1 0 -github.com/echovault/echovault/echovault/api_admin.go:304.71,304.90 1 0 -github.com/echovault/echovault/echovault/api_admin.go:308.2,308.40 1 1 -github.com/echovault/echovault/echovault/api_admin.go:308.40,310.92 1 1 -github.com/echovault/echovault/echovault/api_admin.go:310.92,312.4 1 1 -github.com/echovault/echovault/echovault/api_admin.go:312.6,313.12 1 0 -github.com/echovault/echovault/echovault/api_admin.go:315.3,318.32 1 1 -github.com/echovault/echovault/echovault/api_admin.go:318.32,321.39 2 1 -github.com/echovault/echovault/echovault/api_admin.go:321.39,323.6 1 0 -github.com/echovault/echovault/echovault/api_admin.go:324.5,324.16 1 1 -github.com/echovault/echovault/echovault/api_admin.go:328.111,330.19 2 0 -github.com/echovault/echovault/echovault/api_admin.go:330.19,332.6 1 0 -github.com/echovault/echovault/echovault/api_admin.go:333.5,337.11 1 0 -github.com/echovault/echovault/echovault/api_admin.go:339.94,352.5 1 1 -github.com/echovault/echovault/echovault/api_admin.go:356.2,358.12 2 1 -github.com/echovault/echovault/echovault/api_admin.go:384.76,386.2 1 1 -github.com/echovault/echovault/echovault/api_admin.go:402.59,406.22 3 1 -github.com/echovault/echovault/echovault/api_admin.go:407.9,409.86 1 1 -github.com/echovault/echovault/echovault/api_admin.go:409.86,411.4 1 1 -github.com/echovault/echovault/echovault/api_admin.go:412.9,414.45 1 1 -github.com/echovault/echovault/echovault/api_admin.go:414.45,415.66 1 1 -github.com/echovault/echovault/echovault/api_admin.go:415.66,416.13 1 1 -github.com/echovault/echovault/echovault/api_admin.go:418.4,418.88 1 1 -github.com/echovault/echovault/echovault/api_admin.go:418.88,419.122 1 1 -github.com/echovault/echovault/echovault/api_admin.go:419.122,421.6 1 1 -github.com/echovault/echovault/echovault/api_generic.go:88.91,91.9 2 1 -github.com/echovault/echovault/echovault/api_generic.go:92.18,93.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:94.18,95.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:98.2,98.9 1 1 -github.com/echovault/echovault/echovault/api_generic.go:99.23,100.65 1 1 -github.com/echovault/echovault/echovault/api_generic.go:101.23,102.65 1 1 -github.com/echovault/echovault/echovault/api_generic.go:103.25,104.69 1 1 -github.com/echovault/echovault/echovault/api_generic.go:105.25,106.69 1 1 -github.com/echovault/echovault/echovault/api_generic.go:109.2,109.17 1 1 -github.com/echovault/echovault/echovault/api_generic.go:109.17,111.3 1 1 -github.com/echovault/echovault/echovault/api_generic.go:113.2,114.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:114.16,116.3 1 1 -github.com/echovault/echovault/echovault/api_generic.go:118.2,119.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:119.16,121.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:122.2,122.18 1 1 -github.com/echovault/echovault/echovault/api_generic.go:122.18,124.3 1 1 -github.com/echovault/echovault/echovault/api_generic.go:126.2,126.33 1 1 -github.com/echovault/echovault/echovault/api_generic.go:141.72,144.28 2 1 -github.com/echovault/echovault/echovault/api_generic.go:144.28,146.3 1 1 -github.com/echovault/echovault/echovault/api_generic.go:148.2,149.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:149.16,151.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:153.2,154.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:154.16,156.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:158.2,158.40 1 1 -github.com/echovault/echovault/echovault/api_generic.go:169.58,171.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:171.16,173.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:174.2,174.40 1 1 -github.com/echovault/echovault/echovault/api_generic.go:185.65,187.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:187.16,189.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:190.2,190.45 1 1 -github.com/echovault/echovault/echovault/api_generic.go:200.59,202.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:202.16,204.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:205.2,205.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:216.60,218.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:218.16,220.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:221.2,221.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:231.62,233.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:233.16,235.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:236.2,236.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:246.63,248.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:248.16,250.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:251.2,251.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:261.55,263.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:263.16,265.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:266.2,266.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:276.56,278.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:278.16,280.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:281.2,281.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:296.95,299.9 2 1 -github.com/echovault/echovault/echovault/api_generic.go:300.18,301.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:302.18,303.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:304.18,305.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:306.18,307.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:310.2,311.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:311.16,313.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:315.2,315.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:330.102,333.9 2 1 -github.com/echovault/echovault/echovault/api_generic.go:334.18,335.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:336.18,337.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:338.18,339.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:340.18,341.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:344.2,345.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:345.16,347.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:349.2,349.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:364.102,367.9 2 1 -github.com/echovault/echovault/echovault/api_generic.go:368.18,369.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:370.18,371.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:372.18,373.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:374.18,375.26 1 1 -github.com/echovault/echovault/echovault/api_generic.go:378.2,379.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:379.16,381.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:383.2,383.41 1 1 -github.com/echovault/echovault/echovault/api_generic.go:398.109,401.9 2 1 -github.com/echovault/echovault/echovault/api_generic.go:402.18,403.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:404.18,405.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:406.18,407.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:408.18,409.26 1 0 -github.com/echovault/echovault/echovault/api_generic.go:412.2,413.16 2 1 -github.com/echovault/echovault/echovault/api_generic.go:413.16,415.3 1 0 -github.com/echovault/echovault/echovault/api_generic.go:417.2,417.41 1 1 -github.com/echovault/echovault/echovault/api_hash.go:46.91,49.36 2 1 -github.com/echovault/echovault/echovault/api_hash.go:49.36,51.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:53.2,54.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:54.16,56.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:58.2,58.41 1 1 -github.com/echovault/echovault/echovault/api_hash.go:76.93,79.36 2 1 -github.com/echovault/echovault/echovault/api_hash.go:79.36,81.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:83.2,84.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:84.16,86.3 1 0 -github.com/echovault/echovault/echovault/api_hash.go:88.2,88.41 1 1 -github.com/echovault/echovault/echovault/api_hash.go:104.79,112.16 2 0 -github.com/echovault/echovault/echovault/api_hash.go:112.16,114.3 1 0 -github.com/echovault/echovault/echovault/api_hash.go:115.2,115.45 1 0 -github.com/echovault/echovault/echovault/api_hash.go:132.79,136.16 3 1 -github.com/echovault/echovault/echovault/api_hash.go:136.16,138.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:140.2,140.46 1 1 -github.com/echovault/echovault/echovault/api_hash.go:154.62,156.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:156.16,158.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:159.2,159.45 1 1 -github.com/echovault/echovault/echovault/api_hash.go:175.94,178.24 2 1 -github.com/echovault/echovault/echovault/api_hash.go:178.24,180.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:180.8,182.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:184.2,184.24 1 1 -github.com/echovault/echovault/echovault/api_hash.go:184.24,186.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:188.2,189.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:189.16,191.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:193.2,193.45 1 1 -github.com/echovault/echovault/echovault/api_hash.go:207.56,209.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:209.16,211.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:212.2,212.41 1 1 -github.com/echovault/echovault/echovault/api_hash.go:226.62,228.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:228.16,230.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:231.2,231.45 1 1 -github.com/echovault/echovault/echovault/api_hash.go:250.85,252.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:252.16,254.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:255.2,255.39 1 1 -github.com/echovault/echovault/echovault/api_hash.go:259.94,261.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:261.16,263.3 1 0 -github.com/echovault/echovault/echovault/api_hash.go:264.2,264.39 1 1 -github.com/echovault/echovault/echovault/api_hash.go:279.64,281.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:281.16,283.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:284.2,284.45 1 1 -github.com/echovault/echovault/echovault/api_hash.go:300.67,302.16 2 1 -github.com/echovault/echovault/echovault/api_hash.go:302.16,304.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:305.2,305.41 1 1 -github.com/echovault/echovault/echovault/api_hash.go:321.74,324.16 3 1 -github.com/echovault/echovault/echovault/api_hash.go:324.16,326.3 1 1 -github.com/echovault/echovault/echovault/api_hash.go:327.2,327.41 1 1 -github.com/echovault/echovault/echovault/api_list.go:35.56,38.16 3 1 -github.com/echovault/echovault/echovault/api_list.go:38.16,40.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:41.2,41.41 1 1 -github.com/echovault/echovault/echovault/api_list.go:64.79,66.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:66.16,68.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:69.2,69.45 1 1 -github.com/echovault/echovault/echovault/api_list.go:87.73,89.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:89.16,91.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:92.2,92.40 1 1 -github.com/echovault/echovault/echovault/api_list.go:112.82,114.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:114.16,116.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:117.2,118.40 2 1 -github.com/echovault/echovault/echovault/api_list.go:125.78,127.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:127.16,129.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:130.2,131.40 2 1 -github.com/echovault/echovault/echovault/api_list.go:149.82,156.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:156.16,158.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:159.2,160.40 2 1 -github.com/echovault/echovault/echovault/api_list.go:184.94,186.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:186.16,188.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:189.2,190.40 2 1 -github.com/echovault/echovault/echovault/api_list.go:204.59,206.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:206.16,208.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:209.2,209.40 1 1 -github.com/echovault/echovault/echovault/api_list.go:223.59,225.16 2 1 -github.com/echovault/echovault/echovault/api_list.go:225.16,227.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:228.2,228.40 1 1 -github.com/echovault/echovault/echovault/api_list.go:245.75,248.16 3 1 -github.com/echovault/echovault/echovault/api_list.go:248.16,250.3 1 0 -github.com/echovault/echovault/echovault/api_list.go:251.2,251.41 1 1 -github.com/echovault/echovault/echovault/api_list.go:267.76,270.16 3 1 -github.com/echovault/echovault/echovault/api_list.go:270.16,272.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:273.2,273.41 1 1 -github.com/echovault/echovault/echovault/api_list.go:290.75,293.16 3 1 -github.com/echovault/echovault/echovault/api_list.go:293.16,295.3 1 0 -github.com/echovault/echovault/echovault/api_list.go:296.2,296.41 1 1 -github.com/echovault/echovault/echovault/api_list.go:312.76,315.16 3 1 -github.com/echovault/echovault/echovault/api_list.go:315.16,317.3 1 1 -github.com/echovault/echovault/echovault/api_list.go:318.2,318.41 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:50.86,52.24 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:52.24,54.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:57.2,59.36 3 0 -github.com/echovault/echovault/echovault/api_pubsub.go:59.36,65.3 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:68.2,69.12 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:69.12,71.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:73.2,73.25 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:73.25,78.33 4 0 -github.com/echovault/echovault/echovault/api_pubsub.go:78.33,80.4 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:82.3,82.13 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:93.70,94.24 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:94.24,96.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:98.2,98.36 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:98.36,100.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:102.2,103.115 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:116.87,118.24 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:118.24,120.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:123.2,125.36 3 0 -github.com/echovault/echovault/echovault/api_pubsub.go:125.36,131.3 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:134.2,135.12 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:135.12,137.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:139.2,139.25 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:139.25,144.33 4 0 -github.com/echovault/echovault/echovault/api_pubsub.go:144.33,146.4 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:148.3,148.13 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:159.71,160.24 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:160.24,162.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:164.2,164.36 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:164.36,166.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:168.2,169.115 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:182.73,184.16 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:184.16,186.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:187.2,188.40 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:198.75,200.19 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:200.19,202.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:203.2,204.16 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:204.16,206.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:207.2,207.45 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:213.54,215.16 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:215.16,217.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:218.2,218.41 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:228.82,232.16 3 0 -github.com/echovault/echovault/echovault/api_pubsub.go:232.16,234.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:236.2,238.16 3 0 -github.com/echovault/echovault/echovault/api_pubsub.go:238.16,240.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:242.2,245.28 3 0 -github.com/echovault/echovault/echovault/api_pubsub.go:245.28,248.3 2 0 -github.com/echovault/echovault/echovault/api_pubsub.go:250.2,250.20 1 0 -github.com/echovault/echovault/echovault/api_set.go:36.75,39.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:39.16,41.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:42.2,42.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:56.57,58.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:58.16,60.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:61.2,61.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:78.66,81.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:81.16,83.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:84.2,84.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:91.86,94.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:94.16,96.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:97.2,97.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:114.67,117.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:117.16,119.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:120.2,120.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:138.77,140.15 2 1 -github.com/echovault/echovault/echovault/api_set.go:140.15,142.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:143.2,144.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:144.16,146.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:147.2,147.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:152.87,155.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:155.16,157.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:158.2,158.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:174.70,176.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:176.16,178.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:179.2,179.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:193.65,195.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:195.16,197.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:198.2,198.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:215.84,218.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:218.16,220.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:221.2,221.46 1 1 -github.com/echovault/echovault/echovault/api_set.go:243.82,245.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:245.16,247.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:248.2,248.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:264.73,266.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:266.16,268.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:269.2,269.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:286.79,288.16 2 1 -github.com/echovault/echovault/echovault/api_set.go:288.16,290.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:291.2,291.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:307.75,310.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:310.16,312.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:313.2,313.41 1 1 -github.com/echovault/echovault/echovault/api_set.go:328.67,331.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:331.16,333.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:334.2,334.45 1 1 -github.com/echovault/echovault/echovault/api_set.go:341.87,344.16 3 1 -github.com/echovault/echovault/echovault/api_set.go:344.16,346.3 1 1 -github.com/echovault/echovault/echovault/api_set.go:347.2,347.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:105.87,107.28 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:107.28,108.17 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:108.17,110.18 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:110.18,112.5 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:113.4,114.12 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:116.3,116.23 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:118.2,118.20 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:121.85,123.28 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:123.28,125.17 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:125.17,127.4 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:128.3,129.17 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:129.17,131.18 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:131.18,133.5 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:134.4,134.24 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:137.2,137.20 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:161.105,164.9 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:165.18,166.26 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:167.18,168.26 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:171.2,171.9 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:172.18,173.26 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:174.18,175.26 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:178.2,178.16 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:178.16,180.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:182.2,182.18 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:182.18,184.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:186.2,186.37 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:186.37,188.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:190.2,191.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:191.16,193.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:195.2,195.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:209.57,211.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:211.16,213.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:214.2,214.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:232.76,240.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:240.16,242.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:243.2,243.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:261.93,263.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:263.16,265.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:266.2,267.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:267.16,269.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:271.2,272.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:272.16,274.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:276.2,276.45 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:293.86,296.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:296.16,298.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:299.2,299.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:316.99,319.30 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:319.30,321.45 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:321.45,323.4 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:326.2,326.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:326.29,328.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:330.2,330.24 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:330.24,332.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:334.2,335.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:335.16,337.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:339.2,340.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:340.16,342.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:344.2,344.53 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:363.114,366.30 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:366.30,368.42 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:368.42,370.4 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:373.2,373.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:373.29,375.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:377.2,377.24 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:377.24,379.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:381.2,382.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:382.16,384.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:386.2,386.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:403.99,406.30 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:406.30,408.42 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:408.42,410.4 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:413.2,413.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:413.29,415.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:417.2,417.24 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:417.24,419.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:421.2,422.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:422.16,424.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:426.2,427.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:427.16,429.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:431.2,431.53 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:450.114,453.30 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:453.30,455.42 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:455.42,457.4 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:460.2,460.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:460.29,462.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:464.2,464.24 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:464.24,466.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:468.2,469.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:469.16,471.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:473.2,473.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:492.97,495.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:495.16,497.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:498.2,499.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:499.16,501.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:502.2,502.15 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:519.89,522.9 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:523.19,524.27 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:525.19,526.27 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:527.10,528.27 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:531.2,531.9 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:532.26,533.76 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:534.10,535.59 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:538.2,539.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:539.16,541.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:543.2,543.51 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:561.88,563.33 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:563.33,565.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:567.2,568.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:568.16,570.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:572.2,573.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:573.16,575.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:577.2,578.24 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:578.24,579.14 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:579.14,581.12 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:583.3,584.17 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:584.17,586.4 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:587.3,587.20 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:590.2,590.20 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:610.71,613.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:613.16,615.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:616.2,616.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:635.78,637.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:637.16,639.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:640.2,640.51 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:659.78,661.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:661.16,663.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:664.2,664.51 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:688.98,690.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:690.16,692.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:693.2,693.16 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:693.16,695.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:697.2,698.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:698.16,700.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:702.2,702.51 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:723.101,725.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:725.16,727.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:729.2,730.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:730.16,732.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:734.2,736.19 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:736.19,738.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:740.2,741.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:741.16,743.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:745.2,747.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:747.16,749.17 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:749.17,751.4 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:752.3,752.13 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:755.2,755.17 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:760.104,762.16 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:762.16,764.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:766.2,767.16 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:767.16,769.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:771.2,773.46 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:790.81,793.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:793.16,795.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:797.2,798.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:798.16,800.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:802.2,802.11 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:802.11,804.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:806.2,807.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:807.16,809.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:811.2,811.19 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:827.75,829.33 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:829.33,831.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:832.2,833.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:833.16,835.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:836.2,836.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:854.94,863.16 3 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:863.16,865.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:867.2,867.41 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:885.76,892.16 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:892.16,894.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:895.2,895.41 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:913.81,920.16 2 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:920.16,922.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:923.2,923.41 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:943.109,946.9 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:947.23,948.31 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:949.21,950.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:951.19,952.27 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:953.10,954.31 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:957.2,957.24 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:957.24,959.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:961.2,961.47 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:961.47,963.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:965.2,966.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:966.16,968.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:970.2,971.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:971.16,973.3 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:975.2,975.53 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:997.120,1000.9 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1001.23,1002.31 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1003.21,1004.29 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1005.19,1006.27 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:1007.10,1008.31 1 0 -github.com/echovault/echovault/echovault/api_sorted_set.go:1011.2,1011.47 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1011.47,1013.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1015.2,1016.16 2 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1016.16,1018.3 1 1 -github.com/echovault/echovault/echovault/api_sorted_set.go:1020.2,1020.41 1 1 -github.com/echovault/echovault/echovault/api_string.go:30.84,32.16 2 1 -github.com/echovault/echovault/echovault/api_string.go:32.16,34.3 1 0 -github.com/echovault/echovault/echovault/api_string.go:35.2,35.41 1 1 -github.com/echovault/echovault/echovault/api_string.go:45.58,47.16 2 1 -github.com/echovault/echovault/echovault/api_string.go:47.16,49.3 1 0 -github.com/echovault/echovault/echovault/api_string.go:50.2,50.41 1 1 -github.com/echovault/echovault/echovault/api_string.go:63.77,65.16 2 1 -github.com/echovault/echovault/echovault/api_string.go:65.16,67.3 1 0 -github.com/echovault/echovault/echovault/api_string.go:68.2,68.40 1 1 -github.com/echovault/echovault/echovault/api_string.go:72.79,74.16 2 1 -github.com/echovault/echovault/echovault/api_string.go:74.16,76.3 1 0 -github.com/echovault/echovault/echovault/api_string.go:77.2,77.40 1 1 -github.com/echovault/echovault/echovault/cluster.go:25.45,27.2 1 1 -github.com/echovault/echovault/echovault/cluster.go:29.84,40.16 4 0 -github.com/echovault/echovault/echovault/cluster.go:40.16,42.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:44.2,46.43 2 0 -github.com/echovault/echovault/echovault/cluster.go:46.43,48.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:50.2,52.9 2 0 -github.com/echovault/echovault/echovault/cluster.go:52.9,54.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:56.2,56.20 1 0 -github.com/echovault/echovault/echovault/cluster.go:56.20,58.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:60.2,60.12 1 0 -github.com/echovault/echovault/echovault/cluster.go:63.94,75.16 5 0 -github.com/echovault/echovault/echovault/cluster.go:75.16,77.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:79.2,81.43 2 0 -github.com/echovault/echovault/echovault/cluster.go:81.43,83.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:85.2,87.9 2 0 -github.com/echovault/echovault/echovault/cluster.go:87.9,89.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:91.2,91.20 1 0 -github.com/echovault/echovault/echovault/cluster.go:91.20,93.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:95.2,95.24 1 0 -github.com/echovault/echovault/echovault/config.go:23.36,25.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:112.66,113.36 1 0 -github.com/echovault/echovault/echovault/echovault.go:113.36,115.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:121.66,122.36 1 1 -github.com/echovault/echovault/echovault/echovault.go:122.36,124.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:129.78,138.39 1 1 -github.com/echovault/echovault/echovault/echovault.go:138.39,151.4 12 1 -github.com/echovault/echovault/echovault/echovault.go:154.2,154.33 1 1 -github.com/echovault/echovault/echovault/echovault.go:154.33,156.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:158.2,164.48 2 1 -github.com/echovault/echovault/echovault/echovault.go:164.48,165.52 1 0 -github.com/echovault/echovault/echovault/echovault.go:165.52,167.12 2 0 -github.com/echovault/echovault/echovault/echovault.go:169.3,169.41 1 0 -github.com/echovault/echovault/echovault/echovault.go:173.2,173.52 1 1 -github.com/echovault/echovault/echovault/echovault.go:173.52,175.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:178.2,178.42 1 1 -github.com/echovault/echovault/echovault/echovault.go:178.42,180.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:183.2,184.40 2 1 -github.com/echovault/echovault/echovault/echovault.go:184.40,186.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:189.2,190.43 2 1 -github.com/echovault/echovault/echovault/echovault.go:190.43,192.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:194.2,194.29 1 1 -github.com/echovault/echovault/echovault/echovault.go:194.29,207.49 1 0 -github.com/echovault/echovault/echovault/echovault.go:207.49,209.44 2 0 -github.com/echovault/echovault/echovault/echovault.go:209.44,210.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:210.46,212.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:214.5,214.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:217.3,225.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:226.8,237.65 1 1 -github.com/echovault/echovault/echovault/echovault.go:237.65,239.44 2 0 -github.com/echovault/echovault/echovault/echovault.go:239.44,240.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:240.46,242.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:244.5,244.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:246.72,248.67 2 0 -github.com/echovault/echovault/echovault/echovault.go:248.67,250.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:251.5,251.68 1 0 -github.com/echovault/echovault/echovault/echovault.go:251.68,253.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:254.5,255.34 2 0 -github.com/echovault/echovault/echovault/echovault.go:259.3,265.60 1 1 -github.com/echovault/echovault/echovault/echovault.go:265.60,267.44 2 0 -github.com/echovault/echovault/echovault/echovault.go:267.44,268.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:268.46,270.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:272.5,272.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:274.68,276.67 2 0 -github.com/echovault/echovault/echovault/echovault.go:276.67,278.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:279.5,279.69 1 0 -github.com/echovault/echovault/echovault/echovault.go:279.69,281.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:282.5,283.34 2 0 -github.com/echovault/echovault/echovault/echovault.go:285.51,287.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:287.19,289.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:292.3,292.17 1 1 -github.com/echovault/echovault/echovault/echovault.go:292.17,294.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:295.3,295.34 1 1 -github.com/echovault/echovault/echovault/echovault.go:299.2,299.61 1 1 -github.com/echovault/echovault/echovault/echovault.go:299.61,300.13 1 1 -github.com/echovault/echovault/echovault/echovault.go:300.13,301.8 1 1 -github.com/echovault/echovault/echovault/echovault.go:301.8,303.83 2 1 -github.com/echovault/echovault/echovault/echovault.go:303.83,305.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:310.2,310.69 1 1 -github.com/echovault/echovault/echovault/echovault.go:310.69,312.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:314.2,314.29 1 1 -github.com/echovault/echovault/echovault/echovault.go:314.29,318.36 3 0 -github.com/echovault/echovault/echovault/echovault.go:318.36,320.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:323.2,323.30 1 1 -github.com/echovault/echovault/echovault/echovault.go:323.30,326.34 2 1 -github.com/echovault/echovault/echovault/echovault.go:326.34,328.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:328.18,330.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:334.3,334.71 1 1 -github.com/echovault/echovault/echovault/echovault.go:334.71,336.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:336.18,338.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:342.2,342.23 1 1 -github.com/echovault/echovault/echovault/echovault.go:345.37,354.16 4 0 -github.com/echovault/echovault/echovault/echovault.go:354.16,356.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:358.2,358.15 1 0 -github.com/echovault/echovault/echovault/echovault.go:358.15,361.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:363.2,363.27 1 0 -github.com/echovault/echovault/echovault/echovault.go:363.27,365.15 1 0 -github.com/echovault/echovault/echovault/echovault.go:365.15,367.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:367.9,369.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:371.3,372.49 2 0 -github.com/echovault/echovault/echovault/echovault.go:372.49,374.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:374.18,376.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:377.4,377.42 1 0 -github.com/echovault/echovault/echovault/echovault.go:380.3,383.16 3 0 -github.com/echovault/echovault/echovault/echovault.go:383.16,385.37 2 0 -github.com/echovault/echovault/echovault/echovault.go:385.37,387.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:387.19,389.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:390.5,391.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:391.19,393.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:394.5,394.61 1 0 -github.com/echovault/echovault/echovault/echovault.go:394.61,396.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:400.3,404.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:408.2,408.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:408.6,410.17 2 0 -github.com/echovault/echovault/echovault/echovault.go:410.17,412.12 2 0 -github.com/echovault/echovault/echovault/echovault.go:415.3,415.35 1 0 -github.com/echovault/echovault/echovault/echovault.go:419.58,421.23 1 0 -github.com/echovault/echovault/echovault/echovault.go:421.23,423.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:425.2,431.6 4 0 -github.com/echovault/echovault/echovault/echovault.go:431.6,434.43 2 0 -github.com/echovault/echovault/echovault/echovault.go:434.43,437.9 2 0 -github.com/echovault/echovault/echovault/echovault.go:440.3,440.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:440.17,442.9 2 0 -github.com/echovault/echovault/echovault/echovault.go:445.3,447.43 2 0 -github.com/echovault/echovault/echovault/echovault.go:447.43,448.9 1 0 -github.com/echovault/echovault/echovault/echovault.go:451.3,451.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:451.17,452.87 1 0 -github.com/echovault/echovault/echovault/echovault.go:452.87,454.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:455.4,455.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:458.3,461.20 2 0 -github.com/echovault/echovault/echovault/echovault.go:461.20,462.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:465.3,465.28 1 0 -github.com/echovault/echovault/echovault/echovault.go:465.28,467.12 2 0 -github.com/echovault/echovault/echovault/echovault.go:471.3,472.7 2 0 -github.com/echovault/echovault/echovault/echovault.go:472.7,474.41 1 0 -github.com/echovault/echovault/echovault/echovault.go:474.41,476.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:476.19,478.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:479.5,479.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:481.4,482.21 2 0 -github.com/echovault/echovault/echovault/echovault.go:482.21,483.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:485.4,485.27 1 0 -github.com/echovault/echovault/echovault/echovault.go:489.2,489.37 1 0 -github.com/echovault/echovault/echovault/echovault.go:489.37,491.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:499.34,501.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:504.47,505.38 1 0 -github.com/echovault/echovault/echovault/echovault.go:505.38,507.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:509.2,509.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:509.12,510.27 1 0 -github.com/echovault/echovault/echovault/echovault.go:510.27,512.53 1 0 -github.com/echovault/echovault/echovault/echovault.go:512.53,514.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:515.4,515.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:518.3,518.62 1 0 -github.com/echovault/echovault/echovault/echovault.go:518.62,520.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:523.2,523.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:526.42,528.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:530.43,532.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:534.56,536.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:539.56,541.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:543.44,545.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:547.45,549.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:552.45,553.40 1 0 -github.com/echovault/echovault/echovault/echovault.go:553.40,555.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:556.2,556.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:556.12,557.55 1 0 -github.com/echovault/echovault/echovault/echovault.go:557.55,559.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:561.2,561.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:566.37,567.26 1 0 -github.com/echovault/echovault/echovault/echovault.go:567.26,570.3 2 0 -github.com/echovault/echovault/echovault/echovault.go:573.45,590.2 2 1 -github.com/echovault/echovault/echovault/keyspace.go:38.81,41.34 2 1 -github.com/echovault/echovault/echovault/keyspace.go:41.34,44.3 2 1 -github.com/echovault/echovault/echovault/keyspace.go:46.2,46.6 1 1 -github.com/echovault/echovault/echovault/keyspace.go:46.6,47.10 1 1 -github.com/echovault/echovault/echovault/keyspace.go:48.11,49.35 1 1 -github.com/echovault/echovault/echovault/keyspace.go:49.35,51.5 1 1 -github.com/echovault/echovault/echovault/keyspace.go:52.4,53.10 2 1 -github.com/echovault/echovault/echovault/keyspace.go:53.10,55.5 1 1 -github.com/echovault/echovault/echovault/keyspace.go:56.21,57.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:66.67,67.39 1 1 -github.com/echovault/echovault/echovault/keyspace.go:67.39,69.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:77.82,80.34 2 1 -github.com/echovault/echovault/echovault/keyspace.go:80.34,83.3 2 1 -github.com/echovault/echovault/echovault/keyspace.go:85.2,85.6 1 1 -github.com/echovault/echovault/echovault/keyspace.go:85.6,86.10 1 1 -github.com/echovault/echovault/echovault/keyspace.go:87.11,88.35 1 1 -github.com/echovault/echovault/echovault/keyspace.go:88.35,90.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:91.4,92.10 2 1 -github.com/echovault/echovault/echovault/keyspace.go:92.10,94.5 1 1 -github.com/echovault/echovault/echovault/keyspace.go:95.21,96.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:105.68,106.39 1 1 -github.com/echovault/echovault/echovault/keyspace.go:106.39,108.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:116.74,118.9 2 1 -github.com/echovault/echovault/echovault/keyspace.go:118.9,120.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:122.2,122.82 1 1 -github.com/echovault/echovault/echovault/keyspace.go:122.82,123.28 1 0 -github.com/echovault/echovault/echovault/keyspace.go:123.28,126.18 2 0 -github.com/echovault/echovault/echovault/keyspace.go:126.18,128.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:129.9,129.64 1 0 -github.com/echovault/echovault/echovault/keyspace.go:129.64,132.18 2 0 -github.com/echovault/echovault/echovault/keyspace.go:132.18,134.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:135.9,135.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:135.65,140.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:142.3,142.15 1 0 -github.com/echovault/echovault/echovault/keyspace.go:145.2,145.13 1 1 -github.com/echovault/echovault/echovault/keyspace.go:153.90,154.115 1 1 -github.com/echovault/echovault/echovault/keyspace.go:154.115,156.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:158.2,161.33 3 1 -github.com/echovault/echovault/echovault/keyspace.go:161.33,172.3 5 1 -github.com/echovault/echovault/echovault/keyspace.go:174.2,174.33 1 1 -github.com/echovault/echovault/echovault/keyspace.go:179.80,180.58 1 1 -github.com/echovault/echovault/echovault/keyspace.go:180.58,182.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:183.2,183.32 1 1 -github.com/echovault/echovault/echovault/keyspace.go:190.93,191.115 1 1 -github.com/echovault/echovault/echovault/keyspace.go:191.115,193.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:195.2,201.16 3 1 -github.com/echovault/echovault/echovault/keyspace.go:201.16,203.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:205.2,205.27 1 1 -github.com/echovault/echovault/echovault/keyspace.go:205.27,207.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:209.2,209.12 1 1 -github.com/echovault/echovault/echovault/keyspace.go:214.79,215.58 1 1 -github.com/echovault/echovault/echovault/keyspace.go:215.58,217.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:218.2,218.35 1 1 -github.com/echovault/echovault/echovault/keyspace.go:227.101,235.55 3 1 -github.com/echovault/echovault/echovault/keyspace.go:235.55,237.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:238.2,241.11 2 1 -github.com/echovault/echovault/echovault/keyspace.go:241.11,243.17 2 1 -github.com/echovault/echovault/echovault/keyspace.go:243.17,245.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:251.70,260.97 4 1 -github.com/echovault/echovault/echovault/keyspace.go:260.97,262.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:270.60,272.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:272.6,273.83 1 0 -github.com/echovault/echovault/echovault/keyspace.go:273.83,275.9 2 0 -github.com/echovault/echovault/echovault/keyspace.go:278.2,279.33 2 0 -github.com/echovault/echovault/echovault/keyspace.go:279.33,281.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:282.2,283.13 2 0 -github.com/echovault/echovault/echovault/keyspace.go:290.75,291.52 1 1 -github.com/echovault/echovault/echovault/keyspace.go:291.52,293.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:296.2,303.9 4 1 -github.com/echovault/echovault/echovault/keyspace.go:304.108,305.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:306.108,307.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:310.2,312.12 2 1 -github.com/echovault/echovault/echovault/keyspace.go:317.82,319.83 1 1 -github.com/echovault/echovault/echovault/keyspace.go:319.83,321.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:323.2,323.34 1 1 -github.com/echovault/echovault/echovault/keyspace.go:323.34,325.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:326.2,326.55 1 0 -github.com/echovault/echovault/echovault/keyspace.go:327.28,330.36 3 0 -github.com/echovault/echovault/echovault/keyspace.go:331.28,334.36 3 0 -github.com/echovault/echovault/echovault/keyspace.go:335.29,338.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:338.50,340.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:341.29,344.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:344.50,346.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:348.2,348.54 1 0 -github.com/echovault/echovault/echovault/keyspace.go:348.54,350.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:351.2,351.12 1 0 -github.com/echovault/echovault/echovault/keyspace.go:355.71,357.34 1 0 -github.com/echovault/echovault/echovault/keyspace.go:357.34,359.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:362.2,365.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:365.50,367.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:369.2,371.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:371.50,373.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:377.2,377.9 1 0 -github.com/echovault/echovault/echovault/keyspace.go:378.125,383.7 3 0 -github.com/echovault/echovault/echovault/keyspace.go:383.7,385.40 1 0 -github.com/echovault/echovault/echovault/keyspace.go:385.40,387.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:389.4,390.29 2 0 -github.com/echovault/echovault/echovault/keyspace.go:390.29,392.54 1 0 -github.com/echovault/echovault/echovault/keyspace.go:392.54,394.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:395.10,395.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:395.65,397.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:397.63,399.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:403.4,406.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:406.52,408.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:410.125,415.7 3 0 -github.com/echovault/echovault/echovault/keyspace.go:415.7,417.40 1 0 -github.com/echovault/echovault/echovault/keyspace.go:417.40,419.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:421.4,422.29 2 0 -github.com/echovault/echovault/echovault/keyspace.go:422.29,424.54 1 0 -github.com/echovault/echovault/echovault/keyspace.go:424.54,426.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:427.10,427.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:427.65,430.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:430.63,432.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:436.4,439.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:439.52,441.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:443.105,446.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:446.7,448.33 1 0 -github.com/echovault/echovault/echovault/keyspace.go:448.33,451.5 2 0 -github.com/echovault/echovault/echovault/keyspace.go:453.4,454.40 2 0 -github.com/echovault/echovault/echovault/keyspace.go:454.40,455.17 1 0 -github.com/echovault/echovault/echovault/keyspace.go:455.17,456.31 1 0 -github.com/echovault/echovault/echovault/keyspace.go:456.31,458.56 1 0 -github.com/echovault/echovault/echovault/keyspace.go:458.56,460.8 1 0 -github.com/echovault/echovault/echovault/keyspace.go:461.12,461.67 1 0 -github.com/echovault/echovault/echovault/keyspace.go:461.67,462.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:462.65,464.8 1 0 -github.com/echovault/echovault/echovault/keyspace.go:467.6,470.54 3 0 -github.com/echovault/echovault/echovault/keyspace.go:470.54,472.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:474.5,474.10 1 0 -github.com/echovault/echovault/echovault/keyspace.go:477.106,480.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:480.7,487.29 5 0 -github.com/echovault/echovault/echovault/keyspace.go:487.29,489.54 1 0 -github.com/echovault/echovault/echovault/keyspace.go:489.54,491.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:492.10,492.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:492.65,493.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:493.63,495.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:499.4,502.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:502.52,504.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:506.10,507.13 1 0 -github.com/echovault/echovault/echovault/keyspace.go:516.77,518.57 1 1 -github.com/echovault/echovault/echovault/keyspace.go:518.57,520.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:522.2,527.50 3 1 -github.com/echovault/echovault/echovault/keyspace.go:527.50,529.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:530.2,537.33 6 1 -github.com/echovault/echovault/echovault/keyspace.go:537.33,538.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:538.7,542.35 3 0 -github.com/echovault/echovault/echovault/keyspace.go:542.35,544.10 2 0 -github.com/echovault/echovault/echovault/keyspace.go:548.2,551.25 2 1 -github.com/echovault/echovault/echovault/keyspace.go:551.25,552.52 1 0 -github.com/echovault/echovault/echovault/keyspace.go:552.52,553.12 1 0 -github.com/echovault/echovault/echovault/keyspace.go:557.3,557.57 1 0 -github.com/echovault/echovault/echovault/keyspace.go:557.57,559.12 2 0 -github.com/echovault/echovault/echovault/keyspace.go:563.3,565.28 3 0 -github.com/echovault/echovault/echovault/keyspace.go:565.28,566.51 1 0 -github.com/echovault/echovault/echovault/keyspace.go:566.51,568.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:569.9,569.64 1 0 -github.com/echovault/echovault/echovault/keyspace.go:569.64,570.60 1 0 -github.com/echovault/echovault/echovault/keyspace.go:570.60,572.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:577.2,577.21 1 1 -github.com/echovault/echovault/echovault/keyspace.go:577.21,580.3 2 1 -github.com/echovault/echovault/echovault/keyspace.go:582.2,585.58 2 0 -github.com/echovault/echovault/echovault/keyspace.go:585.58,589.3 2 0 -github.com/echovault/echovault/echovault/keyspace.go:591.2,591.12 1 0 -github.com/echovault/echovault/echovault/modules.go:27.75,30.42 3 1 -github.com/echovault/echovault/echovault/modules.go:30.42,31.46 1 1 -github.com/echovault/echovault/echovault/modules.go:31.46,33.4 1 1 -github.com/echovault/echovault/echovault/modules.go:35.2,35.72 1 1 -github.com/echovault/echovault/echovault/modules.go:38.125,65.2 1 1 -github.com/echovault/echovault/echovault/modules.go:67.137,69.16 2 1 -github.com/echovault/echovault/echovault/modules.go:69.16,71.3 1 0 -github.com/echovault/echovault/echovault/modules.go:73.2,74.16 2 1 -github.com/echovault/echovault/echovault/modules.go:74.16,76.3 1 1 -github.com/echovault/echovault/echovault/modules.go:78.2,82.16 4 1 -github.com/echovault/echovault/echovault/modules.go:82.16,84.3 1 1 -github.com/echovault/echovault/echovault/modules.go:85.2,86.8 2 1 -github.com/echovault/echovault/echovault/modules.go:86.8,89.3 2 1 -github.com/echovault/echovault/echovault/modules.go:91.2,91.51 1 1 -github.com/echovault/echovault/echovault/modules.go:91.51,94.87 1 0 -github.com/echovault/echovault/echovault/modules.go:94.87,96.4 1 0 -github.com/echovault/echovault/echovault/modules.go:100.2,100.50 1 1 -github.com/echovault/echovault/echovault/modules.go:100.50,101.7 1 1 -github.com/echovault/echovault/echovault/modules.go:101.7,102.42 1 1 -github.com/echovault/echovault/echovault/modules.go:102.42,104.10 2 1 -github.com/echovault/echovault/echovault/modules.go:109.2,109.43 1 1 -github.com/echovault/echovault/echovault/modules.go:109.43,111.17 2 1 -github.com/echovault/echovault/echovault/modules.go:111.17,113.4 1 1 -github.com/echovault/echovault/echovault/modules.go:115.3,115.62 1 1 -github.com/echovault/echovault/echovault/modules.go:115.62,117.4 1 1 -github.com/echovault/echovault/echovault/modules.go:119.3,121.18 2 1 -github.com/echovault/echovault/echovault/modules.go:125.2,125.32 1 0 -github.com/echovault/echovault/echovault/modules.go:125.32,128.17 3 0 -github.com/echovault/echovault/echovault/modules.go:128.17,130.4 1 0 -github.com/echovault/echovault/echovault/modules.go:131.3,131.18 1 0 -github.com/echovault/echovault/echovault/modules.go:135.2,135.34 1 0 -github.com/echovault/echovault/echovault/modules.go:135.34,138.3 2 0 -github.com/echovault/echovault/echovault/modules.go:140.2,140.72 1 0 -github.com/echovault/echovault/echovault/plugin.go:37.72,41.41 3 1 -github.com/echovault/echovault/echovault/plugin.go:41.41,42.37 1 1 -github.com/echovault/echovault/echovault/plugin.go:42.37,44.4 1 1 -github.com/echovault/echovault/echovault/plugin.go:45.3,45.44 1 0 -github.com/echovault/echovault/echovault/plugin.go:48.2,49.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:49.16,51.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:53.2,54.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:54.16,56.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:57.2,58.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:58.9,60.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:62.2,63.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:63.16,65.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:66.2,67.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:67.9,69.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:71.2,72.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:72.16,74.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:75.2,76.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:76.9,78.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:80.2,81.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:81.16,83.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:84.2,85.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:85.9,87.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:89.2,90.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:90.16,92.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:93.2,94.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:94.9,96.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:98.2,99.16 2 1 -github.com/echovault/echovault/echovault/plugin.go:99.16,101.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:102.2,115.9 2 1 -github.com/echovault/echovault/echovault/plugin.go:115.9,117.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:120.2,120.91 1 1 -github.com/echovault/echovault/echovault/plugin.go:120.91,122.3 1 1 -github.com/echovault/echovault/echovault/plugin.go:125.2,128.31 1 1 -github.com/echovault/echovault/echovault/plugin.go:128.31,131.36 2 1 -github.com/echovault/echovault/echovault/plugin.go:131.36,133.5 1 1 -github.com/echovault/echovault/echovault/plugin.go:134.4,134.15 1 1 -github.com/echovault/echovault/echovault/plugin.go:139.83,141.18 2 0 -github.com/echovault/echovault/echovault/plugin.go:141.18,143.5 1 0 -github.com/echovault/echovault/echovault/plugin.go:144.4,148.10 1 0 -github.com/echovault/echovault/echovault/plugin.go:150.72,164.4 1 1 -github.com/echovault/echovault/echovault/plugin.go:167.2,167.12 1 1 -github.com/echovault/echovault/echovault/plugin.go:175.54,178.91 3 1 -github.com/echovault/echovault/echovault/plugin.go:178.91,180.3 1 1 -github.com/echovault/echovault/echovault/plugin.go:186.49,190.42 4 1 -github.com/echovault/echovault/echovault/plugin.go:190.42,191.61 1 1 -github.com/echovault/echovault/echovault/plugin.go:191.61,193.4 1 1 -github.com/echovault/echovault/echovault/plugin.go:193.6,195.4 1 1 -github.com/echovault/echovault/echovault/plugin.go:197.2,197.16 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:9.35,16.2 2 1 -github.com/echovault/echovault/echovault/test_helpers.go:18.95,19.61 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:19.61,21.3 1 0 -github.com/echovault/echovault/echovault/test_helpers.go:22.2,22.57 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:22.57,24.3 1 0 -github.com/echovault/echovault/echovault/test_helpers.go:25.2,26.12 2 1 -github.com/echovault/echovault/echovault/test_helpers.go:29.95,34.2 4 1 github.com/echovault/echovault/internal/modules/hash/commands.go:28.68,30.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:30.16,32.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:34.2,37.36 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:37.36,39.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:41.2,41.49 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:41.49,43.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:45.2,45.44 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:45.44,47.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:30.16,32.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:34.2,38.36 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:38.36,40.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:42.2,42.49 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:42.49,44.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:46.2,46.16 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:46.16,47.17 1 1 github.com/echovault/echovault/internal/modules/hash/commands.go:47.17,49.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:50.3,51.70 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:51.70,53.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:54.3,54.59 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:57.2,57.62 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:57.62,59.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:60.2,63.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:63.9,65.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:67.2,68.36 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:68.36,69.53 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:69.53,70.26 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:70.26,73.5 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:74.4,74.12 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:76.3,77.13 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:79.2,79.66 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:79.66,81.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:83.2,83.51 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:86.68,88.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:88.16,90.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:92.2,95.44 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:95.44,97.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:99.2,99.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:99.63,101.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:102.2,105.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:105.9,107.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:109.2,112.31 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:112.31,114.19 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:114.19,116.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:118.3,118.34 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:118.34,120.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:122.3,122.31 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:122.31,124.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:126.3,126.35 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:126.35,129.12 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:131.3,131.32 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:134.2,134.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:137.71,139.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:139.16,141.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:143.2,146.44 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:146.44,148.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:150.2,150.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:150.63,152.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:153.2,156.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:156.9,158.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:160.2,163.31 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:163.31,165.19 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:165.19,167.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:169.3,169.34 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:169.34,171.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:173.3,173.35 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:173.35,176.12 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:178.3,178.31 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:178.31,180.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:182.3,182.18 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:185.2,185.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:188.69,190.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:190.16,192.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:194.2,196.44 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:196.44,198.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:200.2,200.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:200.63,202.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:203.2,206.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:206.9,208.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:210.2,211.27 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:211.27,212.32 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:212.32,214.12 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:216.3,216.33 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:216.33,219.12 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:221.3,221.29 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:221.29,223.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:226.2,226.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:229.74,231.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:231.16,233.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:235.2,238.30 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:238.30,240.17 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:240.17,242.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:243.3,243.13 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:243.13,245.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:246.3,246.12 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:249.2,250.30 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:250.30,251.57 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:251.57,253.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:253.9,255.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:258.2,258.44 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:258.44,260.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:262.2,262.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:262.63,264.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:265.2,268.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:268.9,270.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:273.2,273.24 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:273.24,275.17 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:275.17,277.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:278.3,278.34 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:278.34,280.18 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:280.18,281.36 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:281.36,283.14 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:285.5,285.37 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:285.37,288.14 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:290.5,290.33 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:290.33,292.14 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:296.3,296.26 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:300.2,301.29 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:301.29,303.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:306.2,308.46 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:308.46,312.16 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:312.16,313.59 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:313.59,315.5 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:319.2,320.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:320.16,322.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:323.2,323.38 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:323.38,325.17 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:325.17,326.41 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:326.41,328.13 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:330.4,330.42 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:330.42,333.13 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:335.4,335.38 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:335.38,337.13 2 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:342.2,342.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:345.68,347.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:347.16,349.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:351.2,353.44 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:353.44,355.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:357.2,357.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:357.63,359.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:360.2,363.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:363.9,365.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:367.2,367.55 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:370.69,372.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:372.16,374.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:376.2,378.44 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:378.44,380.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:382.2,382.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:382.63,384.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:385.2,388.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:388.9,390.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:392.2,393.29 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:393.29,395.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:397.2,397.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:400.71,402.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:402.16,404.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:406.2,412.58 5 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:412.58,414.17 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:414.17,416.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:417.3,417.21 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:418.8,420.17 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:420.17,422.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:423.3,423.19 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:426.2,426.44 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:426.44,427.73 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:427.73,429.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:430.3,432.59 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:432.59,434.68 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:434.68,436.5 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:437.4,437.96 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:438.9,440.68 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:440.68,442.5 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:443.4,443.60 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:447.2,447.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:447.63,449.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:450.2,453.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:453.9,455.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:457.2,457.24 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:457.24,459.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:461.2,461.28 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:462.10,463.69 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:464.11,466.59 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:466.59,468.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:468.9,470.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:471.15,473.59 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:473.59,475.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:475.9,477.4 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:480.2,480.66 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:480.66,482.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:484.2,484.40 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:484.40,486.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:488.2,489.47 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:492.71,494.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:494.16,496.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:498.2,500.44 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:500.44,502.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:504.2,504.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:504.63,506.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:507.2,510.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:510.9,512.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:514.2,515.33 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:515.33,517.34 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:517.34,519.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:520.3,520.35 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:520.35,523.4 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:524.3,524.31 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:524.31,526.4 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:529.2,529.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:532.71,534.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:534.16,536.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:538.2,541.44 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:541.44,543.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:545.2,545.63 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:545.63,547.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:548.2,551.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:551.9,553.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:555.2,555.24 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:555.24,557.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:559.2,559.30 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:562.68,564.16 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:564.16,566.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:568.2,571.44 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:571.44,573.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:575.2,575.62 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:575.62,577.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:578.2,581.9 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:581.9,583.3 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:585.2,587.31 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:587.31,588.25 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:588.25,591.4 2 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:594.2,594.66 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:594.66,596.3 1 0 -github.com/echovault/echovault/internal/modules/hash/commands.go:598.2,598.51 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:601.36,725.2 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:50.3,50.95 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:50.95,52.4 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:53.3,53.59 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:56.2,57.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:57.9,59.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:61.2,62.36 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:62.36,63.53 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:63.53,64.26 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:64.26,67.5 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:68.4,68.12 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:70.3,71.13 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:73.2,73.91 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:73.91,75.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:77.2,77.51 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:80.68,82.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:82.16,84.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:86.2,90.16 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:90.16,92.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:94.2,95.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:95.9,97.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:99.2,102.31 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:102.31,104.19 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:104.19,106.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:108.3,108.34 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:108.34,110.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:112.3,112.31 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:112.31,114.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:116.3,116.35 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:116.35,119.12 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:121.3,121.32 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:124.2,124.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:127.71,129.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:129.16,131.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:133.2,137.16 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:137.16,139.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:141.2,142.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:142.9,144.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:146.2,149.31 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:149.31,151.19 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:151.19,153.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:155.3,155.34 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:155.34,157.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:159.3,159.35 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:159.35,162.12 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:164.3,164.31 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:164.31,166.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:168.3,168.18 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:171.2,171.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:174.69,176.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:176.16,178.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:180.2,183.16 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:183.16,185.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:187.2,188.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:188.9,190.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:192.2,193.27 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:193.27,194.32 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:194.32,196.12 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:198.3,198.33 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:198.33,201.12 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:203.3,203.29 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:203.29,205.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:208.2,208.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:211.74,213.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:213.16,215.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:217.2,221.30 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:221.30,223.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:223.17,225.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:226.3,226.13 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:226.13,228.4 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:229.3,229.12 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:232.2,233.30 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:233.30,234.57 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:234.57,236.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:236.9,238.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:241.2,241.16 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:241.16,243.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:245.2,246.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:246.9,248.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:251.2,251.24 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:251.24,253.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:253.17,255.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:256.3,256.34 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:256.34,258.18 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:258.18,259.36 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:259.36,261.14 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:263.5,263.37 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:263.37,266.14 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:268.5,268.33 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:268.33,270.14 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:274.3,274.26 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:278.2,279.29 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:279.29,281.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:284.2,286.46 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:286.46,290.16 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:290.16,291.59 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:291.59,293.5 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:297.2,298.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:298.16,300.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:301.2,301.38 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:301.38,303.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:303.17,304.41 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:304.41,306.13 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:308.4,308.42 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:308.42,311.13 3 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:313.4,313.38 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:313.38,315.13 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:320.2,320.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:323.68,325.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:325.16,327.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:329.2,332.16 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:332.16,334.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:336.2,337.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:337.9,339.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:341.2,341.55 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:344.69,346.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:346.16,348.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:350.2,353.16 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:353.16,355.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:357.2,358.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:358.9,360.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:362.2,363.29 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:363.29,365.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:367.2,367.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:370.71,372.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:372.16,374.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:376.2,383.58 6 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:383.58,385.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:385.17,387.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:388.3,388.21 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:389.8,391.17 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:391.17,393.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:394.3,394.19 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:397.2,397.16 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:397.16,399.59 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:399.59,401.93 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:401.93,403.5 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:404.4,404.96 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:405.9,407.93 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:407.93,409.5 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:410.4,410.60 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:414.2,415.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:415.9,417.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:419.2,419.24 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:419.24,421.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:423.2,423.28 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:424.10,425.69 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:426.11,428.59 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:428.59,430.4 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:430.9,432.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:433.15,435.59 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:435.59,437.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:437.9,439.4 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:442.2,442.91 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:442.91,444.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:446.2,446.40 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:446.40,448.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:450.2,451.47 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:454.71,456.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:456.16,458.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:460.2,463.16 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:463.16,465.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:467.2,468.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:468.9,470.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:472.2,473.33 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:473.33,475.34 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:475.34,477.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:478.3,478.35 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:478.35,481.4 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:482.3,482.31 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:482.31,484.4 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:487.2,487.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:490.71,492.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:492.16,494.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:496.2,500.16 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:500.16,502.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:504.2,505.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:505.9,507.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:509.2,509.24 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:509.24,511.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:513.2,513.30 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:516.68,518.16 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:518.16,520.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:522.2,526.16 4 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:526.16,528.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:530.2,531.9 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:531.9,533.3 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:535.2,537.31 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:537.31,538.25 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:538.25,541.4 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:544.2,544.91 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:544.91,546.3 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:548.2,548.51 1 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:551.36,675.2 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:23.74,24.18 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:24.18,26.3 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:27.2,31.8 1 1 -github.com/echovault/echovault/internal/modules/hash/key_funcs.go:34.76,35.18 1 0 +github.com/echovault/echovault/internal/modules/hash/key_funcs.go:34.76,35.18 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:35.18,37.3 1 0 -github.com/echovault/echovault/internal/modules/hash/key_funcs.go:38.2,42.8 1 0 +github.com/echovault/echovault/internal/modules/hash/key_funcs.go:38.2,42.8 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:45.74,46.18 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:46.18,48.3 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:49.2,53.8 1 1 @@ -2181,194 +1178,167 @@ github.com/echovault/echovault/internal/modules/hash/key_funcs.go:151.74,152.18 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:152.18,154.3 1 1 github.com/echovault/echovault/internal/modules/hash/key_funcs.go:155.2,159.8 1 1 github.com/echovault/echovault/internal/modules/list/commands.go:27.68,29.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:29.16,31.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:33.2,35.44 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:35.44,38.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:40.2,40.63 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:40.63,42.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:43.2,45.74 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:45.74,47.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:49.2,49.57 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:52.70,54.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:54.16,56.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:58.2,61.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:61.9,63.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:65.2,65.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:65.44,67.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:69.2,69.63 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:69.63,71.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:72.2,75.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:75.9,77.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:79.2,79.40 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:79.40,81.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:83.2,83.57 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:86.70,88.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:88.16,90.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:92.2,96.24 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:96.24,98.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:100.2,100.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:100.44,102.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:104.2,104.63 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:104.63,106.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:107.2,110.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:110.9,112.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:115.2,115.40 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:115.40,117.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:120.2,120.51 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:120.51,122.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:124.2,127.15 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:127.15,129.43 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:129.43,132.4 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:133.3,133.20 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:137.2,137.18 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:137.18,139.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:144.2,148.17 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:148.17,150.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:152.2,152.13 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:152.13,155.18 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:155.18,157.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:157.9,159.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:162.2,162.19 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:165.68,167.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:29.16,31.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:33.2,36.16 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:36.16,39.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:41.2,41.90 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:41.90,43.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:45.2,45.57 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:48.70,50.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:50.16,52.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:54.2,58.9 4 1 +github.com/echovault/echovault/internal/modules/list/commands.go:58.9,60.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:62.2,62.16 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:62.16,64.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:66.2,67.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:67.9,69.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:71.2,71.40 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:71.40,73.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:75.2,75.57 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:78.70,80.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:80.16,82.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:84.2,89.24 5 1 +github.com/echovault/echovault/internal/modules/list/commands.go:89.24,91.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:93.2,93.16 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:93.16,95.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:97.2,98.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:98.9,100.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:103.2,103.40 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:103.40,105.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:108.2,108.51 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:108.51,110.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:112.2,115.15 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:115.15,117.43 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:117.43,120.4 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:121.3,121.20 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:125.2,125.18 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:125.18,127.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:132.2,136.17 4 1 +github.com/echovault/echovault/internal/modules/list/commands.go:136.17,138.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:140.2,140.13 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:140.13,143.18 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:143.18,145.4 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:145.9,147.4 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:150.2,150.19 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:153.68,155.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:155.16,157.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:159.2,163.9 4 1 +github.com/echovault/echovault/internal/modules/list/commands.go:163.9,165.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:167.2,167.16 1 1 github.com/echovault/echovault/internal/modules/list/commands.go:167.16,169.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:171.2,174.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:174.9,176.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:178.2,178.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:178.44,180.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:182.2,182.62 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:182.62,184.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:185.2,188.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:188.9,190.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:192.2,192.40 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:192.40,194.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:196.2,197.66 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:197.66,199.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:201.2,201.42 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:204.69,206.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:206.16,208.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:210.2,214.24 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:214.24,216.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:218.2,218.30 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:218.30,220.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:222.2,222.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:222.44,224.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:226.2,226.62 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:226.62,228.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:229.2,232.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:232.9,234.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:236.2,236.40 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:236.40,238.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:240.2,240.34 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:240.34,241.75 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:241.75,243.4 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:244.3,244.43 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:247.2,247.77 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:247.77,249.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:250.2,250.42 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:253.68,255.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:255.16,257.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:259.2,263.9 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:263.9,265.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:267.2,269.44 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:269.44,271.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:273.2,273.62 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:273.62,275.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:276.2,279.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:279.9,281.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:283.2,283.9 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:284.10,284.10 0 0 -github.com/echovault/echovault/internal/modules/list/commands.go:286.17,288.34 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:288.34,289.26 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:289.26,290.10 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:292.4,292.43 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:292.43,295.5 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:297.17,299.39 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:299.39,300.26 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:300.26,301.10 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:303.4,303.43 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:303.43,306.5 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:310.2,310.61 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:310.61,312.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:314.2,314.66 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:314.66,316.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:318.2,318.42 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:321.69,323.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:323.16,325.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:327.2,331.116 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:331.116,333.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:335.2,335.97 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:335.97,337.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:339.2,339.65 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:339.65,341.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:342.2,345.16 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:345.16,347.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:348.2,353.33 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:353.33,355.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:357.2,357.19 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:358.14,360.24 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:360.24,362.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:362.9,362.32 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:362.32,364.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:365.15,367.24 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:367.24,369.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:369.9,369.32 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:369.32,371.4 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:374.2,374.16 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:374.16,376.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:378.2,378.42 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:381.69,383.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:383.16,385.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:387.2,389.42 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:389.42,391.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:393.2,395.44 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:395.44,396.45 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:397.17,398.61 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:399.11,400.73 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:400.73,402.5 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:403.4,403.79 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:403.79,405.5 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:407.8,408.63 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:408.63,410.4 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:412.2,417.9 4 1 -github.com/echovault/echovault/internal/modules/list/commands.go:417.9,419.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:421.2,421.84 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:421.84,423.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:424.2,424.66 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:427.69,429.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:429.16,431.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:433.2,437.42 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:437.42,439.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:441.2,441.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:441.44,442.45 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:443.17,444.61 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:445.11,446.73 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:446.73,448.5 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:449.4,450.79 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:450.79,452.5 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:454.8,455.63 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:455.63,457.4 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:458.3,458.46 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:461.2,465.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:465.9,467.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:469.2,469.84 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:469.84,471.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:472.2,472.66 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:475.67,477.16 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:477.16,479.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:481.2,483.44 2 1 -github.com/echovault/echovault/internal/modules/list/commands.go:483.44,485.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:487.2,487.62 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:487.62,489.3 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:490.2,493.9 3 1 -github.com/echovault/echovault/internal/modules/list/commands.go:493.9,495.3 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:497.2,497.44 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:498.10,499.71 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:499.71,501.4 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:502.3,502.54 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:503.14,504.81 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:504.81,506.4 1 0 -github.com/echovault/echovault/internal/modules/list/commands.go:507.3,507.64 1 1 -github.com/echovault/echovault/internal/modules/list/commands.go:511.36,634.2 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:171.2,172.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:172.9,174.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:176.2,176.40 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:176.40,178.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:180.2,181.91 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:181.91,183.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:185.2,185.42 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:188.69,190.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:190.16,192.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:194.2,199.24 5 1 +github.com/echovault/echovault/internal/modules/list/commands.go:199.24,201.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:203.2,203.30 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:203.30,205.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:207.2,207.16 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:207.16,209.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:211.2,212.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:212.9,214.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:216.2,216.40 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:216.40,218.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:220.2,220.34 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:220.34,221.100 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:221.100,223.4 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:224.3,224.43 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:227.2,227.102 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:227.102,229.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:230.2,230.42 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:233.68,235.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:235.16,237.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:239.2,244.9 5 1 +github.com/echovault/echovault/internal/modules/list/commands.go:244.9,246.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:248.2,250.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:250.16,252.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:254.2,255.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:255.9,257.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:259.2,259.9 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:260.10,260.10 0 0 +github.com/echovault/echovault/internal/modules/list/commands.go:262.17,264.34 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:264.34,265.26 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:265.26,266.10 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:268.4,268.43 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:268.43,271.5 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:273.17,275.39 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:275.39,276.26 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:276.26,277.10 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:279.4,279.43 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:279.43,282.5 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:286.2,286.61 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:286.61,288.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:290.2,290.91 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:290.91,292.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:294.2,294.42 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:297.69,299.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:299.16,301.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:303.2,308.116 5 1 +github.com/echovault/echovault/internal/modules/list/commands.go:308.116,310.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:312.2,312.51 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:312.51,314.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:316.2,320.33 4 1 +github.com/echovault/echovault/internal/modules/list/commands.go:320.33,322.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:324.2,324.19 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:325.14,328.38 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:328.38,329.26 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:329.26,331.6 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:333.5,333.50 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:336.15,339.38 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:339.38,340.26 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:340.26,342.6 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:344.5,344.66 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:349.2,349.16 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:349.16,351.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:353.2,353.42 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:356.69,358.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:358.16,360.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:362.2,364.42 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:364.42,366.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:368.2,371.16 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:371.16,372.45 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:373.17,374.64 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:375.11,376.104 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:376.104,378.5 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:382.2,384.9 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:384.9,386.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:388.2,388.109 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:388.109,390.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:392.2,392.66 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:395.69,397.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:397.16,399.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:401.2,406.42 4 1 +github.com/echovault/echovault/internal/modules/list/commands.go:406.42,408.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:410.2,410.16 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:410.16,411.45 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:412.17,413.64 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:414.11,415.104 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:415.104,417.5 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:421.2,423.9 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:423.9,425.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:427.2,427.109 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:427.109,429.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:430.2,430.66 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:433.67,435.16 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:435.16,437.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:439.2,442.16 3 1 +github.com/echovault/echovault/internal/modules/list/commands.go:442.16,444.3 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:446.2,447.9 2 1 +github.com/echovault/echovault/internal/modules/list/commands.go:447.9,449.3 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:451.2,451.44 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:452.10,453.96 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:453.96,455.4 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:456.3,456.54 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:457.14,458.106 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:458.106,460.4 1 0 +github.com/echovault/echovault/internal/modules/list/commands.go:461.3,461.64 1 1 +github.com/echovault/echovault/internal/modules/list/commands.go:465.36,588.2 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:23.75,24.18 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:24.18,26.3 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:27.2,31.8 1 1 @@ -2551,309 +1521,191 @@ github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:266.47,271.38 4 github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:271.38,273.3 1 1 github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:275.2,275.17 1 1 github.com/echovault/echovault/internal/modules/set/commands.go:26.68,28.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:28.16,30.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:32.2,36.44 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:36.44,38.81 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:38.81,40.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:41.3,41.66 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:41.66,43.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:44.3,45.70 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:48.2,48.62 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:48.62,50.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:51.2,54.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:54.9,56.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:58.2,60.51 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:63.69,65.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:65.16,67.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:69.2,71.44 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:71.44,73.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:75.2,75.63 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:75.63,77.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:78.2,81.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:81.9,83.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:85.2,87.57 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:90.69,92.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:92.16,94.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:97.2,97.57 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:97.57,99.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:100.2,100.76 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:100.76,102.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:103.2,105.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:105.9,107.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:109.2,110.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:110.15,111.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:111.34,112.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:112.14,114.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:118.2,118.40 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:118.40,119.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:119.45,120.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:122.3,122.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:122.64,123.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:125.3,125.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:128.2,129.41 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:129.41,131.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:131.10,132.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:134.3,134.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:137.2,141.26 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:141.26,143.24 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:143.24,145.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:148.2,148.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:151.74,153.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:153.16,155.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:157.2,160.57 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:160.57,162.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:163.2,163.77 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:163.77,165.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:166.2,168.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:168.9,170.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:172.2,173.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:173.15,174.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:174.34,175.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:175.14,177.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:181.2,181.40 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:181.40,182.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:182.45,183.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:185.3,185.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:185.64,186.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:188.3,188.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:191.2,192.40 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:192.40,194.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:194.10,195.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:197.3,197.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:200.2,205.51 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:205.51,206.71 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:206.71,208.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:209.3,209.75 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:209.75,211.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:212.3,213.26 2 0 -github.com/echovault/echovault/internal/modules/set/commands.go:216.2,216.79 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:216.79,218.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:219.2,219.74 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:219.74,221.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:222.2,224.25 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:227.70,229.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:229.16,231.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:233.2,234.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:234.15,235.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:235.34,236.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:236.14,238.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:242.2,242.36 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:242.36,243.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:243.45,246.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:247.3,247.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:247.64,249.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:250.3,250.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:253.2,255.28 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:255.28,257.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:257.10,260.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:261.3,261.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:264.2,264.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:264.20,266.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:268.2,272.26 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:272.26,274.24 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:274.24,276.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:279.2,279.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:282.74,284.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:284.16,286.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:289.2,290.67 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:290.67,292.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:293.2,293.35 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:293.35,295.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:296.2,296.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:296.20,298.38 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:298.38,300.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:302.3,302.71 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:302.71,304.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:304.9,306.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:309.2,310.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:310.15,311.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:311.34,312.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:312.14,314.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:318.2,318.36 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:318.36,319.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:319.45,322.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:323.3,323.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:323.64,325.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:326.3,326.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:329.2,331.28 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:331.28,333.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:333.10,336.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:337.3,337.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:340.2,340.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:340.20,342.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:344.2,346.69 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:349.75,351.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:351.16,353.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:355.2,356.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:356.15,357.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:357.34,358.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:358.14,360.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:364.2,364.36 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:364.36,365.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:365.45,368.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:369.3,369.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:369.64,371.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:372.3,372.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:375.2,377.28 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:377.28,379.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:379.10,382.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:383.3,383.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:386.2,389.51 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:389.51,390.71 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:390.71,392.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:393.8,394.80 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:394.80,396.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:399.2,399.79 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:399.79,401.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:402.2,404.69 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:407.73,409.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:409.16,411.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:413.2,415.44 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:415.44,417.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:419.2,419.63 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:419.63,421.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:422.2,425.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:425.9,427.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:429.2,429.38 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:429.38,431.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:433.2,433.30 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:436.72,438.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:438.16,440.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:442.2,444.44 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:444.44,446.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:448.2,448.63 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:448.63,450.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:451.2,454.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:454.9,456.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:458.2,461.26 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:461.26,463.24 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:463.24,465.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:468.2,468.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:471.74,473.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:473.16,475.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:477.2,480.44 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:480.44,482.29 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:482.29,484.27 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:484.27,486.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:488.3,488.26 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:491.2,491.63 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:491.63,493.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:494.2,497.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:497.9,499.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:501.2,502.36 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:502.36,503.31 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:503.31,505.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:505.9,507.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:509.2,511.25 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:514.69,516.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:516.16,518.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:520.2,523.47 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:523.47,525.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:527.2,527.65 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:527.65,529.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:530.2,533.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:533.9,535.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:537.2,539.52 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:539.52,541.80 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:541.80,543.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:544.3,546.85 3 0 -github.com/echovault/echovault/internal/modules/set/commands.go:546.85,548.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:549.8,551.72 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:551.72,553.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:554.3,556.10 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:28.16,30.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:32.2,37.16 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:37.16,39.91 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:39.91,41.4 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:42.3,42.70 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:45.2,46.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:46.9,48.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:50.2,52.51 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:55.69,57.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:57.16,59.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:61.2,64.16 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:64.16,66.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:68.2,69.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:69.9,71.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:73.2,75.57 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:78.69,80.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:80.16,82.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:84.2,87.34 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:87.34,89.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:91.2,92.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:92.9,94.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:96.2,97.41 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:97.41,99.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:99.10,100.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:102.3,102.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:105.2,109.26 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:109.26,111.24 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:111.24,113.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:116.2,116.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:119.74,121.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:121.16,123.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:125.2,129.34 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:129.34,131.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:133.2,134.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:134.9,136.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:138.2,139.40 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:139.40,141.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:141.10,142.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:144.3,144.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:147.2,152.99 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:152.99,154.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:156.2,156.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:159.70,161.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:161.16,163.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:165.2,169.37 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:169.37,170.14 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:170.14,172.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:173.3,174.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:174.10,177.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:178.3,178.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:181.2,181.20 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:181.20,183.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:185.2,189.26 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:189.26,191.24 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:191.24,193.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:196.2,196.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:199.74,201.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:201.16,203.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:205.2,209.67 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:209.67,211.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:212.2,212.35 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:212.35,214.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:215.2,215.20 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:215.20,217.38 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:217.38,219.4 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:221.3,221.71 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:221.71,223.4 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:223.9,225.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:228.2,230.37 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:230.37,231.14 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:231.14,233.4 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:234.3,235.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:235.10,238.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:239.3,239.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:242.2,242.20 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:242.20,244.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:246.2,248.69 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:251.75,253.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:253.16,255.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:257.2,261.37 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:261.37,262.14 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:262.14,264.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:265.3,266.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:266.10,269.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:270.3,270.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:273.2,276.104 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:276.104,278.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:280.2,280.69 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:283.73,285.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:285.16,287.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:289.2,292.16 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:292.16,294.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:296.2,297.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:297.9,299.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:301.2,301.38 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:301.38,303.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:305.2,305.30 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:308.72,310.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:310.16,312.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:314.2,317.16 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:317.16,319.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:321.2,322.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:322.9,324.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:326.2,329.26 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:329.26,331.24 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:331.24,333.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:336.2,336.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:339.74,341.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:341.16,343.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:345.2,349.16 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:349.16,351.29 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:351.29,353.27 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:353.27,355.5 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:357.3,357.26 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:360.2,361.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:361.9,363.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:365.2,366.36 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:366.36,367.31 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:367.31,369.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:369.9,371.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:373.2,375.25 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:378.69,380.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:380.16,382.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:384.2,388.24 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:388.24,390.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:392.2,395.9 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:395.9,397.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:399.2,400.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:400.9,402.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:404.2,406.49 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:409.68,411.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:411.16,413.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:415.2,419.30 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:419.30,421.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:421.10,423.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:424.3,424.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:427.2,427.16 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:427.16,429.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:431.2,432.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:432.9,434.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:436.2,439.28 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:439.28,441.26 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:441.26,443.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:446.2,446.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:449.75,451.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:451.16,453.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:455.2,459.30 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:459.30,461.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:461.10,463.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:464.3,464.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:467.2,467.16 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:467.16,469.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:471.2,472.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:472.9,474.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:476.2,479.28 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:479.28,481.26 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:481.26,483.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:486.2,486.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:489.68,491.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:491.16,493.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:495.2,499.16 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:499.16,501.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:503.2,504.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:504.9,506.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:508.2,510.51 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:513.70,515.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:515.16,517.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:519.2,522.33 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:522.33,524.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:524.10,526.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:527.3,527.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:530.2,533.35 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:533.35,535.33 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:535.33,537.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:540.2,540.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:543.75,545.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:545.16,547.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:549.2,554.33 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:554.33,556.10 2 1 github.com/echovault/echovault/internal/modules/set/commands.go:556.10,558.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:559.3,559.22 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:562.2,564.49 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:567.68,569.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:569.16,571.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:573.2,576.30 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:576.30,578.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:578.10,580.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:581.3,581.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:584.2,584.44 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:584.44,586.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:588.2,588.62 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:588.62,590.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:591.2,594.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:594.9,596.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:598.2,601.28 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:601.28,603.26 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:603.26,605.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:608.2,608.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:611.75,613.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:613.16,615.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:617.2,620.30 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:620.30,622.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:622.10,624.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:625.3,625.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:628.2,628.44 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:628.44,630.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:632.2,632.62 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:632.62,634.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:635.2,638.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:638.9,640.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:642.2,645.28 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:645.28,647.26 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:647.26,649.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:652.2,652.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:655.68,657.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:657.16,659.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:661.2,664.44 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:664.44,666.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:668.2,668.62 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:668.62,670.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:671.2,674.9 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:674.9,676.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:678.2,680.51 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:683.70,685.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:685.16,687.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:689.2,690.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:690.15,691.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:691.34,692.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:692.14,694.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:698.2,698.36 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:698.36,699.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:699.45,700.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:702.3,702.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:702.64,704.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:705.3,705.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:708.2,710.33 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:710.33,711.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:711.14,712.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:714.3,715.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:715.10,717.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:718.3,718.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:721.2,724.35 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:724.35,726.33 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:726.33,728.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:731.2,731.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:734.75,736.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:736.16,738.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:740.2,741.15 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:741.15,742.34 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:742.34,743.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:743.14,745.5 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:749.2,749.36 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:749.36,750.45 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:750.45,751.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:753.3,753.64 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:753.64,755.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:756.3,756.20 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:759.2,761.33 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:761.33,762.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:762.14,763.12 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:765.3,766.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:766.10,768.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:769.3,769.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:772.2,776.51 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:776.51,777.71 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:777.71,779.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:780.8,781.80 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:781.80,783.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:785.2,787.75 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:787.75,789.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:790.2,790.65 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:793.36,946.2 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:559.3,559.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:562.2,564.100 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:564.100,566.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:567.2,567.65 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:570.36,723.2 1 1 github.com/echovault/echovault/internal/modules/set/key_funcs.go:25.74,26.18 1 1 github.com/echovault/echovault/internal/modules/set/key_funcs.go:26.18,28.3 1 1 github.com/echovault/echovault/internal/modules/set/key_funcs.go:29.2,33.8 1 1 @@ -2960,692 +1812,500 @@ github.com/echovault/echovault/internal/modules/set/set.go:184.31,185.19 1 1 github.com/echovault/echovault/internal/modules/set/set.go:186.9,187.17 1 1 github.com/echovault/echovault/internal/modules/set/set.go:188.9,191.15 3 1 github.com/echovault/echovault/internal/modules/set/set.go:192.10,195.28 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:27.78,33.29 4 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:33.29,34.54 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:34.54,40.42 4 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:40.42,42.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:44.4,47.12 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:50.3,50.36 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:50.36,57.43 5 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:57.43,59.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:61.4,63.21 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:67.2,69.25 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:72.76,76.35 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:76.35,77.65 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:77.65,78.41 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:78.41,80.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:81.4,81.12 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:83.3,83.13 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:86.2,86.51 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:89.75,90.29 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:91.9,96.36 4 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:96.36,97.66 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:97.66,98.52 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:98.52,102.6 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:103.5,103.13 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:105.4,106.14 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:108.3,109.26 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:111.9,115.56 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:115.56,117.4 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:118.3,118.53 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:118.53,122.37 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:122.37,123.67 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:123.67,124.53 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:124.53,125.59 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:125.59,129.8 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:131.6,131.14 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:133.5,133.54 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:133.54,136.6 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:138.9,138.61 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:138.61,142.37 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:142.37,143.67 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:143.67,144.53 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:144.53,146.24 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:146.24,149.8 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:151.6,151.14 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:153.5,153.33 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:153.33,156.6 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:158.9,158.60 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:158.60,162.37 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:162.37,163.67 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:163.67,164.53 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:164.53,165.55 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:165.55,169.8 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:171.6,171.14 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:173.5,173.50 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:173.50,176.6 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:178.9,180.4 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:181.3,182.26 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:183.10,184.54 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:188.75,190.2 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:192.36,200.84 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:225.86,229.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:267.84,271.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:272.73,273.49 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:273.49,275.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:276.5,276.45 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:285.84,289.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:290.73,292.18 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:292.18,294.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:295.5,295.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:304.84,308.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:309.73,310.47 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:310.47,312.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:313.5,313.45 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:321.84,325.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:335.86,339.7 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:340.75,341.34 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:341.34,343.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:344.7,345.34 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:345.34,347.8 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:348.7,348.75 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:348.75,350.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:351.7,351.47 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:361.86,365.7 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:366.75,367.35 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:367.35,369.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:370.7,371.47 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:380.86,384.7 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:385.75,388.38 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:388.38,390.8 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:391.7,391.30 1 1 github.com/echovault/echovault/internal/modules/sorted_set/commands.go:29.68,31.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:31.16,33.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:35.2,44.43 7 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:44.43,45.29 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:45.29,46.9 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:48.3,48.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:49.15,50.85 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:50.85,52.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:53.16,54.25 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:55.12,56.25 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:60.2,60.77 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:60.77,62.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:64.2,66.63 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:66.63,67.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:67.15,68.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:70.3,71.23 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:72.11,73.64 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:74.15,76.49 2 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:76.49,82.5 2 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:83.4,83.49 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:83.49,89.5 2 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:90.16,95.6 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:96.12,101.6 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:106.2,106.27 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:106.27,108.34 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:108.34,109.70 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:109.70,112.61 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:112.61,114.6 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:115.5,115.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:117.4,117.70 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:117.70,121.36 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:121.36,123.6 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:124.5,124.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:126.4,126.39 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:126.39,128.13 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:130.4,130.41 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:130.41,133.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:133.25,135.6 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:136.5,136.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:138.4,138.55 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:142.2,142.43 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:142.43,145.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:145.17,147.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:148.3,150.10 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:150.10,152.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:153.3,154.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:154.17,156.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:158.3,158.18 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:158.18,161.4 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:163.3,163.52 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:167.2,167.71 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:167.71,169.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:170.2,173.65 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:173.65,175.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:177.2,177.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:180.69,182.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:182.16,184.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:185.2,187.44 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:187.44,189.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:191.2,191.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:191.63,193.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:194.2,197.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:197.9,199.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:201.2,201.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:204.70,206.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:206.16,208.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:210.2,213.54 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:214.10,215.60 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:216.14,217.51 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:217.51,219.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:219.9,221.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:222.15,224.21 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:225.11,227.21 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:230.2,231.54 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:232.10,233.60 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:234.14,235.51 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:235.51,237.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:237.9,239.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:240.15,242.21 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:243.11,245.21 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:248.2,248.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:248.44,250.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:252.2,252.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:252.63,254.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:255.2,258.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:258.9,260.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:262.2,263.33 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:263.33,264.47 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:264.47,266.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:269.2,269.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:272.73,274.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:274.16,276.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:278.2,282.44 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:282.44,284.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:286.2,286.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:286.63,288.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:289.2,292.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:292.9,294.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:296.2,299.38 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:299.38,300.45 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:300.45,302.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:305.2,307.28 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:307.28,309.81 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:309.81,311.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:314.2,314.51 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:317.69,319.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:319.16,321.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:323.2,323.74 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:323.74,325.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:326.2,326.49 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:326.49,328.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:330.2,331.15 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:331.15,332.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:332.34,333.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:333.14,335.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:340.2,340.57 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:340.57,343.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:344.2,344.76 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:344.76,346.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:347.2,349.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:349.9,351.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:354.2,356.42 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:356.42,357.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:357.58,358.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:360.3,361.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:361.17,363.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:364.3,366.10 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:366.10,368.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:369.3,369.27 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:372.2,377.34 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:377.34,378.20 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:378.20,380.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:380.9,382.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:385.2,387.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:390.74,392.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:392.16,394.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:396.2,399.15 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:399.15,400.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:400.34,401.14 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:401.14,403.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:408.2,408.57 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:408.57,411.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:412.2,412.76 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:412.76,414.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:415.2,417.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:417.9,419.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:421.2,423.42 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:423.42,424.57 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:424.57,425.78 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:425.78,427.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:428.4,429.11 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:429.11,431.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:432.4,432.28 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:436.2,438.51 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:438.51,439.71 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:439.71,441.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:442.8,443.80 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:443.80,445.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:447.2,449.74 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:449.74,451.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:453.2,453.64 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:456.71,458.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:458.16,460.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:462.2,466.54 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:467.10,468.55 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:469.14,470.68 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:470.68,472.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:472.9,472.75 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:472.75,474.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:474.9,476.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:477.15,479.23 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:480.11,482.23 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:485.2,485.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:485.44,488.72 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:488.72,490.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:491.3,495.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:495.17,497.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:498.3,499.99 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:502.2,502.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:502.62,504.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:505.2,507.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:507.9,509.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:510.2,516.23 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:516.23,518.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:519.2,520.74 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:523.70,525.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:525.16,527.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:529.2,530.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:530.16,532.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:534.2,535.15 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:535.15,536.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:536.34,537.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:537.14,539.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:543.2,545.33 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:545.33,546.49 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:546.49,549.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:550.3,550.68 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:550.68,552.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:553.3,555.10 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:555.10,557.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:558.3,561.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:564.2,568.33 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:568.33,569.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:569.40,570.18 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:570.18,572.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:572.10,574.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:578.2,580.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:583.75,585.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:585.16,587.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:589.2,592.63 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:592.63,594.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:596.2,597.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:597.16,599.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:601.2,602.15 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:602.15,603.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:603.34,604.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:604.14,606.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:610.2,612.33 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:612.33,613.49 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:613.49,615.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:616.3,616.68 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:616.68,618.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:619.3,621.10 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:621.10,623.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:624.3,627.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:630.2,632.82 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:632.82,633.71 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:633.71,635.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:636.8,636.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:636.40,637.80 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:637.80,639.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:641.2,643.79 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:643.79,645.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:647.2,647.69 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:650.69,652.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:652.16,654.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:656.2,661.67 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:661.67,663.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:664.2,664.20 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:664.20,665.19 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:665.19,667.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:668.3,668.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:668.40,670.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:671.3,672.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:672.17,674.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:675.3,675.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:675.13,677.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:678.3,679.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:683.2,683.68 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:683.68,685.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:686.2,686.21 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:686.21,687.20 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:687.20,689.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:690.3,691.53 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:691.53,693.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:696.2,696.43 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:696.43,697.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:697.58,698.78 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:698.78,699.13 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:701.4,702.35 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:702.35,704.13 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:706.4,707.18 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:707.18,710.5 2 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:711.4,715.38 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:715.38,717.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:719.4,721.27 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:725.2,725.30 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:728.68,730.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:730.16,732.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:734.2,738.53 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:738.53,740.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:742.2,742.30 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:742.30,744.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:744.17,746.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:747.3,747.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:747.12,749.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:752.2,752.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:752.44,754.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:756.2,756.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:756.62,758.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:759.2,762.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:762.9,764.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:766.2,767.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:767.16,769.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:771.2,772.36 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:772.36,774.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:776.2,778.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:781.71,783.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:783.16,785.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:787.2,789.44 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:789.44,791.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:793.2,793.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:793.63,795.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:796.2,799.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:799.9,801.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:803.2,809.36 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:809.36,811.21 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:811.21,813.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:813.9,815.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:818.2,820.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:823.75,825.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:825.16,827.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:829.2,832.30 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:832.30,834.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:834.17,836.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:837.3,837.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:837.13,839.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:842.2,843.30 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:843.30,844.57 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:844.57,846.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:846.9,848.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:851.2,851.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:851.44,853.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:855.2,855.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:855.63,857.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:858.2,861.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:861.9,863.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:865.2,868.28 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:868.28,869.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:869.17,871.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:871.9,873.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:876.2,878.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:881.69,883.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:883.16,885.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:887.2,891.84 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:891.84,893.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:895.2,895.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:895.44,897.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:899.2,899.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:899.63,901.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:902.2,905.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:905.9,907.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:909.2,910.54 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:910.54,911.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:911.55,913.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:914.3,914.39 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:917.2,917.36 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:917.36,918.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:918.40,919.18 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:919.18,922.5 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:922.10,924.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:928.2,928.31 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:931.68,933.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:933.16,935.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:937.2,939.44 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:939.44,941.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:943.2,943.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:943.62,945.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:946.2,949.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:949.9,951.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:953.2,954.39 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:954.39,955.27 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:955.27,957.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:960.2,960.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:963.70,965.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:965.16,967.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:969.2,971.44 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:971.44,973.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:974.2,974.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:974.63,976.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:977.2,979.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:979.9,981.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:982.2,983.20 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:983.20,985.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:987.2,989.69 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:992.80,994.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:994.16,996.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:998.2,1003.16 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1003.16,1005.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1007.2,1008.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1008.16,1010.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1012.2,1012.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1012.44,1014.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1016.2,1016.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1016.62,1018.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1019.2,1022.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1022.9,1024.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1026.2,1026.33 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1026.33,1027.61 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1027.61,1030.4 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1033.2,1033.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1036.79,1038.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1038.16,1040.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1042.2,1045.16 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1045.16,1047.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1049.2,1050.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1050.16,1052.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1054.2,1054.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1054.44,1056.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1058.2,1058.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1058.62,1060.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1061.2,1064.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1064.9,1066.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1068.2,1068.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1068.15,1070.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1071.2,1071.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1071.14,1073.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1075.2,1075.88 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1075.88,1077.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1079.2,1080.54 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1080.54,1082.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1084.2,1086.18 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1086.18,1087.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1087.34,1090.4 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1091.8,1092.34 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1092.34,1095.4 2 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1098.2,1098.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1101.78,1103.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1103.16,1105.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1107.2,1111.44 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1111.44,1113.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1115.2,1115.62 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1115.62,1117.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1118.2,1121.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1121.9,1123.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1125.2,1128.38 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1128.38,1129.45 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1129.45,1131.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1134.2,1137.28 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1137.28,1139.81 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1139.81,1142.4 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1145.2,1145.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1148.70,1150.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1150.16,1152.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1154.2,1163.76 9 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1163.76,1165.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1167.2,1167.73 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1167.73,1169.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1171.2,1171.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1171.65,1173.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1173.5,1175.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1175.8,1178.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1178.17,1180.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1181.3,1182.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1182.17,1184.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1187.2,1187.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1187.65,1189.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1189.5,1190.72 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1190.72,1192.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1193.3,1193.61 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1193.61,1195.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1196.3,1197.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1197.17,1199.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1200.3,1200.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1200.17,1202.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1203.3,1204.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1204.17,1206.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1209.2,1209.44 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1209.44,1211.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1213.2,1213.63 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1213.63,1215.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1216.2,1219.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1219.9,1221.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1223.2,1223.32 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1223.32,1225.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1226.2,1226.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1226.15,1228.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1230.2,1231.42 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1231.42,1232.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1232.55,1234.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1234.15,1236.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1237.4,1237.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1240.2,1240.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1240.40,1242.39 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1242.39,1243.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1243.46,1245.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1247.3,1247.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1247.55,1248.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1248.15,1250.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1251.4,1251.64 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1255.2,1257.35 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1257.35,1258.24 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1258.24,1259.9 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1261.3,1261.43 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1261.43,1262.85 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1262.85,1264.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1265.4,1265.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1267.3,1268.90 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1268.90,1270.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1273.2,1275.34 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1275.34,1276.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1276.17,1278.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1278.9,1280.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1283.2,1285.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1288.75,1290.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1290.16,1292.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1294.2,1304.73 10 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1304.73,1306.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1308.2,1308.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1308.65,1310.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1310.5,1312.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1312.8,1315.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1315.17,1317.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1318.3,1319.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1319.17,1321.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1324.2,1324.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1324.65,1326.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1326.5,1327.72 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1327.72,1329.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1330.3,1330.61 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1330.61,1332.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1333.3,1334.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1334.17,1336.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1337.3,1337.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1337.17,1339.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1340.3,1341.17 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1341.17,1343.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1346.2,1346.47 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1346.47,1348.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1350.2,1350.66 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1350.66,1352.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1353.2,1356.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1356.9,1358.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1360.2,1360.32 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1360.32,1362.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1363.2,1363.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1363.15,1365.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1367.2,1368.42 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1368.42,1369.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1369.55,1371.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1371.15,1373.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1374.4,1374.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1377.2,1377.40 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1377.40,1379.39 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1379.39,1380.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1380.46,1382.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1384.3,1384.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1384.55,1385.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1385.15,1387.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1388.4,1388.64 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1392.2,1394.35 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1394.35,1395.24 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1395.24,1396.9 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1398.3,1398.43 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1398.43,1399.85 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1399.85,1401.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1402.4,1402.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1404.3,1405.90 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1405.90,1407.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1410.2,1412.51 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1412.51,1413.71 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1413.71,1415.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1416.8,1417.80 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1417.80,1419.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1421.2,1423.82 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1423.82,1425.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1427.2,1427.72 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1430.70,1431.57 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1431.57,1433.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1435.2,1436.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1436.16,1438.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1440.2,1441.15 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1441.15,1442.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1442.34,1443.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1443.14,1445.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1449.2,1451.33 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1451.33,1452.48 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1452.48,1453.69 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1453.69,1455.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1456.4,1458.11 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1458.11,1460.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1461.4,1464.6 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1468.2,1471.35 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1471.35,1472.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1472.17,1474.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1474.9,1476.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1479.2,1481.25 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1484.75,1486.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1486.16,1488.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1490.2,1493.73 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1493.73,1495.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1497.2,1498.16 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1498.16,1500.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1502.2,1503.15 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1503.15,1504.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1504.34,1505.14 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1505.14,1507.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1511.2,1513.33 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1513.33,1514.48 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1514.48,1515.69 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1515.69,1517.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1518.4,1520.11 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1520.11,1522.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1523.4,1526.6 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1530.2,1532.51 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1532.51,1533.71 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1533.71,1535.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1536.8,1537.80 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1537.80,1539.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1541.2,1543.75 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1543.75,1545.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1547.2,1547.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1550.36,1822.2 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:31.16,33.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:35.2,45.43 8 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:45.43,46.29 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:46.29,47.9 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:49.3,49.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:50.15,51.85 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:51.85,53.5 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:54.16,55.25 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:56.12,57.25 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:61.2,61.77 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:61.77,63.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:65.2,67.63 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:67.63,68.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:68.15,69.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:71.3,72.23 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:73.11,74.64 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:75.15,77.49 2 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:77.49,83.5 2 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:84.4,84.49 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:84.49,90.5 2 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:91.16,96.6 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:97.12,102.6 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:107.2,107.27 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:107.27,109.34 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:109.34,110.70 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:110.70,113.61 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:113.61,115.6 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:116.5,116.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:118.4,118.70 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:118.70,122.36 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:122.36,124.6 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:125.5,125.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:127.4,127.39 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:127.39,129.13 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:131.4,131.41 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:131.41,134.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:134.25,136.6 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:137.5,137.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:139.4,139.55 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:143.2,143.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:143.15,146.10 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:146.10,148.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:149.3,150.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:150.17,152.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:154.3,154.18 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:154.18,157.4 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:159.3,159.52 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:163.2,164.90 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:164.90,166.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:168.2,168.63 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:171.69,173.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:173.16,175.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:177.2,180.16 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:180.16,182.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:184.2,185.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:185.9,187.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:189.2,189.63 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:192.70,194.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:194.16,196.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:198.2,202.54 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:203.10,204.60 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:205.14,206.51 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:206.51,208.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:208.9,210.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:211.15,213.21 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:214.11,216.21 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:219.2,220.54 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:221.10,222.60 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:223.14,224.51 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:224.51,226.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:226.9,228.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:229.15,231.21 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:232.11,234.21 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:237.2,237.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:237.16,239.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:241.2,242.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:242.9,244.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:246.2,247.33 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:247.33,248.47 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:248.47,250.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:253.2,253.58 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:256.73,258.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:258.16,260.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:262.2,267.16 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:267.16,269.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:271.2,272.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:272.9,274.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:276.2,279.38 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:279.38,280.45 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:280.45,282.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:285.2,287.28 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:287.28,289.81 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:289.81,291.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:294.2,294.51 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:297.69,299.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:299.16,301.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:303.2,305.74 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:305.74,307.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:308.2,308.49 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:308.49,310.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:313.2,313.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:313.34,316.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:318.2,319.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:319.9,321.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:324.2,326.42 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:326.42,327.35 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:327.35,328.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:330.3,331.10 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:331.10,333.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:334.3,334.27 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:337.2,342.34 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:342.34,343.20 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:343.20,346.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:346.9,348.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:351.2,353.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:356.74,358.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:358.16,360.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:362.2,366.34 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:366.34,369.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:371.2,372.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:372.9,374.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:376.2,378.42 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:378.42,379.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:379.34,381.11 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:381.11,383.5 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:384.4,384.28 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:388.2,389.99 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:389.99,391.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:393.2,393.64 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:396.71,398.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:398.16,400.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:402.2,408.54 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:409.10,410.55 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:411.14,412.68 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:412.68,414.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:414.9,414.75 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:414.75,416.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:416.9,418.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:419.15,421.23 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:422.11,424.23 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:427.2,427.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:427.16,435.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:435.17,437.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:438.3,438.99 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:441.2,442.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:442.9,444.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:445.2,451.23 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:451.23,453.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:454.2,455.74 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:458.70,460.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:460.16,462.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:464.2,465.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:465.16,467.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:468.2,473.33 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:473.33,474.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:474.26,477.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:478.3,479.10 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:479.10,481.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:482.3,485.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:488.2,492.33 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:492.33,493.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:493.40,494.18 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:494.18,496.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:496.10,498.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:502.2,504.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:507.75,509.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:509.16,511.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:513.2,517.63 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:517.63,519.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:521.2,522.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:522.16,524.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:526.2,529.33 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:529.33,530.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:530.26,532.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:533.3,534.10 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:534.10,536.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:537.3,540.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:543.2,546.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:546.17,548.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:550.2,550.69 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:553.69,555.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:555.16,557.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:559.2,566.67 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:566.67,568.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:569.2,569.20 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:569.20,570.19 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:570.19,572.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:573.3,573.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:573.40,575.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:576.3,577.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:577.17,579.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:580.3,580.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:580.13,582.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:583.3,584.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:588.2,588.68 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:588.68,590.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:591.2,591.21 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:591.21,592.20 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:592.20,594.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:595.3,596.53 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:596.53,598.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:601.2,601.43 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:601.43,602.35 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:602.35,604.35 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:604.35,605.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:607.4,608.18 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:608.18,610.5 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:612.4,614.38 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:614.38,616.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:618.4,620.27 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:624.2,624.30 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:627.68,629.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:629.16,631.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:633.2,638.53 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:638.53,640.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:642.2,642.30 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:642.30,644.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:644.17,646.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:647.3,647.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:647.12,649.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:652.2,652.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:652.16,654.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:656.2,657.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:657.9,659.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:661.2,662.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:662.16,664.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:666.2,667.36 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:667.36,670.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:672.2,674.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:677.71,679.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:679.16,681.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:683.2,686.16 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:686.16,688.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:690.2,691.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:691.9,693.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:695.2,701.36 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:701.36,703.21 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:703.21,705.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:705.9,707.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:710.2,712.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:715.75,717.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:717.16,719.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:721.2,725.30 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:725.30,727.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:727.17,729.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:730.3,730.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:730.13,732.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:735.2,736.30 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:736.30,737.57 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:737.57,739.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:739.9,741.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:744.2,744.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:744.16,746.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:748.2,749.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:749.9,751.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:753.2,756.28 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:756.28,757.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:757.17,759.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:759.9,761.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:764.2,766.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:769.69,771.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:771.16,773.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:775.2,780.84 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:780.84,782.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:784.2,784.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:784.16,786.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:788.2,789.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:789.9,791.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:793.2,794.54 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:794.54,795.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:795.55,797.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:798.3,798.39 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:801.2,801.36 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:801.36,802.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:802.40,803.18 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:803.18,806.5 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:806.10,808.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:812.2,812.31 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:815.68,817.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:817.16,819.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:821.2,824.16 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:824.16,826.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:828.2,829.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:829.9,831.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:833.2,834.39 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:834.39,835.27 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:835.27,837.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:840.2,840.58 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:843.70,845.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:845.16,847.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:849.2,852.16 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:852.16,854.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:856.2,857.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:857.9,859.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:860.2,861.20 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:861.20,863.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:865.2,867.69 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:870.80,872.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:872.16,874.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:876.2,882.16 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:882.16,884.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:886.2,887.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:887.16,889.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:891.2,891.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:891.16,893.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:895.2,896.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:896.9,898.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:900.2,900.33 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:900.33,901.61 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:901.61,904.4 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:907.2,907.58 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:910.79,912.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:912.16,914.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:916.2,920.16 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:920.16,922.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:924.2,925.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:925.16,927.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:929.2,929.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:929.16,931.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:933.2,934.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:934.9,936.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:938.2,938.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:938.15,940.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:941.2,941.14 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:941.14,943.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:945.2,945.88 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:945.88,947.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:949.2,950.54 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:950.54,952.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:954.2,956.18 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:956.18,957.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:957.34,960.4 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:961.8,962.34 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:962.34,965.4 2 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:968.2,968.58 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:971.78,973.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:973.16,975.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:977.2,982.16 5 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:982.16,984.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:986.2,987.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:987.9,989.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:991.2,994.38 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:994.38,995.45 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:995.45,997.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1000.2,1003.28 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1003.28,1005.81 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1005.81,1008.4 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1011.2,1011.58 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1014.70,1016.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1016.16,1018.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1020.2,1031.76 10 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1031.76,1033.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1035.2,1035.73 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1035.73,1037.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1039.2,1039.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1039.65,1041.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1041.5,1043.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1043.8,1046.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1046.17,1048.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1049.3,1050.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1050.17,1052.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1055.2,1055.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1055.65,1057.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1057.5,1058.72 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1058.72,1060.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1061.3,1061.61 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1061.61,1063.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1064.3,1065.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1065.17,1067.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1068.3,1068.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1068.17,1070.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1071.3,1072.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1072.17,1074.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1077.2,1077.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1077.16,1079.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1081.2,1082.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1082.9,1084.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1086.2,1086.32 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1086.32,1088.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1089.2,1089.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1089.15,1091.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1093.2,1094.42 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1094.42,1095.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1095.55,1097.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1097.15,1099.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1100.4,1100.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1103.2,1103.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1103.40,1105.39 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1105.39,1106.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1106.46,1108.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1110.3,1110.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1110.55,1111.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1111.15,1113.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1114.4,1114.64 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1118.2,1120.35 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1120.35,1121.24 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1121.24,1122.9 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1124.3,1124.43 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1124.43,1125.85 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1125.85,1127.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1128.4,1128.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1130.3,1131.90 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1131.90,1133.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1136.2,1138.34 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1138.34,1139.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1139.17,1141.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1141.9,1143.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1146.2,1148.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1151.75,1153.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1153.16,1155.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1157.2,1168.73 11 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1168.73,1170.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1172.2,1172.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1172.65,1174.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1174.5,1176.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1176.8,1179.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1179.17,1181.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1182.3,1183.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1183.17,1185.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1188.2,1188.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1188.65,1190.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1190.5,1191.72 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1191.72,1193.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1194.3,1194.61 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1194.61,1196.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1197.3,1198.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1198.17,1200.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1201.3,1201.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1201.17,1203.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1204.3,1205.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1205.17,1207.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1210.2,1210.19 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1210.19,1212.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1214.2,1215.9 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1215.9,1217.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1219.2,1219.32 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1219.32,1221.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1222.2,1222.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1222.15,1224.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1226.2,1227.42 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1227.42,1228.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1228.55,1230.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1230.15,1232.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1233.4,1233.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1236.2,1236.40 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1236.40,1238.39 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1238.39,1239.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1239.46,1241.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1243.3,1243.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1243.55,1244.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1244.15,1246.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1247.4,1247.64 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1251.2,1253.35 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1253.35,1254.24 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1254.24,1255.9 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1257.3,1257.43 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1257.43,1258.85 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1258.85,1260.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1261.4,1261.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1263.3,1264.90 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1264.90,1266.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1269.2,1272.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1272.17,1274.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1276.2,1276.72 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1279.70,1280.57 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1280.57,1282.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1284.2,1285.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1285.16,1287.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1289.2,1294.33 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1294.33,1295.25 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1295.25,1297.11 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1297.11,1299.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1300.4,1303.6 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1307.2,1310.35 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1310.35,1311.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1311.17,1313.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1313.9,1315.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1318.2,1320.25 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1323.75,1325.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1325.16,1327.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1329.2,1332.73 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1332.73,1334.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1336.2,1337.16 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1337.16,1339.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1341.2,1346.33 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1346.33,1347.25 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1347.25,1349.11 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1349.11,1351.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1352.4,1355.6 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1359.2,1362.17 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1362.17,1364.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1366.2,1366.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/commands.go:1369.36,1641.2 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:25.74,26.18 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:26.18,28.3 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:29.2,33.8 1 1 @@ -3682,13 +2342,11 @@ github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:130.2,13 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:133.81,134.18 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:134.18,136.3 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:137.2,137.58 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:137.58,140.39 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:140.39,142.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:143.3,143.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:145.2,145.18 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:145.18,151.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:152.2,152.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:152.17,158.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:137.58,141.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:143.2,143.18 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:143.18,149.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:151.2,151.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:151.17,157.3 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:159.2,159.84 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:162.75,163.18 1 1 github.com/echovault/echovault/internal/modules/sorted_set/key_funcs.go:163.18,165.3 1 1 @@ -3840,16 +2498,16 @@ github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:262.28, github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:263.33,264.29 1 1 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:264.29,266.5 1 1 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:269.2,269.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:278.53,279.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:278.53,279.46 1 0 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:279.46,281.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:282.2,282.28 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:282.28,284.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:285.2,285.37 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:285.37,286.36 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:282.2,282.28 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:282.28,284.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:285.2,285.37 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:285.37,286.36 1 0 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:286.36,288.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:289.3,289.52 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:289.3,289.52 1 0 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:289.52,291.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:293.2,293.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:293.2,293.13 1 0 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:297.70,298.24 1 1 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:299.9,300.39 1 0 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:301.9,303.52 2 1 @@ -3977,6 +2635,69 @@ github.com/echovault/echovault/internal/modules/sorted_set/utils.go:162.3,162.13 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:163.12,164.16 1 1 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:164.16,166.4 1 1 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:167.3,167.13 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:24.72,26.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:26.16,28.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:30.2,34.9 4 1 +github.com/echovault/echovault/internal/modules/string/commands.go:34.9,36.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:38.2,40.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:40.16,42.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:44.2,45.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:45.9,47.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:50.2,50.24 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:50.24,52.94 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:52.94,54.4 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:55.3,55.58 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:59.2,59.16 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:59.16,61.94 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:61.94,63.4 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:64.3,64.58 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:67.2,69.35 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:69.35,71.24 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:71.24,74.12 3 1 +github.com/echovault/echovault/internal/modules/string/commands.go:77.3,78.8 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:81.2,81.103 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:81.103,83.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:85.2,85.59 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:88.70,90.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:90.16,92.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:94.2,97.16 3 1 +github.com/echovault/echovault/internal/modules/string/commands.go:97.16,99.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:101.2,103.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:103.9,105.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:107.2,107.56 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:110.70,112.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:112.16,114.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:116.2,123.24 6 1 +github.com/echovault/echovault/internal/modules/string/commands.go:123.24,125.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:127.2,127.16 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:127.16,129.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:131.2,132.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:132.9,134.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:136.2,136.15 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:136.15,138.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:139.2,139.13 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:139.13,141.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:143.2,143.30 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:143.30,145.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:147.2,147.22 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:147.22,149.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:151.2,151.17 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:151.17,154.3 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:156.2,158.14 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:158.14,160.38 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:160.38,162.4 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:163.3,163.12 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:166.2,166.65 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:169.36,209.2 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:23.78,24.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:24.19,26.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:27.2,31.8 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:34.76,35.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:35.19,37.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:38.2,42.8 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:45.76,46.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:46.19,48.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:49.2,53.8 1 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:55.56,56.30 1 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:56.30,58.3 1 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:61.59,62.30 1 1 @@ -4079,76 +2800,1014 @@ github.com/echovault/echovault/internal/snapshot/snapshot.go:354.94,356.3 1 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:358.2,360.12 2 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:363.46,365.2 1 0 github.com/echovault/echovault/internal/snapshot/snapshot.go:367.42,369.2 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:24.72,26.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:26.16,28.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:30.2,33.9 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:33.9,35.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:37.2,39.44 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:39.44,40.72 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:40.72,42.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:43.3,43.69 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:43.69,45.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:46.3,47.58 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:50.2,50.63 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:50.63,52.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:53.2,56.9 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:56.9,58.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:61.2,61.24 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:61.24,63.69 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:63.69,65.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:66.3,66.58 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:70.2,70.16 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:70.16,72.69 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:72.69,74.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:75.3,75.58 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:78.2,80.35 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:80.35,82.24 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:82.24,85.12 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:88.3,89.8 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:92.2,92.78 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:92.78,94.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:96.2,96.59 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:99.70,101.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:101.16,103.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:105.2,107.44 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:107.44,109.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:111.2,111.64 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:111.64,113.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:114.2,118.9 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:118.9,120.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:122.2,122.56 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:125.70,127.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:127.16,129.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:131.2,137.24 5 1 -github.com/echovault/echovault/internal/modules/string/commands.go:137.24,139.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:141.2,141.44 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:141.44,143.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:145.2,145.63 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:145.63,147.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:148.2,151.9 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:151.9,153.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:155.2,155.15 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:155.15,157.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:158.2,158.13 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:158.13,160.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:162.2,162.30 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:162.30,164.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:166.2,166.22 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:166.22,168.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:170.2,170.17 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:170.17,173.3 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:175.2,177.14 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:177.14,179.38 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:179.38,181.4 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:182.3,182.12 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:185.2,185.65 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:188.36,228.2 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:23.78,24.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:24.19,26.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:27.2,31.8 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:34.76,35.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:35.19,37.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:38.2,42.8 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:45.76,46.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:46.19,48.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:49.2,53.8 1 1 +github.com/echovault/echovault/echovault/api_acl.go:126.71,128.23 2 0 +github.com/echovault/echovault/echovault/api_acl.go:128.23,130.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:131.2,132.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:132.16,134.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:135.2,135.45 1 0 +github.com/echovault/echovault/echovault/api_acl.go:139.55,141.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:141.16,143.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:144.2,144.45 1 0 +github.com/echovault/echovault/echovault/api_acl.go:155.62,158.18 2 0 +github.com/echovault/echovault/echovault/api_acl.go:158.18,160.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:160.8,162.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:164.2,164.21 1 0 +github.com/echovault/echovault/echovault/api_acl.go:164.21,166.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:168.2,168.17 1 0 +github.com/echovault/echovault/echovault/api_acl.go:168.17,170.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:172.2,172.21 1 0 +github.com/echovault/echovault/echovault/api_acl.go:172.21,174.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:176.2,176.20 1 0 +github.com/echovault/echovault/echovault/api_acl.go:176.20,178.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:180.2,180.20 1 0 +github.com/echovault/echovault/echovault/api_acl.go:180.20,182.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:184.2,184.24 1 0 +github.com/echovault/echovault/echovault/api_acl.go:184.24,186.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:188.2,188.50 1 0 +github.com/echovault/echovault/echovault/api_acl.go:188.50,190.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:192.2,192.53 1 0 +github.com/echovault/echovault/echovault/api_acl.go:192.53,194.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:196.2,196.49 1 0 +github.com/echovault/echovault/echovault/api_acl.go:196.49,198.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:200.2,200.52 1 0 +github.com/echovault/echovault/echovault/api_acl.go:200.52,202.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:204.2,204.50 1 0 +github.com/echovault/echovault/echovault/api_acl.go:204.50,206.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:208.2,208.50 1 0 +github.com/echovault/echovault/echovault/api_acl.go:208.50,210.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:212.2,212.47 1 0 +github.com/echovault/echovault/echovault/api_acl.go:212.47,214.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:216.2,216.47 1 0 +github.com/echovault/echovault/echovault/api_acl.go:216.47,218.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:220.2,220.48 1 0 +github.com/echovault/echovault/echovault/api_acl.go:220.48,222.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:224.2,224.43 1 0 +github.com/echovault/echovault/echovault/api_acl.go:224.43,226.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:228.2,228.44 1 0 +github.com/echovault/echovault/echovault/api_acl.go:228.44,230.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:232.2,232.47 1 0 +github.com/echovault/echovault/echovault/api_acl.go:232.47,234.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:236.2,236.47 1 0 +github.com/echovault/echovault/echovault/api_acl.go:236.47,238.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:240.2,241.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:241.16,243.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:245.2,246.40 2 0 +github.com/echovault/echovault/echovault/api_acl.go:293.83,295.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:295.16,297.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:299.2,301.16 3 0 +github.com/echovault/echovault/echovault/api_acl.go:301.16,303.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:305.2,308.35 3 0 +github.com/echovault/echovault/echovault/api_acl.go:308.35,314.35 4 0 +github.com/echovault/echovault/echovault/api_acl.go:314.35,316.4 1 0 +github.com/echovault/echovault/echovault/api_acl.go:319.2,319.20 1 0 +github.com/echovault/echovault/echovault/api_acl.go:329.72,332.16 3 0 +github.com/echovault/echovault/echovault/api_acl.go:332.16,334.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:335.2,336.40 2 0 +github.com/echovault/echovault/echovault/api_acl.go:340.54,342.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:342.16,344.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:345.2,345.45 1 0 +github.com/echovault/echovault/echovault/api_acl.go:356.72,358.9 2 0 +github.com/echovault/echovault/echovault/api_acl.go:359.21,360.29 1 0 +github.com/echovault/echovault/echovault/api_acl.go:361.23,362.31 1 0 +github.com/echovault/echovault/echovault/api_acl.go:363.10,364.31 1 0 +github.com/echovault/echovault/echovault/api_acl.go:367.2,368.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:368.16,370.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:372.2,373.40 2 0 +github.com/echovault/echovault/echovault/api_acl.go:379.50,381.16 2 0 +github.com/echovault/echovault/echovault/api_acl.go:381.16,383.3 1 0 +github.com/echovault/echovault/echovault/api_acl.go:384.2,385.40 2 0 +github.com/echovault/echovault/echovault/api_admin.go:142.84,145.9 2 0 +github.com/echovault/echovault/echovault/api_admin.go:146.28,147.71 1 0 +github.com/echovault/echovault/echovault/api_admin.go:148.29,149.73 1 0 +github.com/echovault/echovault/echovault/api_admin.go:150.28,151.71 1 0 +github.com/echovault/echovault/echovault/api_admin.go:154.2,155.16 2 0 +github.com/echovault/echovault/echovault/api_admin.go:155.16,157.3 1 0 +github.com/echovault/echovault/echovault/api_admin.go:159.2,159.45 1 0 +github.com/echovault/echovault/echovault/api_admin.go:165.54,167.16 2 0 +github.com/echovault/echovault/echovault/api_admin.go:167.16,169.3 1 0 +github.com/echovault/echovault/echovault/api_admin.go:170.2,170.41 1 0 +github.com/echovault/echovault/echovault/api_admin.go:174.49,176.16 2 0 +github.com/echovault/echovault/echovault/api_admin.go:176.16,178.3 1 0 +github.com/echovault/echovault/echovault/api_admin.go:179.2,179.40 1 0 +github.com/echovault/echovault/echovault/api_admin.go:183.50,185.16 2 0 +github.com/echovault/echovault/echovault/api_admin.go:185.16,187.3 1 0 +github.com/echovault/echovault/echovault/api_admin.go:188.2,188.41 1 0 +github.com/echovault/echovault/echovault/api_admin.go:192.55,194.16 2 0 +github.com/echovault/echovault/echovault/api_admin.go:194.16,196.3 1 0 +github.com/echovault/echovault/echovault/api_admin.go:197.2,197.40 1 0 +github.com/echovault/echovault/echovault/api_admin.go:209.67,213.36 3 1 +github.com/echovault/echovault/echovault/api_admin.go:213.36,214.52 1 1 +github.com/echovault/echovault/echovault/api_admin.go:214.52,216.4 1 0 +github.com/echovault/echovault/echovault/api_admin.go:219.2,219.63 1 1 +github.com/echovault/echovault/echovault/api_admin.go:219.63,224.32 1 1 +github.com/echovault/echovault/echovault/api_admin.go:224.32,227.44 2 1 +github.com/echovault/echovault/echovault/api_admin.go:227.44,229.6 1 0 +github.com/echovault/echovault/echovault/api_admin.go:230.5,230.16 1 1 +github.com/echovault/echovault/echovault/api_admin.go:234.111,236.19 2 0 +github.com/echovault/echovault/echovault/api_admin.go:236.19,238.6 1 0 +github.com/echovault/echovault/echovault/api_admin.go:239.5,243.11 1 0 +github.com/echovault/echovault/echovault/api_admin.go:245.94,253.5 1 1 +github.com/echovault/echovault/echovault/api_admin.go:255.3,255.13 1 1 +github.com/echovault/echovault/echovault/api_admin.go:259.2,262.31 1 1 +github.com/echovault/echovault/echovault/api_admin.go:262.31,265.43 2 1 +github.com/echovault/echovault/echovault/api_admin.go:265.43,267.5 1 0 +github.com/echovault/echovault/echovault/api_admin.go:268.4,268.15 1 1 +github.com/echovault/echovault/echovault/api_admin.go:272.83,274.4 1 0 +github.com/echovault/echovault/echovault/api_admin.go:275.71,275.90 1 0 +github.com/echovault/echovault/echovault/api_admin.go:279.2,279.40 1 1 +github.com/echovault/echovault/echovault/api_admin.go:279.40,281.92 1 1 +github.com/echovault/echovault/echovault/api_admin.go:281.92,283.4 1 1 +github.com/echovault/echovault/echovault/api_admin.go:283.6,284.12 1 0 +github.com/echovault/echovault/echovault/api_admin.go:286.3,289.32 1 1 +github.com/echovault/echovault/echovault/api_admin.go:289.32,292.39 2 1 +github.com/echovault/echovault/echovault/api_admin.go:292.39,294.6 1 0 +github.com/echovault/echovault/echovault/api_admin.go:295.5,295.16 1 1 +github.com/echovault/echovault/echovault/api_admin.go:299.111,301.19 2 0 +github.com/echovault/echovault/echovault/api_admin.go:301.19,303.6 1 0 +github.com/echovault/echovault/echovault/api_admin.go:304.5,308.11 1 0 +github.com/echovault/echovault/echovault/api_admin.go:310.94,318.5 1 1 +github.com/echovault/echovault/echovault/api_admin.go:322.2,324.12 2 1 +github.com/echovault/echovault/echovault/api_admin.go:350.76,352.2 1 1 +github.com/echovault/echovault/echovault/api_admin.go:368.59,372.22 3 1 +github.com/echovault/echovault/echovault/api_admin.go:373.9,375.86 1 1 +github.com/echovault/echovault/echovault/api_admin.go:375.86,377.4 1 1 +github.com/echovault/echovault/echovault/api_admin.go:378.9,380.45 1 1 +github.com/echovault/echovault/echovault/api_admin.go:380.45,381.66 1 1 +github.com/echovault/echovault/echovault/api_admin.go:381.66,382.13 1 1 +github.com/echovault/echovault/echovault/api_admin.go:384.4,384.88 1 1 +github.com/echovault/echovault/echovault/api_admin.go:384.88,385.122 1 1 +github.com/echovault/echovault/echovault/api_admin.go:385.122,387.6 1 1 +github.com/echovault/echovault/echovault/api_generic.go:88.91,91.9 2 1 +github.com/echovault/echovault/echovault/api_generic.go:92.18,93.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:94.18,95.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:98.2,98.9 1 1 +github.com/echovault/echovault/echovault/api_generic.go:99.23,100.65 1 1 +github.com/echovault/echovault/echovault/api_generic.go:101.23,102.65 1 1 +github.com/echovault/echovault/echovault/api_generic.go:103.25,104.69 1 1 +github.com/echovault/echovault/echovault/api_generic.go:105.25,106.69 1 1 +github.com/echovault/echovault/echovault/api_generic.go:109.2,109.17 1 1 +github.com/echovault/echovault/echovault/api_generic.go:109.17,111.3 1 1 +github.com/echovault/echovault/echovault/api_generic.go:113.2,114.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:114.16,116.3 1 1 +github.com/echovault/echovault/echovault/api_generic.go:118.2,119.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:119.16,121.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:122.2,122.18 1 1 +github.com/echovault/echovault/echovault/api_generic.go:122.18,124.3 1 1 +github.com/echovault/echovault/echovault/api_generic.go:126.2,126.33 1 1 +github.com/echovault/echovault/echovault/api_generic.go:141.72,144.28 2 1 +github.com/echovault/echovault/echovault/api_generic.go:144.28,146.3 1 1 +github.com/echovault/echovault/echovault/api_generic.go:148.2,149.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:149.16,151.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:153.2,154.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:154.16,156.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:158.2,158.40 1 1 +github.com/echovault/echovault/echovault/api_generic.go:169.58,171.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:171.16,173.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:174.2,174.40 1 1 +github.com/echovault/echovault/echovault/api_generic.go:185.65,187.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:187.16,189.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:190.2,190.45 1 1 +github.com/echovault/echovault/echovault/api_generic.go:200.59,202.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:202.16,204.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:205.2,205.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:216.60,218.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:218.16,220.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:221.2,221.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:231.62,233.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:233.16,235.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:236.2,236.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:246.63,248.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:248.16,250.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:251.2,251.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:261.55,263.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:263.16,265.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:266.2,266.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:276.56,278.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:278.16,280.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:281.2,281.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:296.95,299.9 2 1 +github.com/echovault/echovault/echovault/api_generic.go:300.18,301.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:302.18,303.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:304.18,305.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:306.18,307.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:310.2,311.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:311.16,313.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:315.2,315.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:330.102,333.9 2 1 +github.com/echovault/echovault/echovault/api_generic.go:334.18,335.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:336.18,337.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:338.18,339.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:340.18,341.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:344.2,345.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:345.16,347.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:349.2,349.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:364.102,367.9 2 1 +github.com/echovault/echovault/echovault/api_generic.go:368.18,369.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:370.18,371.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:372.18,373.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:374.18,375.26 1 1 +github.com/echovault/echovault/echovault/api_generic.go:378.2,379.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:379.16,381.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:383.2,383.41 1 1 +github.com/echovault/echovault/echovault/api_generic.go:398.109,401.9 2 1 +github.com/echovault/echovault/echovault/api_generic.go:402.18,403.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:404.18,405.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:406.18,407.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:408.18,409.26 1 0 +github.com/echovault/echovault/echovault/api_generic.go:412.2,413.16 2 1 +github.com/echovault/echovault/echovault/api_generic.go:413.16,415.3 1 0 +github.com/echovault/echovault/echovault/api_generic.go:417.2,417.41 1 1 +github.com/echovault/echovault/echovault/api_hash.go:46.91,49.36 2 1 +github.com/echovault/echovault/echovault/api_hash.go:49.36,51.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:53.2,54.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:54.16,56.3 1 0 +github.com/echovault/echovault/echovault/api_hash.go:58.2,58.41 1 1 +github.com/echovault/echovault/echovault/api_hash.go:76.93,79.36 2 1 +github.com/echovault/echovault/echovault/api_hash.go:79.36,81.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:83.2,84.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:84.16,86.3 1 0 +github.com/echovault/echovault/echovault/api_hash.go:88.2,88.41 1 1 +github.com/echovault/echovault/echovault/api_hash.go:104.79,112.16 2 0 +github.com/echovault/echovault/echovault/api_hash.go:112.16,114.3 1 0 +github.com/echovault/echovault/echovault/api_hash.go:115.2,115.45 1 0 +github.com/echovault/echovault/echovault/api_hash.go:132.79,136.16 3 1 +github.com/echovault/echovault/echovault/api_hash.go:136.16,138.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:140.2,140.46 1 1 +github.com/echovault/echovault/echovault/api_hash.go:154.62,156.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:156.16,158.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:159.2,159.45 1 1 +github.com/echovault/echovault/echovault/api_hash.go:175.94,178.24 2 1 +github.com/echovault/echovault/echovault/api_hash.go:178.24,180.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:180.8,182.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:184.2,184.24 1 1 +github.com/echovault/echovault/echovault/api_hash.go:184.24,186.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:188.2,189.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:189.16,191.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:193.2,193.45 1 1 +github.com/echovault/echovault/echovault/api_hash.go:207.56,209.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:209.16,211.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:212.2,212.41 1 1 +github.com/echovault/echovault/echovault/api_hash.go:226.62,228.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:228.16,230.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:231.2,231.45 1 1 +github.com/echovault/echovault/echovault/api_hash.go:250.85,252.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:252.16,254.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:255.2,255.39 1 1 +github.com/echovault/echovault/echovault/api_hash.go:259.94,261.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:261.16,263.3 1 0 +github.com/echovault/echovault/echovault/api_hash.go:264.2,264.39 1 1 +github.com/echovault/echovault/echovault/api_hash.go:279.64,281.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:281.16,283.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:284.2,284.45 1 1 +github.com/echovault/echovault/echovault/api_hash.go:300.67,302.16 2 1 +github.com/echovault/echovault/echovault/api_hash.go:302.16,304.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:305.2,305.41 1 1 +github.com/echovault/echovault/echovault/api_hash.go:321.74,324.16 3 1 +github.com/echovault/echovault/echovault/api_hash.go:324.16,326.3 1 1 +github.com/echovault/echovault/echovault/api_hash.go:327.2,327.41 1 1 +github.com/echovault/echovault/echovault/api_list.go:34.56,36.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:36.16,38.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:39.2,39.41 1 1 +github.com/echovault/echovault/echovault/api_list.go:62.79,64.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:64.16,66.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:67.2,67.45 1 1 +github.com/echovault/echovault/echovault/api_list.go:85.73,87.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:87.16,89.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:90.2,90.40 1 1 +github.com/echovault/echovault/echovault/api_list.go:110.82,112.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:112.16,114.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:115.2,116.40 2 1 +github.com/echovault/echovault/echovault/api_list.go:123.78,125.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:125.16,127.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:128.2,129.40 2 1 +github.com/echovault/echovault/echovault/api_list.go:147.82,154.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:154.16,156.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:157.2,158.40 2 1 +github.com/echovault/echovault/echovault/api_list.go:182.94,184.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:184.16,186.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:187.2,188.40 2 1 +github.com/echovault/echovault/echovault/api_list.go:202.59,204.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:204.16,206.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:207.2,207.40 1 1 +github.com/echovault/echovault/echovault/api_list.go:221.59,223.16 2 1 +github.com/echovault/echovault/echovault/api_list.go:223.16,225.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:226.2,226.40 1 1 +github.com/echovault/echovault/echovault/api_list.go:243.75,246.16 3 1 +github.com/echovault/echovault/echovault/api_list.go:246.16,248.3 1 0 +github.com/echovault/echovault/echovault/api_list.go:249.2,249.41 1 1 +github.com/echovault/echovault/echovault/api_list.go:265.76,268.16 3 1 +github.com/echovault/echovault/echovault/api_list.go:268.16,270.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:271.2,271.41 1 1 +github.com/echovault/echovault/echovault/api_list.go:288.75,291.16 3 1 +github.com/echovault/echovault/echovault/api_list.go:291.16,293.3 1 0 +github.com/echovault/echovault/echovault/api_list.go:294.2,294.41 1 1 +github.com/echovault/echovault/echovault/api_list.go:310.76,313.16 3 1 +github.com/echovault/echovault/echovault/api_list.go:313.16,315.3 1 1 +github.com/echovault/echovault/echovault/api_list.go:316.2,316.41 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:50.86,52.24 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:52.24,54.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:57.2,59.36 3 0 +github.com/echovault/echovault/echovault/api_pubsub.go:59.36,65.3 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:68.2,69.12 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:69.12,71.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:73.2,73.25 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:73.25,78.33 4 0 +github.com/echovault/echovault/echovault/api_pubsub.go:78.33,80.4 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:82.3,82.13 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:93.70,94.24 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:94.24,96.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:98.2,98.36 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:98.36,100.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:102.2,103.115 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:116.87,118.24 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:118.24,120.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:123.2,125.36 3 0 +github.com/echovault/echovault/echovault/api_pubsub.go:125.36,131.3 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:134.2,135.12 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:135.12,137.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:139.2,139.25 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:139.25,144.33 4 0 +github.com/echovault/echovault/echovault/api_pubsub.go:144.33,146.4 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:148.3,148.13 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:159.71,160.24 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:160.24,162.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:164.2,164.36 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:164.36,166.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:168.2,169.115 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:182.73,184.16 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:184.16,186.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:187.2,188.40 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:198.75,200.19 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:200.19,202.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:203.2,204.16 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:204.16,206.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:207.2,207.45 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:213.54,215.16 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:215.16,217.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:218.2,218.41 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:228.82,232.16 3 0 +github.com/echovault/echovault/echovault/api_pubsub.go:232.16,234.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:236.2,238.16 3 0 +github.com/echovault/echovault/echovault/api_pubsub.go:238.16,240.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:242.2,245.28 3 0 +github.com/echovault/echovault/echovault/api_pubsub.go:245.28,248.3 2 0 +github.com/echovault/echovault/echovault/api_pubsub.go:250.2,250.20 1 0 +github.com/echovault/echovault/echovault/api_set.go:36.75,39.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:39.16,41.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:42.2,42.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:56.57,58.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:58.16,60.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:61.2,61.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:78.66,81.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:81.16,83.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:84.2,84.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:91.86,94.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:94.16,96.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:97.2,97.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:114.67,117.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:117.16,119.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:120.2,120.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:138.77,140.15 2 1 +github.com/echovault/echovault/echovault/api_set.go:140.15,142.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:143.2,144.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:144.16,146.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:147.2,147.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:152.87,155.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:155.16,157.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:158.2,158.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:174.70,176.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:176.16,178.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:179.2,179.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:193.65,195.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:195.16,197.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:198.2,198.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:215.84,218.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:218.16,220.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:221.2,221.46 1 1 +github.com/echovault/echovault/echovault/api_set.go:243.82,245.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:245.16,247.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:248.2,248.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:264.73,266.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:266.16,268.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:269.2,269.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:286.79,288.16 2 1 +github.com/echovault/echovault/echovault/api_set.go:288.16,290.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:291.2,291.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:307.75,310.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:310.16,312.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:313.2,313.41 1 1 +github.com/echovault/echovault/echovault/api_set.go:328.67,331.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:331.16,333.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:334.2,334.45 1 1 +github.com/echovault/echovault/echovault/api_set.go:341.87,344.16 3 1 +github.com/echovault/echovault/echovault/api_set.go:344.16,346.3 1 1 +github.com/echovault/echovault/echovault/api_set.go:347.2,347.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:105.87,107.28 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:107.28,108.17 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:108.17,110.18 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:110.18,112.5 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:113.4,114.12 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:116.3,116.23 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:118.2,118.20 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:121.85,123.28 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:123.28,125.17 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:125.17,127.4 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:128.3,129.17 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:129.17,131.18 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:131.18,133.5 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:134.4,134.24 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:137.2,137.20 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:161.105,164.9 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:165.18,166.26 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:167.18,168.26 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:171.2,171.9 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:172.18,173.26 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:174.18,175.26 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:178.2,178.16 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:178.16,180.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:182.2,182.18 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:182.18,184.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:186.2,186.37 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:186.37,188.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:190.2,191.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:191.16,193.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:195.2,195.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:209.57,211.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:211.16,213.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:214.2,214.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:232.76,240.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:240.16,242.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:243.2,243.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:261.93,263.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:263.16,265.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:266.2,267.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:267.16,269.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:271.2,272.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:272.16,274.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:276.2,276.45 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:293.86,296.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:296.16,298.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:299.2,299.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:316.99,319.30 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:319.30,321.45 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:321.45,323.4 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:326.2,326.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:326.29,328.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:330.2,330.24 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:330.24,332.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:334.2,335.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:335.16,337.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:339.2,340.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:340.16,342.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:344.2,344.53 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:363.114,366.30 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:366.30,368.42 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:368.42,370.4 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:373.2,373.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:373.29,375.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:377.2,377.24 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:377.24,379.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:381.2,382.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:382.16,384.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:386.2,386.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:403.99,406.30 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:406.30,408.42 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:408.42,410.4 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:413.2,413.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:413.29,415.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:417.2,417.24 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:417.24,419.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:421.2,422.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:422.16,424.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:426.2,427.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:427.16,429.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:431.2,431.53 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:450.114,453.30 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:453.30,455.42 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:455.42,457.4 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:460.2,460.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:460.29,462.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:464.2,464.24 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:464.24,466.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:468.2,469.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:469.16,471.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:473.2,473.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:492.97,495.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:495.16,497.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:498.2,499.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:499.16,501.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:502.2,502.15 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:519.89,522.9 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:523.19,524.27 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:525.19,526.27 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:527.10,528.27 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:531.2,531.9 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:532.26,533.76 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:534.10,535.59 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:538.2,539.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:539.16,541.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:543.2,543.51 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:561.88,563.33 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:563.33,565.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:567.2,568.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:568.16,570.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:572.2,573.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:573.16,575.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:577.2,578.24 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:578.24,579.14 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:579.14,581.12 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:583.3,584.17 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:584.17,586.4 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:587.3,587.20 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:590.2,590.20 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:610.71,613.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:613.16,615.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:616.2,616.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:635.78,637.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:637.16,639.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:640.2,640.51 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:659.78,661.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:661.16,663.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:664.2,664.51 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:688.98,690.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:690.16,692.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:693.2,693.16 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:693.16,695.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:697.2,698.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:698.16,700.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:702.2,702.51 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:723.101,725.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:725.16,727.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:729.2,730.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:730.16,732.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:734.2,736.19 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:736.19,738.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:740.2,741.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:741.16,743.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:745.2,747.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:747.16,749.17 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:749.17,751.4 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:752.3,752.13 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:755.2,755.17 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:760.104,762.16 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:762.16,764.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:766.2,767.16 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:767.16,769.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:771.2,773.46 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:790.81,793.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:793.16,795.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:797.2,798.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:798.16,800.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:802.2,802.11 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:802.11,804.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:806.2,807.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:807.16,809.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:811.2,811.19 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:827.75,829.33 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:829.33,831.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:832.2,833.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:833.16,835.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:836.2,836.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:854.94,863.16 3 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:863.16,865.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:867.2,867.41 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:885.76,892.16 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:892.16,894.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:895.2,895.41 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:913.81,920.16 2 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:920.16,922.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:923.2,923.41 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:943.109,946.9 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:947.23,948.31 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:949.21,950.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:951.19,952.27 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:953.10,954.31 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:957.2,957.24 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:957.24,959.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:961.2,961.47 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:961.47,963.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:965.2,966.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:966.16,968.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:970.2,971.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:971.16,973.3 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:975.2,975.53 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:997.120,1000.9 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1001.23,1002.31 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1003.21,1004.29 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1005.19,1006.27 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:1007.10,1008.31 1 0 +github.com/echovault/echovault/echovault/api_sorted_set.go:1011.2,1011.47 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1011.47,1013.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1015.2,1016.16 2 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1016.16,1018.3 1 1 +github.com/echovault/echovault/echovault/api_sorted_set.go:1020.2,1020.41 1 1 +github.com/echovault/echovault/echovault/api_string.go:30.84,32.16 2 1 +github.com/echovault/echovault/echovault/api_string.go:32.16,34.3 1 0 +github.com/echovault/echovault/echovault/api_string.go:35.2,35.41 1 1 +github.com/echovault/echovault/echovault/api_string.go:45.58,47.16 2 1 +github.com/echovault/echovault/echovault/api_string.go:47.16,49.3 1 0 +github.com/echovault/echovault/echovault/api_string.go:50.2,50.41 1 1 +github.com/echovault/echovault/echovault/api_string.go:63.77,65.16 2 1 +github.com/echovault/echovault/echovault/api_string.go:65.16,67.3 1 0 +github.com/echovault/echovault/echovault/api_string.go:68.2,68.40 1 1 +github.com/echovault/echovault/echovault/api_string.go:72.79,74.16 2 1 +github.com/echovault/echovault/echovault/api_string.go:74.16,76.3 1 0 +github.com/echovault/echovault/echovault/api_string.go:77.2,77.40 1 1 +github.com/echovault/echovault/echovault/cluster.go:25.45,27.2 1 1 +github.com/echovault/echovault/echovault/cluster.go:29.84,40.16 4 0 +github.com/echovault/echovault/echovault/cluster.go:40.16,42.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:44.2,46.43 2 0 +github.com/echovault/echovault/echovault/cluster.go:46.43,48.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:50.2,52.9 2 0 +github.com/echovault/echovault/echovault/cluster.go:52.9,54.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:56.2,56.20 1 0 +github.com/echovault/echovault/echovault/cluster.go:56.20,58.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:60.2,60.12 1 0 +github.com/echovault/echovault/echovault/cluster.go:63.94,75.16 5 1 +github.com/echovault/echovault/echovault/cluster.go:75.16,77.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:79.2,81.43 2 1 +github.com/echovault/echovault/echovault/cluster.go:81.43,83.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:85.2,87.9 2 1 +github.com/echovault/echovault/echovault/cluster.go:87.9,89.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:91.2,91.20 1 1 +github.com/echovault/echovault/echovault/cluster.go:91.20,93.3 1 0 +github.com/echovault/echovault/echovault/cluster.go:95.2,95.24 1 1 +github.com/echovault/echovault/echovault/config.go:23.36,25.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:111.66,112.36 1 0 +github.com/echovault/echovault/echovault/echovault.go:112.36,114.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:120.66,121.36 1 1 +github.com/echovault/echovault/echovault/echovault.go:121.36,123.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:128.78,136.39 1 1 +github.com/echovault/echovault/echovault/echovault.go:136.39,149.4 12 1 +github.com/echovault/echovault/echovault/echovault.go:152.2,152.33 1 1 +github.com/echovault/echovault/echovault/echovault.go:152.33,154.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:156.2,162.48 2 1 +github.com/echovault/echovault/echovault/echovault.go:162.48,163.52 1 0 +github.com/echovault/echovault/echovault/echovault.go:163.52,165.12 2 0 +github.com/echovault/echovault/echovault/echovault.go:167.3,167.41 1 0 +github.com/echovault/echovault/echovault/echovault.go:171.2,171.52 1 1 +github.com/echovault/echovault/echovault/echovault.go:171.52,173.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:176.2,176.42 1 1 +github.com/echovault/echovault/echovault/echovault.go:176.42,178.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:181.2,182.40 2 1 +github.com/echovault/echovault/echovault/echovault.go:182.40,184.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:187.2,188.43 2 1 +github.com/echovault/echovault/echovault/echovault.go:188.43,190.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:192.2,192.29 1 1 +github.com/echovault/echovault/echovault/echovault.go:192.29,202.38 1 1 +github.com/echovault/echovault/echovault/echovault.go:202.38,206.5 3 0 +github.com/echovault/echovault/echovault/echovault.go:207.49,209.44 2 0 +github.com/echovault/echovault/echovault/echovault.go:209.44,210.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:210.46,212.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:214.5,214.17 1 0 +github.com/echovault/echovault/echovault/echovault.go:217.3,225.5 1 1 +github.com/echovault/echovault/echovault/echovault.go:226.8,237.65 1 1 +github.com/echovault/echovault/echovault/echovault.go:237.65,239.44 2 0 +github.com/echovault/echovault/echovault/echovault.go:239.44,240.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:240.46,242.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:244.5,244.17 1 0 +github.com/echovault/echovault/echovault/echovault.go:246.72,248.93 2 0 +github.com/echovault/echovault/echovault/echovault.go:248.93,250.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:251.5,251.56 1 0 +github.com/echovault/echovault/echovault/echovault.go:255.3,261.60 1 1 +github.com/echovault/echovault/echovault/echovault.go:261.60,263.44 2 0 +github.com/echovault/echovault/echovault/echovault.go:263.44,264.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:264.46,266.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:268.5,268.17 1 0 +github.com/echovault/echovault/echovault/echovault.go:270.68,272.94 2 0 +github.com/echovault/echovault/echovault/echovault.go:272.94,274.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:275.5,275.57 1 0 +github.com/echovault/echovault/echovault/echovault.go:277.51,279.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:279.19,281.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:284.3,284.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:284.17,286.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:287.3,287.34 1 1 +github.com/echovault/echovault/echovault/echovault.go:291.2,291.61 1 1 +github.com/echovault/echovault/echovault/echovault.go:291.61,292.13 1 1 +github.com/echovault/echovault/echovault/echovault.go:292.13,293.8 1 1 +github.com/echovault/echovault/echovault/echovault.go:293.8,295.83 2 1 +github.com/echovault/echovault/echovault/echovault.go:295.83,297.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:302.2,302.69 1 1 +github.com/echovault/echovault/echovault/echovault.go:302.69,304.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:306.2,306.29 1 1 +github.com/echovault/echovault/echovault/echovault.go:306.29,310.36 3 1 +github.com/echovault/echovault/echovault/echovault.go:310.36,312.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:315.2,315.30 1 1 +github.com/echovault/echovault/echovault/echovault.go:315.30,318.34 2 1 +github.com/echovault/echovault/echovault/echovault.go:318.34,320.18 2 0 +github.com/echovault/echovault/echovault/echovault.go:320.18,322.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:326.3,326.71 1 1 +github.com/echovault/echovault/echovault/echovault.go:326.71,328.18 2 0 +github.com/echovault/echovault/echovault/echovault.go:328.18,330.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:334.2,334.23 1 1 +github.com/echovault/echovault/echovault/echovault.go:337.37,346.16 4 1 +github.com/echovault/echovault/echovault/echovault.go:346.16,348.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:350.2,350.15 1 1 +github.com/echovault/echovault/echovault/echovault.go:350.15,353.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:355.2,355.27 1 1 +github.com/echovault/echovault/echovault/echovault.go:355.27,357.15 1 0 +github.com/echovault/echovault/echovault/echovault.go:357.15,359.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:359.9,361.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:363.3,364.49 2 0 +github.com/echovault/echovault/echovault/echovault.go:364.49,366.18 2 0 +github.com/echovault/echovault/echovault/echovault.go:366.18,368.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:369.4,369.42 1 0 +github.com/echovault/echovault/echovault/echovault.go:372.3,375.16 3 0 +github.com/echovault/echovault/echovault/echovault.go:375.16,377.37 2 0 +github.com/echovault/echovault/echovault/echovault.go:377.37,379.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:379.19,381.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:382.5,383.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:383.19,385.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:386.5,386.61 1 0 +github.com/echovault/echovault/echovault/echovault.go:386.61,388.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:392.3,396.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:400.2,400.6 1 1 +github.com/echovault/echovault/echovault/echovault.go:400.6,402.17 2 1 +github.com/echovault/echovault/echovault/echovault.go:402.17,404.12 2 0 +github.com/echovault/echovault/echovault/echovault.go:407.3,407.35 1 1 +github.com/echovault/echovault/echovault/echovault.go:411.58,413.23 1 1 +github.com/echovault/echovault/echovault/echovault.go:413.23,415.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:417.2,423.6 4 1 +github.com/echovault/echovault/echovault/echovault.go:423.6,426.43 2 1 +github.com/echovault/echovault/echovault/echovault.go:426.43,429.9 2 0 +github.com/echovault/echovault/echovault/echovault.go:432.3,432.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:432.17,434.9 2 0 +github.com/echovault/echovault/echovault/echovault.go:437.3,439.43 2 1 +github.com/echovault/echovault/echovault/echovault.go:439.43,440.9 1 1 +github.com/echovault/echovault/echovault/echovault.go:443.3,443.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:443.17,444.87 1 0 +github.com/echovault/echovault/echovault/echovault.go:444.87,446.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:447.4,447.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:450.3,453.20 2 1 +github.com/echovault/echovault/echovault/echovault.go:453.20,454.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:457.3,457.28 1 1 +github.com/echovault/echovault/echovault/echovault.go:457.28,459.12 2 1 +github.com/echovault/echovault/echovault/echovault.go:463.3,464.7 2 0 +github.com/echovault/echovault/echovault/echovault.go:464.7,466.41 1 0 +github.com/echovault/echovault/echovault/echovault.go:466.41,468.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:468.19,470.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:471.5,471.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:473.4,474.21 2 0 +github.com/echovault/echovault/echovault/echovault.go:474.21,475.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:477.4,477.27 1 0 +github.com/echovault/echovault/echovault/echovault.go:481.2,481.37 1 1 +github.com/echovault/echovault/echovault/echovault.go:481.37,483.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:491.34,493.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:496.47,497.38 1 0 +github.com/echovault/echovault/echovault/echovault.go:497.38,499.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:501.2,501.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:501.12,502.27 1 0 +github.com/echovault/echovault/echovault/echovault.go:502.27,504.53 1 0 +github.com/echovault/echovault/echovault/echovault.go:504.53,506.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:507.4,507.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:510.3,510.62 1 0 +github.com/echovault/echovault/echovault/echovault.go:510.62,512.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:515.2,515.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:518.42,520.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:522.43,524.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:526.56,528.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:531.56,533.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:535.44,537.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:539.45,541.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:544.45,545.40 1 0 +github.com/echovault/echovault/echovault/echovault.go:545.40,547.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:548.2,548.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:548.12,549.55 1 0 +github.com/echovault/echovault/echovault/echovault.go:549.55,551.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:553.2,553.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:558.37,559.26 1 0 +github.com/echovault/echovault/echovault/echovault.go:559.26,562.3 2 0 +github.com/echovault/echovault/echovault/echovault.go:565.45,582.2 2 1 +github.com/echovault/echovault/echovault/keyspace.go:32.67,38.27 4 1 +github.com/echovault/echovault/echovault/keyspace.go:38.27,41.3 2 1 +github.com/echovault/echovault/echovault/keyspace.go:43.2,43.15 1 1 +github.com/echovault/echovault/echovault/keyspace.go:46.58,51.9 4 1 +github.com/echovault/echovault/echovault/keyspace.go:51.9,53.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:55.2,55.23 1 1 +github.com/echovault/echovault/echovault/keyspace.go:58.95,64.27 4 1 +github.com/echovault/echovault/echovault/keyspace.go:64.27,66.10 2 1 +github.com/echovault/echovault/echovault/keyspace.go:66.10,68.12 2 1 +github.com/echovault/echovault/echovault/keyspace.go:71.3,71.83 1 1 +github.com/echovault/echovault/echovault/keyspace.go:71.83,72.29 1 0 +github.com/echovault/echovault/echovault/keyspace.go:72.29,75.19 2 0 +github.com/echovault/echovault/echovault/keyspace.go:75.19,77.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:78.10,78.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:78.65,81.19 2 0 +github.com/echovault/echovault/echovault/keyspace.go:81.19,83.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:84.10,84.66 1 0 +github.com/echovault/echovault/echovault/keyspace.go:84.66,89.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:90.4,91.12 2 0 +github.com/echovault/echovault/echovault/keyspace.go:94.3,94.28 1 1 +github.com/echovault/echovault/echovault/keyspace.go:98.2,98.46 1 1 +github.com/echovault/echovault/echovault/keyspace.go:98.46,99.61 1 1 +github.com/echovault/echovault/echovault/keyspace.go:99.61,101.4 1 0 +github.com/echovault/echovault/echovault/keyspace.go:104.2,104.15 1 1 +github.com/echovault/echovault/echovault/keyspace.go:107.95,111.115 3 1 +github.com/echovault/echovault/echovault/keyspace.go:111.115,113.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:115.2,115.34 1 1 +github.com/echovault/echovault/echovault/keyspace.go:115.34,117.37 2 1 +github.com/echovault/echovault/echovault/keyspace.go:117.37,119.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:120.3,124.28 2 1 +github.com/echovault/echovault/echovault/keyspace.go:124.28,126.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:130.2,130.63 1 1 +github.com/echovault/echovault/echovault/keyspace.go:130.63,131.31 1 1 +github.com/echovault/echovault/echovault/keyspace.go:131.31,133.18 2 1 +github.com/echovault/echovault/echovault/keyspace.go:133.18,135.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:139.2,139.12 1 1 +github.com/echovault/echovault/echovault/keyspace.go:142.101,153.55 5 1 +github.com/echovault/echovault/echovault/keyspace.go:153.55,155.3 1 1 +github.com/echovault/echovault/echovault/keyspace.go:156.2,159.11 2 1 +github.com/echovault/echovault/echovault/keyspace.go:159.11,160.44 1 1 +github.com/echovault/echovault/echovault/keyspace.go:160.44,162.18 2 1 +github.com/echovault/echovault/echovault/keyspace.go:162.18,164.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:169.54,176.97 4 1 +github.com/echovault/echovault/echovault/keyspace.go:176.97,178.3 1 1 +github.com/echovault/echovault/echovault/keyspace.go:181.2,181.9 1 1 +github.com/echovault/echovault/echovault/keyspace.go:182.108,183.36 1 0 +github.com/echovault/echovault/echovault/keyspace.go:184.108,185.36 1 0 +github.com/echovault/echovault/echovault/keyspace.go:188.2,190.12 2 1 +github.com/echovault/echovault/echovault/keyspace.go:193.60,195.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:195.6,196.83 1 0 +github.com/echovault/echovault/echovault/keyspace.go:196.83,198.9 2 0 +github.com/echovault/echovault/echovault/keyspace.go:201.2,202.33 2 0 +github.com/echovault/echovault/echovault/keyspace.go:202.33,204.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:205.2,206.13 2 0 +github.com/echovault/echovault/echovault/keyspace.go:211.86,212.27 1 1 +github.com/echovault/echovault/echovault/keyspace.go:212.27,214.84 1 1 +github.com/echovault/echovault/echovault/keyspace.go:214.84,216.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:218.3,218.35 1 1 +github.com/echovault/echovault/echovault/keyspace.go:218.35,220.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:221.3,221.56 1 0 +github.com/echovault/echovault/echovault/keyspace.go:222.29,225.34 3 0 +github.com/echovault/echovault/echovault/keyspace.go:226.29,229.34 3 0 +github.com/echovault/echovault/echovault/keyspace.go:230.30,232.51 2 0 +github.com/echovault/echovault/echovault/keyspace.go:232.51,234.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:235.4,235.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:236.30,238.51 2 0 +github.com/echovault/echovault/echovault/keyspace.go:238.51,240.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:241.4,241.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:243.3,243.55 1 0 +github.com/echovault/echovault/echovault/keyspace.go:243.55,245.4 1 0 +github.com/echovault/echovault/echovault/keyspace.go:247.2,247.12 1 0 +github.com/echovault/echovault/echovault/keyspace.go:251.71,253.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:253.34,255.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:258.2,261.50 3 0 +github.com/echovault/echovault/echovault/keyspace.go:261.50,263.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:265.2,267.50 3 0 +github.com/echovault/echovault/echovault/keyspace.go:267.50,269.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:273.2,275.9 3 0 +github.com/echovault/echovault/echovault/keyspace.go:276.125,281.7 3 0 +github.com/echovault/echovault/echovault/keyspace.go:281.7,283.40 1 0 +github.com/echovault/echovault/echovault/keyspace.go:283.40,285.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:287.4,288.29 2 0 +github.com/echovault/echovault/echovault/keyspace.go:288.29,290.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:290.49,292.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:293.10,293.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:293.65,295.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:295.63,297.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:301.4,304.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:304.52,306.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:308.125,313.7 3 0 +github.com/echovault/echovault/echovault/keyspace.go:313.7,315.40 1 0 +github.com/echovault/echovault/echovault/keyspace.go:315.40,317.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:319.4,320.29 2 0 +github.com/echovault/echovault/echovault/keyspace.go:320.29,322.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:322.49,324.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:325.10,325.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:325.65,328.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:328.63,330.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:334.4,337.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:337.52,339.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:341.105,344.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:344.7,347.30 2 0 +github.com/echovault/echovault/echovault/keyspace.go:347.30,351.5 3 0 +github.com/echovault/echovault/echovault/keyspace.go:353.4,354.37 2 0 +github.com/echovault/echovault/echovault/keyspace.go:354.37,355.17 1 0 +github.com/echovault/echovault/echovault/keyspace.go:355.17,356.31 1 0 +github.com/echovault/echovault/echovault/keyspace.go:356.31,358.51 1 0 +github.com/echovault/echovault/echovault/keyspace.go:358.51,360.8 1 0 +github.com/echovault/echovault/echovault/keyspace.go:361.12,361.67 1 0 +github.com/echovault/echovault/echovault/keyspace.go:361.67,362.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:362.65,364.8 1 0 +github.com/echovault/echovault/echovault/keyspace.go:367.6,370.54 3 0 +github.com/echovault/echovault/echovault/keyspace.go:370.54,372.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:374.5,374.10 1 0 +github.com/echovault/echovault/echovault/keyspace.go:377.106,380.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:380.7,387.29 5 0 +github.com/echovault/echovault/echovault/keyspace.go:387.29,389.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:389.49,391.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:392.10,392.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:392.65,393.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:393.63,395.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:399.4,402.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:402.52,404.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:406.10,407.13 1 0 +github.com/echovault/echovault/echovault/keyspace.go:416.77,418.57 1 1 +github.com/echovault/echovault/echovault/keyspace.go:418.57,420.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:422.2,427.50 3 1 +github.com/echovault/echovault/echovault/keyspace.go:427.50,429.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:430.2,437.33 6 1 +github.com/echovault/echovault/echovault/keyspace.go:437.33,438.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:438.7,442.35 3 0 +github.com/echovault/echovault/echovault/keyspace.go:442.35,444.10 2 0 +github.com/echovault/echovault/echovault/keyspace.go:448.2,453.25 4 1 +github.com/echovault/echovault/echovault/keyspace.go:453.25,456.28 2 0 +github.com/echovault/echovault/echovault/keyspace.go:456.28,457.46 1 0 +github.com/echovault/echovault/echovault/keyspace.go:457.46,459.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:460.9,460.64 1 0 +github.com/echovault/echovault/echovault/keyspace.go:460.64,461.60 1 0 +github.com/echovault/echovault/echovault/keyspace.go:461.60,463.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:468.2,468.21 1 1 +github.com/echovault/echovault/echovault/keyspace.go:468.21,470.3 1 1 +github.com/echovault/echovault/echovault/keyspace.go:472.2,475.58 2 0 +github.com/echovault/echovault/echovault/keyspace.go:475.58,479.3 2 0 +github.com/echovault/echovault/echovault/keyspace.go:481.2,481.12 1 0 +github.com/echovault/echovault/echovault/modules.go:27.75,30.42 3 1 +github.com/echovault/echovault/echovault/modules.go:30.42,31.46 1 1 +github.com/echovault/echovault/echovault/modules.go:31.46,33.4 1 1 +github.com/echovault/echovault/echovault/modules.go:35.2,35.72 1 1 +github.com/echovault/echovault/echovault/modules.go:38.125,58.37 1 1 +github.com/echovault/echovault/echovault/modules.go:58.37,62.4 3 1 +github.com/echovault/echovault/echovault/modules.go:66.137,68.16 2 1 +github.com/echovault/echovault/echovault/modules.go:68.16,70.3 1 1 +github.com/echovault/echovault/echovault/modules.go:72.2,72.19 1 1 +github.com/echovault/echovault/echovault/modules.go:72.19,74.3 1 0 +github.com/echovault/echovault/echovault/modules.go:76.2,77.16 2 1 +github.com/echovault/echovault/echovault/modules.go:77.16,79.3 1 1 +github.com/echovault/echovault/echovault/modules.go:81.2,85.16 4 1 +github.com/echovault/echovault/echovault/modules.go:85.16,87.3 1 1 +github.com/echovault/echovault/echovault/modules.go:88.2,89.8 2 1 +github.com/echovault/echovault/echovault/modules.go:89.8,92.3 2 1 +github.com/echovault/echovault/echovault/modules.go:94.2,94.51 1 1 +github.com/echovault/echovault/echovault/modules.go:94.51,97.87 1 1 +github.com/echovault/echovault/echovault/modules.go:97.87,99.4 1 0 +github.com/echovault/echovault/echovault/modules.go:103.2,103.50 1 1 +github.com/echovault/echovault/echovault/modules.go:103.50,104.7 1 1 +github.com/echovault/echovault/echovault/modules.go:104.7,105.42 1 1 +github.com/echovault/echovault/echovault/modules.go:105.42,107.10 2 1 +github.com/echovault/echovault/echovault/modules.go:112.2,112.43 1 1 +github.com/echovault/echovault/echovault/modules.go:112.43,114.17 2 1 +github.com/echovault/echovault/echovault/modules.go:114.17,116.4 1 1 +github.com/echovault/echovault/echovault/modules.go:118.3,118.62 1 1 +github.com/echovault/echovault/echovault/modules.go:118.62,120.4 1 1 +github.com/echovault/echovault/echovault/modules.go:122.3,124.18 2 1 +github.com/echovault/echovault/echovault/modules.go:128.2,128.32 1 1 +github.com/echovault/echovault/echovault/modules.go:128.32,131.17 3 1 +github.com/echovault/echovault/echovault/modules.go:131.17,133.4 1 0 +github.com/echovault/echovault/echovault/modules.go:134.3,134.18 1 1 +github.com/echovault/echovault/echovault/modules.go:138.2,138.34 1 0 +github.com/echovault/echovault/echovault/modules.go:138.34,141.3 2 0 +github.com/echovault/echovault/echovault/modules.go:143.2,143.72 1 0 +github.com/echovault/echovault/echovault/plugin.go:37.72,41.41 3 1 +github.com/echovault/echovault/echovault/plugin.go:41.41,42.37 1 1 +github.com/echovault/echovault/echovault/plugin.go:42.37,44.4 1 1 +github.com/echovault/echovault/echovault/plugin.go:45.3,45.44 1 0 +github.com/echovault/echovault/echovault/plugin.go:48.2,49.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:49.16,51.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:53.2,54.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:54.16,56.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:57.2,58.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:58.9,60.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:62.2,63.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:63.16,65.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:66.2,67.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:67.9,69.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:71.2,72.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:72.16,74.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:75.2,76.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:76.9,78.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:80.2,81.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:81.16,83.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:84.2,85.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:85.9,87.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:89.2,90.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:90.16,92.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:93.2,94.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:94.9,96.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:98.2,99.16 2 1 +github.com/echovault/echovault/echovault/plugin.go:99.16,101.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:102.2,110.9 2 1 +github.com/echovault/echovault/echovault/plugin.go:110.9,112.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:115.2,115.91 1 1 +github.com/echovault/echovault/echovault/plugin.go:115.91,117.3 1 1 +github.com/echovault/echovault/echovault/plugin.go:120.2,123.31 1 1 +github.com/echovault/echovault/echovault/plugin.go:123.31,126.36 2 1 +github.com/echovault/echovault/echovault/plugin.go:126.36,128.5 1 1 +github.com/echovault/echovault/echovault/plugin.go:129.4,129.15 1 1 +github.com/echovault/echovault/echovault/plugin.go:134.83,136.18 2 0 +github.com/echovault/echovault/echovault/plugin.go:136.18,138.5 1 0 +github.com/echovault/echovault/echovault/plugin.go:139.4,143.10 1 0 +github.com/echovault/echovault/echovault/plugin.go:145.72,154.4 1 1 +github.com/echovault/echovault/echovault/plugin.go:157.2,157.12 1 1 +github.com/echovault/echovault/echovault/plugin.go:165.54,168.91 3 1 +github.com/echovault/echovault/echovault/plugin.go:168.91,170.3 1 1 +github.com/echovault/echovault/echovault/plugin.go:176.49,180.42 4 1 +github.com/echovault/echovault/echovault/plugin.go:180.42,181.61 1 1 +github.com/echovault/echovault/echovault/plugin.go:181.61,183.4 1 1 +github.com/echovault/echovault/echovault/plugin.go:183.6,185.4 1 1 +github.com/echovault/echovault/echovault/plugin.go:187.2,187.16 1 1 +github.com/echovault/echovault/echovault/test_helpers.go:9.35,16.2 2 1 +github.com/echovault/echovault/echovault/test_helpers.go:18.95,19.82 1 1 +github.com/echovault/echovault/echovault/test_helpers.go:19.82,21.3 1 0 +github.com/echovault/echovault/echovault/test_helpers.go:22.2,22.12 1 1 +github.com/echovault/echovault/echovault/test_helpers.go:25.95,28.2 2 1 diff --git a/docker-compose.yaml b/docker-compose.yaml index d125e446..72f71595 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -63,9 +63,9 @@ services: - RAFT_PORT=8000 - ML_PORT=7946 - SERVER_ID=1 - - JOIN_ADDR=cluster_node_2:7946 + - JOIN_ADDR=2/cluster_node_2:7946 - DATA_DIR=/var/lib/echovault - - IN_MEMORY=false + - IN_MEMORY=true - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=true @@ -110,9 +110,9 @@ services: - RAFT_PORT=8000 - ML_PORT=7946 - SERVER_ID=2 - - JOIN_ADDR=cluster_node_3:7946 + - JOIN_ADDR=3/cluster_node_3:7946 - DATA_DIR=/var/lib/echovault - - IN_MEMORY=false + - IN_MEMORY=true - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false @@ -157,9 +157,9 @@ services: - RAFT_PORT=8000 - ML_PORT=7946 - SERVER_ID=3 - - JOIN_ADDR=cluster_node_4:7946 + - JOIN_ADDR=4/cluster_node_4:7946 - DATA_DIR=/var/lib/echovault - - IN_MEMORY=false + - IN_MEMORY=true - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false @@ -204,9 +204,9 @@ services: - RAFT_PORT=8000 - ML_PORT=7946 - SERVER_ID=4 - - JOIN_ADDR=cluster_node_5:7946 + - JOIN_ADDR=5/cluster_node_5:7946 - DATA_DIR=/var/lib/echovault - - IN_MEMORY=false + - IN_MEMORY=true - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false @@ -251,9 +251,9 @@ services: - RAFT_PORT=8000 - ML_PORT=7946 - SERVER_ID=5 - - JOIN_ADDR=cluster_node_1:7946 + - JOIN_ADDR=1/cluster_node_1:7946 - DATA_DIR=/var/lib/echovault - - IN_MEMORY=false + - IN_MEMORY=true - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false diff --git a/echovault/api_admin.go b/echovault/api_admin.go index 9f3c2aaa..5ae23053 100644 --- a/echovault/api_admin.go +++ b/echovault/api_admin.go @@ -57,42 +57,18 @@ type CommandHandlerFunc func(params CommandHandlerFuncParams) ([]byte, error) // // Command is the string slice command containing the command that triggered this handler. // -// KeyExists returns true if the key passed to it exists in the store. +// KeysExist returns a map that specifies whether the key exists in the store. // -// CreateKeyAndLock creates the new key and immediately write locks it. If the key already exists, then -// it is simply write locked which makes this function safe to call even if the key already exists. Always call -// KeyUnlock when done after CreateKeyAndLock. +// GetValues returns a map with the values held at the specified keys. When a key does not exist in the store the +// associated value will be nil. // -// KeyLock acquires a write lock for the specified key. If the lock is successfully acquired, the function will return -// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyUnlock -// when done after KeyLock. -// -// KeyUnlock releases the write lock for the specified key. Always call this after KeyLock otherwise the key will not be -// lockable by any future invocations of this command or other commands. -// -// KeyRLock acquires a read lock for the specified key. If the lock is successfully acquired, the function will return -// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyRUnlock -// when done after KeyRLock. -// -// KeyRUnlock releases the real lock for the specified key. Always call this after KeyRLock otherwise the key will not be -// write-lockable by any future invocations of this command or other commands. -// -// GetValue returns the value held at the specified key as an interface{}. Make sure to invoke KeyLock or KeyRLock on the -// key before GetValue to ensure thread safety. -// -// SetValue sets the value at the specified key. Make sure to invoke KeyLock on the key before -// SetValue to ensure thread safety. +// SetValues sets the keys given with their associated values. type CommandHandlerFuncParams struct { - Context context.Context - Command []string - KeyExists func(ctx context.Context, key string) bool - CreateKeyAndLock func(ctx context.Context, key string) (bool, error) - KeyLock func(ctx context.Context, key string) (bool, error) - KeyUnlock func(ctx context.Context, key string) - KeyRLock func(ctx context.Context, key string) (bool, error) - KeyRUnlock func(ctx context.Context, key string) - GetValue func(ctx context.Context, key string) interface{} - SetValue func(ctx context.Context, key string, value interface{}) error + Context context.Context + Command []string + KeysExist func(keys []string) map[string]bool + GetValues func(ctx context.Context, keys []string) map[string]interface{} + SetValues func(ctx context.Context, entries map[string]interface{}) error } // CommandOptions provides the specification of the command to be added to the EchoVault instance. @@ -268,16 +244,11 @@ func (server *EchoVault) AddCommand(command CommandOptions) error { }), HandlerFunc: internal.HandlerFunc(func(params internal.HandlerFuncParams) ([]byte, error) { return command.HandlerFunc(CommandHandlerFuncParams{ - Context: params.Context, - Command: params.Command, - KeyLock: params.KeyLock, - KeyUnlock: params.KeyUnlock, - KeyRLock: params.KeyRLock, - KeyRUnlock: params.KeyRUnlock, - KeyExists: params.KeyExists, - CreateKeyAndLock: params.CreateKeyAndLock, - GetValue: params.GetValue, - SetValue: params.SetValue, + Context: params.Context, + Command: params.Command, + KeysExist: params.KeysExist, + GetValues: params.GetValues, + SetValues: params.SetValues, }) }), }) @@ -338,16 +309,11 @@ func (server *EchoVault) AddCommand(command CommandOptions) error { }), HandlerFunc: internal.HandlerFunc(func(params internal.HandlerFuncParams) ([]byte, error) { return sc.HandlerFunc(CommandHandlerFuncParams{ - Context: params.Context, - Command: params.Command, - KeyLock: params.KeyLock, - KeyUnlock: params.KeyUnlock, - KeyRLock: params.KeyRLock, - KeyRUnlock: params.KeyRUnlock, - KeyExists: params.KeyExists, - CreateKeyAndLock: params.CreateKeyAndLock, - GetValue: params.GetValue, - SetValue: params.SetValue, + Context: params.Context, + Command: params.Command, + KeysExist: params.KeysExist, + GetValues: params.GetValues, + SetValues: params.SetValues, }) }), } diff --git a/echovault/api_admin_test.go b/echovault/api_admin_test.go index 5204747e..24f38090 100644 --- a/echovault/api_admin_test.go +++ b/echovault/api_admin_test.go @@ -243,7 +243,6 @@ func TestEchoVault_ExecuteCommand(t *testing.T) { r := resp.NewReader(bytes.NewReader(b)) v, _, _ := r.ReadValue() if v.Integer() != tt.wantRes { - fmt.Println("RES: ", string(b)) t.Errorf("ExecuteCommand() response = %d, wantRes %d", v.Integer(), tt.wantRes) } }) diff --git a/echovault/api_hash_test.go b/echovault/api_hash_test.go index 10774d77..00f49ad3 100644 --- a/echovault/api_hash_test.go +++ b/echovault/api_hash_test.go @@ -616,13 +616,13 @@ func TestEchoVault_HSET(t *testing.T) { wantErr: false, }, { - name: "HSET returns error when the target key is not a map", + name: "HSET overwrites when the target key is not a map", key: "key6", presetValue: "Default preset value", fieldValuePairs: map[string]string{"field1": "value1"}, hsetFunc: server.HSet, - want: 0, - wantErr: true, + want: 1, + wantErr: false, }, } for _, tt := range tests { diff --git a/echovault/api_list.go b/echovault/api_list.go index d43dd302..3f0d718c 100644 --- a/echovault/api_list.go +++ b/echovault/api_list.go @@ -15,7 +15,6 @@ package echovault import ( - "fmt" "github.com/echovault/echovault/internal" "strconv" "strings" @@ -34,7 +33,6 @@ import ( // "LLen command on non-list item" - when the provided key exists but is not a list. func (server *EchoVault) LLen(key string) (int, error) { b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"LLEN", key}), nil, false, true) - fmt.Println(key, string(b), err) if err != nil { return 0, err } diff --git a/echovault/api_set_test.go b/echovault/api_set_test.go index f347f9aa..a882671f 100644 --- a/echovault/api_set_test.go +++ b/echovault/api_set_test.go @@ -376,7 +376,7 @@ func TestEchoVault_SINTER(t *testing.T) { "key12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), "key13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), }, - keys: []string{"non-existent", "key7", "key8"}, + keys: []string{"non-existent", "key12", "key13"}, want: []string{}, wantErr: false, }, @@ -467,13 +467,12 @@ func TestEchoVault_SINTERCARD(t *testing.T) { wantErr: false, }, { - name: "Return 0 if any of the keys does not exist", + name: "Return error if any of the keys is non-existent", presetValues: map[string]interface{}{ "key11": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "key12": "Default value", "key13": set.NewSet([]string{"one"}), }, - keys: []string{"key11", "key12", "key13", "non-existent"}, + keys: []string{"key11", "key12", "key13"}, limit: 0, want: 0, wantErr: false, @@ -578,7 +577,7 @@ func TestEchoVault_SINTERSTORE(t *testing.T) { "key12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), "key13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), }, - keys: []string{"non-existent", "key7", "key8"}, + keys: []string{"non-existent", "key12", "key13"}, want: 0, wantErr: false, }, diff --git a/echovault/echovault.go b/echovault/echovault.go index a19b9028..1d3b47c5 100644 --- a/echovault/echovault.go +++ b/echovault/echovault.go @@ -61,9 +61,8 @@ type EchoVault struct { // the new number is the new connection's ID. connId atomic.Uint64 - store map[string]internal.KeyData // Data store to hold the keys and their associated data, expiry time, etc. - keyLocks map[string]*sync.RWMutex // Map to hold all the individual key locks. - keyCreationLock *sync.Mutex // The mutex for creating a new key. Only one goroutine should be able to create a key at a time. + storeLock *sync.RWMutex // Global read-write mutex for entire store. + store map[string]internal.KeyData // Data store to hold the keys and their associated data, expiry time, etc. // Holds all the keys that are currently associated with an expiry. keysWithExpiry struct { @@ -128,21 +127,20 @@ func WithConfig(config config.Config) func(echovault *EchoVault) { // This functions accepts the WithContext, WithConfig and WithCommands options. func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { echovault := &EchoVault{ - clock: clock.NewClock(), - context: context.Background(), - config: config.DefaultConfig(), - store: make(map[string]internal.KeyData), - keyLocks: make(map[string]*sync.RWMutex), - keyCreationLock: &sync.Mutex{}, - commandsRWMut: sync.RWMutex{}, + clock: clock.NewClock(), + context: context.Background(), + config: config.DefaultConfig(), + storeLock: &sync.RWMutex{}, + store: make(map[string]internal.KeyData), + commandsRWMut: sync.RWMutex{}, commands: func() []internal.Command { var commands []internal.Command commands = append(commands, acl.Commands()...) commands = append(commands, admin.Commands()...) + commands = append(commands, connection.Commands()...) commands = append(commands, generic.Commands()...) commands = append(commands, hash.Commands()...) commands = append(commands, list.Commands()...) - commands = append(commands, connection.Commands()...) commands = append(commands, pubsub.Commands()...) commands = append(commands, set.Commands()...) commands = append(commands, sorted_set.Commands()...) @@ -195,15 +193,17 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { echovault.raft = raft.NewRaft(raft.Opts{ Config: echovault.config, GetCommand: echovault.getCommand, - CreateKeyAndLock: echovault.CreateKeyAndLock, - SetValue: echovault.SetValue, - SetExpiry: echovault.SetExpiry, - KeyUnlock: echovault.KeyUnlock, - DeleteKey: echovault.DeleteKey, + SetValues: echovault.setValues, + SetExpiry: echovault.setExpiry, StartSnapshot: echovault.startSnapshot, FinishSnapshot: echovault.finishSnapshot, SetLatestSnapshotTime: echovault.setLatestSnapshot, GetHandlerFuncParams: echovault.getHandlerFuncParams, + DeleteKey: func(key string) error { + echovault.storeLock.Lock() + defer echovault.storeLock.Unlock() + return echovault.deleteKey(key) + }, GetState: func() map[string]internal.KeyData { state := make(map[string]internal.KeyData) for k, v := range echovault.getState() { @@ -245,14 +245,10 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { }), snapshot.WithSetKeyDataFunc(func(key string, data internal.KeyData) { ctx := context.Background() - if _, err := echovault.CreateKeyAndLock(ctx, key); err != nil { + if err := echovault.setValues(ctx, map[string]interface{}{key: data.Value}); err != nil { log.Println(err) } - if err := echovault.SetValue(ctx, key, data.Value); err != nil { - log.Println(err) - } - echovault.SetExpiry(ctx, key, data.ExpireAt, false) - echovault.KeyUnlock(ctx, key) + echovault.setExpiry(ctx, key, data.ExpireAt, false) }), ) // Set up standalone AOF engine @@ -273,14 +269,10 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { }), aof.WithSetKeyDataFunc(func(key string, value internal.KeyData) { ctx := context.Background() - if _, err := echovault.CreateKeyAndLock(ctx, key); err != nil { - log.Println(err) - } - if err := echovault.SetValue(ctx, key, value.Value); err != nil { + if err := echovault.setValues(ctx, map[string]interface{}{key: value.Value}); err != nil { log.Println(err) } - echovault.SetExpiry(ctx, key, value.ExpireAt, false) - echovault.KeyUnlock(ctx, key) + echovault.setExpiry(ctx, key, value.ExpireAt, false) }), aof.WithHandleCommandFunc(func(command []byte) { _, err := echovault.handleCommand(context.Background(), command, nil, true, false) diff --git a/echovault/echovault_test.go b/echovault/echovault_test.go new file mode 100644 index 00000000..3c488039 --- /dev/null +++ b/echovault/echovault_test.go @@ -0,0 +1,202 @@ +package echovault + +import ( + "fmt" + "github.com/echovault/echovault/internal" + "github.com/tidwall/resp" + "net" + "strings" + "sync" + "testing" + "time" +) + +type ClientServerPair struct { + serverId string + bindAddr string + port int + raftPort int + mlPort int + bootstrapCluster bool + client *resp.Conn + server *EchoVault +} + +var bindLock sync.Mutex +var bindNum byte = 10 + +func getBindAddrNet(network byte) net.IP { + bindLock.Lock() + defer bindLock.Unlock() + + result := net.IPv4(127, 0, network, bindNum) + bindNum++ + if bindNum > 255 { + bindNum = 10 + } + + return result +} + +func getBindAddr() net.IP { + return getBindAddrNet(0) +} + +var setupLock sync.Mutex + +func setupServer( + serverId string, + bootstrapCluster bool, + bindAddr, + joinAddr string, + port, + raftPort, + mlPort int, +) (*EchoVault, error) { + setupLock.Lock() + defer setupLock.Unlock() + + config := DefaultConfig() + config.DataDir = "./testdata" + config.BindAddr = bindAddr + config.JoinAddr = joinAddr + config.Port = uint16(port) + config.InMemory = true + config.ServerID = serverId + config.RaftBindPort = uint16(raftPort) + config.MemberListBindPort = uint16(mlPort) + config.BootstrapCluster = bootstrapCluster + return NewEchoVault(WithConfig(config)) +} + +func Test_ClusterReplication(t *testing.T) { + pairs := make([]ClientServerPair, 3) + + for i := 0; i < len(pairs); i++ { + serverId := fmt.Sprintf("SERVER-%d", i) + bindAddr := getBindAddr().String() + bootstrapCluster := i == 0 + joinAddr := "" + if !bootstrapCluster { + joinAddr = fmt.Sprintf("%s/%s:%d", pairs[0].serverId, pairs[0].bindAddr, pairs[0].mlPort) + } + port, err := internal.GetFreePort() + if err != nil { + t.Errorf("could not get free port: %v", err) + } + raftPort, err := internal.GetFreePort() + if err != nil { + t.Errorf("could not get free raft port: %v", err) + } + memberlistPort, err := internal.GetFreePort() + if err != nil { + t.Errorf("could not get free memberlist port: %v", err) + } + server, err := setupServer(serverId, bootstrapCluster, bindAddr, joinAddr, port, raftPort, memberlistPort) + if err != nil { + t.Errorf("could not start server; %v", err) + } + + // Start the server + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + server.Start() + }() + + <-time.After(500 * time.Millisecond) // Yield to allow server start. + + if i > 0 { + // If the node is a follower, wait until it's joined the raft cluster before moving forward. + for { + if server.raft.HasJoinedCluster() { + break + } + } + } + + // Setup client connection. + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) + if err != nil { + t.Errorf("could not open tcp connection: %v", err) + } + for { + // Wait until connection is no longer nil + if conn != nil { + break + } + } + client := resp.NewConn(conn) + + pairs[i] = ClientServerPair{ + serverId: serverId, + bindAddr: bindAddr, + port: port, + raftPort: raftPort, + mlPort: memberlistPort, + bootstrapCluster: bootstrapCluster, + client: client, + server: server, + } + } + + // Prepare the write data for the cluster + tests := []struct { + key string + value string + }{ + { + key: "key1", + value: "value1", + }, + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + } + + // Write all the data to the cluster leader + for i, test := range tests { + node := pairs[0] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.value), + }); err != nil { + t.Errorf("could not write data to leader node (test %d): %v", i, err) + } + // Read response and make sure we received "ok" response + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read response from leader node (test %d): %v", i, err) + } + if !strings.EqualFold(rd.String(), "ok") { + t.Errorf("expected response for test %d to be \"OK\", got %s", i, rd.String()) + } + } + + // On each of the follower nodes, get the values and check if they have been replicated + for i, test := range tests { + for j := 1; j < len(pairs); j++ { + node := pairs[i] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write data to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.String() != test.value { + t.Errorf("expected value \"%s\" for follower node %d (test %d), got \"%s\"", test.value, j, i, rd.String()) + } + } + } +} diff --git a/echovault/keyspace.go b/echovault/keyspace.go index 29fe875c..70ce7025 100644 --- a/echovault/keyspace.go +++ b/echovault/keyspace.go @@ -26,205 +26,123 @@ import ( "runtime" "slices" "strings" - "sync" "time" ) -// KeyLock tries to acquire the write lock for the specified key. -// If the context passed to the function finishes before the lock is acquired, an error is returned. -// -// If this functions is called on a node in a replication cluster, the key is only locked -// on that particular node. -func (server *EchoVault) KeyLock(ctx context.Context, key string) (bool, error) { - // If context did not set deadline, set the default deadline - var cancelFunc context.CancelFunc - if _, ok := ctx.Deadline(); !ok { - ctx, cancelFunc = context.WithTimeoutCause(ctx, 250*time.Millisecond, fmt.Errorf("timeout for key %s", key)) - defer cancelFunc() - } - // Attempt to acquire the lock until lock is acquired or deadline is reached. - for { - select { - default: - if server.keyLocks[key] == nil { - return false, fmt.Errorf("key %s not found", key) - } - ok := server.keyLocks[key].TryLock() - if ok { - return true, nil - } - case <-ctx.Done(): - return false, context.Cause(ctx) - } - } -} +func (server *EchoVault) keysExist(keys []string) map[string]bool { + server.storeLock.RLock() + defer server.storeLock.RUnlock() -// KeyUnlock releases the write lock for the specified key. -// -// If this functions is called on a node in a replication cluster, the key is only unlocked -// on that particular node. -func (server *EchoVault) KeyUnlock(_ context.Context, key string) { - if _, ok := server.keyLocks[key]; ok { - server.keyLocks[key].Unlock() - } -} + exists := make(map[string]bool, len(keys)) -// KeyRLock tries to acquire the read lock for the specified key. -// If the context passed to the function finishes before the lock is acquired, an error is returned. -// -// If this functions is called on a node in a replication cluster, the key is only locked -// on that particular node. -func (server *EchoVault) KeyRLock(ctx context.Context, key string) (bool, error) { - // If context did not set deadline, set the default deadline - var cancelFunc context.CancelFunc - if _, ok := ctx.Deadline(); !ok { - ctx, cancelFunc = context.WithTimeoutCause(ctx, 250*time.Millisecond, fmt.Errorf("timeout for key %s", key)) - defer cancelFunc() - } - // Attempt to acquire the lock until lock is acquired or deadline is reached. - for { - select { - default: - if server.keyLocks[key] == nil { - return false, fmt.Errorf("key %s not found", key) - } - ok := server.keyLocks[key].TryRLock() - if ok { - return true, nil - } - case <-ctx.Done(): - return false, context.Cause(ctx) - } + for _, key := range keys { + _, ok := server.store[key] + exists[key] = ok } -} -// KeyRUnlock releases the read lock for the specified key. -// -// If this functions is called on a node in a replication cluster, the key is only unlocked -// on that particular node. -func (server *EchoVault) KeyRUnlock(_ context.Context, key string) { - if _, ok := server.keyLocks[key]; ok { - server.keyLocks[key].RUnlock() - } + return exists } -// KeyExists returns true if the key exists in the store. -// -// If the key is volatile and expired, checking for its existence with KeyExists will trigger a key deletion and -// then return false. If the key is determined to be expired by KeyExists, it will be evicted across the entire -// replication cluster. -func (server *EchoVault) KeyExists(ctx context.Context, key string) bool { +func (server *EchoVault) getExpiry(key string) time.Time { + server.storeLock.RLock() + defer server.storeLock.RUnlock() + entry, ok := server.store[key] if !ok { - return false - } - - if entry.ExpireAt != (time.Time{}) && entry.ExpireAt.Before(server.clock.Now()) { - if !server.isInCluster() { - // If in standalone mode, delete the key directly. - err := server.DeleteKey(ctx, key) - if err != nil { - log.Printf("keyExists: %+v\n", err) - } - } else if server.isInCluster() && server.raft.IsRaftLeader() { - // If we're in a raft cluster, and we're the leader, send command to delete the key in the cluster. - err := server.raftApplyDeleteKey(ctx, key) - if err != nil { - log.Printf("keyExists: %+v\n", err) - } - } else if server.isInCluster() && !server.raft.IsRaftLeader() { - // Forward message to leader to initiate key deletion. - // This is always called regardless of ForwardCommand config value - // because we always want to remove expired keys. - server.memberList.ForwardDeleteKey(ctx, key) - } - - return false + return time.Time{} } - return true + return entry.ExpireAt } -// CreateKeyAndLock creates a new key lock and immediately locks it if the key does not exist. -// If the key exists, the existing key is locked. -// -// If this functions is called on a node in a replication cluster, the key is only created/locked -// on that particular node. -func (server *EchoVault) CreateKeyAndLock(ctx context.Context, key string) (bool, error) { - if internal.IsMaxMemoryExceeded(server.config.MaxMemory) && server.config.EvictionPolicy == constants.NoEviction { - return false, errors.New("max memory reached, key not created") - } +func (server *EchoVault) getValues(ctx context.Context, keys []string) map[string]interface{} { + server.storeLock.Lock() + defer server.storeLock.Unlock() - server.keyCreationLock.Lock() - defer server.keyCreationLock.Unlock() + values := make(map[string]interface{}, len(keys)) - if !server.KeyExists(ctx, key) { - // Create Lock - keyLock := &sync.RWMutex{} - keyLock.Lock() - server.keyLocks[key] = keyLock - // Create key entry - server.store[key] = internal.KeyData{ - Value: nil, - ExpireAt: time.Time{}, + for _, key := range keys { + entry, ok := server.store[key] + if !ok { + values[key] = nil + continue } - return true, nil - } - return server.KeyLock(ctx, key) -} + if entry.ExpireAt != (time.Time{}) && entry.ExpireAt.Before(server.clock.Now()) { + if !server.isInCluster() { + // If in standalone mode, delete the key directly. + err := server.deleteKey(key) + if err != nil { + log.Printf("keyExists: %+v\n", err) + } + } else if server.isInCluster() && server.raft.IsRaftLeader() { + // If we're in a raft cluster, and we're the leader, send command to delete the key in the cluster. + err := server.raftApplyDeleteKey(ctx, key) + if err != nil { + log.Printf("keyExists: %+v\n", err) + } + } else if server.isInCluster() && !server.raft.IsRaftLeader() { + // Forward message to leader to initiate key deletion. + // This is always called regardless of ForwardCommand config value + // because we always want to remove expired keys. + server.memberList.ForwardDeleteKey(ctx, key) + } + values[key] = nil + continue + } -// GetValue retrieves the current value at the specified key. -// The key must be read-locked before calling this function. -func (server *EchoVault) GetValue(ctx context.Context, key string) interface{} { - if err := server.updateKeyInCache(ctx, key); err != nil { - log.Printf("GetValue error: %+v\n", err) + values[key] = entry.Value } - return server.store[key].Value + + // Asynchronously update the keys in the cache. + go func(ctx context.Context, keys []string) { + if err := server.updateKeysInCache(ctx, keys); err != nil { + log.Printf("getValues error: %+v\n", err) + } + }(ctx, keys) + + return values } -// SetValue updates the value in the store at the specified key with the given value. -// If we're in not in cluster (i.e. in standalone mode), then the change count is incremented in the snapshot engine. -// This count triggers a snapshot when the threshold is reached. -// The key must be locked prior to calling this function. -func (server *EchoVault) SetValue(ctx context.Context, key string, value interface{}) error { +func (server *EchoVault) setValues(ctx context.Context, entries map[string]interface{}) error { + server.storeLock.Lock() + defer server.storeLock.Unlock() + if internal.IsMaxMemoryExceeded(server.config.MaxMemory) && server.config.EvictionPolicy == constants.NoEviction { return errors.New("max memory reached, key value not set") } - server.store[key] = internal.KeyData{ - Value: value, - ExpireAt: server.store[key].ExpireAt, - } - - err := server.updateKeyInCache(ctx, key) - if err != nil { - log.Printf("SetValue error: %+v\n", err) + for key, value := range entries { + expireAt := time.Time{} + if _, ok := server.store[key]; ok { + expireAt = server.store[key].ExpireAt + } + server.store[key] = internal.KeyData{ + Value: value, + ExpireAt: expireAt, + } + if !server.isInCluster() { + server.snapshotEngine.IncrementChangeCount() + } } - if !server.isInCluster() { - server.snapshotEngine.IncrementChangeCount() - } + // Asynchronously update the keys in the cache. + go func(ctx context.Context, entries map[string]interface{}) { + for key, _ := range entries { + err := server.updateKeysInCache(ctx, []string{key}) + if err != nil { + log.Printf("setValues error: %+v\n", err) + } + } + }(ctx, entries) return nil } -// The GetExpiry function returns the expiry time associated with the provided key. -// The key must be read locked before calling this function. -func (server *EchoVault) GetExpiry(ctx context.Context, key string) time.Time { - if err := server.updateKeyInCache(ctx, key); err != nil { - log.Printf("GetKeyExpiry error: %+v\n", err) - } - return server.store[key].ExpireAt -} +func (server *EchoVault) setExpiry(ctx context.Context, key string, expireAt time.Time, touch bool) { + server.storeLock.Lock() + defer server.storeLock.Unlock() -// The SetExpiry receiver function sets the expiry time of a key. -// The key parameter represents the key whose expiry time is to be set/updated. -// The expireAt parameter is the new expiry time. -// The touch parameter determines whether to update the keys access count on lfu eviction policy, -// or the access time on lru eviction policy. -// The key must be locked prior to calling this function. -func (server *EchoVault) SetExpiry(ctx context.Context, key string, expireAt time.Time, touch bool) { server.store[key] = internal.KeyData{ Value: server.store[key].Value, ExpireAt: expireAt, @@ -239,34 +157,39 @@ func (server *EchoVault) SetExpiry(ctx context.Context, key string, expireAt tim // If touch is true, update the keys status in the cache. if touch { - err := server.updateKeyInCache(ctx, key) - if err != nil { - log.Printf("SetKeyExpiry error: %+v\n", err) - } + go func(ctx context.Context, key string) { + err := server.updateKeysInCache(ctx, []string{key}) + if err != nil { + log.Printf("SetKeyExpiry error: %+v\n", err) + } + }(ctx, key) } } -// RemoveExpiry is called by commands that remove key expiry (e.g. Persist). -// The key must be locked prior ro calling this function. -func (server *EchoVault) RemoveExpiry(_ context.Context, key string) { - // Reset expiry time - server.store[key] = internal.KeyData{ - Value: server.store[key].Value, - ExpireAt: time.Time{}, - } - // Remove key from slice of keys associated with expiry +func (server *EchoVault) deleteKey(key string) error { + // Delete the key from keyLocks and store. + delete(server.store, key) + + // Remove key from slice of keys associated with expiry. server.keysWithExpiry.rwMutex.Lock() defer server.keysWithExpiry.rwMutex.Unlock() server.keysWithExpiry.keys = slices.DeleteFunc(server.keysWithExpiry.keys, func(k string) bool { return k == key }) + + // Remove the key from the cache. + switch { + case slices.Contains([]string{constants.AllKeysLFU, constants.VolatileLFU}, server.config.EvictionPolicy): + server.lfuCache.cache.Delete(key) + case slices.Contains([]string{constants.AllKeysLRU, constants.VolatileLRU}, server.config.EvictionPolicy): + server.lruCache.cache.Delete(key) + } + + log.Printf("deleted key %s\n", key) + + return nil } -// GetState creates a deep copy of the store map. -// It is used to retrieve the current state for persistence but can also be used for other -// functions that require a deep copy of the state. -// The copy only starts when there's no current copy in progress (represented by stateCopyInProgress atomic boolean) -// and when there's no current state mutation in progress (represented by stateMutationInProgress atomic boolean) func (server *EchoVault) getState() map[string]interface{} { // Wait unit there's no state mutation or copy in progress before starting a new copy process. for { @@ -283,70 +206,43 @@ func (server *EchoVault) getState() map[string]interface{} { return data } -// DeleteKey removes the key from store, keyLocks and keyExpiry maps. -// -// If this functions is called on a node in a replication cluster, the key is only deleted -// on that particular node. -func (server *EchoVault) DeleteKey(ctx context.Context, key string) error { - if _, err := server.KeyLock(ctx, key); err != nil { - return fmt.Errorf("deleteKey error: %+v", err) - } - - // Remove key expiry. - server.RemoveExpiry(ctx, key) - - // Delete the key from keyLocks and store. - delete(server.keyLocks, key) - delete(server.store, key) - - // Remove the key from the cache. - switch { - case slices.Contains([]string{constants.AllKeysLFU, constants.VolatileLFU}, server.config.EvictionPolicy): - server.lfuCache.cache.Delete(key) - case slices.Contains([]string{constants.AllKeysLRU, constants.VolatileLRU}, server.config.EvictionPolicy): - server.lruCache.cache.Delete(key) - } - - log.Printf("deleted key %s\n", key) - - return nil -} - -// updateKeyInCache updates either the key access count or the most recent access time in the cache +// updateKeysInCache updates either the key access count or the most recent access time in the cache // depending on whether an LFU or LRU strategy was used. -func (server *EchoVault) updateKeyInCache(ctx context.Context, key string) error { - // Only update cache when in standalone mode or when raft leader - if server.isInCluster() || (server.isInCluster() && !server.raft.IsRaftLeader()) { - return nil - } - // If max memory is 0, there's no max so no need to update caches - if server.config.MaxMemory == 0 { - return nil - } - switch strings.ToLower(server.config.EvictionPolicy) { - case constants.AllKeysLFU: - server.lfuCache.mutex.Lock() - defer server.lfuCache.mutex.Unlock() - server.lfuCache.cache.Update(key) - case constants.AllKeysLRU: - server.lruCache.mutex.Lock() - defer server.lruCache.mutex.Unlock() - server.lruCache.cache.Update(key) - case constants.VolatileLFU: - server.lfuCache.mutex.Lock() - defer server.lfuCache.mutex.Unlock() - if server.store[key].ExpireAt != (time.Time{}) { - server.lfuCache.cache.Update(key) +func (server *EchoVault) updateKeysInCache(ctx context.Context, keys []string) error { + for _, key := range keys { + // Only update cache when in standalone mode or when raft leader. + if server.isInCluster() || (server.isInCluster() && !server.raft.IsRaftLeader()) { + return nil } - case constants.VolatileLRU: - server.lruCache.mutex.Lock() - defer server.lruCache.mutex.Unlock() - if server.store[key].ExpireAt != (time.Time{}) { + // If max memory is 0, there's no max so no need to update caches. + if server.config.MaxMemory == 0 { + return nil + } + switch strings.ToLower(server.config.EvictionPolicy) { + case constants.AllKeysLFU: + server.lfuCache.mutex.Lock() + server.lfuCache.cache.Update(key) + server.lfuCache.mutex.Unlock() + case constants.AllKeysLRU: + server.lruCache.mutex.Lock() server.lruCache.cache.Update(key) + server.lruCache.mutex.Unlock() + case constants.VolatileLFU: + server.lfuCache.mutex.Lock() + if server.store[key].ExpireAt != (time.Time{}) { + server.lfuCache.cache.Update(key) + } + server.lfuCache.mutex.Unlock() + case constants.VolatileLRU: + server.lruCache.mutex.Lock() + if server.store[key].ExpireAt != (time.Time{}) { + server.lruCache.cache.Update(key) + } + server.lruCache.mutex.Unlock() + } + if err := server.adjustMemoryUsage(ctx); err != nil { + return fmt.Errorf("updateKeysInCache: %+v", err) } - } - if err := server.adjustMemoryUsage(ctx); err != nil { - return fmt.Errorf("updateKeyInCache: %+v", err) } return nil } @@ -374,6 +270,8 @@ func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error { // We've done a GC, but we're still at or above the max memory limit. // Start a loop that evicts keys until either the heap is empty or // we're below the max memory limit. + server.storeLock.Lock() + defer server.storeLock.Unlock() switch { case slices.Contains([]string{constants.AllKeysLFU, constants.VolatileLFU}, strings.ToLower(server.config.EvictionPolicy)): // Remove keys from LFU cache until we're below the max memory limit or @@ -389,7 +287,7 @@ func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error { key := heap.Pop(&server.lfuCache.cache).(string) if !server.isInCluster() { // If in standalone mode, directly delete the key - if err := server.DeleteKey(ctx, key); err != nil { + if err := server.deleteKey(key); err != nil { return fmt.Errorf("adjustMemoryUsage -> LFU cache eviction: %+v", err) } } else if server.isInCluster() && server.raft.IsRaftLeader() { @@ -421,7 +319,7 @@ func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error { key := heap.Pop(&server.lruCache.cache).(string) if !server.isInCluster() { // If in standalone mode, directly delete the key. - if err := server.DeleteKey(ctx, key); err != nil { + if err := server.deleteKey(key); err != nil { return fmt.Errorf("adjustMemoryUsage -> LRU cache eviction: %+v", err) } } else if server.isInCluster() && server.raft.IsRaftLeader() { @@ -444,18 +342,20 @@ func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error { // Remove random keys until we're below the max memory limit // or there are no more keys remaining. for { + server.storeLock.Lock() // If there are no keys, return error - if len(server.keyLocks) == 0 { + if len(server.store) == 0 { err := errors.New("no keys to evict") + server.storeLock.Unlock() return fmt.Errorf("adjustMemoryUsage -> all keys random: %+v", err) } // Get random key - idx := rand.Intn(len(server.keyLocks)) - for key, _ := range server.keyLocks { + idx := rand.Intn(len(server.store)) + for key, _ := range server.store { if idx == 0 { if !server.isInCluster() { // If in standalone mode, directly delete the key - if err := server.DeleteKey(ctx, key); err != nil { + if err := server.deleteKey(key); err != nil { return fmt.Errorf("adjustMemoryUsage -> all keys random: %+v", err) } } else if server.isInCluster() && server.raft.IsRaftLeader() { @@ -486,7 +386,7 @@ func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error { if !server.isInCluster() { // If in standalone mode, directly delete the key - if err := server.DeleteKey(ctx, key); err != nil { + if err := server.deleteKey(key); err != nil { return fmt.Errorf("adjustMemoryUsage -> volatile keys random: %+v", err) } } else if server.isInCluster() && server.raft.IsRaftLeader() { @@ -548,22 +448,13 @@ func (server *EchoVault) evictKeysWithExpiredTTL(ctx context.Context) error { server.keysWithExpiry.rwMutex.RUnlock() // Loop through the keys and delete them if they're expired + server.storeLock.Lock() + defer server.storeLock.Unlock() for _, k := range keys { - if _, err := server.KeyRLock(ctx, k); err != nil { - continue - } - - // If the current key is not expired, skip to the next key - if server.store[k].ExpireAt.After(server.clock.Now()) { - server.KeyRUnlock(ctx, k) - continue - } - // Delete the expired key deletedCount += 1 - server.KeyRUnlock(ctx, k) if !server.isInCluster() { - if err := server.DeleteKey(ctx, k); err != nil { + if err := server.deleteKey(k); err != nil { return fmt.Errorf("evictKeysWithExpiredTTL -> standalone delete: %+v", err) } } else if server.isInCluster() && server.raft.IsRaftLeader() { @@ -575,7 +466,6 @@ func (server *EchoVault) evictKeysWithExpiredTTL(ctx context.Context) error { // If sampleSize is 0, there's no need to calculate deleted percentage. if sampleSize == 0 { - log.Println("no keys to sample, skipping eviction") return nil } diff --git a/echovault/modules.go b/echovault/modules.go index daf56549..2d35b0c3 100644 --- a/echovault/modules.go +++ b/echovault/modules.go @@ -40,17 +40,11 @@ func (server *EchoVault) getHandlerFuncParams(ctx context.Context, cmd []string, Context: ctx, Command: cmd, Connection: conn, - KeyExists: server.KeyExists, - CreateKeyAndLock: server.CreateKeyAndLock, - KeyLock: server.KeyLock, - KeyRLock: server.KeyRLock, - KeyUnlock: server.KeyUnlock, - KeyRUnlock: server.KeyRUnlock, - GetValue: server.GetValue, - SetValue: server.SetValue, - GetExpiry: server.GetExpiry, - SetExpiry: server.SetExpiry, - DeleteKey: server.DeleteKey, + KeysExist: server.keysExist, + GetExpiry: server.getExpiry, + GetValues: server.getValues, + SetValues: server.setValues, + SetExpiry: server.setExpiry, TakeSnapshot: server.takeSnapshot, GetLatestSnapshotTime: server.getLatestSnapshotTime, RewriteAOF: server.rewriteAOF, @@ -61,6 +55,11 @@ func (server *EchoVault) getHandlerFuncParams(ctx context.Context, cmd []string, GetPubSub: server.getPubSub, GetACL: server.getACL, GetAllCommands: server.getCommands, + DeleteKey: func(key string) error { + server.storeLock.Lock() + defer server.storeLock.Unlock() + return server.deleteKey(key) + }, } } @@ -70,6 +69,10 @@ func (server *EchoVault) handleCommand(ctx context.Context, message []byte, conn return nil, err } + if len(cmd) == 0 { + return nil, errors.New("empty command") + } + command, err := server.getCommand(cmd[0]) if err != nil { return nil, err diff --git a/echovault/plugin.go b/echovault/plugin.go index dd883d60..956a9248 100644 --- a/echovault/plugin.go +++ b/echovault/plugin.go @@ -102,14 +102,9 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { handlerFunc, ok := handlerFuncSymbol.(func( ctx context.Context, command []string, - keyExists func(ctx context.Context, key string) bool, - keyLock func(ctx context.Context, key string) (bool, error), - keyUnlock func(ctx context.Context, key string), - keyRLock func(ctx context.Context, key string) (bool, error), - keyRUnlock func(ctx context.Context, key string), - createKeyAndLock func(ctx context.Context, key string) (bool, error), - getValue func(ctx context.Context, key string) interface{}, - setValue func(ctx context.Context, key string, value interface{}) error, + keysExist func(key []string) map[string]bool, + getValues func(ctx context.Context, key []string) map[string]interface{}, + setValues func(ctx context.Context, entries map[string]interface{}) error, args ...string, ) ([]byte, error)) if !ok { @@ -151,14 +146,9 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { return handlerFunc( params.Context, params.Command, - params.KeyExists, - params.KeyLock, - params.KeyUnlock, - params.KeyRLock, - params.KeyRUnlock, - params.CreateKeyAndLock, - params.GetValue, - params.SetValue, + params.KeysExist, + params.GetValues, + params.SetValues, args..., ) }, diff --git a/echovault/test_helpers.go b/echovault/test_helpers.go index 45607897..6be0ca4d 100644 --- a/echovault/test_helpers.go +++ b/echovault/test_helpers.go @@ -16,19 +16,13 @@ func createEchoVault() *EchoVault { } func presetValue(server *EchoVault, ctx context.Context, key string, value interface{}) error { - if _, err := server.CreateKeyAndLock(ctx, key); err != nil { + if err := server.setValues(ctx, map[string]interface{}{key: value}); err != nil { return err } - if err := server.SetValue(ctx, key, value); err != nil { - return err - } - server.KeyUnlock(ctx, key) return nil } func presetKeyData(server *EchoVault, ctx context.Context, key string, data internal.KeyData) { - _, _ = server.CreateKeyAndLock(ctx, key) - defer server.KeyUnlock(ctx, key) - _ = server.SetValue(ctx, key, data.Value) - server.SetExpiry(ctx, key, data.ExpireAt, false) + _ = server.setValues(ctx, map[string]interface{}{key: data.Value}) + server.setExpiry(ctx, key, data.ExpireAt, false) } diff --git a/internal/memberlist/broadcast.go b/internal/memberlist/broadcast.go index 2939eb6e..74462a95 100644 --- a/internal/memberlist/broadcast.go +++ b/internal/memberlist/broadcast.go @@ -16,8 +16,8 @@ package memberlist import ( "encoding/json" - "fmt" "github.com/hashicorp/memberlist" + "log" ) type BroadcastMessage struct { @@ -53,7 +53,7 @@ func (broadcastMessage *BroadcastMessage) Message() []byte { msg, err := json.Marshal(broadcastMessage) if err != nil { - fmt.Println(err) + log.Println(err) return []byte{} } diff --git a/internal/memberlist/delegate.go b/internal/memberlist/delegate.go index 18fb3d10..4fe6da95 100644 --- a/internal/memberlist/delegate.go +++ b/internal/memberlist/delegate.go @@ -81,7 +81,7 @@ func (delegate *Delegate) NotifyMsg(msgBytes []byte) { } err := delegate.options.addVoter(msg.NodeMeta.ServerID, msg.NodeMeta.RaftAddr, 0, 0) if err != nil { - fmt.Println(err) + log.Println(err) } case "DeleteKey": diff --git a/internal/memberlist/event_delegate.go b/internal/memberlist/event_delegate.go index daa49c84..0a60b514 100644 --- a/internal/memberlist/event_delegate.go +++ b/internal/memberlist/event_delegate.go @@ -16,8 +16,8 @@ package memberlist import ( "encoding/json" - "fmt" "github.com/hashicorp/memberlist" + "log" ) type EventDelegate struct { @@ -50,14 +50,14 @@ func (eventDelegate *EventDelegate) NotifyLeave(node *memberlist.Node) { err := json.Unmarshal(node.Meta, &meta) if err != nil { - fmt.Println("Could not get leaving node's metadata.") + log.Println("Could not get leaving node's metadata.") return } err = eventDelegate.options.removeRaftServer(meta) if err != nil { - fmt.Println(err) + log.Println(err) } } diff --git a/internal/memberlist/memberlist.go b/internal/memberlist/memberlist.go index 52470858..4b5e4981 100644 --- a/internal/memberlist/memberlist.go +++ b/internal/memberlist/memberlist.go @@ -60,7 +60,9 @@ func NewMemberList(opts Opts) *MemberList { } func (m *MemberList) MemberListInit(ctx context.Context) { - cfg := memberlist.DefaultLocalConfig() + cfg := memberlist.DefaultLANConfig() + cfg.RequireNodeNames = true + cfg.Name = m.options.Config.ServerID cfg.BindAddr = m.options.Config.BindAddr cfg.BindPort = int(m.options.Config.MemberListBindPort) cfg.Delegate = NewDelegate(DelegateOpts{ @@ -166,5 +168,5 @@ func (m *MemberList) MemberListShutdown() { log.Fatal("Could not gracefully shutdown memberlist background maintenance") } - fmt.Println("Successfully shutdown memberlist") + log.Println("Successfully shutdown memberlist") } diff --git a/internal/modules/acl/acl.go b/internal/modules/acl/acl.go index 5dc7d644..4cf20bdb 100644 --- a/internal/modules/acl/acl.go +++ b/internal/modules/acl/acl.go @@ -72,7 +72,7 @@ func NewACL(config config.Config) *ACL { } else { defer func() { if err := f.Close(); err != nil { - fmt.Println("acl config file close error: ", err) + log.Println("acl config file close error: ", err) } }() diff --git a/internal/modules/acl/commands_test.go b/internal/modules/acl/commands_test.go index 097ee1db..ec2a1d99 100644 --- a/internal/modules/acl/commands_test.go +++ b/internal/modules/acl/commands_test.go @@ -204,11 +204,20 @@ func Test_HandleAuth(t *testing.T) { if err != nil { t.Error(err) } + + for { + // Wait until connection is not nil before breaking out. + if conn != nil { + break + } + } + defer func() { if conn != nil { _ = conn.Close() } }() + r := resp.NewConn(conn) tests := []struct { @@ -423,6 +432,14 @@ func Test_HandleUsers(t *testing.T) { if err != nil { t.Error(err) } + + for { + // Wait until connection is not nil before continuing. + if conn != nil { + break + } + } + defer func() { if conn != nil { _ = conn.Close() diff --git a/internal/modules/generic/commands.go b/internal/modules/generic/commands.go index 9b443ea2..946ca75a 100644 --- a/internal/modules/generic/commands.go +++ b/internal/modules/generic/commands.go @@ -37,6 +37,7 @@ func handleSet(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] value := params.Command[2] res := []byte(constants.OkResponse) clock := params.GetClock() @@ -49,41 +50,28 @@ func handleSet(params internal.HandlerFuncParams) ([]byte, error) { // If Get is provided, the response should be the current stored value. // If there's no current value, then the response should be nil. if options.get { - if !params.KeyExists(params.Context, key) { + if !keyExists { res = []byte("$-1\r\n") } else { - res = []byte(fmt.Sprintf("+%v\r\n", params.GetValue(params.Context, key))) + res = []byte(fmt.Sprintf("+%v\r\n", params.GetValues(params.Context, []string{key})[key])) } } if "xx" == strings.ToLower(options.exists) { // If XX is specified, make sure the key exists. - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, fmt.Errorf("key %s does not exist", key) } - _, err = params.KeyLock(params.Context, key) } else if "nx" == strings.ToLower(options.exists) { // If NX is specified, make sure that the key does not currently exist. - if params.KeyExists(params.Context, key) { + if keyExists { return nil, fmt.Errorf("key %s already exists", key) } - _, err = params.CreateKeyAndLock(params.Context, key) - } else { - // Neither XX not NX are specified, lock or create the lock - if !params.KeyExists(params.Context, key) { - // Key does not exist, create it - _, err = params.CreateKeyAndLock(params.Context, key) - } else { - // Key exists, acquire the lock - _, err = params.KeyLock(params.Context, key) - } - } - if err != nil { - return nil, err } - defer params.KeyUnlock(params.Context, key) - if err = params.SetValue(params.Context, key, internal.AdaptType(value)); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{ + key: internal.AdaptType(value), + }); err != nil { return nil, err } @@ -101,52 +89,18 @@ func handleMSet(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - entries := make(map[string]KeyObject) - - // Release all acquired key locks - defer func() { - for k, v := range entries { - if v.locked { - params.KeyUnlock(params.Context, k) - entries[k] = KeyObject{ - value: v.value, - locked: false, - } - } - } - }() + entries := make(map[string]interface{}) // Extract all the key/value pairs for i, key := range params.Command[1:] { if i%2 == 0 { - entries[key] = KeyObject{ - value: internal.AdaptType(params.Command[1:][i+1]), - locked: false, - } + entries[key] = internal.AdaptType(params.Command[1:][i+1]) } } - // Acquire all the locks for each key first - // If any key cannot be acquired, abandon transaction and release all currently held keys - for k, v := range entries { - if params.KeyExists(params.Context, k) { - if _, err := params.KeyLock(params.Context, k); err != nil { - return nil, err - } - entries[k] = KeyObject{value: v.value, locked: true} - continue - } - if _, err := params.CreateKeyAndLock(params.Context, k); err != nil { - return nil, err - } - entries[k] = KeyObject{value: v.value, locked: true} - } - // Set all the values - for k, v := range entries { - if err := params.SetValue(params.Context, k, v.value); err != nil { - return nil, err - } + if err = params.SetValues(params.Context, entries); err != nil { + return nil, err } return []byte(constants.OkResponse), nil @@ -158,18 +112,13 @@ func handleGet(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } key := keys.ReadKeys[0] + keyExists := params.KeysExist([]string{key})[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - _, err = params.KeyRLock(params.Context, key) - if err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - value := params.GetValue(params.Context, key) + value := params.GetValues(params.Context, []string{key})[key] return []byte(fmt.Sprintf("+%v\r\n", value)), nil } @@ -181,34 +130,12 @@ func handleMGet(params internal.HandlerFuncParams) ([]byte, error) { } values := make(map[string]string) - - locks := make(map[string]bool) - for _, key := range keys.ReadKeys { - if _, ok := values[key]; ok { - // Skip if we have already locked this key + for key, value := range params.GetValues(params.Context, keys.ReadKeys) { + if value == nil { + values[key] = "" continue } - if params.KeyExists(params.Context, key) { - _, err = params.KeyRLock(params.Context, key) - if err != nil { - return nil, fmt.Errorf("could not obtain lock for %s key", key) - } - locks[key] = true - continue - } - values[key] = "" - } - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - locks[key] = false - } - } - }() - - for key, _ := range locks { - values[key] = fmt.Sprintf("%v", params.GetValue(params.Context, key)) + values[key] = fmt.Sprintf("%v", value) } bytes := []byte(fmt.Sprintf("*%d\r\n", len(params.Command[1:]))) @@ -230,8 +157,11 @@ func handleDel(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } count := 0 - for _, key := range keys.WriteKeys { - err = params.DeleteKey(params.Context, key) + for key, exists := range params.KeysExist(keys.WriteKeys) { + if !exists { + continue + } + err = params.DeleteKey(key) if err != nil { log.Printf("could not delete key %s due to error: %+v\n", key, err) continue @@ -248,17 +178,13 @@ func handlePersist(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - expireAt := params.GetExpiry(params.Context, key) + expireAt := params.GetExpiry(key) if expireAt == (time.Time{}) { return []byte(":0\r\n"), nil } @@ -275,17 +201,13 @@ func handleExpireTime(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":-2\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - expireAt := params.GetExpiry(params.Context, key) + expireAt := params.GetExpiry(key) if expireAt == (time.Time{}) { return []byte(":-1\r\n"), nil @@ -306,19 +228,15 @@ func handleTTL(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] clock := params.GetClock() - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":-2\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - expireAt := params.GetExpiry(params.Context, key) + expireAt := params.GetExpiry(key) if expireAt == (time.Time{}) { return []byte(":-1\r\n"), nil @@ -343,6 +261,7 @@ func handleExpire(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] // Extract time n, err := strconv.ParseInt(params.Command[2], 10, 64) @@ -354,21 +273,16 @@ func handleExpire(params internal.HandlerFuncParams) ([]byte, error) { expireAt = params.GetClock().Now().Add(time.Duration(n) * time.Millisecond) } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return []byte(":0\r\n"), err - } - defer params.KeyUnlock(params.Context, key) - if len(params.Command) == 3 { params.SetExpiry(params.Context, key, expireAt, true) return []byte(":1\r\n"), nil } - currentExpireAt := params.GetExpiry(params.Context, key) + currentExpireAt := params.GetExpiry(key) switch strings.ToLower(params.Command[3]) { case "nx": @@ -411,6 +325,7 @@ func handleExpireAt(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] // Extract time n, err := strconv.ParseInt(params.Command[2], 10, 64) @@ -422,21 +337,16 @@ func handleExpireAt(params internal.HandlerFuncParams) ([]byte, error) { expireAt = time.UnixMilli(n) } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - if len(params.Command) == 3 { params.SetExpiry(params.Context, key, expireAt, true) return []byte(":1\r\n"), nil } - currentExpireAt := params.GetExpiry(params.Context, key) + currentExpireAt := params.GetExpiry(key) switch strings.ToLower(params.Command[3]) { case "nx": diff --git a/internal/modules/generic/commands_test.go b/internal/modules/generic/commands_test.go index 25de089f..84f8c8ae 100644 --- a/internal/modules/generic/commands_test.go +++ b/internal/modules/generic/commands_test.go @@ -15,8 +15,6 @@ package generic_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -26,15 +24,15 @@ import ( "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" "net" - "reflect" "strings" + "sync" "testing" "time" - "unsafe" ) +var addr string +var port int var mockServer *echovault.EchoVault - var mockClock clock.Clock type KeyData struct { @@ -44,65 +42,31 @@ type KeyData struct { func init() { mockClock = clock.NewClock() - + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } - } - return nil -} - -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - getClock := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getClock")).(func() clock.Clock) - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, - GetExpiry: mockServer.GetExpiry, - SetExpiry: mockServer.SetExpiry, - DeleteKey: mockServer.DeleteKey, - GetClock: getClock, +func Test_HandleSET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) } -} + client := resp.NewConn(conn) -func Test_HandleSET(t *testing.T) { tests := []struct { name string command []string @@ -126,7 +90,7 @@ func Test_HandleSET(t *testing.T) { command: []string{"SET", "SetKey2", "1245678910"}, presetValues: nil, expectedResponse: "OK", - expectedValue: 1245678910, + expectedValue: "1245678910", expectedExpiry: time.Time{}, expectedErr: nil, }, @@ -135,7 +99,7 @@ func Test_HandleSET(t *testing.T) { command: []string{"SET", "SetKey3", "45782.11341"}, presetValues: nil, expectedResponse: "OK", - expectedValue: 45782.11341, + expectedValue: "45782.11341", expectedExpiry: time.Time{}, expectedErr: nil, }, @@ -409,35 +373,41 @@ func Test_HandleSET(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SET, %d", i+1)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + cmd := []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(k), + resp.StringValue(v.Value.(string))} + err := client.WriteArray(cmd) + if err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + rd, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(rd.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", rd.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for j, c := range test.command { + command[j] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + res, _, err := client.ReadValue() + if test.expectedErr != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedErr.Error()) - } - if test.expectedErr.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), err.Error()) } return @@ -446,48 +416,57 @@ func Test_HandleSET(t *testing.T) { t.Error(err) } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - switch test.expectedResponse.(type) { case string: - if test.expectedResponse != rv.String() { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, rv.String()) + if test.expectedResponse != res.String() { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } case nil: - if !rv.IsNull() { - t.Errorf("expcted nil response, got %+v", rv) + if !res.IsNull() { + t.Errorf("expcted nil response, got %+v", res) } default: t.Error("test expected result should be nil or string") } - // Compare expected value and expected time key := test.command[1] - var value interface{} - var expireAt time.Time - if _, err = mockServer.KeyLock(ctx, key); err != nil { + // Compare expected value to response value + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } - value = mockServer.GetValue(ctx, key) - expireAt = mockServer.GetExpiry(ctx, key) - mockServer.KeyUnlock(ctx, key) - - if value != test.expectedValue { - t.Errorf("expected value %+v, got %+v", test.expectedValue, value) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.String() != test.expectedValue.(string) { + t.Errorf("expected value %s, got %s", test.expectedValue.(string), res.String()) } - if test.expectedExpiry.Unix() != expireAt.Unix() { - t.Errorf("expected expiry time %d, got %d, cmd: %+v", test.expectedExpiry.Unix(), expireAt.Unix(), test.command) + + // Compare expected expiry to response expiry + if !test.expectedExpiry.Equal(time.Time{}) { + if err = client.WriteArray([]resp.Value{resp.StringValue("EXPIRETIME"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.Integer() != int(test.expectedExpiry.Unix()) { + t.Errorf("expected expiry time %d, got %d", test.expectedExpiry.Unix(), res.Integer()) + } } }) } } func Test_HandleMSET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -511,73 +490,70 @@ func Test_HandleMSET(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("MSET, %d", i)) + command := make([]resp.Value, len(test.command)) + for j, c := range test.command { + command[j] = resp.StringValue(c) + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedErr != nil { - if err.Error() != test.expectedErr.Error() { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { t.Errorf("expected error %s, got %s", test.expectedErr.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected response %s, got %s", test.expectedResponse, rv.String()) + + if res.String() != test.expectedResponse { + t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) } + for key, expectedValue := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { + // Get value from server + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } + res, _, err = client.ReadValue() switch expectedValue.(type) { default: t.Error("unexpected type for expectedValue") case int: ev, _ := expectedValue.(int) - value, ok := mockServer.GetValue(ctx, key).(int) - if !ok { - t.Errorf("expected integer type for key %s, got another type", key) - } - if value != ev { - t.Errorf("expected value %d for key %s, got %d", ev, key, value) + if res.Integer() != ev { + t.Errorf("expected value %d for key %s, got %d", ev, key, res.Integer()) } case float64: ev, _ := expectedValue.(float64) - value, ok := mockServer.GetValue(ctx, key).(float64) - if !ok { - t.Errorf("expected float type for key %s, got another type", key) - } - if value != ev { - t.Errorf("expected value %f for key %s, got %f", ev, key, value) + if res.Float() != ev { + t.Errorf("expected value %f for key %s, got %f", ev, key, res.Float()) } case string: ev, _ := expectedValue.(string) - value, ok := mockServer.GetValue(ctx, key).(string) - if !ok { - t.Errorf("expected string type for key %s, got another type", key) - } - if value != ev { - t.Errorf("expected value %s for key %s, got %s", ev, key, value) + if res.String() != ev { + t.Errorf("expected value %s for key %s, got %s", ev, key, res.String()) } } - mockServer.KeyRUnlock(ctx, key) } }) } } func Test_HandleGET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string key string @@ -600,45 +576,50 @@ func Test_HandleGET(t *testing.T) { }, } // Test successful Get command - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("GET, %d", i)) func(key, value string) { - - _, err := mockServer.CreateKeyAndLock(ctx, key) + // Preset the values + err = client.WriteArray([]resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}) if err != nil { t.Error(err) } - if err = mockServer.SetValue(ctx, key, value); err != nil { + + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) - handler := getHandler("GET") - if handler == nil { - t.Error("no handler found for command GET") - return + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", res.String()) } - res, err := handler(getHandlerFuncParams(ctx, []string{"GET", key}, nil)) + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !bytes.Equal(res, []byte(fmt.Sprintf("+%v\r\n", value))) { - t.Errorf("expected %s, got: %s", fmt.Sprintf("+%v\r\n", value), string(res)) + + if res.String() != test.value { + t.Errorf("expected value %s, got %s", test.value, res.String()) } }(test.key, test.value) }) } // Test get non-existent key - res, err := getHandler("GET")(getHandlerFuncParams(context.Background(), []string{"GET", "test4"}, nil)) + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue("test4")}); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if !bytes.Equal(res, []byte("$-1\r\n")) { - t.Errorf("expected %+v, got: %+v", "+nil\r\n", res) + if !res.IsNull() { + t.Errorf("expected nil, got: %+v", res) } errorTests := []struct { @@ -659,16 +640,21 @@ func Test_HandleGET(t *testing.T) { } for _, test := range errorTests { t.Run(test.name, func(t *testing.T) { - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err = handler(getHandlerFuncParams(context.Background(), test.command, nil)) - if res != nil { - t.Errorf("expected nil response, got: %+v", res) + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - if err.Error() != test.expected { + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.Contains(res.Error().Error(), test.expected) { t.Errorf("expected error '%s', got: %s", test.expected, err.Error()) } }) @@ -676,6 +662,12 @@ func Test_HandleGET(t *testing.T) { } func Test_HandleMGET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string presetKeys []string @@ -710,47 +702,53 @@ func Test_HandleMGET(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("MGET, %d", i)) // Set up the values for i, key := range test.presetKeys { - _, err := mockServer.CreateKeyAndLock(ctx, key) - if err != nil { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(test.presetValues[i]), + }); err != nil { t.Error(err) } - if err = mockServer.SetValue(ctx, key, test.presetValues[i]); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got \"%s\"", res.String()) + } } + // Test the command and its results - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { // If we expect and error, branch out and check error - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error %+v, got: %+v", test.expectedError, err) } return } - if err != nil { - t.Error(err) - } - rr := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rr.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Type().String() != "Array" { - t.Errorf("expected type Array, got: %s", rv.Type().String()) + + if res.Type().String() != "Array" { + t.Errorf("expected type Array, got: %s", res.Type().String()) } - for i, value := range rv.Array() { + for i, value := range res.Array() { if test.expected[i] == nil { if !value.IsNull() { t.Errorf("expected nil value, got %+v", value) @@ -766,10 +764,16 @@ func Test_HandleMGET(t *testing.T) { } func Test_HandleDEL(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string - presetValues map[string]KeyData + presetValues map[string]string expectedResponse int expectToExist map[string]bool expectedErr error @@ -777,11 +781,11 @@ func Test_HandleDEL(t *testing.T) { { name: "1. Delete multiple keys", command: []string{"DEL", "DelKey1", "DelKey2", "DelKey3", "DelKey4", "DelKey5"}, - presetValues: map[string]KeyData{ - "DelKey1": {Value: "value1", ExpireAt: time.Time{}}, - "DelKey2": {Value: "value2", ExpireAt: time.Time{}}, - "DelKey3": {Value: "value3", ExpireAt: time.Time{}}, - "DelKey4": {Value: "value4", ExpireAt: time.Time{}}, + presetValues: map[string]string{ + "DelKey1": "value1", + "DelKey2": "value2", + "DelKey3": "value3", + "DelKey4": "value4", }, expectedResponse: 4, expectToExist: map[string]bool{ @@ -803,57 +807,63 @@ func Test_HandleDEL(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("DEL, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(k), + resp.StringValue(v), + }); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedErr != nil { - if err == nil { - t.Errorf("exected error \"%s\", got nil", test.expectedErr.Error()) - } - if test.expectedErr.Error() != err.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), err.Error()) - } - return - } - if err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if test.expectedErr != nil { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - for k, expected := range test.expectToExist { - exists := mockServer.KeyExists(ctx, k) + for key, expected := range test.expectToExist { + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + exists := !res.IsNull() if exists != expected { - t.Errorf("expected exists status to be %+v, got %+v", expected, exists) + t.Errorf("expected existence of key %s to be %v, got %v", key, expected, exists) } } }) @@ -861,6 +871,12 @@ func Test_HandleDEL(t *testing.T) { } func Test_HandlePERSIST(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -919,76 +935,100 @@ func Test_HandlePERSIST(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("PERSIST, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedError.Error()) - } - if test.expectedError.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } if test.expectedValues == nil { return } - for k, expected := range test.expectedValues { - if _, err = mockServer.KeyLock(ctx, k); err != nil { + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } - value := mockServer.GetValue(ctx, k) - expiry := mockServer.GetExpiry(ctx, k) - if value != expected.Value { - t.Errorf("expected value %+v, got %+v", expected.Value, value) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if expiry.UnixMilli() != expected.ExpireAt.UnixMilli() { - t.Errorf("expected exiry %d, got %d", expected.ExpireAt.UnixMilli(), expiry.UnixMilli()) + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + } + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue + } + if res.Integer() != int(expected.ExpireAt.UnixMilli()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.UnixMilli(), res.Integer()) } - mockServer.KeyUnlock(ctx, k) } }) } } func Test_HandleEXPIRETIME(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -1046,57 +1086,65 @@ func Test_HandleEXPIRETIME(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("EXPIRETIME/PEXPIRETIME, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedError.Error()) - } - if test.expectedError.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } }) } } func Test_HandleTTL(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -1154,57 +1202,65 @@ func Test_HandleTTL(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("TTL/PTTL, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedError.Error()) - } - if test.expectedError.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } }) } } func Test_HandleEXPIRE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -1393,76 +1449,100 @@ func Test_HandleEXPIRE(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("PERSIST, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedError.Error()) - } - if test.expectedError.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } if test.expectedValues == nil { return } - for k, expected := range test.expectedValues { - if _, err = mockServer.KeyLock(ctx, k); err != nil { + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + } + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { t.Error(err) } - value := mockServer.GetValue(ctx, k) - expiry := mockServer.GetExpiry(ctx, k) - if value != expected.Value { - t.Errorf("expected value %+v, got %+v", expected.Value, value) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue } - if expiry.UnixMilli() != expected.ExpireAt.UnixMilli() { - t.Errorf("expected expiry %d, got %d", expected.ExpireAt.UnixMilli(), expiry.UnixMilli()) + if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) } - mockServer.KeyUnlock(ctx, k) } }) } } func Test_HandleEXPIREAT(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string command []string @@ -1675,70 +1755,88 @@ func Test_HandleEXPIREAT(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("PERSIST, %d", i)) - if test.presetValues != nil { for k, v := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, k); err != nil { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, k, v.Value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.SetExpiry(ctx, k, v.ExpireAt, false) - mockServer.KeyUnlock(ctx, k) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err == nil { - t.Errorf("expected error \"%s\", got nil", test.expectedError.Error()) - } - if test.expectedError.Error() != err.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } if test.expectedValues == nil { return } - for k, expected := range test.expectedValues { - if _, err = mockServer.KeyLock(ctx, k); err != nil { + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { t.Error(err) } - value := mockServer.GetValue(ctx, k) - expiry := mockServer.GetExpiry(ctx, k) - if value != expected.Value { - t.Errorf("expected value %+v, got %+v", expected.Value, value) + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + } + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue } - if expiry.UnixMilli() != expected.ExpireAt.UnixMilli() { - t.Errorf("expected expiry %d, got %d", expected.ExpireAt.UnixMilli(), expiry.UnixMilli()) + if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) } - mockServer.KeyUnlock(ctx, k) } }) } diff --git a/internal/modules/hash/commands.go b/internal/modules/hash/commands.go index 63741c04..bb93f7e9 100644 --- a/internal/modules/hash/commands.go +++ b/internal/modules/hash/commands.go @@ -32,6 +32,7 @@ func handleHSET(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] entries := make(map[string]interface{}) if len(params.Command[2:])%2 != 0 { @@ -42,26 +43,19 @@ func handleHSET(params internal.HandlerFuncParams) ([]byte, error) { entries[params.Command[i]] = internal.AdaptType(params.Command[i+1]) } - if !params.KeyExists(params.Context, key) { - _, err = params.CreateKeyAndLock(params.Context, key) + if !keyExists { if err != nil { return nil, err } - defer params.KeyUnlock(params.Context, key) - if err = params.SetValue(params.Context, key, entries); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: entries}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", len(entries))), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { - return nil, fmt.Errorf("value at %s is not a hash", key) + hash = make(map[string]interface{}) } count := 0 @@ -76,7 +70,7 @@ func handleHSET(params internal.HandlerFuncParams) ([]byte, error) { hash[field] = value count += 1 } - if err = params.SetValue(params.Context, key, hash); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: hash}); err != nil { return nil, err } @@ -90,18 +84,14 @@ func handleHGET(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] fields := params.Command[2:] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -141,18 +131,14 @@ func handleHSTRLEN(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] fields := params.Command[2:] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -192,17 +178,13 @@ func handleHVALS(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -233,6 +215,7 @@ func handleHRANDFIELD(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] count := 1 if len(params.Command) >= 3 { @@ -255,16 +238,11 @@ func handleHRANDFIELD(params internal.HandlerFuncParams) ([]byte, error) { } } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -349,17 +327,13 @@ func handleHLEN(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -374,17 +348,13 @@ func handleHKEYS(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -404,6 +374,7 @@ func handleHINCRBY(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] field := params.Command[2] var intIncrement int @@ -423,33 +394,24 @@ func handleHINCRBY(params internal.HandlerFuncParams) ([]byte, error) { intIncrement = i } - if !params.KeyExists(params.Context, key) { - if _, err := params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) + if !keyExists { hash := make(map[string]interface{}) if strings.EqualFold(params.Command[0], "hincrbyfloat") { hash[field] = floatIncrement - if err = params.SetValue(params.Context, key, hash); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: hash}); err != nil { return nil, err } return []byte(fmt.Sprintf("+%s\r\n", strconv.FormatFloat(floatIncrement, 'f', -1, 64))), nil } else { hash[field] = intIncrement - if err = params.SetValue(params.Context, key, hash); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: hash}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", intIncrement)), nil } } - if _, err := params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -477,7 +439,7 @@ func handleHINCRBY(params internal.HandlerFuncParams) ([]byte, error) { } } - if err = params.SetValue(params.Context, key, hash); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: hash}); err != nil { return nil, err } @@ -496,17 +458,13 @@ func handleHGETALL(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -536,18 +494,14 @@ func handleHEXISTS(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] field := params.Command[2] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -566,18 +520,14 @@ func handleHDEL(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] fields := params.Command[2:] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - hash, ok := params.GetValue(params.Context, key).(map[string]interface{}) + hash, ok := params.GetValues(params.Context, []string{key})[key].(map[string]interface{}) if !ok { return nil, fmt.Errorf("value at %s is not a hash", key) } @@ -591,7 +541,7 @@ func handleHDEL(params internal.HandlerFuncParams) ([]byte, error) { } } - if err = params.SetValue(params.Context, key, hash); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: hash}); err != nil { return nil, err } diff --git a/internal/modules/hash/commands_test.go b/internal/modules/hash/commands_test.go index 0a77ca2e..09ebc707 100644 --- a/internal/modules/hash/commands_test.go +++ b/internal/modules/hash/commands_test.go @@ -15,8 +15,6 @@ package hash_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -25,206 +23,209 @@ import ( "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" "net" - "reflect" "slices" + "strconv" "strings" + "sync" "testing" - "unsafe" ) var mockServer *echovault.EchoVault +var addr = "localhost" +var port int func init() { + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } - } - return nil -} - -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, +func Test_HandleHSET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) } -} + client := resp.NewConn(conn) -func Test_HandleHSET(t *testing.T) { // Tests for both HSet and HSetNX tests := []struct { name string - preset bool key string presetValue interface{} command []string expectedResponse int // Change count - expectedValue map[string]interface{} + expectedValue map[string]string expectedError error }{ { name: "1. HSETNX set field on non-existent hash map", - preset: false, key: "HsetKey1", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HSETNX", "HsetKey1", "field1", "value1"}, expectedResponse: 1, - expectedValue: map[string]interface{}{"field1": "value1"}, + expectedValue: map[string]string{"field1": "value1"}, expectedError: nil, }, { name: "2. HSETNX set field on existing hash map", - preset: true, key: "HsetKey2", - presetValue: map[string]interface{}{"field1": "value1"}, + presetValue: map[string]string{"field1": "value1"}, command: []string{"HSETNX", "HsetKey2", "field2", "value2"}, expectedResponse: 1, - expectedValue: map[string]interface{}{"field1": "value1", "field2": "value2"}, + expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, expectedError: nil, }, { name: "3. HSETNX skips operation when setting on existing field", - preset: true, key: "HsetKey3", - presetValue: map[string]interface{}{"field1": "value1"}, + presetValue: map[string]string{"field1": "value1"}, command: []string{"HSETNX", "HsetKey3", "field1", "value1-new"}, expectedResponse: 0, - expectedValue: map[string]interface{}{"field1": "value1"}, + expectedValue: map[string]string{"field1": "value1"}, expectedError: nil, }, { name: "4. Regular HSET command on non-existent hash map", - preset: false, key: "HsetKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HSET", "HsetKey4", "field1", "value1", "field2", "value2"}, expectedResponse: 2, - expectedValue: map[string]interface{}{"field1": "value1", "field2": "value2"}, + expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, expectedError: nil, }, { name: "5. Regular HSET update on existing hash map", - preset: true, key: "HsetKey5", - presetValue: map[string]interface{}{"field1": "value1", "field2": "value2"}, + presetValue: map[string]string{"field1": "value1", "field2": "value2"}, command: []string{"HSET", "HsetKey5", "field1", "value1-new", "field2", "value2-ne2", "field3", "value3"}, expectedResponse: 3, - expectedValue: map[string]interface{}{"field1": "value1-new", "field2": "value2-ne2", "field3": "value3"}, + expectedValue: map[string]string{"field1": "value1-new", "field2": "value2-ne2", "field3": "value3"}, expectedError: nil, }, { - name: "6. HSET returns error when the target key is not a map", - preset: true, + name: "6. HSET overwrites when the target key is not a map", key: "HsetKey6", presetValue: "Default preset value", command: []string{"HSET", "HsetKey6", "field1", "value1"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, - expectedError: errors.New("value at HsetKey6 is not a hash"), + expectedResponse: 1, + expectedValue: map[string]string{"field1": "value1"}, + expectedError: nil, }, { name: "7. HSET returns error when there's a mismatch in key/values", - preset: false, key: "HsetKey7", presetValue: nil, command: []string{"HSET", "HsetKey7", "field1", "value1", "field2"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedValue: map[string]string{}, expectedError: errors.New("each field must have a corresponding value"), }, { name: "8. Command too short", - preset: true, key: "HsetKey8", presetValue: nil, command: []string{"HSET", "field1"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedValue: map[string]string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HSET/HSETNX, %d", i)) - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } + // Check that all the values are what is expected - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { t.Error(err) } - h, ok := mockServer.GetValue(ctx, test.key).(map[string]interface{}) - if !ok { - t.Errorf("value at key \"%s\" is not a hash map", test.key) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - for field, value := range h { - if value != test.expectedValue[field] { - t.Errorf("expected value \"%+v\" for field \"%+v\", got \"%+v\"", test.expectedValue[field], field, value) + + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } }) @@ -232,174 +233,196 @@ func Test_HandleHSET(t *testing.T) { } func Test_HandleHINCRBY(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + // Tests for both HIncrBy and HIncrByFloat tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} // Change count - expectedValue map[string]interface{} + expectedResponse string // Change count + expectedValue map[string]string expectedError error }{ { name: "1. Increment by integer on non-existent hash should create a new one", - preset: false, key: "HincrbyKey1", presetValue: nil, command: []string{"HINCRBY", "HincrbyKey1", "field1", "1"}, - expectedResponse: 1, - expectedValue: map[string]interface{}{"field1": 1}, + expectedResponse: "1", + expectedValue: map[string]string{"field1": "1"}, expectedError: nil, }, { name: "2. Increment by float on non-existent hash should create one", - preset: false, key: "HincrbyKey2", presetValue: nil, command: []string{"HINCRBYFLOAT", "HincrbyKey2", "field1", "3.142"}, - expectedResponse: 3.142, - expectedValue: map[string]interface{}{"field1": 3.142}, + expectedResponse: "3.142", + expectedValue: map[string]string{"field1": "3.142"}, expectedError: nil, }, { name: "3. Increment by integer on existing hash", - preset: true, key: "HincrbyKey3", - presetValue: map[string]interface{}{"field1": 1}, + presetValue: map[string]string{"field1": "1"}, command: []string{"HINCRBY", "HincrbyKey3", "field1", "10"}, - expectedResponse: 11, - expectedValue: map[string]interface{}{"field1": 11}, + expectedResponse: "11", + expectedValue: map[string]string{"field1": "11"}, expectedError: nil, }, { name: "4. Increment by float on an existing hash", - preset: true, key: "HincrbyKey4", - presetValue: map[string]interface{}{"field1": 3.142}, + presetValue: map[string]string{"field1": "3.142"}, command: []string{"HINCRBYFLOAT", "HincrbyKey4", "field1", "3.142"}, - expectedResponse: 6.284, - expectedValue: map[string]interface{}{"field1": 6.284}, + expectedResponse: "6.284", + expectedValue: map[string]string{"field1": "6.284"}, expectedError: nil, }, { name: "5. Command too short", - preset: false, key: "HincrbyKey5", presetValue: nil, command: []string{"HINCRBY", "HincrbyKey5"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, key: "HincrbyKey6", presetValue: nil, command: []string{"HINCRBY", "HincrbyKey6", "field1", "23", "45"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "7. Error when increment by float does not pass valid float", - preset: false, key: "HincrbyKey7", presetValue: nil, command: []string{"HINCRBYFLOAT", "HincrbyKey7", "field1", "three point one four two"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New("increment must be a float"), }, { name: "8. Error when increment does not pass valid integer", - preset: false, key: "HincrbyKey8", presetValue: nil, command: []string{"HINCRBY", "HincrbyKey8", "field1", "three"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New("increment must be an integer"), }, { name: "9. Error when trying to increment on a key that is not a hash", - preset: true, key: "HincrbyKey9", presetValue: "Default value", command: []string{"HINCRBY", "HincrbyKey9", "field1", "3"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New("value at HincrbyKey9 is not a hash"), }, { name: "10. Error when trying to increment a hash field that is not a number", - preset: true, key: "HincrbyKey10", - presetValue: map[string]interface{}{"field1": "value1"}, + presetValue: map[string]string{"field1": "value1"}, command: []string{"HINCRBY", "HincrbyKey10", "field1", "3"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: "0", + expectedValue: nil, expectedError: errors.New("value at field field1 is not a number"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HINCRBY, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - switch test.expectedResponse.(type) { - default: - t.Error("expectedResponse must be an integer or string") - case int: - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response \"%+v\", got \"%d\"", test.expectedResponse, rv.Integer()) - } - case float64: - if rv.Float() != test.expectedResponse { - t.Errorf("expected response \"%+v\", got \"%+v\"", test.expectedResponse, rv.Float()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } + return } + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + // Check that all the values are what is expected - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { t.Error(err) } - h, ok := mockServer.GetValue(ctx, test.key).(map[string]interface{}) - if !ok { - t.Errorf("value at key \"%s\" is not a hash map", test.key) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - for field, value := range h { - if value != test.expectedValue[field] { - t.Errorf("expected value \"%+v\" for field \"%+v\", got \"%+v\"", test.expectedValue[field], field, value) + + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } }) @@ -407,112 +430,149 @@ func Test_HandleHINCRBY(t *testing.T) { } func Test_HandleHGET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} // Change count - expectedValue map[string]interface{} + expectedResponse []string // Change count + expectedValue map[string]string expectedError error }{ { - name: "1. Return nil when attempting to get from non-existed key", - preset: true, + name: "1. Get values from existing hash.", key: "HgetKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 365, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, command: []string{"HGET", "HgetKey1", "field1", "field2", "field3", "field4"}, - expectedResponse: []interface{}{"value1", 365, "3.142", nil}, - expectedValue: map[string]interface{}{}, + expectedResponse: []string{"value1", "365", "3.142", ""}, + expectedValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, expectedError: nil, }, { name: "2. Return nil when attempting to get from non-existed key", - preset: false, key: "HgetKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HGET", "HgetKey2", "field1"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: nil, }, { name: "3. Error when trying to get from a value that is not a hash map", - preset: true, key: "HgetKey3", presetValue: "Default Value", command: []string{"HGET", "HgetKey3", "field1"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: nil, + expectedValue: nil, expectedError: errors.New("value at HgetKey3 is not a hash"), }, { name: "4. Command too short", - preset: false, key: "HgetKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HGET", "HgetKey4"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: nil, + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HINCRBY, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } + if test.expectedResponse == nil { - if !rv.IsNull() { - t.Errorf("expected nil response, got %+v", rv) + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) } return } - if expectedArr, ok := test.expectedResponse.([]interface{}); ok { - for i, v := range rv.Array() { - switch v.Type().String() { - default: - t.Error("unexpected type encountered") - case "Integer": - if v.Integer() != expectedArr[i] { - t.Errorf("expected \"%+v\", got \"%d\"", expectedArr[i], v.Integer()) - } - case "BulkString": - if len(v.String()) == 0 && expectedArr[i] == nil { - continue - } - if v.String() != expectedArr[i] { - t.Errorf("expected \"%+v\", got \"%s\"", expectedArr[i], v.String()) - } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } + + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) } } } @@ -521,102 +581,152 @@ func Test_HandleHGET(t *testing.T) { } func Test_HandleHSTRLEN(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} // Change count - expectedValue map[string]interface{} + expectedResponse []int // Change count + expectedValue map[string]string expectedError error }{ { // Return lengths of field values. // If the key does not exist, its length should be 0. name: "1. Return lengths of field values.", - preset: true, key: "HstrlenKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HSTRLEN", "HstrlenKey1", "field1", "field2", "field3", "field4"}, expectedResponse: []int{len("value1"), len("123456789"), len("3.142"), 0}, - expectedValue: map[string]interface{}{}, + expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, expectedError: nil, }, { name: "2. Nil response when trying to get HSTRLEN non-existent key", - preset: false, key: "HstrlenKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HSTRLEN", "HstrlenKey2", "field1"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HstrlenKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HSTRLEN", "HstrlenKey3"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: nil, + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Trying to get lengths on a non hash map returns error", - preset: true, key: "HstrlenKey4", presetValue: "Default value", command: []string{"HSTRLEN", "HstrlenKey4", "field1"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: nil, + expectedValue: nil, expectedError: errors.New("value at HstrlenKey4 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HSTRLEN, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } + if test.expectedResponse == nil { - if !rv.IsNull() { - t.Errorf("expected nil response, got %+v", rv) + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) } return } - expectedResponse, _ := test.expectedResponse.([]int) - for i, v := range rv.Array() { - if v.Integer() != expectedResponse[i] { - t.Errorf("expected \"%d\", got \"%d\"", expectedResponse[i], v.Integer()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.Integer()) { + t.Errorf("unexpected element \"%d\" in response", item.Integer()) + } + } + + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } }) @@ -624,127 +734,136 @@ func Test_HandleHSTRLEN(t *testing.T) { } func Test_HandleHVALS(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse []interface{} - expectedValue map[string]interface{} + expectedResponse []string + expectedValue map[string]string expectedError error }{ { name: "1. Return all the values from a hash", - preset: true, key: "HvalsKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HVALS", "HvalsKey1"}, - expectedResponse: []interface{}{"value1", 123456789, "3.142"}, - expectedValue: map[string]interface{}{}, + expectedResponse: []string{"value1", "123456789", "3.142"}, + expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, expectedError: nil, }, { name: "2. Empty array response when trying to get HSTRLEN non-existent key", - preset: false, key: "HvalsKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HVALS", "HvalsKey2"}, - expectedResponse: []interface{}{}, - expectedValue: map[string]interface{}{}, + expectedResponse: []string{}, + expectedValue: nil, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HvalsKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HVALS"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "HvalsKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HVALS", "HvalsKey4", "HvalsKey4"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non hash map returns error", - preset: true, key: "HvalsKey5", presetValue: "Default value", command: []string{"HVALS", "HvalsKey5"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: errors.New("value at HvalsKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HVALS, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) + } + return } - switch len(test.expectedResponse) { - case 0: - if len(rv.Array()) != 0 { - t.Errorf("expected empty array, got length \"%d\"", len(rv.Array())) - } - default: - for _, v := range rv.Array() { - switch v.Type().String() { - default: - t.Errorf("unexpected error type") - case "Integer": - // Value is an integer, check if it is contained in the expected response - if !slices.ContainsFunc(test.expectedResponse, func(e interface{}) bool { - expectedValue, ok := e.(int) - return ok && expectedValue == v.Integer() - }) { - t.Errorf("couldn't find response value \"%d\" in expected values", v.Integer()) - } - case "BulkString": - // Value is a string, check if it is contained in the expected response - if !slices.ContainsFunc(test.expectedResponse, func(e interface{}) bool { - expectedValue, ok := e.(string) - return ok && expectedValue == v.String() - }) { - t.Errorf("couldn't find response value \"%s\" in expected values", v.String()) - } - } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -752,70 +871,61 @@ func Test_HandleHVALS(t *testing.T) { } func Test_HandleHRANDFIELD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - withValues bool - expectedCount int expectedResponse []string expectedError error }{ { name: "1. Get a random field", - preset: true, key: "HrandfieldKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HRANDFIELD", "HrandfieldKey1"}, - withValues: false, - expectedCount: 1, expectedResponse: []string{"field1", "field2", "field3"}, expectedError: nil, }, { name: "2. Get a random field with a value", - preset: true, key: "HrandfieldKey2", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HRANDFIELD", "HrandfieldKey2", "1", "WITHVALUES"}, - withValues: true, - expectedCount: 2, expectedResponse: []string{"field1", "value1", "field2", "123456789", "field3", "3.142"}, expectedError: nil, }, { - name: "3. Get several random fields", - preset: true, - key: "HrandfieldKey3", - presetValue: map[string]interface{}{ + name: "3. Get several random fields", + key: "HrandfieldKey3", + presetValue: map[string]string{ "field1": "value1", - "field2": 123456789, - "field3": 3.142, + "field2": "123456789", + "field3": "3.142", "field4": "value4", "field5": "value5", }, command: []string{"HRANDFIELD", "HrandfieldKey3", "3"}, - withValues: false, - expectedCount: 3, expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, expectedError: nil, }, { - name: "4. Get several random fields with their corresponding values", - preset: true, - key: "HrandfieldKey4", - presetValue: map[string]interface{}{ + name: "4. Get several random fields with their corresponding values", + key: "HrandfieldKey4", + presetValue: map[string]string{ "field1": "value1", - "field2": 123456789, - "field3": 3.142, + "field2": "123456789", + "field3": "3.142", "field4": "value4", "field5": "value5", }, - command: []string{"HRANDFIELD", "HrandfieldKey4", "3", "WITHVALUES"}, - withValues: true, - expectedCount: 6, + command: []string{"HRANDFIELD", "HrandfieldKey4", "3", "WITHVALUES"}, expectedResponse: []string{ "field1", "value1", "field2", "123456789", "field3", "3.142", "field4", "value4", "field5", "value5", @@ -823,36 +933,30 @@ func Test_HandleHRANDFIELD(t *testing.T) { expectedError: nil, }, { - name: "5. Get the entire hash", - preset: true, - key: "HrandfieldKey5", - presetValue: map[string]interface{}{ + name: "5. Get the entire hash", + key: "HrandfieldKey5", + presetValue: map[string]string{ "field1": "value1", - "field2": 123456789, - "field3": 3.142, + "field2": "123456789", + "field3": "3.142", "field4": "value4", "field5": "value5", }, command: []string{"HRANDFIELD", "HrandfieldKey5", "5"}, - withValues: false, - expectedCount: 5, expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, expectedError: nil, }, { - name: "6. Get the entire hash with values", - preset: true, - key: "HrandfieldKey5", - presetValue: map[string]interface{}{ + name: "6. Get the entire hash with values", + key: "HrandfieldKey5", + presetValue: map[string]string{ "field1": "value1", - "field2": 123456789, - "field3": 3.142, + "field2": "123456789", + "field3": "3.142", "field4": "value4", "field5": "value5", }, - command: []string{"HRANDFIELD", "HrandfieldKey5", "5", "WITHVALUES"}, - withValues: true, - expectedCount: 10, + command: []string{"HRANDFIELD", "HrandfieldKey5", "5", "WITHVALUES"}, expectedResponse: []string{ "field1", "value1", "field2", "123456789", "field3", "3.142", "field4", "value4", "field5", "value5", @@ -861,23 +965,20 @@ func Test_HandleHRANDFIELD(t *testing.T) { }, { name: "7. Command too short", - preset: false, key: "HrandfieldKey10", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HRANDFIELD"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "8. Command too long", - preset: false, key: "HrandfieldKey11", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HRANDFIELD", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "9. Trying to get random field on a non hash map returns error", - preset: true, key: "HrandfieldKey12", presetValue: "Default value", command: []string{"HRANDFIELD", "HrandfieldKey12"}, @@ -885,7 +986,6 @@ func Test_HandleHRANDFIELD(t *testing.T) { }, { name: "10. Throw error when count provided is not an integer", - preset: true, key: "HrandfieldKey12", presetValue: "Default value", command: []string{"HRANDFIELD", "HrandfieldKey12", "COUNT"}, @@ -893,7 +993,6 @@ func Test_HandleHRANDFIELD(t *testing.T) { }, { name: "11. If fourth argument is provided, it must be \"WITHVALUES\"", - preset: true, key: "HrandfieldKey12", presetValue: "Default value", command: []string{"HRANDFIELD", "HrandfieldKey12", "10", "FLAG"}, @@ -901,67 +1000,74 @@ func Test_HandleHRANDFIELD(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HRANDFIELD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if len(rv.Array()) != test.expectedCount { - t.Errorf("expected response array of length \"%d\", got length \"%d\"", test.expectedCount, len(rv.Array())) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - switch test.withValues { - case false: - for _, v := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected string) bool { - return expected == v.String() - }) { - t.Errorf("could not find response element \"%s\" in expected response", v.String()) - } + + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) } - case true: - responseArray := rv.Array() - for i := 0; i < len(responseArray); i++ { - if i%2 == 0 { - field := responseArray[i].String() - value := responseArray[i+1].String() - - expectedFieldIndex := slices.Index(test.expectedResponse, field) - if expectedFieldIndex == -1 { - t.Errorf("could not find response value \"%s\" in expected values", field) - } - expectedValue := test.expectedResponse[expectedFieldIndex+1] - - if value != expectedValue { - t.Errorf("expected value \"%s\", got \"%s\"", expectedValue, value) - } - } + return + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -969,567 +1075,642 @@ func Test_HandleHRANDFIELD(t *testing.T) { } func Test_HandleHLEN(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} // Change count - expectedValue map[string]interface{} + expectedResponse int // Change count expectedError error }{ { name: "1. Return the correct length of the hash", - preset: true, key: "HlenKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HLEN", "HlenKey1"}, expectedResponse: 3, - expectedValue: map[string]interface{}{}, expectedError: nil, }, { name: "2. 0 response when trying to call HLEN on non-existent key", - preset: false, key: "HlenKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HLEN", "HlenKey2"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HlenKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HLEN"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, - key: "HlenKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HLEN", "HlenKey4", "HlenKey4"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non hash map returns error", - preset: true, key: "HlenKey5", presetValue: "Default value", command: []string{"HLEN", "HlenKey5"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, expectedError: errors.New("value at HlenKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HLEN, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expectedResponse, ok := test.expectedResponse.(int); ok { - if rv.Integer() != expectedResponse { - t.Errorf("expected ineger \"%d\", got \"%d\"", expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - t.Error("expected integer response, got another type") + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } }) } } func Test_HandleHKeys(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} // Change count - expectedValue map[string]interface{} + expectedResponse []string expectedError error }{ { name: "1. Return an array containing all the keys of the hash", - preset: true, key: "HkeysKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HKEYS", "HkeysKey1"}, expectedResponse: []string{"field1", "field2", "field3"}, - expectedValue: map[string]interface{}{}, expectedError: nil, }, { name: "2. Empty array response when trying to call HKEYS on non-existent key", - preset: false, key: "HkeysKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HKEYS", "HkeysKey2"}, expectedResponse: []string{}, - expectedValue: map[string]interface{}{}, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HkeysKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HKEYS"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "HkeysKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HKEYS", "HkeysKey4", "HkeysKey4"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "5. Trying to get lengths on a non hash map returns error", - preset: true, - key: "HkeysKey5", - presetValue: "Default value", - command: []string{"HKEYS", "HkeysKey5"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, - expectedError: errors.New("value at HkeysKey5 is not a hash"), + name: "5. Trying to get lengths on a non hash map returns error", + key: "HkeysKey5", + presetValue: "Default value", + command: []string{"HKEYS", "HkeysKey5"}, + expectedError: errors.New("value at HkeysKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HKEYS, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expectedResponse, ok := test.expectedResponse.([]string); ok { - if len(rv.Array()) != len(expectedResponse) { - t.Errorf("expected length \"%d\", got \"%d\"", len(expectedResponse), len(rv.Array())) - } - for _, field := range expectedResponse { - if !slices.ContainsFunc(rv.Array(), func(value resp.Value) bool { - return value.String() == field - }) { - t.Errorf("could not find expected to find key \"%s\" in response", field) - } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - t.Error("expected array response, got another type") + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected value \"%s\" in response", item.String()) + } + } }) } } func Test_HandleHGETALL(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse []string - expectedValue map[string]interface{} + expectedResponse map[string]string expectedError error }{ { name: "1. Return an array containing all the fields and values of the hash", - preset: true, key: "HGetAllKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HGETALL", "HGetAllKey1"}, - expectedResponse: []string{"field1", "value1", "field2", "123456789", "field3", "3.142"}, - expectedValue: map[string]interface{}{}, + expectedResponse: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, expectedError: nil, }, { name: "2. Empty array response when trying to call HGETALL on non-existent key", - preset: false, key: "HGetAllKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HGETALL", "HGetAllKey2"}, - expectedResponse: []string{}, - expectedValue: map[string]interface{}{}, + expectedResponse: nil, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HGetAllKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HGETALL"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "HGetAllKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HGETALL", "HGetAllKey4", "HGetAllKey4"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non hash map returns error", - preset: true, key: "HGetAllKey5", presetValue: "Default value", command: []string{"HGETALL", "HGetAllKey5"}, expectedResponse: nil, - expectedValue: map[string]interface{}{}, expectedError: errors.New("value at HGetAllKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HGETALL, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if len(rv.Array()) != len(test.expectedResponse) { - t.Errorf("expected length \"%d\", got \"%d\"", len(test.expectedResponse), len(rv.Array())) + + if test.expectedResponse == nil { + if len(res.Array()) != 0 { + t.Errorf("expected response to be empty array, got %+v", res) + } + return } - // In the response: - // The order of results is not guaranteed, - // However, each field in the array will be reliably followed by its corresponding value - responseArray := rv.Array() - for i := 0; i < len(responseArray); i++ { - if i%2 == 0 { - // We're on a field in the response - field := responseArray[i].String() - value := responseArray[i+1].String() - expectedFieldIndex := slices.Index(test.expectedResponse, field) - if expectedFieldIndex == -1 { - t.Errorf("received unexpected field \"%s\" in response", field) - } - expectedValue := test.expectedResponse[expectedFieldIndex+1] - if expectedValue != value { - t.Errorf("expected entry \"%s\", got \"%s\"", expectedValue, value) + for i, item := range res.Array() { + if i%2 == 0 { + field := item.String() + value := res.Array()[i+1].String() + if test.expectedResponse[field] != value { + t.Errorf("expected value at field \"%s\" to be \"%s\", got \"%s\"", field, test.expectedResponse[field], value) } } - } - return + }) } } func Test_HandleHEXISTS(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} - expectedValue map[string]interface{} + expectedResponse bool expectedError error }{ { name: "1. Return 1 if the field exists in the hash", - preset: true, key: "HexistsKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, command: []string{"HEXISTS", "HexistsKey1", "field1"}, - expectedResponse: 1, - expectedValue: map[string]interface{}{}, + expectedResponse: true, expectedError: nil, }, { name: "2. 0 response when trying to call HEXISTS on non-existent key", - preset: false, key: "HexistsKey2", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HEXISTS", "HexistsKey2", "field1"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: false, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "HexistsKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HEXISTS", "HexistsKey3"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedResponse: false, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "HexistsKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HEXISTS", "HexistsKey4", "field1", "field2"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedResponse: false, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non hash map returns error", - preset: true, key: "HexistsKey5", presetValue: "Default value", command: []string{"HEXISTS", "HexistsKey5", "field1"}, - expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedResponse: false, expectedError: errors.New("value at HexistsKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HEXISTS, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expectedResponse, ok := test.expectedResponse.(int); ok { - if rv.Integer() != expectedResponse { - t.Errorf("expected \"%d\", got \"%d\"", expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - t.Error("expected integer response, got another type") + + if res.Bool() != test.expectedResponse { + t.Errorf("expected response to be %v, got %v", test.expectedResponse, res.Bool()) + } }) } } func Test_HandleHDEL(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} - expectedValue map[string]interface{} + expectedResponse int + expectedValue map[string]string expectedError error }{ { name: "1. Return count of deleted fields in the specified hash", - preset: true, key: "HdelKey1", - presetValue: map[string]interface{}{"field1": "value1", "field2": 123456789, "field3": 3.142, "field7": "value7"}, + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142", "field7": "value7"}, command: []string{"HDEL", "HdelKey1", "field1", "field2", "field3", "field4", "field5", "field6"}, expectedResponse: 3, - expectedValue: map[string]interface{}{"field1": nil, "field2": nil, "field3": nil, "field7": "value1"}, + expectedValue: map[string]string{"field7": "value7"}, expectedError: nil, }, { name: "2. 0 response when passing delete fields that are non-existent on valid hash", - preset: true, key: "HdelKey2", - presetValue: map[string]interface{}{"field1": "value1", "field2": "value2", "field3": "value3"}, + presetValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, command: []string{"HDEL", "HdelKey2", "field4", "field5", "field6"}, expectedResponse: 0, - expectedValue: map[string]interface{}{"field1": "value1", "field2": "value2", "field3": "value3"}, + expectedValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, expectedError: nil, }, { name: "3. 0 response when trying to call HDEL on non-existent key", - preset: false, key: "HdelKey3", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HDEL", "HdelKey3", "field1"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: nil, }, { name: "4. Command too short", - preset: false, key: "HdelKey4", - presetValue: map[string]interface{}{}, + presetValue: nil, command: []string{"HDEL", "HdelKey4"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, + expectedResponse: 0, + expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non hash map returns error", - preset: true, key: "HdelKey5", presetValue: "Default value", command: []string{"HDEL", "HdelKey5", "field1"}, expectedResponse: 0, - expectedValue: map[string]interface{}{}, + expectedValue: nil, expectedError: errors.New("value at HdelKey5 is not a hash"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("HDEL, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expectedResponse, ok := test.expectedResponse.(int); ok { - if rv.Integer() != expectedResponse { - t.Errorf("expected \"%d\", got \"%d\"", expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - if h, ok := mockServer.GetValue(ctx, test.key).(map[string]interface{}); ok { - for field, value := range h { - if value != test.expectedValue[field] { - t.Errorf("expected value \"%+v\", got \"%+v\"", test.expectedValue[field], value) + + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) } } - return } - t.Error("expected hash value but got another type") }) } } diff --git a/internal/modules/list/commands.go b/internal/modules/list/commands.go index cea234c4..74b353ef 100644 --- a/internal/modules/list/commands.go +++ b/internal/modules/list/commands.go @@ -31,18 +31,14 @@ func handleLLen(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { // If key does not exist, return 0 return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - if list, ok := params.GetValue(params.Context, key).([]interface{}); ok { + if list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}); ok { return []byte(fmt.Sprintf(":%d\r\n", len(list))), nil } @@ -56,22 +52,18 @@ func handleLIndex(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] index, ok := internal.AdaptType(params.Command[2]).(int) if !ok { return nil, errors.New("index must be an integer") } - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, errors.New("LINDEX command on non-list item") } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - list, ok := params.GetValue(params.Context, key).([]interface{}) - params.KeyRUnlock(params.Context, key) - + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, errors.New("LINDEX command on non-list item") } @@ -90,6 +82,7 @@ func handleLRange(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] start, startOk := internal.AdaptType(params.Command[2]).(int) end, endOk := internal.AdaptType(params.Command[3]).(int) @@ -97,16 +90,11 @@ func handleLRange(params internal.HandlerFuncParams) ([]byte, error) { return nil, errors.New("start and end indices must be integers") } - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, errors.New("LRANGE command on non-list item") } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - list, ok := params.GetValue(params.Context, key).([]interface{}) + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, errors.New("LRANGE command on non-list item") } @@ -169,22 +157,18 @@ func handleLSet(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] index, ok := internal.AdaptType(params.Command[2]).(int) if !ok { return nil, errors.New("index must be an integer") } - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, errors.New("LSET command on non-list item") } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - list, ok := params.GetValue(params.Context, key).([]interface{}) + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, errors.New("LSET command on non-list item") } @@ -194,7 +178,7 @@ func handleLSet(params internal.HandlerFuncParams) ([]byte, error) { } list[index] = internal.AdaptType(params.Command[3]) - if err = params.SetValue(params.Context, key, list); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list}); err != nil { return nil, err } @@ -208,6 +192,7 @@ func handleLTrim(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] start, startOk := internal.AdaptType(params.Command[2]).(int) end, endOk := internal.AdaptType(params.Command[3]).(int) @@ -219,16 +204,11 @@ func handleLTrim(params internal.HandlerFuncParams) ([]byte, error) { return nil, errors.New("end index must be greater than start index or -1") } - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, errors.New("LTRIM command on non-list item") } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - list, ok := params.GetValue(params.Context, key).([]interface{}) + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, errors.New("LTRIM command on non-list item") } @@ -238,13 +218,13 @@ func handleLTrim(params internal.HandlerFuncParams) ([]byte, error) { } if end == -1 || end > len(list) { - if err = params.SetValue(params.Context, key, list[start:]); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list[start:]}); err != nil { return nil, err } return []byte(constants.OkResponse), nil } - if err = params.SetValue(params.Context, key, list[start:end]); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list[start:end]}); err != nil { return nil, err } return []byte(constants.OkResponse), nil @@ -257,6 +237,7 @@ func handleLRem(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] value := params.Command[3] count, ok := internal.AdaptType(params.Command[2]).(int) @@ -266,16 +247,11 @@ func handleLRem(params internal.HandlerFuncParams) ([]byte, error) { absoluteCount := internal.AbsInt(count) - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, errors.New("LREM command on non-list item") } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - list, ok := params.GetValue(params.Context, key).([]interface{}) + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, errors.New("LREM command on non-list item") } @@ -311,7 +287,7 @@ func handleLRem(params internal.HandlerFuncParams) ([]byte, error) { return elem == nil }) - if err = params.SetValue(params.Context, key, list); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list}); err != nil { return nil, err } @@ -324,6 +300,7 @@ func handleLMove(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keysExist := params.KeysExist(keys.WriteKeys) source, destination := keys.WriteKeys[0], keys.WriteKeys[1] whereFrom := strings.ToLower(params.Command[3]) whereTo := strings.ToLower(params.Command[4]) @@ -332,23 +309,13 @@ func handleLMove(params internal.HandlerFuncParams) ([]byte, error) { return nil, errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT") } - if !params.KeyExists(params.Context, source) || !params.KeyExists(params.Context, destination) { + if !keysExist[source] || !keysExist[destination] { return nil, errors.New("both source and destination must be lists") } - if _, err = params.KeyLock(params.Context, source); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, source) - - _, err = params.KeyLock(params.Context, destination) - if err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, destination) - - sourceList, sourceOk := params.GetValue(params.Context, source).([]interface{}) - destinationList, destinationOk := params.GetValue(params.Context, destination).([]interface{}) + lists := params.GetValues(params.Context, keys.WriteKeys) + sourceList, sourceOk := lists[source].([]interface{}) + destinationList, destinationOk := lists[destination].([]interface{}) if !sourceOk || !destinationOk { return nil, errors.New("both source and destination must be lists") @@ -356,19 +323,27 @@ func handleLMove(params internal.HandlerFuncParams) ([]byte, error) { switch whereFrom { case "left": - err = params.SetValue(params.Context, source, append([]interface{}{}, sourceList[1:]...)) - if whereTo == "left" { - err = params.SetValue(params.Context, destination, append(sourceList[0:1], destinationList...)) - } else if whereTo == "right" { - err = params.SetValue(params.Context, destination, append(destinationList, sourceList[0])) - } + err = params.SetValues(params.Context, map[string]interface{}{ + source: append([]interface{}{}, sourceList[1:]...), + destination: func() []interface{} { + if whereTo == "left" { + return append(sourceList[0:1], destinationList...) + } + // whereTo == "right" + return append(destinationList, sourceList[0]) + }(), + }) case "right": - err = params.SetValue(params.Context, source, append([]interface{}{}, sourceList[:len(sourceList)-1]...)) - if whereTo == "left" { - err = params.SetValue(params.Context, destination, append(sourceList[len(sourceList)-1:], destinationList...)) - } else if whereTo == "right" { - err = params.SetValue(params.Context, destination, append(destinationList, sourceList[len(sourceList)-1])) - } + err = params.SetValues(params.Context, map[string]interface{}{ + source: append([]interface{}{}, sourceList[:len(sourceList)-1]...), + destination: func() []interface{} { + if whereTo == "left" { + return append(sourceList[len(sourceList)-1:], destinationList...) + } + // whereTo == "right" + return append(destinationList, sourceList[len(sourceList)-1]) + }(), + }) } if err != nil { @@ -391,36 +366,29 @@ func handleLPush(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { switch strings.ToLower(params.Command[0]) { case "lpushx": - return nil, errors.New("LPUSHX command on non-list item") + return nil, errors.New("LPUSHX command on non-existent key") default: - if _, err = params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - if err = params.SetValue(params.Context, key, []interface{}{}); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: []interface{}{}}); err != nil { return nil, err } } - } else { - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } } - defer params.KeyUnlock(params.Context, key) - - currentList := params.GetValue(params.Context, key) + currentList := params.GetValues(params.Context, []string{key})[key] l, ok := currentList.([]interface{}) if !ok { return nil, errors.New("LPUSH command on non-list item") } - if err = params.SetValue(params.Context, key, append(newElems, l...)); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: append(newElems, l...)}); err != nil { return nil, err } + return []byte(fmt.Sprintf(":%d\r\n", len(l)+len(newElems))), nil } @@ -431,6 +399,7 @@ func handleRPush(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] var newElems []interface{} @@ -438,35 +407,24 @@ func handleRPush(params internal.HandlerFuncParams) ([]byte, error) { newElems = append(newElems, internal.AdaptType(elem)) } - if !params.KeyExists(params.Context, key) { + if !keyExists { switch strings.ToLower(params.Command[0]) { case "rpushx": - return nil, errors.New("RPUSHX command on non-list item") + return nil, errors.New("RPUSHX command on non-existent key") default: - if _, err = params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - if err = params.SetValue(params.Context, key, []interface{}{}); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: []interface{}{}}); err != nil { return nil, err } } - } else { - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) } - currentList := params.GetValue(params.Context, key) - + currentList := params.GetValues(params.Context, []string{key})[key] l, ok := currentList.([]interface{}) - if !ok { return nil, errors.New("RPUSH command on non-list item") } - if err = params.SetValue(params.Context, key, append(l, newElems...)); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: append(l, newElems...)}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", len(l)+len(newElems))), nil @@ -479,29 +437,25 @@ func handlePop(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, fmt.Errorf("%s command on non-list item", strings.ToUpper(params.Command[0])) } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - list, ok := params.GetValue(params.Context, key).([]interface{}) + list, ok := params.GetValues(params.Context, []string{key})[key].([]interface{}) if !ok { return nil, fmt.Errorf("%s command on non-list item", strings.ToUpper(params.Command[0])) } switch strings.ToLower(params.Command[0]) { default: - if err = params.SetValue(params.Context, key, list[1:]); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list[1:]}); err != nil { return nil, err } return []byte(fmt.Sprintf("+%v\r\n", list[0])), nil case "rpop": - if err = params.SetValue(params.Context, key, list[:len(list)-1]); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: list[:len(list)-1]}); err != nil { return nil, err } return []byte(fmt.Sprintf("+%v\r\n", list[len(list)-1])), nil diff --git a/internal/modules/list/commands_test.go b/internal/modules/list/commands_test.go index bf2b5a31..d8f7c402 100644 --- a/internal/modules/list/commands_test.go +++ b/internal/modules/list/commands_test.go @@ -15,8 +15,6 @@ package list_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -25,329 +23,327 @@ import ( "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" "net" - "reflect" + "slices" + "strconv" "strings" + "sync" "testing" - "unsafe" ) var mockServer *echovault.EchoVault +var addr = "localhost" +var port int func init() { + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } - } - return nil -} - -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, +func Test_HandleLLEN(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) } -} + client := resp.NewConn(conn) -func Test_HandleLLEN(t *testing.T) { tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} - expectedValue []interface{} + expectedResponse int expectedError error }{ { name: "1. If key exists and is a list, return the lists length", - preset: true, key: "LlenKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LLEN", "LlenKey1"}, expectedResponse: 4, - expectedValue: nil, expectedError: nil, }, { name: "2. If key does not exist, return 0", - preset: false, key: "LlenKey2", presetValue: nil, command: []string{"LLEN", "LlenKey2"}, expectedResponse: 0, - expectedValue: nil, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "LlenKey3", presetValue: nil, command: []string{"LLEN"}, expectedResponse: 0, - expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "LlenKey4", presetValue: nil, command: []string{"LLEN", "LlenKey4", "LlenKey4"}, expectedResponse: 0, - expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to get lengths on a non-list returns error", - preset: true, key: "LlenKey5", presetValue: "Default value", command: []string{"LLEN", "LlenKey5"}, expectedResponse: 0, - expectedValue: nil, expectedError: errors.New("LLEN command on non-list item"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LLEN, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected integer response \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response to be %d, got %d", test.expectedResponse, res.Integer()) } }) } } func Test_HandleLINDEX(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} - expectedValue []interface{} + expectedResponse string expectedError error }{ { name: "1. Return last element within range", - preset: true, key: "LindexKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LINDEX", "LindexKey1", "3"}, expectedResponse: "value4", - expectedValue: nil, expectedError: nil, }, { name: "2. Return first element within range", - preset: true, key: "LindexKey2", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LINDEX", "LindexKey1", "0"}, expectedResponse: "value1", - expectedValue: nil, expectedError: nil, }, { name: "3. Return middle element within range", - preset: true, key: "LindexKey3", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LINDEX", "LindexKey1", "1"}, expectedResponse: "value2", - expectedValue: nil, expectedError: nil, }, { name: "4. If key does not exist, return error", - preset: false, key: "LindexKey4", presetValue: nil, command: []string{"LINDEX", "LindexKey4", "0"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New("LINDEX command on non-list item"), }, { name: "5. Command too short", - preset: false, key: "LindexKey3", presetValue: nil, command: []string{"LINDEX", "LindexKey3"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New(constants.WrongArgsResponse), }, { name: " 6. Command too long", - preset: false, key: "LindexKey4", presetValue: nil, command: []string{"LINDEX", "LindexKey4", "0", "20"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New(constants.WrongArgsResponse), }, { name: "7. Trying to get element by index on a non-list returns error", - preset: true, key: "LindexKey5", presetValue: "Default value", command: []string{"LINDEX", "LindexKey5", "0"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New("LINDEX command on non-list item"), }, { name: "8. Trying to get index out of range index beyond last index", - preset: true, key: "LindexKey6", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LINDEX", "LindexKey6", "3"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New("index must be within list range"), }, { name: "9. Trying to get index out of range with negative index", - preset: true, key: "LindexKey7", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LINDEX", "LindexKey7", "-1"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New("index must be within list range"), }, { name: " 10. Return error when index is not an integer", - preset: false, key: "LindexKey8", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LINDEX", "LindexKey8", "index"}, - expectedResponse: 0, - expectedValue: nil, + expectedResponse: "", expectedError: errors.New("index must be an integer"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LINDEX, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, rv.String()) + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } }) } } func Test_HandleLRANGE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse []interface{} - expectedValue []interface{} + expectedResponse []string expectedError error }{ { @@ -355,165 +351,164 @@ func Test_HandleLRANGE(t *testing.T) { // Both start and end indices are positive. // End index is greater than start index. name: "1. Return sub-list within range.", - preset: true, key: "LrangeKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, command: []string{"LRANGE", "LrangeKey1", "3", "6"}, - expectedResponse: []interface{}{"value4", "value5", "value6", "value7"}, - expectedValue: nil, + expectedResponse: []string{"value4", "value5", "value6", "value7"}, expectedError: nil, }, { name: "2. Return sub-list from start index to the end of the list when end index is -1", - preset: true, key: "LrangeKey2", - presetValue: []interface{}{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, command: []string{"LRANGE", "LrangeKey2", "3", "-1"}, - expectedResponse: []interface{}{"value4", "value5", "value6", "value7", "value8"}, - expectedValue: nil, + expectedResponse: []string{"value4", "value5", "value6", "value7", "value8"}, expectedError: nil, }, { name: "3. Return the reversed sub-list when the end index is greater than -1 but less than start index", - preset: true, key: "LrangeKey3", - presetValue: []interface{}{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, command: []string{"LRANGE", "LrangeKey3", "3", "0"}, - expectedResponse: []interface{}{"value4", "value3", "value2", "value1"}, - expectedValue: nil, + expectedResponse: []string{"value4", "value3", "value2", "value1"}, expectedError: nil, }, { name: "4. If key does not exist, return error", - preset: false, key: "LrangeKey4", presetValue: nil, command: []string{"LRANGE", "LrangeKey4", "0", "2"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("LRANGE command on non-list item"), }, { name: "5. Command too short", - preset: false, key: "LrangeKey5", presetValue: nil, command: []string{"LRANGE", "LrangeKey5"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, key: "LrangeKey6", presetValue: nil, command: []string{"LRANGE", "LrangeKey6", "0", "element", "element"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "7. Error when executing command on non-list command", - preset: true, key: "LrangeKey5", presetValue: "Default value", command: []string{"LRANGE", "LrangeKey5", "0", "3"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("LRANGE command on non-list item"), }, { name: "8. Error when start index is less than 0", - preset: true, key: "LrangeKey7", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LRANGE", "LrangeKey7", "-1", "3"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("start index must be within list boundary"), }, { name: "9. Error when start index is higher than the length of the list", - preset: true, key: "LrangeKey8", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LRANGE", "LrangeKey8", "10", "11"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("start index must be within list boundary"), }, { name: "10. Return error when start index is not an integer", - preset: false, key: "LrangeKey9", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LRANGE", "LrangeKey9", "start", "7"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("start and end indices must be integers"), }, { name: "11. Return error when end index is not an integer", - preset: false, key: "LrangeKey10", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LRANGE", "LrangeKey10", "0", "end"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("start and end indices must be integers"), }, { name: "12. Error when start and end indices are equal", - preset: true, key: "LrangeKey11", - presetValue: []interface{}{"value1", "value2", "value3"}, + presetValue: []string{"value1", "value2", "value3"}, command: []string{"LRANGE", "LrangeKey11", "1", "1"}, expectedResponse: nil, - expectedValue: nil, expectedError: errors.New("start and end indices cannot be equal"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LRANGE, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - responseArray := rv.Array() - if len(responseArray) != len(test.expectedResponse) { - t.Errorf("expected response of length \"%d\", got \"%d\"", len(test.expectedResponse), len(responseArray)) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response of length %d, got length %d", len(test.expectedResponse), len(res.Array())) } - for i := 0; i < len(responseArray); i++ { - if responseArray[i].String() != test.expectedResponse[i] { - t.Errorf("expected value \"%s\" at index %d, got \"%s\"", test.expectedResponse[i], i, responseArray[i].String()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -521,763 +516,815 @@ func Test_HandleLRANGE(t *testing.T) { } func Test_HandleLSET(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { - name string - preset bool - key string - presetValue interface{} - command []string - expectedResponse interface{} - expectedValue []interface{} - expectedError error + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error }{ { - name: "1. Return last element within range", - preset: true, - key: "LsetKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey1", "3", "new-value"}, - expectedResponse: "OK", - expectedValue: []interface{}{"value1", "value2", "value3", "new-value"}, - expectedError: nil, + name: "1. Return last element within range", + key: "LsetKey1", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey1", "3", "new-value"}, + expectedValue: []string{"value1", "value2", "value3", "new-value"}, + expectedError: nil, }, { - name: "2. Return first element within range", - preset: true, - key: "LsetKey2", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey2", "0", "new-value"}, - expectedResponse: "OK", - expectedValue: []interface{}{"new-value", "value2", "value3", "value4"}, - expectedError: nil, + name: "2. Return first element within range", + key: "LsetKey2", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey2", "0", "new-value"}, + expectedValue: []string{"new-value", "value2", "value3", "value4"}, + expectedError: nil, }, { - name: "3. Return middle element within range", - preset: true, - key: "LsetKey3", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey3", "1", "new-value"}, - expectedResponse: "OK", - expectedValue: []interface{}{"value1", "new-value", "value3", "value4"}, - expectedError: nil, + name: "3. Return middle element within range", + key: "LsetKey3", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey3", "1", "new-value"}, + expectedValue: []string{"value1", "new-value", "value3", "value4"}, + expectedError: nil, }, { - name: "4. If key does not exist, return error", - preset: false, - key: "LsetKey4", - presetValue: nil, - command: []string{"LSET", "LsetKey4", "0", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("LSET command on non-list item"), + name: "4. If key does not exist, return error", + key: "LsetKey4", + presetValue: nil, + command: []string{"LSET", "LsetKey4", "0", "element"}, + expectedValue: nil, + expectedError: errors.New("LSET command on non-list item"), }, { - name: "5. Command too short", - preset: false, - key: "LsetKey5", - presetValue: nil, - command: []string{"LSET", "LsetKey5"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "5. Command too short", + key: "LsetKey5", + presetValue: nil, + command: []string{"LSET", "LsetKey5"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "6. Command too long", - preset: false, - key: "LsetKey6", - presetValue: nil, - command: []string{"LSET", "LsetKey6", "0", "element", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "6. Command too long", + key: "LsetKey6", + presetValue: nil, + command: []string{"LSET", "LsetKey6", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "7. Trying to get element by index on a non-list returns error", - preset: true, - key: "LsetKey5", - presetValue: "Default value", - command: []string{"LSET", "LsetKey5", "0", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("LSET command on non-list item"), + name: "7. Trying to get element by index on a non-list returns error", + key: "LsetKey5", + presetValue: "Default value", + command: []string{"LSET", "LsetKey5", "0", "element"}, + expectedValue: nil, + expectedError: errors.New("LSET command on non-list item"), }, { - name: "8. Trying to get index out of range index beyond last index", - preset: true, - key: "LsetKey6", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey6", "3", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("index must be within list range"), + name: "8. Trying to get index out of range index beyond last index", + key: "LsetKey6", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey6", "3", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be within list range"), }, { - name: "9. Trying to get index out of range with negative index", - preset: true, - key: "LsetKey7", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey7", "-1", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("index must be within list range"), + name: "9. Trying to get index out of range with negative index", + key: "LsetKey7", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey7", "-1", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be within list range"), }, { - name: "10. Return error when index is not an integer", - preset: false, - key: "LsetKey8", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey8", "index", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("index must be an integer"), + name: "10. Return error when index is not an integer", + key: "LsetKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey8", "index", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be an integer"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LSET, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleLTRIM(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { - name string - preset bool - key string - presetValue interface{} - command []string - expectedResponse interface{} - expectedValue []interface{} - expectedError error + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error }{ { // Return trim within range. // Both start and end indices are positive. // End index is greater than start index. - name: "1. Return trim within range.", - preset: true, - key: "LtrimKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LTRIM", "LtrimKey1", "3", "6"}, - expectedResponse: "OK", - expectedValue: []interface{}{"value4", "value5", "value6"}, - expectedError: nil, + name: "1. Return trim within range.", + key: "LtrimKey1", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LTRIM", "LtrimKey1", "3", "6"}, + expectedValue: []string{"value4", "value5", "value6"}, + expectedError: nil, }, { - name: "2. Return element from start index to end index when end index is greater than length of the list", - preset: true, - key: "LtrimKey2", - presetValue: []interface{}{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LTRIM", "LtrimKey2", "5", "-1"}, - expectedResponse: "OK", - expectedValue: []interface{}{"value6", "value7", "value8"}, - expectedError: nil, + name: "2. Return element from start index to end index when end index is greater than length of the list", + key: "LtrimKey2", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LTRIM", "LtrimKey2", "5", "-1"}, + expectedValue: []string{"value6", "value7", "value8"}, + expectedError: nil, }, { - name: "3. Return error when end index is smaller than start index but greater than -1", - preset: true, - key: "LtrimKey3", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, - command: []string{"LTRIM", "LtrimKey3", "3", "1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("end index must be greater than start index or -1"), + name: "3. Return error when end index is smaller than start index but greater than -1", + key: "LtrimKey3", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LTRIM", "LtrimKey3", "3", "1"}, + expectedValue: nil, + expectedError: errors.New("end index must be greater than start index or -1"), }, { - name: "4. If key does not exist, return error", - preset: false, - key: "LtrimKey4", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey4", "0", "2"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("LTRIM command on non-list item"), + name: "4. If key does not exist, return error", + key: "LtrimKey4", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey4", "0", "2"}, + expectedValue: nil, + expectedError: errors.New("LTRIM command on non-list item"), }, { - name: "5. Command too short", - preset: false, - key: "LtrimKey5", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey5"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "5. Command too short", + key: "LtrimKey5", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey5"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "6. Command too long", - preset: false, - key: "LtrimKey6", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey6", "0", "element", "element"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "6. Command too long", + key: "LtrimKey6", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey6", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "7. Trying to get element by index on a non-list returns error", - preset: true, - key: "LtrimKey5", - presetValue: "Default value", - command: []string{"LTRIM", "LtrimKey5", "0", "3"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("LTRIM command on non-list item"), + name: "7. Trying to get element by index on a non-list returns error", + key: "LtrimKey5", + presetValue: "Default value", + command: []string{"LTRIM", "LtrimKey5", "0", "3"}, + expectedValue: nil, + expectedError: errors.New("LTRIM command on non-list item"), }, { - name: "8. Error when start index is less than 0", - preset: true, - key: "LtrimKey7", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, - command: []string{"LTRIM", "LtrimKey7", "-1", "3"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("start index must be within list boundary"), + name: "8. Error when start index is less than 0", + key: "LtrimKey7", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LTRIM", "LtrimKey7", "-1", "3"}, + expectedValue: nil, + expectedError: errors.New("start index must be within list boundary"), }, { - name: "9. Error when start index is higher than the length of the list", - preset: true, - key: "LtrimKey8", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey8", "10", "11"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("start index must be within list boundary"), + name: "9. Error when start index is higher than the length of the list", + key: "LtrimKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey8", "10", "11"}, + expectedValue: nil, + expectedError: errors.New("start index must be within list boundary"), }, { - name: "10. Return error when start index is not an integer", - preset: false, - key: "LtrimKey9", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey9", "start", "7"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("start and end indices must be integers"), + name: "10. Return error when start index is not an integer", + key: "LtrimKey9", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey9", "start", "7"}, + expectedValue: nil, + expectedError: errors.New("start and end indices must be integers"), }, { - name: "11. Return error when end index is not an integer", - preset: false, - key: "LtrimKey10", - presetValue: []interface{}{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey10", "0", "end"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("start and end indices must be integers"), + name: "11. Return error when end index is not an integer", + key: "LtrimKey10", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey10", "0", "end"}, + expectedValue: nil, + expectedError: errors.New("start and end indices must be integers"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LTRIM, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleLREM(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { - name string - preset bool - key string - presetValue interface{} - command []string - expectedResponse interface{} - expectedValue []interface{} - expectedError error + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error }{ { - name: "1. Remove the first 3 elements that appear in the list", - preset: true, - key: "LremKey1", - presetValue: []interface{}{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, - command: []string{"LREM", "LremKey1", "3", "4"}, - expectedResponse: "OK", - expectedValue: []interface{}{"1", "2", "5", "6", "7", "8", "4", "9", "10", "5", "4"}, - expectedError: nil, + name: "1. Remove the first 3 elements that appear in the list", + key: "LremKey1", + presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, + command: []string{"LREM", "LremKey1", "3", "4"}, + expectedValue: []string{"1", "2", "5", "6", "7", "8", "4", "9", "10", "5", "4"}, + expectedError: nil, }, { - name: "2. Remove the last 3 elements that appear in the list", - preset: true, - key: "LremKey1", - presetValue: []interface{}{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, - command: []string{"LREM", "LremKey1", "-3", "4"}, - expectedResponse: "OK", - expectedValue: []interface{}{"1", "2", "4", "4", "5", "6", "7", "8", "9", "10", "5"}, - expectedError: nil, + name: "2. Remove the last 3 elements that appear in the list", + key: "LremKey2", + presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, + command: []string{"LREM", "LremKey2", "-3", "4"}, + expectedValue: []string{"1", "2", "4", "4", "5", "6", "7", "8", "9", "10", "5"}, + expectedError: nil, }, { - name: "3. Command too short", - preset: false, - key: "LremKey5", - presetValue: nil, - command: []string{"LREM", "LremKey5"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "3. Command too short", + key: "LremKey3", + presetValue: nil, + command: []string{"LREM", "LremKey3"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "4. Command too long", - preset: false, - key: "LremKey6", - presetValue: nil, - command: []string{"LREM", "LremKey6", "0", "element", "element"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), + name: "4. Command too long", + key: "LremKey4", + presetValue: nil, + command: []string{"LREM", "LremKey4", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "5. Throw error when count is not an integer", - preset: false, - key: "LremKey7", - presetValue: nil, - command: []string{"LREM", "LremKey7", "count", "value1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("count must be an integer"), + name: "5. Throw error when count is not an integer", + key: "LremKey5", + presetValue: nil, + command: []string{"LREM", "LremKey5", "count", "value1"}, + expectedValue: nil, + expectedError: errors.New("count must be an integer"), }, { - name: "6. Throw error on non-list item", - preset: true, - key: "LremKey8", - presetValue: "Default value", - command: []string{"LREM", "LremKey8", "0", "value1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("LREM command on non-list item"), + name: "6. Throw error on non-list item", + key: "LremKey6", + presetValue: "Default value", + command: []string{"LREM", "LremKey6", "0", "value1"}, + expectedValue: nil, + expectedError: errors.New("LREM command on non-list item"), }, { - name: "7. Throw error on non-existent item", - preset: false, - key: "LremKey9", - presetValue: "Default value", - command: []string{"LREM", "LremKey9", "0", "value1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("LREM command on non-list item"), + name: "7. Throw error on non-existent item", + key: "LremKey7", + presetValue: "Default value", + command: []string{"LREM", "LremKey7", "0", "value1"}, + expectedValue: nil, + expectedError: errors.New("LREM command on non-list item"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LREM, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleLMOVE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { - name string - preset bool - presetValue map[string]interface{} - command []string - expectedResponse interface{} - expectedValue map[string]interface{} - expectedError error + name string + presetValue map[string]interface{} + command []string + expectedValue map[string][]string + expectedError error }{ { - name: "1. Move element from LEFT of left list to LEFT of right list", - preset: true, + name: "1. Move element from LEFT of left list to LEFT of right list", presetValue: map[string]interface{}{ - "source1": []interface{}{"one", "two", "three"}, - "destination1": []interface{}{"one", "two", "three"}, + "source1": []string{"one", "two", "three"}, + "destination1": []string{"one", "two", "three"}, }, - command: []string{"LMOVE", "source1", "destination1", "LEFT", "LEFT"}, - expectedResponse: "OK", - expectedValue: map[string]interface{}{ - "source1": []interface{}{"two", "three"}, - "destination1": []interface{}{"one", "one", "two", "three"}, + command: []string{"LMOVE", "source1", "destination1", "LEFT", "LEFT"}, + expectedValue: map[string][]string{ + "source1": {"two", "three"}, + "destination1": {"one", "one", "two", "three"}, }, expectedError: nil, }, { - name: "2. Move element from LEFT of left list to RIGHT of right list", - preset: true, + name: "2. Move element from LEFT of left list to RIGHT of right list", presetValue: map[string]interface{}{ - "source2": []interface{}{"one", "two", "three"}, - "destination2": []interface{}{"one", "two", "three"}, + "source2": []string{"one", "two", "three"}, + "destination2": []string{"one", "two", "three"}, }, - command: []string{"LMOVE", "source2", "destination2", "LEFT", "RIGHT"}, - expectedResponse: "OK", - expectedValue: map[string]interface{}{ - "source2": []interface{}{"two", "three"}, - "destination2": []interface{}{"one", "two", "three", "one"}, + command: []string{"LMOVE", "source2", "destination2", "LEFT", "RIGHT"}, + expectedValue: map[string][]string{ + "source2": {"two", "three"}, + "destination2": {"one", "two", "three", "one"}, }, expectedError: nil, }, { - name: "3. Move element from RIGHT of left list to LEFT of right list", - preset: true, + name: "3. Move element from RIGHT of left list to LEFT of right list", presetValue: map[string]interface{}{ - "source3": []interface{}{"one", "two", "three"}, - "destination3": []interface{}{"one", "two", "three"}, + "source3": []string{"one", "two", "three"}, + "destination3": []string{"one", "two", "three"}, }, - command: []string{"LMOVE", "source3", "destination3", "RIGHT", "LEFT"}, - expectedResponse: "OK", - expectedValue: map[string]interface{}{ - "source3": []interface{}{"one", "two"}, - "destination3": []interface{}{"three", "one", "two", "three"}, + command: []string{"LMOVE", "source3", "destination3", "RIGHT", "LEFT"}, + expectedValue: map[string][]string{ + "source3": {"one", "two"}, + "destination3": {"three", "one", "two", "three"}, }, expectedError: nil, }, { - name: "4. Move element from RIGHT of left list to RIGHT of right list", - preset: true, + name: "4. Move element from RIGHT of left list to RIGHT of right list", presetValue: map[string]interface{}{ - "source4": []interface{}{"one", "two", "three"}, - "destination4": []interface{}{"one", "two", "three"}, + "source4": []string{"one", "two", "three"}, + "destination4": []string{"one", "two", "three"}, }, - command: []string{"LMOVE", "source4", "destination4", "RIGHT", "RIGHT"}, - expectedResponse: "OK", - expectedValue: map[string]interface{}{ - "source4": []interface{}{"one", "two"}, - "destination4": []interface{}{"one", "two", "three", "three"}, + command: []string{"LMOVE", "source4", "destination4", "RIGHT", "RIGHT"}, + expectedValue: map[string][]string{ + "source4": {"one", "two"}, + "destination4": {"one", "two", "three", "three"}, }, expectedError: nil, }, { - name: "5. Throw error when the right list is non-existent", - preset: true, + name: "5. Throw error when the right list is non-existent", presetValue: map[string]interface{}{ - "source5": []interface{}{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source5", "destination5", "LEFT", "LEFT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{ - "source5": []interface{}{"one", "two", "three"}, + "source5": []string{"one", "two", "three"}, }, + command: []string{"LMOVE", "source5", "destination5", "LEFT", "LEFT"}, + expectedValue: nil, expectedError: errors.New("both source and destination must be lists"), }, { - name: "6. Throw error when right list in not a list", - preset: true, + name: "6. Throw error when right list in not a list", presetValue: map[string]interface{}{ - "source6": []interface{}{"one", "two", "tree"}, - "destination6": "Default value", - }, - command: []string{"LMOVE", "source6", "destination6", "LEFT", "LEFT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{ - "source5": []interface{}{"one", "two", "three"}, + "source6": []string{"one", "two", "tree"}, "destination6": "Default value", }, + command: []string{"LMOVE", "source6", "destination6", "LEFT", "LEFT"}, + expectedValue: nil, expectedError: errors.New("both source and destination must be lists"), }, { - name: "7. Throw error when left list is non-existent", - preset: true, + name: "7. Throw error when left list is non-existent", presetValue: map[string]interface{}{ - "destination7": []interface{}{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source7", "destination7", "LEFT", "LEFT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{ - "destination7": []interface{}{""}, + "destination7": []string{"one", "two", "three"}, }, + command: []string{"LMOVE", "source7", "destination7", "LEFT", "LEFT"}, + expectedValue: nil, expectedError: errors.New("both source and destination must be lists"), }, { - name: "8. Throw error when left list is not a list", - preset: true, + name: "8. Throw error when left list is not a list", presetValue: map[string]interface{}{ "source8": "Default value", - "destination8": []interface{}{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source8", "destination8", "LEFT", "LEFT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{ - "source5": "Default value", - "destination6": []interface{}{"one", "two", "three"}, + "destination8": []string{"one", "two", "three"}, }, + command: []string{"LMOVE", "source8", "destination8", "LEFT", "LEFT"}, + expectedValue: nil, expectedError: errors.New("both source and destination must be lists"), }, { - name: "9. Throw error when command is too short", - preset: false, - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source9", "destination9"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, - expectedError: errors.New(constants.WrongArgsResponse), + name: "9. Throw error when command is too short", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source9", "destination9"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "10. Throw error when command is too long", - preset: false, - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source10", "destination10", "LEFT", "LEFT", "RIGHT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, - expectedError: errors.New(constants.WrongArgsResponse), + name: "10. Throw error when command is too long", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source10", "destination10", "LEFT", "LEFT", "RIGHT"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "11. Throw error when WHEREFROM argument is not LEFT/RIGHT", - preset: false, - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source11", "destination11", "UP", "RIGHT"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, - expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), + name: "11. Throw error when WHEREFROM argument is not LEFT/RIGHT", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source11", "destination11", "UP", "RIGHT"}, + expectedValue: nil, + expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), }, { - name: "12. Throw error when WHERETO argument is not LEFT/RIGHT", - preset: false, - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source11", "destination11", "LEFT", "DOWN"}, - expectedResponse: nil, - expectedValue: map[string]interface{}{}, - expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), + name: "12. Throw error when WHERETO argument is not LEFT/RIGHT", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source11", "destination11", "LEFT", "DOWN"}, + expectedValue: nil, + expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LMOVE, %d", i)) - - if test.preset { + if test.presetValue != nil { for key, value := range test.presetValue { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + + var command []resp.Value + var expected string + + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(key)} + for _, element := range value.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(value.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) } - for key, value := range test.expectedValue { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { + + for key, list := range test.expectedValue { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") - } - expectedList, ok := value.([]interface{}) - if !ok { - t.Error("expected test value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(expectedList) { - t.Errorf("expected list length to be %d, got %d", len(expectedList), len(l)) + + if len(res.Array()) != len(list) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != expectedList[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, expectedList[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(list, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list %s", item.String(), key) } } - mockServer.KeyRUnlock(ctx, key) } }) } } func Test_HandleLPUSH(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string expectedResponse int - expectedValue []interface{} + expectedValue []string expectedError error }{ { name: "1. LPUSHX to existing list prepends the element to the list", - preset: true, key: "LpushKey1", - presetValue: []interface{}{"1", "2", "4", "5"}, + presetValue: []string{"1", "2", "4", "5"}, command: []string{"LPUSHX", "LpushKey1", "value1", "value2"}, expectedResponse: 6, - expectedValue: []interface{}{"value1", "value2", "1", "2", "4", "5"}, + expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, expectedError: nil, }, { name: "2. LPUSH on existing list prepends the elements to the list", - preset: true, key: "LpushKey2", - presetValue: []interface{}{"1", "2", "4", "5"}, + presetValue: []string{"1", "2", "4", "5"}, command: []string{"LPUSH", "LpushKey2", "value1", "value2"}, expectedResponse: 6, - expectedValue: []interface{}{"value1", "value2", "1", "2", "4", "5"}, + expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, expectedError: nil, }, { name: "3. LPUSH on non-existent list creates the list", - preset: false, key: "LpushKey3", presetValue: nil, command: []string{"LPUSH", "LpushKey3", "value1", "value2"}, expectedResponse: 2, - expectedValue: []interface{}{"value1", "value2"}, + expectedValue: []string{"value1", "value2"}, expectedError: nil, }, { name: "4. Command too short", - preset: false, key: "LpushKey5", presetValue: nil, command: []string{"LPUSH", "LpushKey5"}, @@ -1287,115 +1334,147 @@ func Test_HandleLPUSH(t *testing.T) { }, { name: "5. LPUSHX command returns error on non-existent list", - preset: false, key: "LpushKey6", presetValue: nil, command: []string{"LPUSHX", "LpushKey7", "count", "value1"}, expectedResponse: 0, expectedValue: nil, - expectedError: errors.New("LPUSHX command on non-list item"), + expectedError: errors.New("LPUSHX command on non-existent key"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LPUSH/LPUSHX, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected \"%d\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleRPUSH(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string expectedResponse int - expectedValue []interface{} + expectedValue []string expectedError error }{ { name: "1. RPUSHX to existing list prepends the element to the list", - preset: true, key: "RpushKey1", - presetValue: []interface{}{"1", "2", "4", "5"}, + presetValue: []string{"1", "2", "4", "5"}, command: []string{"RPUSHX", "RpushKey1", "value1", "value2"}, expectedResponse: 6, - expectedValue: []interface{}{"1", "2", "4", "5", "value1", "value2"}, + expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, expectedError: nil, }, { name: "2. RPUSH on existing list prepends the elements to the list", - preset: true, key: "RpushKey2", - presetValue: []interface{}{"1", "2", "4", "5"}, + presetValue: []string{"1", "2", "4", "5"}, command: []string{"RPUSH", "RpushKey2", "value1", "value2"}, expectedResponse: 6, - expectedValue: []interface{}{"1", "2", "4", "5", "value1", "value2"}, + expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, expectedError: nil, }, { name: "3. RPUSH on non-existent list creates the list", - preset: false, key: "RpushKey3", presetValue: nil, command: []string{"RPUSH", "RpushKey3", "value1", "value2"}, expectedResponse: 2, - expectedValue: []interface{}{"value1", "value2"}, + expectedValue: []string{"value1", "value2"}, expectedError: nil, }, { name: "4. Command too short", - preset: false, key: "RpushKey5", presetValue: nil, command: []string{"RPUSH", "RpushKey5"}, @@ -1405,195 +1484,257 @@ func Test_HandleRPUSH(t *testing.T) { }, { name: "5. RPUSHX command returns error on non-existent list", - preset: false, key: "RpushKey6", presetValue: nil, command: []string{"RPUSHX", "RpushKey7", "count", "value1"}, expectedResponse: 0, expectedValue: nil, - expectedError: errors.New("RPUSHX command on non-list item"), + expectedError: errors.New("RPUSHX command on non-existent key"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("RPUSH/RPUSHX, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected \"%d\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandlePOP(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string - expectedResponse interface{} - expectedValue []interface{} + expectedResponse string + expectedValue []string expectedError error }{ { name: "1. LPOP returns last element and removed first element from the list", - preset: true, key: "PopKey1", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"LPOP", "PopKey1"}, expectedResponse: "value1", - expectedValue: []interface{}{"value2", "value3", "value4"}, + expectedValue: []string{"value2", "value3", "value4"}, expectedError: nil, }, { name: "2. RPOP returns last element and removed last element from the list", - preset: true, key: "PopKey2", - presetValue: []interface{}{"value1", "value2", "value3", "value4"}, + presetValue: []string{"value1", "value2", "value3", "value4"}, command: []string{"RPOP", "PopKey2"}, expectedResponse: "value4", - expectedValue: []interface{}{"value1", "value2", "value3"}, + expectedValue: []string{"value1", "value2", "value3"}, expectedError: nil, }, { name: "3. Command too short", - preset: false, key: "PopKey3", presetValue: nil, command: []string{"LPOP"}, - expectedResponse: 0, + expectedResponse: "", expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, key: "PopKey4", presetValue: nil, command: []string{"LPOP", "PopKey4", "PopKey4"}, - expectedResponse: 0, + expectedResponse: "", expectedValue: nil, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Trying to execute LPOP from a non-list item return an error", - preset: true, key: "PopKey5", presetValue: "Default value", command: []string{"LPOP", "PopKey5"}, - expectedResponse: 0, + expectedResponse: "", expectedValue: nil, expectedError: errors.New("LPOP command on non-list item"), }, { name: "6. Trying to execute RPOP from a non-list item return an error", - preset: true, key: "PopKey6", presetValue: "Default value", command: []string{"RPOP", "PopKey6"}, - expectedResponse: 0, + expectedResponse: "", expectedValue: nil, expectedError: errors.New("RPOP command on non-list item"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("LPOP/RPOP, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + if err = client.WriteArray(command); err != nil { + t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.String() != test.expectedResponse { - t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.String() != test.expectedResponse { + t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - l, ok := mockServer.GetValue(ctx, test.key).([]interface{}) - if !ok { - t.Error("expected value to be list, got another type") + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if len(l) != len(test.expectedValue) { - t.Errorf("expected list length to be %d, got %d", len(test.expectedValue), len(l)) + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) } - for i := 0; i < len(l); i++ { - if l[i] != test.expectedValue[i] { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedValue[i], l[i]) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } diff --git a/internal/modules/set/commands.go b/internal/modules/set/commands.go index f32e7320..225022d6 100644 --- a/internal/modules/set/commands.go +++ b/internal/modules/set/commands.go @@ -30,27 +30,19 @@ func handleSADD(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] var set *Set - if !params.KeyExists(params.Context, key) { + if !keyExists { set = NewSet(params.Command[2:]) - if ok, err := params.CreateKeyAndLock(params.Context, key); !ok && err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: set}); err != nil { return nil, err } - if err = params.SetValue(params.Context, key, set); err != nil { - return nil, err - } - params.KeyUnlock(params.Context, key) return []byte(fmt.Sprintf(":%d\r\n", len(params.Command[2:]))), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -67,17 +59,13 @@ func handleSCARD(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(fmt.Sprintf(":0\r\n")), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -93,41 +81,21 @@ func handleSDIFF(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(keys.ReadKeys) + // Extract base set first - if !params.KeyExists(params.Context, keys.ReadKeys[0]) { + if !keyExists[keys.ReadKeys[0]] { return nil, fmt.Errorf("key for base set \"%s\" does not exist", keys.ReadKeys[0]) } - if _, err = params.KeyRLock(params.Context, keys.ReadKeys[0]); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, keys.ReadKeys[0]) - baseSet, ok := params.GetValue(params.Context, keys.ReadKeys[0]).(*Set) + + baseSet, ok := params.GetValues(params.Context, []string{keys.ReadKeys[0]})[keys.ReadKeys[0]].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", keys.ReadKeys[0]) } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys[1:] { - if !params.KeyExists(params.Context, key) { - continue - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - continue - } - locks[key] = true - } - var sets []*Set for _, key := range params.Command[2:] { - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { continue } @@ -155,42 +123,21 @@ func handleSDIFFSTORE(params internal.HandlerFuncParams) ([]byte, error) { } destination := keys.WriteKeys[0] + keyExists := params.KeysExist(append(keys.WriteKeys, keys.ReadKeys...)) // Extract base set first - if !params.KeyExists(params.Context, keys.ReadKeys[0]) { + if !keyExists[keys.ReadKeys[0]] { return nil, fmt.Errorf("key for base set \"%s\" does not exist", keys.ReadKeys[0]) } - if _, err := params.KeyRLock(params.Context, keys.ReadKeys[0]); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, keys.ReadKeys[0]) - baseSet, ok := params.GetValue(params.Context, keys.ReadKeys[0]).(*Set) + + baseSet, ok := params.GetValues(params.Context, []string{keys.ReadKeys[0]})[keys.ReadKeys[0]].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", keys.ReadKeys[0]) } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys[1:] { - if !params.KeyExists(params.Context, key) { - continue - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - continue - } - locks[key] = true - } - var sets []*Set for _, key := range keys.ReadKeys[1:] { - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { continue } @@ -202,24 +149,9 @@ func handleSDIFFSTORE(params internal.HandlerFuncParams) ([]byte, error) { res := fmt.Sprintf(":%d\r\n", len(elems)) - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - if err = params.SetValue(params.Context, destination, diff); err != nil { - return nil, err - } - params.KeyUnlock(params.Context, destination) - return []byte(res), nil - } - - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - if err = params.SetValue(params.Context, destination, diff); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{destination: diff}); err != nil { return nil, err } - params.KeyUnlock(params.Context, destination) return []byte(res), nil } @@ -230,30 +162,15 @@ func handleSINTER(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys { - if !params.KeyExists(params.Context, key) { - // If key does not exist, then there is no intersection - return []byte("*0\r\n"), nil - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - locks[key] = true - } + keyExists := params.KeysExist(keys.ReadKeys) var sets []*Set - for key, _ := range locks { - set, ok := params.GetValue(params.Context, key).(*Set) + for key, exists := range keyExists { + if !exists { + return []byte("*0\r\n"), nil + } + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { // If the value at the key is not a set, return error return nil, fmt.Errorf("value at key %s is not a set", key) @@ -285,6 +202,8 @@ func handleSINTERCARD(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(keys.ReadKeys) + // Extract the limit from the command var limit int limitIdx := slices.IndexFunc(params.Command, func(s string) bool { @@ -306,30 +225,13 @@ func handleSINTERCARD(params internal.HandlerFuncParams) ([]byte, error) { } } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() + var sets []*Set - for _, key := range keys.ReadKeys { - if !params.KeyExists(params.Context, key) { - // If key does not exist, then there is no intersection + for key, exists := range keyExists { + if !exists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - locks[key] = true - } - - var sets []*Set - - for key, _ := range locks { - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { // If the value at the key is not a set, return error return nil, fmt.Errorf("value at key %s is not a set", key) @@ -352,30 +254,15 @@ func handleSINTERSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys { - if !params.KeyExists(params.Context, key) { - // If key does not exist, then there is no intersection - return []byte(":0\r\n"), nil - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - locks[key] = true - } + keyExists := params.KeysExist(keys.ReadKeys) var sets []*Set - for key, _ := range locks { - set, ok := params.GetValue(params.Context, key).(*Set) + for key, exists := range keyExists { + if !exists { + return []byte(":0\r\n"), err + } + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { // If the value at the key is not a set, return error return nil, fmt.Errorf("value at key %s is not a set", key) @@ -386,20 +273,9 @@ func handleSINTERSTORE(params internal.HandlerFuncParams) ([]byte, error) { intersect, _ := Intersection(0, sets...) destination := keys.WriteKeys[0] - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - - if err = params.SetValue(params.Context, destination, intersect); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{destination: intersect}); err != nil { return nil, err } - params.KeyUnlock(params.Context, destination) return []byte(fmt.Sprintf(":%d\r\n", intersect.Cardinality())), nil } @@ -411,17 +287,13 @@ func handleSISMEMBER(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -440,17 +312,13 @@ func handleSMEMBERS(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -475,9 +343,10 @@ func handleSMISMEMBER(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] members := params.Command[2:] - if !params.KeyExists(params.Context, key) { + if !keyExists { res := fmt.Sprintf("*%d", len(members)) for i, _ := range members { res = fmt.Sprintf("%s\r\n:0", res) @@ -488,12 +357,7 @@ func handleSMISMEMBER(params internal.HandlerFuncParams) ([]byte, error) { return []byte(res), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -518,45 +382,23 @@ func handleSMOVE(params internal.HandlerFuncParams) ([]byte, error) { } source, destination := keys.WriteKeys[0], keys.WriteKeys[1] + keyExists := params.KeysExist(keys.WriteKeys) member := params.Command[3] - if !params.KeyExists(params.Context, source) { + if !keyExists[source] { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, source); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, source) + sets := params.GetValues(params.Context, keys.WriteKeys) - sourceSet, ok := params.GetValue(params.Context, source).(*Set) + sourceSet, ok := sets[source].(*Set) if !ok { return nil, errors.New("source is not a set") } - var destinationSet *Set - - if !params.KeyExists(params.Context, destination) { - // Destination key does not exist - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, destination) - destinationSet = NewSet([]string{}) - if err = params.SetValue(params.Context, destination, destinationSet); err != nil { - return nil, err - } - } else { - // Destination key exists - if _, err := params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, destination) - ds, ok := params.GetValue(params.Context, destination).(*Set) - if !ok { - return nil, errors.New("destination is not a set") - } - destinationSet = ds + destinationSet, ok := sets[destination].(*Set) + if !ok { + return nil, errors.New("destination is not a set") } res := sourceSet.Move(destinationSet, member) @@ -571,6 +413,7 @@ func handleSPOP(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] count := 1 if len(params.Command) == 3 { @@ -581,16 +424,11 @@ func handleSPOP(params internal.HandlerFuncParams) ([]byte, error) { count = c } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*-1\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at %s is not a set", key) } @@ -615,6 +453,7 @@ func handleSRANDMEMBER(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] count := 1 if len(params.Command) == 3 { @@ -625,16 +464,11 @@ func handleSRANDMEMBER(params internal.HandlerFuncParams) ([]byte, error) { count = c } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*-1\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at %s is not a set", key) } @@ -659,18 +493,14 @@ func handleSREM(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] members := params.Command[2:] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*Set) + set, ok := params.GetValues(params.Context, []string{key})[key].(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -686,32 +516,11 @@ func handleSUNION(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys { - if !params.KeyExists(params.Context, key) { - continue - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - locks[key] = true - } - var sets []*Set - for key, locked := range locks { - if !locked { - continue - } - set, ok := params.GetValue(params.Context, key).(*Set) + values := params.GetValues(params.Context, keys.ReadKeys) + for key, value := range values { + set, ok := value.(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -737,32 +546,13 @@ func handleSUNIONSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - - for _, key := range keys.ReadKeys { - if !params.KeyExists(params.Context, key) { - continue - } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - locks[key] = true - } + destination := keys.WriteKeys[0] var sets []*Set - for key, locked := range locks { - if !locked { - continue - } - set, ok := params.GetValue(params.Context, key).(*Set) + values := params.GetValues(params.Context, keys.ReadKeys) + for key, value := range values { + set, ok := value.(*Set) if !ok { return nil, fmt.Errorf("value at key %s is not a set", key) } @@ -771,20 +561,7 @@ func handleSUNIONSTORE(params internal.HandlerFuncParams) ([]byte, error) { union := Union(sets...) - destination := keys.WriteKeys[0] - - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - defer params.KeyUnlock(params.Context, destination) - - if err = params.SetValue(params.Context, destination, union); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{destination: union}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", union.Cardinality())), nil diff --git a/internal/modules/set/commands_test.go b/internal/modules/set/commands_test.go index 4c20d4cb..bbc80db6 100644 --- a/internal/modules/set/commands_test.go +++ b/internal/modules/set/commands_test.go @@ -15,8 +15,6 @@ package set_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -26,68 +24,43 @@ import ( "github.com/echovault/echovault/internal/modules/set" "github.com/tidwall/resp" "net" - "reflect" "slices" + "strconv" "strings" + "sync" "testing" - "unsafe" ) var mockServer *echovault.EchoVault +var addr = "localhost" +var port int func init() { + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } - } - return nil -} - -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, +func Test_HandleSADD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() } -} + client := resp.NewConn(conn) -func Test_HandleSADD(t *testing.T) { tests := []struct { name string preset bool @@ -138,69 +111,101 @@ func Test_HandleSADD(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SADD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected integer response %d, got %d", test.expectedResponse, rv.Integer()) + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return } - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { t.Error(err) } - currSet, ok := mockServer.GetValue(ctx, test.key).(*set.Set) - if !ok { - t.Errorf("expected set value at key \"%s\"", test.key) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if currSet.Cardinality() != test.expectedValue.Cardinality() { - t.Errorf("expected resulting cardinality to be %d, got %d", test.expectedValue.Cardinality(), currSet.Cardinality()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) } - for _, member := range currSet.GetAll() { - if !test.expectedValue.Contains(member) { - t.Errorf("could not find member \"%s\" in expected set", member) + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleSCARD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string @@ -210,7 +215,6 @@ func Test_HandleSCARD(t *testing.T) { }{ { name: "1. Get cardinality of valid set.", - preset: true, presetValue: set.NewSet([]string{"one", "two", "three", "four"}), key: "ScardKey1", command: []string{"SCARD", "ScardKey1"}, @@ -220,7 +224,6 @@ func Test_HandleSCARD(t *testing.T) { }, { name: "2. Return 0 when trying to get cardinality on non-existent key", - preset: false, presetValue: nil, key: "ScardKey2", command: []string{"SCARD", "ScardKey2"}, @@ -230,7 +233,6 @@ func Test_HandleSCARD(t *testing.T) { }, { name: "3. Throw error when trying to get cardinality of a value that is not a set", - preset: true, presetValue: "Default value", key: "ScardKey3", command: []string{"SCARD", "ScardKey3"}, @@ -239,7 +241,6 @@ func Test_HandleSCARD(t *testing.T) { }, { name: "4. Command too short", - preset: false, key: "ScardKey4", command: []string{"SCARD"}, expectedValue: nil, @@ -248,7 +249,6 @@ func Test_HandleSCARD(t *testing.T) { }, { name: "5. Command too long", - preset: false, key: "ScardKey5", command: []string{"SCARD", "ScardKey5", "ScardKey5"}, expectedValue: nil, @@ -257,61 +257,84 @@ func Test_HandleSCARD(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SCARD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected integer response %d, got %d", test.expectedResponse, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleSDIFF(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse []string expectedError error }{ { - name: "1. Get the difference between 2 sets.", - preset: true, + name: "1. Get the difference between 2 sets.", presetValues: map[string]interface{}{ "SdiffKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SdiffKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -321,8 +344,7 @@ func Test_HandleSDIFF(t *testing.T) { expectedError: nil, }, { - name: "2. Get the difference between 3 sets.", - preset: true, + name: "2. Get the difference between 3 sets.", presetValues: map[string]interface{}{ "SdiffKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SdiffKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -333,20 +355,18 @@ func Test_HandleSDIFF(t *testing.T) { expectedError: nil, }, { - name: "3. Return base set element if base set is the only valid set", - preset: true, + name: "3. Return base set element if base set is the only valid set", presetValues: map[string]interface{}{ "SdiffKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SdiffKey7": "Default value", - "SdiffKey8": 123456789, + "SdiffKey8": "123456789", }, command: []string{"SDIFF", "SdiffKey6", "SdiffKey7", "SdiffKey8"}, expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, expectedError: nil, }, { - name: "4. Throw error when base set is not a set.", - preset: true, + name: "4. Throw error when base set is not a set.", presetValues: map[string]interface{}{ "SdiffKey9": "Default value", "SdiffKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -357,8 +377,7 @@ func Test_HandleSDIFF(t *testing.T) { expectedError: errors.New("value at key SdiffKey9 is not a set"), }, { - name: "5. Throw error when base set is non-existent.", - preset: true, + name: "5. Throw error when base set is non-existent.", presetValues: map[string]interface{}{ "SdiffKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), "SdiffKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), @@ -369,54 +388,76 @@ func Test_HandleSDIFF(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"SDIFF"}, expectedResponse: []string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SDIFF, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - for _, responseElement := range rv.Array() { - if !slices.Contains(test.expectedResponse, responseElement.String()) { - t.Errorf("could not find response element \"%s\" from expected response array", responseElement.String()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -424,9 +465,14 @@ func Test_HandleSDIFF(t *testing.T) { } func Test_HandleSDIFFSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -435,8 +481,7 @@ func Test_HandleSDIFFSTORE(t *testing.T) { expectedError error }{ { - name: "1. Get the difference between 2 sets.", - preset: true, + name: "1. Get the difference between 2 sets.", presetValues: map[string]interface{}{ "SdiffStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SdiffStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -448,8 +493,7 @@ func Test_HandleSDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "2. Get the difference between 3 sets.", - preset: true, + name: "2. Get the difference between 3 sets.", presetValues: map[string]interface{}{ "SdiffStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SdiffStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -462,12 +506,11 @@ func Test_HandleSDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "3. Return base set element if base set is the only valid set", - preset: true, + name: "3. Return base set element if base set is the only valid set", presetValues: map[string]interface{}{ "SdiffStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SdiffStoreKey7": "Default value", - "SdiffStoreKey8": 123456789, + "SdiffStoreKey8": "123456789", }, destination: "SdiffStoreDestination3", command: []string{"SDIFFSTORE", "SdiffStoreDestination3", "SdiffStoreKey6", "SdiffStoreKey7", "SdiffStoreKey8"}, @@ -476,8 +519,7 @@ func Test_HandleSDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "4. Throw error when base set is not a set.", - preset: true, + name: "4. Throw error when base set is not a set.", presetValues: map[string]interface{}{ "SdiffStoreKey9": "Default value", "SdiffStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -491,7 +533,6 @@ func Test_HandleSDIFFSTORE(t *testing.T) { }, { name: "5. Throw error when base set is non-existent.", - preset: true, destination: "SdiffStoreDestination5", presetValues: map[string]interface{}{ "SdiffStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -504,85 +545,118 @@ func Test_HandleSDIFFSTORE(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"SDIFFSTORE", "SdiffStoreDestination6"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SDIFFSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - currSet, ok := mockServer.GetValue(ctx, test.destination).(*set.Set) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) - } - for _, elem := range currSet.GetAll() { - if !test.expectedValue.Contains(elem) { - t.Errorf("could not find element %s in the expected values", elem) - } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } } func Test_HandleSINTER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse []string expectedError error }{ { - name: "1. Get the intersection between 2 sets.", - preset: true, + name: "1. Get the intersection between 2 sets.", presetValues: map[string]interface{}{ "SinterKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SinterKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -592,8 +666,7 @@ func Test_HandleSINTER(t *testing.T) { expectedError: nil, }, { - name: "2. Get the intersection between 3 sets.", - preset: true, + name: "2. Get the intersection between 3 sets.", presetValues: map[string]interface{}{ "SinterKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -604,8 +677,7 @@ func Test_HandleSINTER(t *testing.T) { expectedError: nil, }, { - name: "3. Throw an error if any of the provided keys are not sets", - preset: true, + name: "3. Throw an error if any of the provided keys are not sets", presetValues: map[string]interface{}{ "SinterKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterKey7": "Default value", @@ -616,8 +688,7 @@ func Test_HandleSINTER(t *testing.T) { expectedError: errors.New("value at key SinterKey7 is not a set"), }, { - name: "4. Throw error when base set is not a set.", - preset: true, + name: "4. Throw error when base set is not a set.", presetValues: map[string]interface{}{ "SinterKey9": "Default value", "SinterKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -628,8 +699,7 @@ func Test_HandleSINTER(t *testing.T) { expectedError: errors.New("value at key SinterKey9 is not a set"), }, { - name: "5. If any of the keys does not exist, return an empty array.", - preset: true, + name: "5. If any of the keys does not exist, return an empty array.", presetValues: map[string]interface{}{ "SinterKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), "SinterKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), @@ -640,54 +710,76 @@ func Test_HandleSINTER(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"SINTER"}, expectedResponse: []string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SINTER, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - for _, responseElement := range rv.Array() { - if !slices.Contains(test.expectedResponse, responseElement.String()) { - t.Errorf("could not find response element \"%s\" from expected response array", responseElement.String()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -695,17 +787,21 @@ func Test_HandleSINTER(t *testing.T) { } func Test_HandleSINTERCARD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse int expectedError error }{ { - name: "1. Get the full intersect cardinality between 2 sets.", - preset: true, + name: "1. Get the full intersect cardinality between 2 sets.", presetValues: map[string]interface{}{ "SinterCardKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SinterCardKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -715,8 +811,7 @@ func Test_HandleSINTERCARD(t *testing.T) { expectedError: nil, }, { - name: "2. Get an intersect cardinality between 2 sets with a limit", - preset: true, + name: "2. Get an intersect cardinality between 2 sets with a limit", presetValues: map[string]interface{}{ "SinterCardKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}), "SinterCardKey4": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"}), @@ -726,8 +821,7 @@ func Test_HandleSINTERCARD(t *testing.T) { expectedError: nil, }, { - name: "3. Get the full intersect cardinality between 3 sets.", - preset: true, + name: "3. Get the full intersect cardinality between 3 sets.", presetValues: map[string]interface{}{ "SinterCardKey5": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterCardKey6": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -738,8 +832,7 @@ func Test_HandleSINTERCARD(t *testing.T) { expectedError: nil, }, { - name: "4. Get the intersection of 3 sets with a limit", - preset: true, + name: "4. Get the intersection of 3 sets with a limit", presetValues: map[string]interface{}{ "SinterCardKey8": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterCardKey9": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -750,8 +843,7 @@ func Test_HandleSINTERCARD(t *testing.T) { expectedError: nil, }, { - name: "5. Return 0 if any of the keys does not exist", - preset: true, + name: "5. Return 0 if any of the keys does not exist", presetValues: map[string]interface{}{ "SinterCardKey11": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterCardKey12": "Default value", @@ -762,8 +854,7 @@ func Test_HandleSINTERCARD(t *testing.T) { expectedError: nil, }, { - name: "6. Throw error when one of the keys is not a valid set.", - preset: true, + name: "6. Throw error when one of the keys is not a valid set.", presetValues: map[string]interface{}{ "SinterCardKey14": "Default value", "SinterCardKey15": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -775,62 +866,85 @@ func Test_HandleSINTERCARD(t *testing.T) { }, { name: "7. Command too short", - preset: false, command: []string{"SINTERCARD"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SINTERCARD, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } + } func Test_HandleSINTERSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -839,8 +953,7 @@ func Test_HandleSINTERSTORE(t *testing.T) { expectedError error }{ { - name: "1. Get the intersection between 2 sets and store it at the destination.", - preset: true, + name: "1. Get the intersection between 2 sets and store it at the destination.", presetValues: map[string]interface{}{ "SinterStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SinterStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -852,8 +965,7 @@ func Test_HandleSINTERSTORE(t *testing.T) { expectedError: nil, }, { - name: "2. Get the intersection between 3 sets and store it at the destination key.", - preset: true, + name: "2. Get the intersection between 3 sets and store it at the destination key.", presetValues: map[string]interface{}{ "SinterStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -866,8 +978,7 @@ func Test_HandleSINTERSTORE(t *testing.T) { expectedError: nil, }, { - name: "3. Throw error when any of the keys is not a set", - preset: true, + name: "3. Throw error when any of the keys is not a set", presetValues: map[string]interface{}{ "SinterStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SinterStoreKey7": "Default value", @@ -880,8 +991,7 @@ func Test_HandleSINTERSTORE(t *testing.T) { expectedError: errors.New("value at key SinterStoreKey7 is not a set"), }, { - name: "4. Throw error when base set is not a set.", - preset: true, + name: "4. Throw error when base set is not a set.", presetValues: map[string]interface{}{ "SinterStoreKey9": "Default value", "SinterStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -895,7 +1005,6 @@ func Test_HandleSINTERSTORE(t *testing.T) { }, { name: "5. Return an empty intersection if one of the keys does not exist.", - preset: true, destination: "SinterStoreDestination5", presetValues: map[string]interface{}{ "SinterStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -908,75 +1017,111 @@ func Test_HandleSINTERSTORE(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"SINTERSTORE", "SinterStoreDestination6"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SINTERSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - currSet, ok := mockServer.GetValue(ctx, test.destination).(*set.Set) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) - } - for _, elem := range currSet.GetAll() { - if !test.expectedValue.Contains(elem) { - t.Errorf("could not find element %s in the expected values", elem) - } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } } func Test_HandleSISMEMBER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string @@ -985,7 +1130,6 @@ func Test_HandleSISMEMBER(t *testing.T) { }{ { name: "1. Return 1 when element is a member of the set", - preset: true, presetValue: set.NewSet([]string{"one", "two", "three", "four"}), key: "SIsMemberKey1", command: []string{"SISMEMBER", "SIsMemberKey1", "three"}, @@ -994,7 +1138,6 @@ func Test_HandleSISMEMBER(t *testing.T) { }, { name: "2. Return 0 when element is not a member of the set", - preset: true, presetValue: set.NewSet([]string{"one", "two", "three", "four"}), key: "SIsMemberKey2", command: []string{"SISMEMBER", "SIsMemberKey2", "five"}, @@ -1003,7 +1146,6 @@ func Test_HandleSISMEMBER(t *testing.T) { }, { name: "3. Throw error when trying to assert membership when the key does not hold a valid set", - preset: true, presetValue: "Default value", key: "SIsMemberKey3", command: []string{"SISMEMBER", "SIsMemberKey3", "one"}, @@ -1012,7 +1154,6 @@ func Test_HandleSISMEMBER(t *testing.T) { }, { name: "4. Command too short", - preset: false, key: "SIsMemberKey4", command: []string{"SISMEMBER", "SIsMemberKey4"}, expectedResponse: 0, @@ -1020,7 +1161,6 @@ func Test_HandleSISMEMBER(t *testing.T) { }, { name: "5. Command too long", - preset: false, key: "SIsMemberKey5", command: []string{"SISMEMBER", "SIsMemberKey5", "one", "two", "three"}, expectedResponse: 0, @@ -1028,52 +1168,77 @@ func Test_HandleSISMEMBER(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SISMEMBER, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected integer response %d, got %d", test.expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleSMEMBERS(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string @@ -1082,7 +1247,6 @@ func Test_HandleSMEMBERS(t *testing.T) { }{ { name: "1. Return all the members of the set.", - preset: true, key: "SmembersKey1", presetValue: set.NewSet([]string{"one", "two", "three", "four", "five"}), command: []string{"SMEMBERS", "SmembersKey1"}, @@ -1091,16 +1255,14 @@ func Test_HandleSMEMBERS(t *testing.T) { }, { name: "2. If the key does not exist, return an empty array.", - preset: false, key: "SmembersKey2", presetValue: nil, command: []string{"SMEMBERS", "SmembersKey2"}, - expectedResponse: []string{}, + expectedResponse: nil, expectedError: nil, }, { name: "3. Throw error when the provided key is not a set.", - preset: true, key: "SmembersKey3", presetValue: "Default value", command: []string{"SMEMBERS", "SmembersKey3"}, @@ -1109,61 +1271,81 @@ func Test_HandleSMEMBERS(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"SMEMBERS"}, expectedResponse: []string{}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Command too long", - preset: false, command: []string{"SMEMBERS", "SmembersKey5", "SmembersKey6"}, expectedResponse: []string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SMEMBERS, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if len(rv.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(rv.Array())) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - for _, responseElement := range rv.Array() { - if !slices.Contains(test.expectedResponse, responseElement.String()) { - t.Errorf("could not find response element \"%s\" from expected response array", responseElement.String()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -1171,9 +1353,14 @@ func Test_HandleSMEMBERS(t *testing.T) { } func Test_HandleSMISMEMBER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string @@ -1186,7 +1373,6 @@ func Test_HandleSMISMEMBER(t *testing.T) { // The placement of the membership status flag should me consistent with the order the elements // are in within the original command name: "1. Return set membership status for multiple elements", - preset: true, presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven"}), key: "SmismemberKey1", command: []string{"SMISMEMBER", "SmismemberKey1", "three", "four", "five", "six", "eight", "nine", "seven"}, @@ -1195,7 +1381,6 @@ func Test_HandleSMISMEMBER(t *testing.T) { }, { name: "2. If the set key does not exist, return an array of zeroes as long as the list of members", - preset: false, presetValue: nil, key: "SmismemberKey2", command: []string{"SMISMEMBER", "SmismemberKey2", "one", "two", "three", "four"}, @@ -1204,7 +1389,6 @@ func Test_HandleSMISMEMBER(t *testing.T) { }, { name: "3. Throw error when trying to assert membership when the key does not hold a valid set", - preset: true, presetValue: "Default value", key: "SmismemberKey3", command: []string{"SMISMEMBER", "SmismemberKey3", "one"}, @@ -1213,7 +1397,7 @@ func Test_HandleSMISMEMBER(t *testing.T) { }, { name: "4. Command too short", - preset: false, + presetValue: nil, key: "SmismemberKey4", command: []string{"SMISMEMBER", "SmismemberKey4"}, expectedResponse: nil, @@ -1221,45 +1405,69 @@ func Test_HandleSMISMEMBER(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SMISMEMBER, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - responseArray := rv.Array() - for i := 0; i < len(responseArray); i++ { - if responseArray[i].Integer() != test.expectedResponse[i] { - t.Errorf("expected integer %d at index %d, got %d", test.expectedResponse[i], i, responseArray[i].Integer()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.Integer()) { + t.Errorf("unexpected element \"%d\" in response", item.Integer()) } } }) @@ -1267,9 +1475,14 @@ func Test_HandleSMISMEMBER(t *testing.T) { } func Test_HandleSMOVE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedValues map[string]interface{} @@ -1277,8 +1490,7 @@ func Test_HandleSMOVE(t *testing.T) { expectedError error }{ { - name: "1. Return 1 after a successful move of a member from source set to destination set", - preset: true, + name: "1. Return 1 after a successful move of a member from source set to destination set", presetValues: map[string]interface{}{ "SmoveSource1": set.NewSet([]string{"one", "two", "three", "four"}), "SmoveDestination1": set.NewSet([]string{"five", "six", "seven", "eight"}), @@ -1292,8 +1504,7 @@ func Test_HandleSMOVE(t *testing.T) { expectedError: nil, }, { - name: "2. Return 0 when trying to move a member from source set to destination set when it doesn't exist in source", - preset: true, + name: "2. Return 0 when trying to move a member from source set to destination set when it doesn't exist in source", presetValues: map[string]interface{}{ "SmoveSource2": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SmoveDestination2": set.NewSet([]string{"five", "six", "seven", "eight"}), @@ -1307,8 +1518,7 @@ func Test_HandleSMOVE(t *testing.T) { expectedError: nil, }, { - name: "3. Return error when the source key is not a set", - preset: true, + name: "3. Return error when the source key is not a set", presetValues: map[string]interface{}{ "SmoveSource3": "Default value", "SmoveDestination3": set.NewSet([]string{"five", "six", "seven", "eight"}), @@ -1322,8 +1532,7 @@ func Test_HandleSMOVE(t *testing.T) { expectedError: errors.New("source is not a set"), }, { - name: "4. Return error when the destination key is not a set", - preset: true, + name: "4. Return error when the destination key is not a set", presetValues: map[string]interface{}{ "SmoveSource4": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SmoveDestination4": "Default value", @@ -1338,88 +1547,133 @@ func Test_HandleSMOVE(t *testing.T) { }, { name: "5. Command too short", - preset: false, + presetValues: nil, command: []string{"SMOVE", "SmoveSource5", "SmoveSource6"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, + presetValues: nil, command: []string{"SMOVE", "SmoveSource5", "SmoveSource6", "member1", "member2"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SMOVE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + // Check if the resulting set(s) contain the expected members. + if test.expectedValues == nil { + return } + for key, value := range test.expectedValues { - expectedSet, ok := value.(*set.Set) - if !ok { - t.Errorf("expected value at \"%s\" should be a set", key) - } - if _, err = mockServer.KeyRLock(ctx, key); err != nil { - t.Error(key) - } - currSet, ok := mockServer.GetValue(ctx, key).(*set.Set) - if !ok { - t.Errorf("expected set \"%s\" to be a set, got another type", key) - } - if expectedSet.Cardinality() != currSet.Cardinality() { - t.Errorf("expected set to have cardinaltity %d, got %d", expectedSet.Cardinality(), currSet.Cardinality()) - } - for _, element := range expectedSet.GetAll() { - if !currSet.Contains(element) { - t.Errorf("could not find element \"%s\" in the expected set", element) + switch value.(type) { + case string: + if err := client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.String() != value.(string) { + t.Errorf("expected value at key \"%s\" to be \"%s\", got \"%s\"", key, value.(string), res.String()) + } + case *set.Set: + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != value.(*set.Set).Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + key, value.(*set.Set).Cardinality(), len(res.Array())) + } + + for _, item := range res.Array() { + if !value.(*set.Set).Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } } } - mockServer.KeyRUnlock(ctx, key) } }) } } func Test_HandleSPOP(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string @@ -1429,7 +1683,6 @@ func Test_HandleSPOP(t *testing.T) { }{ { name: "1. Return multiple popped elements and modify the set", - preset: true, key: "SpopKey1", presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), command: []string{"SPOP", "SpopKey1", "3"}, @@ -1439,100 +1692,125 @@ func Test_HandleSPOP(t *testing.T) { }, { name: "2. Return error when the source key is not a set", - preset: true, key: "SpopKey2", presetValue: "Default value", command: []string{"SPOP", "SpopKey2"}, expectedValue: 0, - expectedResponse: []string{}, + expectedResponse: nil, expectedError: errors.New("value at SpopKey2 is not a set"), }, { - name: "3. Command too short", - preset: false, - command: []string{"SPOP"}, - expectedError: errors.New(constants.WrongArgsResponse), + name: "3. Command too short", + presetValue: nil, + command: []string{"SPOP"}, + expectedValue: 0, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), }, { name: "4. Command too long", - preset: false, + presetValue: nil, command: []string{"SPOP", "SpopSource5", "SpopSource6", "member1", "member2"}, + expectedValue: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Throw error when count is not an integer", - preset: false, + presetValue: nil, command: []string{"SPOP", "SpopKey1", "count"}, + expectedValue: 0, expectedError: errors.New("count must be an integer"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SPOP, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - // 1. Check if the response array members are all included in test.expectedResponse. - for _, element := range rv.Array() { - if !slices.Contains(test.expectedResponse, element.String()) { - t.Errorf("expected response array does not contain element \"%s\"", element.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } + return } - // 2. Fetch the set and check if its cardinality is what we expect. - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) + + // Check that each returned element is in the list of expected elements. + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - currSet, ok := mockServer.GetValue(ctx, test.key).(*set.Set) - if !ok { - t.Errorf("expected value at key \"%s\" to be a set, got another type", test.key) + + // Check if the resulting set's cardinality is as expected. + if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { + t.Error(err) } - if currSet.Cardinality() != test.expectedValue { - t.Errorf("expected cardinality of final set to be %d, got %d", test.expectedValue, currSet.Cardinality()) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - // 3. Check if all the popped elements we received are no longer in the set. - for _, element := range rv.Array() { - if currSet.Contains(element.String()) { - t.Errorf("expected element \"%s\" to not be in set but it was found", element.String()) - } + + if res.Integer() != test.expectedValue { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue, res.Integer()) } }) } } func Test_HandleSRANDMEMBER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string @@ -1545,7 +1823,6 @@ func Test_HandleSRANDMEMBER(t *testing.T) { // 1. Return multiple random elements without removing them // Count is positive, do not allow repeated elements name: "1. Return multiple random elements without removing them", - preset: true, key: "SRandMemberKey1", presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), command: []string{"SRANDMEMBER", "SRandMemberKey1", "3"}, @@ -1558,7 +1835,6 @@ func Test_HandleSRANDMEMBER(t *testing.T) { // 2. Return multiple random elements without removing them // Count is negative, so allow repeated numbers name: "2. Return multiple random elements without removing them", - preset: true, key: "SRandMemberKey2", presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), command: []string{"SRANDMEMBER", "SRandMemberKey2", "-5"}, @@ -1569,7 +1845,6 @@ func Test_HandleSRANDMEMBER(t *testing.T) { }, { name: "3. Return error when the source key is not a set", - preset: true, key: "SRandMemberKey3", presetValue: "Default value", command: []string{"SRANDMEMBER", "SRandMemberKey3"}, @@ -1579,101 +1854,123 @@ func Test_HandleSRANDMEMBER(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"SRANDMEMBER"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Command too long", - preset: false, command: []string{"SRANDMEMBER", "SRandMemberSource5", "SRandMemberSource6", "member1", "member2"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Throw error when count is not an integer", - preset: false, command: []string{"SRANDMEMBER", "SRandMemberKey1", "count"}, expectedError: errors.New("count must be an integer"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SRANDMEMBER, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - // 1. Check if the response array members are all included in test.expectedResponse. - for _, element := range rv.Array() { - if !slices.Contains(test.expectedResponse, element.String()) { - t.Errorf("expected response array does not contain element \"%s\"", element.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } + return } - // 2. Fetch the set and check if its cardinality is what we expect. - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) - } - currSet, ok := mockServer.GetValue(ctx, test.key).(*set.Set) - if !ok { - t.Errorf("expected value at key \"%s\" to be a set, got another type", test.key) - } - if currSet.Cardinality() != test.expectedValue { - t.Errorf("expected cardinality of final set to be %d, got %d", test.expectedValue, currSet.Cardinality()) - } - // 3. Check if all the returned elements we received are still in the set. - for _, element := range rv.Array() { - if !currSet.Contains(element.String()) { - t.Errorf("expected element \"%s\" to be in set but it was not found", element.String()) + + // Check that each returned element is in the list of expected elements. + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } - // 4. If allowRepeat is false, check that all the elements make a valid set + + // If no repeats are allowed, check if the response contains any repeated elements if !test.allowRepeat { - var elems []string - for _, e := range rv.Array() { - elems = append(elems, e.String()) - } - s := set.NewSet(elems) - if s.Cardinality() != len(elems) { - t.Errorf("expected non-repeating elements for random elements at key \"%s\"", test.key) + s := set.NewSet(func() []string { + elements := make([]string, len(res.Array())) + for i, item := range res.Array() { + elements[i] = item.String() + } + return elements + }()) + if s.Cardinality() != len(res.Array()) { + t.Error("response has repeated elements, expected only unique elements.") } } + + // Check if the resulting set's cardinality is as expected. + if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if res.Integer() != test.expectedValue { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue, res.Integer()) + } }) } } func Test_HandleSREM(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string @@ -1683,7 +1980,6 @@ func Test_HandleSREM(t *testing.T) { }{ { name: "1. Remove multiple elements and return the number of elements removed", - preset: true, key: "SremKey1", presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, @@ -1693,7 +1989,6 @@ func Test_HandleSREM(t *testing.T) { }, { name: "2. If key does not exist, return 0", - preset: false, key: "SremKey2", presetValue: nil, command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, @@ -1703,7 +1998,6 @@ func Test_HandleSREM(t *testing.T) { }, { name: "3. Return error when the source key is not a set", - preset: true, key: "SremKey3", presetValue: "Default value", command: []string{"SREM", "SremKey3", "one"}, @@ -1713,81 +2007,113 @@ func Test_HandleSREM(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"SREM", "SremKey"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SREM, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected integer response %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) - } - currSet, ok := mockServer.GetValue(ctx, test.key).(*set.Set) - if !ok { - t.Errorf("expected value at key \"%s\" to be a set, got another type", test.key) - } - for _, element := range currSet.GetAll() { - if !test.expectedValue.Contains(element) { - t.Errorf("element \"%s\" not found in expected set values but found in set", element) - } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } - mockServer.KeyRUnlock(ctx, test.key) } }) } } func Test_HandleSUNION(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse []string expectedError error }{ { - name: "1. Get the union between 2 sets.", - preset: true, + name: "1. Get the union between 2 sets.", presetValues: map[string]interface{}{ "SunionKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SunionKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -1797,8 +2123,7 @@ func Test_HandleSUNION(t *testing.T) { expectedError: nil, }, { - name: "2. Get the union between 3 sets.", - preset: true, + name: "2. Get the union between 3 sets.", presetValues: map[string]interface{}{ "SunionKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SunionKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -1812,8 +2137,7 @@ func Test_HandleSUNION(t *testing.T) { expectedError: nil, }, { - name: "3. Throw an error if any of the provided keys are not sets", - preset: true, + name: "3. Throw an error if any of the provided keys are not sets", presetValues: map[string]interface{}{ "SunionKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SunionKey7": "Default value", @@ -1824,8 +2148,7 @@ func Test_HandleSUNION(t *testing.T) { expectedError: errors.New("value at key SunionKey7 is not a set"), }, { - name: "4. Throw error any of the keys does not hold a set.", - preset: true, + name: "4. Throw error any of the keys does not hold a set.", presetValues: map[string]interface{}{ "SunionKey9": "Default value", "SunionKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), @@ -1837,53 +2160,76 @@ func Test_HandleSUNION(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"SUNION"}, expectedResponse: []string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SUNION, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - for _, responseElement := range rv.Array() { - if !slices.Contains(test.expectedResponse, responseElement.String()) { - t.Errorf("could not find response element \"%s\" from expected response array", responseElement.String()) + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) } } }) @@ -1891,9 +2237,14 @@ func Test_HandleSUNION(t *testing.T) { } func Test_HandleSUNIONSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -1902,8 +2253,7 @@ func Test_HandleSUNIONSTORE(t *testing.T) { expectedError error }{ { - name: "1. Get the intersection between 2 sets and store it at the destination.", - preset: true, + name: "1. Get the intersection between 2 sets and store it at the destination.", presetValues: map[string]interface{}{ "SunionStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), "SunionStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), @@ -1915,8 +2265,7 @@ func Test_HandleSUNIONSTORE(t *testing.T) { expectedError: nil, }, { - name: "2. Get the intersection between 3 sets and store it at the destination key.", - preset: true, + name: "2. Get the intersection between 3 sets and store it at the destination key.", presetValues: map[string]interface{}{ "SunionStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SunionStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), @@ -1932,8 +2281,7 @@ func Test_HandleSUNIONSTORE(t *testing.T) { expectedError: nil, }, { - name: "3. Throw error when any of the keys is not a set", - preset: true, + name: "3. Throw error when any of the keys is not a set", presetValues: map[string]interface{}{ "SunionStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), "SunionStoreKey7": "Default value", @@ -1947,67 +2295,97 @@ func Test_HandleSUNIONSTORE(t *testing.T) { }, { name: "5. Command too short", - preset: false, command: []string{"SUNIONSTORE", "SunionStoreDestination6"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SUNIONSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - currSet, ok := mockServer.GetValue(ctx, test.destination).(*set.Set) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) - } - for _, elem := range currSet.GetAll() { - if !test.expectedValue.Contains(elem) { - t.Errorf("could not find element %s in the expected values", elem) - } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } diff --git a/internal/modules/sorted_set/commands.go b/internal/modules/sorted_set/commands.go index f53c963c..0c6d60e2 100644 --- a/internal/modules/sorted_set/commands.go +++ b/internal/modules/sorted_set/commands.go @@ -33,6 +33,7 @@ func handleZADD(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] var updatePolicy interface{} = nil var comparison interface{} = nil @@ -139,14 +140,9 @@ func handleZADD(params internal.HandlerFuncParams) ([]byte, error) { } } - if params.KeyExists(params.Context, key) { + if keyExists { // Key exists - _, err = params.KeyLock(params.Context, key) - if err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -163,14 +159,9 @@ func handleZADD(params internal.HandlerFuncParams) ([]byte, error) { return []byte(fmt.Sprintf(":%d\r\n", count)), nil } - // Key does not exist - if _, err = params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - + // Key does not exist. set := NewSortedSet(members) - if err = params.SetValue(params.Context, key, set); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: set}); err != nil { return nil, err } @@ -182,18 +173,15 @@ func handleZCARD(params internal.HandlerFuncParams) ([]byte, error) { if err != nil { return nil, err } + key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -208,6 +196,7 @@ func handleZCOUNT(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] minimum := Score(math.Inf(-1)) switch internal.AdaptType(params.Command[2]).(type) { @@ -245,16 +234,11 @@ func handleZCOUNT(params internal.HandlerFuncParams) ([]byte, error) { maximum = Score(s) } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -276,19 +260,15 @@ func handleZLEXCOUNT(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] minimum := params.Command[2] maximum := params.Command[3] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -320,6 +300,8 @@ func handleZDIFF(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(keys.ReadKeys) + withscoresIndex := slices.IndexFunc(params.Command, func(s string) bool { return strings.EqualFold(s, "withscores") }) @@ -327,25 +309,13 @@ func handleZDIFF(params internal.HandlerFuncParams) ([]byte, error) { return nil, errors.New(constants.WrongArgsResponse) } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - // Extract base set - if !params.KeyExists(params.Context, keys.ReadKeys[0]) { + if !keyExists[keys.ReadKeys[0]] { // If base set does not exist, return an empty array return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, keys.ReadKeys[0]); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, keys.ReadKeys[0]) - baseSortedSet, ok := params.GetValue(params.Context, keys.ReadKeys[0]).(*SortedSet) + + baseSortedSet, ok := params.GetValues(params.Context, []string{keys.ReadKeys[0]})[keys.ReadKeys[0]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys.ReadKeys[0]) } @@ -354,15 +324,10 @@ func handleZDIFF(params internal.HandlerFuncParams) ([]byte, error) { var sets []*SortedSet for i := 1; i < len(keys.ReadKeys); i++ { - if !params.KeyExists(params.Context, keys.ReadKeys[i]) { + if !keyExists[keys.ReadKeys[i]] { continue } - locked, err := params.KeyRLock(params.Context, keys.ReadKeys[i]) - if err != nil { - return nil, err - } - locks[keys.ReadKeys[i]] = locked - set, ok := params.GetValue(params.Context, keys.ReadKeys[i]).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{keys.ReadKeys[i]})[keys.ReadKeys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys.ReadKeys[i]) } @@ -376,7 +341,8 @@ func handleZDIFF(params internal.HandlerFuncParams) ([]byte, error) { for _, m := range diff.GetAll() { if includeScores { - res += fmt.Sprintf("\r\n*2\r\n$%d\r\n%s\r\n+%s", len(m.Value), m.Value, strconv.FormatFloat(float64(m.Score), 'f', -1, 64)) + res += fmt.Sprintf("\r\n*2\r\n$%d\r\n%s\r\n+%s", + len(m.Value), m.Value, strconv.FormatFloat(float64(m.Score), 'f', -1, 64)) } else { res += fmt.Sprintf("\r\n*1\r\n$%d\r\n%s", len(m.Value), m.Value) } @@ -393,27 +359,16 @@ func handleZDIFFSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(keys.ReadKeys) destination := keys.WriteKeys[0] - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - // Extract base set - if !params.KeyExists(params.Context, keys.ReadKeys[0]) { + if !keyExists[keys.ReadKeys[0]] { // If base set does not exist, return 0 return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, keys.ReadKeys[0]); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, keys.ReadKeys[0]) - baseSortedSet, ok := params.GetValue(params.Context, keys.ReadKeys[0]).(*SortedSet) + + baseSortedSet, ok := params.GetValues(params.Context, []string{keys.ReadKeys[0]})[keys.ReadKeys[0]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys.ReadKeys[0]) } @@ -421,11 +376,8 @@ func handleZDIFFSTORE(params internal.HandlerFuncParams) ([]byte, error) { var sets []*SortedSet for i := 1; i < len(keys.ReadKeys); i++ { - if params.KeyExists(params.Context, keys.ReadKeys[i]) { - if _, err = params.KeyRLock(params.Context, keys.ReadKeys[i]); err != nil { - return nil, err - } - set, ok := params.GetValue(params.Context, keys.ReadKeys[i]).(*SortedSet) + if keyExists[keys.ReadKeys[i]] { + set, ok := params.GetValues(params.Context, []string{keys.ReadKeys[i]})[keys.ReadKeys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys.ReadKeys[i]) } @@ -434,19 +386,7 @@ func handleZDIFFSTORE(params internal.HandlerFuncParams) ([]byte, error) { } diff := baseSortedSet.Subtract(sets) - - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - defer params.KeyUnlock(params.Context, destination) - - if err = params.SetValue(params.Context, destination, diff); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{destination: diff}); err != nil { return nil, err } @@ -460,6 +400,8 @@ func handleZINCRBY(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] + member := Value(params.Command[3]) var increment Score @@ -482,28 +424,21 @@ func handleZINCRBY(params internal.HandlerFuncParams) ([]byte, error) { increment = Score(s) } - if !params.KeyExists(params.Context, key) { + if !keyExists { // If the key does not exist, create a new sorted set at the key with // the member and increment as the first value - if _, err = params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - if err = params.SetValue( + if err = params.SetValues( params.Context, - key, - NewSortedSet([]MemberParam{{Value: member, Score: increment}}), + map[string]interface{}{ + key: NewSortedSet([]MemberParam{{Value: member, Score: increment}}), + }, ); err != nil { return nil, err } - params.KeyUnlock(params.Context, key) return []byte(fmt.Sprintf("+%s\r\n", strconv.FormatFloat(float64(increment), 'f', -1, 64))), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -530,28 +465,17 @@ func handleZINTER(params internal.HandlerFuncParams) ([]byte, error) { if err != nil { return nil, err } - - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() + keyExists := params.KeysExist(keys) var setParams []SortedSetParam + values := params.GetValues(params.Context, keys) for i := 0; i < len(keys); i++ { - if !params.KeyExists(params.Context, keys[i]) { + if !keyExists[keys[i]] { // If any of the keys is non-existent, return an empty array as there's no intersect return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, keys[i]); err != nil { - return nil, err - } - locks[keys[i]] = true - set, ok := params.GetValue(params.Context, keys[i]).(*SortedSet) + set, ok := values[keys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys[i]) } @@ -586,6 +510,7 @@ func handleZINTERSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(k.ReadKeys) destination := k.WriteKeys[0] // Remove the destination keys from the command before parsing it @@ -598,26 +523,14 @@ func handleZINTERSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() - var setParams []SortedSetParam + values := params.GetValues(params.Context, keys) for i := 0; i < len(keys); i++ { - if !params.KeyExists(params.Context, keys[i]) { + if !keyExists[keys[i]] { return []byte(":0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, keys[i]); err != nil { - return nil, err - } - locks[keys[i]] = true - set, ok := params.GetValue(params.Context, keys[i]).(*SortedSet) + set, ok := values[keys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys[i]) } @@ -628,19 +541,9 @@ func handleZINTERSTORE(params internal.HandlerFuncParams) ([]byte, error) { } intersect := Intersect(aggregate, setParams...) - - if params.KeyExists(params.Context, destination) && intersect.Cardinality() > 0 { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else if intersect.Cardinality() > 0 { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - defer params.KeyUnlock(params.Context, destination) - - if err = params.SetValue(params.Context, destination, intersect); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{ + destination: intersect, + }); err != nil { return nil, err } @@ -653,6 +556,8 @@ func handleZMPOP(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } + keyExists := params.KeysExist(keys.WriteKeys) + count := 1 policy := "min" modifierIdx := -1 @@ -694,21 +599,15 @@ func handleZMPOP(params internal.HandlerFuncParams) ([]byte, error) { } for i := 0; i < len(keys.WriteKeys); i++ { - if params.KeyExists(params.Context, keys.WriteKeys[i]) { - if _, err = params.KeyLock(params.Context, keys.WriteKeys[i]); err != nil { - continue - } - v, ok := params.GetValue(params.Context, keys.WriteKeys[i]).(*SortedSet) + if keyExists[keys.WriteKeys[i]] { + v, ok := params.GetValues(params.Context, []string{keys.WriteKeys[i]})[keys.WriteKeys[i]].(*SortedSet) if !ok || v.Cardinality() == 0 { - params.KeyUnlock(params.Context, keys.WriteKeys[i]) continue } popped, err := v.Pop(count, policy) if err != nil { - params.KeyUnlock(params.Context, keys.WriteKeys[i]) return nil, err } - params.KeyUnlock(params.Context, keys.WriteKeys[i]) res := fmt.Sprintf("*%d", popped.Cardinality()) @@ -732,6 +631,7 @@ func handleZPOP(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] count := 1 policy := "min" @@ -749,16 +649,11 @@ func handleZPOP(params internal.HandlerFuncParams) ([]byte, error) { } } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at key %s is not a sorted set", key) } @@ -770,7 +665,8 @@ func handleZPOP(params internal.HandlerFuncParams) ([]byte, error) { res := fmt.Sprintf("*%d", popped.Cardinality()) for _, m := range popped.GetAll() { - res += fmt.Sprintf("\r\n*2\r\n$%d\r\n%s\r\n+%s", len(m.Value), m.Value, strconv.FormatFloat(float64(m.Score), 'f', -1, 64)) + res += fmt.Sprintf("\r\n*2\r\n$%d\r\n%s\r\n+%s", + len(m.Value), m.Value, strconv.FormatFloat(float64(m.Score), 'f', -1, 64)) } res += "\r\n" @@ -785,17 +681,13 @@ func handleZMSCORE(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -827,6 +719,7 @@ func handleZRANDMEMBER(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] count := 1 if len(params.Command) >= 3 { @@ -848,16 +741,11 @@ func handleZRANDMEMBER(params internal.HandlerFuncParams) ([]byte, error) { } } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -885,6 +773,7 @@ func handleZRANK(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] member := params.Command[2] withscores := false @@ -892,16 +781,11 @@ func handleZRANK(params internal.HandlerFuncParams) ([]byte, error) { withscores = true } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -935,17 +819,13 @@ func handleZREM(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -967,15 +847,13 @@ func handleZSCORE(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("$-1\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - set, ok := params.GetValue(params.Context, key).(*SortedSet) + + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -996,6 +874,7 @@ func handleZREMRANGEBYSCORE(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] deletedCount := 0 @@ -1009,16 +888,11 @@ func handleZREMRANGEBYSCORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -1040,6 +914,7 @@ func handleZREMRANGEBYRANK(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] start, err := strconv.Atoi(params.Command[2]) if err != nil { @@ -1051,16 +926,11 @@ func handleZREMRANGEBYRANK(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -1105,19 +975,15 @@ func handleZREMRANGEBYLEX(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] minimum := params.Command[2] maximum := params.Command[3] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err = params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -1152,6 +1018,8 @@ func handleZRANGE(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] + policy := "byscore" scoreStart := math.Inf(-1) // Lower bound if policy is "byscore" scoreStop := math.Inf(1) // Upper bound if policy is "byscore" @@ -1206,16 +1074,11 @@ func handleZRANGE(params internal.HandlerFuncParams) ([]byte, error) { } } - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - set, ok := params.GetValue(params.Context, key).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{key})[key].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", key) } @@ -1293,6 +1156,7 @@ func handleZRANGESTORE(params internal.HandlerFuncParams) ([]byte, error) { destination := keys.WriteKeys[0] source := keys.ReadKeys[0] + sourceExists := params.KeysExist(keys.ReadKeys)[source] policy := "byscore" scoreStart := math.Inf(-1) // Lower bound if policy is "byscore" scoreStop := math.Inf(1) // Upper bound if policy is "byfloat" @@ -1343,16 +1207,11 @@ func handleZRANGESTORE(params internal.HandlerFuncParams) ([]byte, error) { } } - if !params.KeyExists(params.Context, source) { + if !sourceExists { return []byte("*0\r\n"), nil } - if _, err = params.KeyRLock(params.Context, source); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, source) - - set, ok := params.GetValue(params.Context, source).(*SortedSet) + set, ok := params.GetValues(params.Context, []string{source})[source].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", source) } @@ -1408,19 +1267,9 @@ func handleZRANGESTORE(params internal.HandlerFuncParams) ([]byte, error) { } newSortedSet := NewSortedSet(resultMembers) - - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - defer params.KeyUnlock(params.Context, destination) - - if err = params.SetValue(params.Context, destination, newSortedSet); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{ + destination: newSortedSet, + }); err != nil { return nil, err } @@ -1437,24 +1286,14 @@ func handleZUNION(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() + keyExists := params.KeysExist(keys) var setParams []SortedSetParam + values := params.GetValues(params.Context, keys) for i := 0; i < len(keys); i++ { - if params.KeyExists(params.Context, keys[i]) { - if _, err = params.KeyRLock(params.Context, keys[i]); err != nil { - return nil, err - } - locks[keys[i]] = true - set, ok := params.GetValue(params.Context, keys[i]).(*SortedSet) + if keyExists[keys[i]] { + set, ok := values[keys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys[i]) } @@ -1499,24 +1338,14 @@ func handleZUNIONSTORE(params internal.HandlerFuncParams) ([]byte, error) { return nil, err } - locks := make(map[string]bool) - defer func() { - for key, locked := range locks { - if locked { - params.KeyRUnlock(params.Context, key) - } - } - }() + keyExists := params.KeysExist(keys) var setParams []SortedSetParam + values := params.GetValues(params.Context, keys) for i := 0; i < len(keys); i++ { - if params.KeyExists(params.Context, keys[i]) { - if _, err = params.KeyRLock(params.Context, keys[i]); err != nil { - return nil, err - } - locks[keys[i]] = true - set, ok := params.GetValue(params.Context, keys[i]).(*SortedSet) + if keyExists[keys[i]] { + set, ok := values[keys[i]].(*SortedSet) if !ok { return nil, fmt.Errorf("value at %s is not a sorted set", keys[i]) } @@ -1528,19 +1357,9 @@ func handleZUNIONSTORE(params internal.HandlerFuncParams) ([]byte, error) { } union := Union(aggregate, setParams...) - - if params.KeyExists(params.Context, destination) { - if _, err = params.KeyLock(params.Context, destination); err != nil { - return nil, err - } - } else { - if _, err = params.CreateKeyAndLock(params.Context, destination); err != nil { - return nil, err - } - } - defer params.KeyUnlock(params.Context, destination) - - if err = params.SetValue(params.Context, destination, union); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{ + destination: union, + }); err != nil { return nil, err } diff --git a/internal/modules/sorted_set/commands_test.go b/internal/modules/sorted_set/commands_test.go index b190e125..b420be7e 100644 --- a/internal/modules/sorted_set/commands_test.go +++ b/internal/modules/sorted_set/commands_test.go @@ -15,8 +15,6 @@ package sorted_set_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -27,359 +25,264 @@ import ( "github.com/tidwall/resp" "math" "net" - "reflect" "slices" "strconv" "strings" + "sync" "testing" - "unsafe" ) var mockServer *echovault.EchoVault +var addr = "localhost" +var port int func init() { + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } - } - return nil -} - -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, +func Test_HandleZADD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() } -} + client := resp.NewConn(conn) -func Test_HandleZADD(t *testing.T) { tests := []struct { name string - preset bool presetValue *sorted_set.SortedSet key string command []string - expectedValue *sorted_set.SortedSet expectedResponse int expectedError error }{ { - name: "1. Create new sorted set and return the cardinality of the new sorted set", - preset: false, - presetValue: nil, - key: "ZaddKey1", - command: []string{"ZADD", "ZaddKey1", "5.5", "member1", "67.77", "member2", "10", "member3", "-inf", "member4", "+inf", "member5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - {Value: "member4", Score: sorted_set.Score(math.Inf(-1))}, - {Value: "member5", Score: sorted_set.Score(math.Inf(1))}, - }), + name: "1. Create new sorted set and return the cardinality of the new sorted set", + presetValue: nil, + key: "ZaddKey1", + command: []string{"ZADD", "ZaddKey1", "5.5", "member1", "67.77", "member2", "10", "member3", "-inf", "member4", "+inf", "member5"}, expectedResponse: 5, expectedError: nil, }, { - name: "2. Only add the elements that do not currently exist in the sorted set when NX flag is provided", - preset: true, + name: "2. Only add the elements that do not currently exist in the sorted set when NX flag is provided", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey2", - command: []string{"ZADD", "ZaddKey2", "NX", "5.5", "member1", "67.77", "member4", "10", "member5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - {Value: "member4", Score: sorted_set.Score(67.77)}, - {Value: "member5", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey2", + command: []string{"ZADD", "ZaddKey2", "NX", "5.5", "member1", "67.77", "member4", "10", "member5"}, expectedResponse: 2, expectedError: nil, }, { - name: "Do not add any elements when providing existing members with NX flag", - preset: true, + name: "Do not add any elements when providing existing members with NX flag", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey3", - command: []string{"ZADD", "ZaddKey3", "NX", "5.5", "member1", "67.77", "member2", "10", "member3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey3", + command: []string{"ZADD", "ZaddKey3", "NX", "5.5", "member1", "67.77", "member2", "10", "member3"}, expectedResponse: 0, expectedError: nil, }, { - name: "Successfully add elements to an existing set when XX flag is provided with existing elements", - preset: true, + name: "Successfully add elements to an existing set when XX flag is provided with existing elements", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey4", - command: []string{"ZADD", "ZaddKey4", "XX", "CH", "55", "member1", "1005", "member2", "15", "member3", "99.75", "member4"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(55)}, - {Value: "member2", Score: sorted_set.Score(1005)}, - {Value: "member3", Score: sorted_set.Score(15)}, - }), + key: "ZaddKey4", + command: []string{"ZADD", "ZaddKey4", "XX", "CH", "55", "member1", "1005", "member2", "15", "member3", "99.75", "member4"}, expectedResponse: 3, expectedError: nil, }, { - name: "5. Fail to add element when providing XX flag with elements that do not exist in the sorted set.", - preset: true, + name: "5. Fail to add element when providing XX flag with elements that do not exist in the sorted set.", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey5", - command: []string{"ZADD", "ZaddKey5", "XX", "5.5", "member4", "100.5", "member5", "15", "member6"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey5", + command: []string{"ZADD", "ZaddKey5", "XX", "5.5", "member4", "100.5", "member5", "15", "member6"}, expectedResponse: 0, expectedError: nil, }, { // 6. Only update the elements where provided score is greater than current score and GT flag is provided // Return only the new elements added by default - name: "6. Only update the elements where provided score is greater than current score and GT flag is provided", - preset: true, + name: "6. Only update the elements where provided score is greater than current score and GT flag is provided", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey6", - command: []string{"ZADD", "ZaddKey6", "XX", "CH", "GT", "7.5", "member1", "100.5", "member4", "15", "member5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(7.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey6", + command: []string{"ZADD", "ZaddKey6", "XX", "CH", "GT", "7.5", "member1", "100.5", "member4", "15", "member5"}, expectedResponse: 1, expectedError: nil, }, { // 7. Only update the elements where provided score is less than current score if LT flag is provided // Return only the new elements added by default. - name: "7. Only update the elements where provided score is less than current score if LT flag is provided", - preset: true, + name: "7. Only update the elements where provided score is less than current score if LT flag is provided", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey7", - command: []string{"ZADD", "ZaddKey7", "XX", "LT", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(3.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey7", + command: []string{"ZADD", "ZaddKey7", "XX", "LT", "3.5", "member1", "100.5", "member4", "15", "member5"}, expectedResponse: 0, expectedError: nil, }, { - name: "8. Return all the elements that were updated AND added when CH flag is provided", - preset: true, + name: "8. Return all the elements that were updated AND added when CH flag is provided", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey8", - command: []string{"ZADD", "ZaddKey8", "XX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(3.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), + key: "ZaddKey8", + command: []string{"ZADD", "ZaddKey8", "XX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, expectedResponse: 1, expectedError: nil, }, { - name: "9. Increment the member by score", - preset: true, + name: "9. Increment the member by score", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, {Value: "member3", Score: sorted_set.Score(10)}, }), - key: "ZaddKey9", - command: []string{"ZADD", "ZaddKey9", "INCR", "5.5", "member3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(15.5)}, - }), + key: "ZaddKey9", + command: []string{"ZADD", "ZaddKey9", "INCR", "5.5", "member3"}, expectedResponse: 0, expectedError: nil, }, { name: "10. Fail when GT/LT flag is provided alongside NX flag", - preset: false, presetValue: nil, key: "ZaddKey10", command: []string{"ZADD", "ZaddKey10", "NX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("GT/LT flags not allowed if NX flag is provided"), }, { name: "11. Command is too short", - preset: false, presetValue: nil, key: "ZaddKey11", command: []string{"ZADD", "ZaddKey11"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "12. Throw error when score/member entries are do not match", - preset: false, presetValue: nil, key: "ZaddKey11", command: []string{"ZADD", "ZaddKey12", "10.5", "member1", "12.5"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("score/member pairs must be float/string"), }, { name: "13. Throw error when INCR flag is passed with more than one score/member pair", - preset: false, presetValue: nil, key: "ZaddKey13", command: []string{"ZADD", "ZaddKey13", "INCR", "10.5", "member1", "12.5", "member2"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("cannot pass more than one score/member pair when INCR flag is provided"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZADD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if res.Integer() != test.presetValue.Cardinality() { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d at key \"%s\", got %d", test.expectedResponse, test.key, rv.Integer()) - } - // Fetch the sorted set from the echovault and check it against the expected result - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) - } - sortedSet, ok := mockServer.GetValue(ctx, test.key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected the value at key \"%s\" to be a sorted set, got another type", test.key) - } - if test.expectedValue == nil { + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } return } - if !sortedSet.Equals(test.expectedValue) { - t.Errorf("expected sorted set %+v, got %+v", test.expectedValue, sortedSet) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleZCARD(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string - expectedValue *sorted_set.SortedSet expectedResponse int expectedError error }{ { - name: "1. Get cardinality of valid sorted set.", - preset: true, + name: "1. Get cardinality of valid sorted set.", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, @@ -387,109 +290,125 @@ func Test_HandleZCARD(t *testing.T) { }), key: "ZcardKey1", command: []string{"ZCARD", "ZcardKey1"}, - expectedValue: nil, expectedResponse: 3, expectedError: nil, }, { name: "2. Return 0 when trying to get cardinality from non-existent key", - preset: false, presetValue: nil, key: "ZcardKey2", command: []string{"ZCARD", "ZcardKey2"}, - expectedValue: nil, expectedResponse: 0, expectedError: nil, }, { name: "3. Command is too short", - preset: false, presetValue: nil, key: "ZcardKey3", command: []string{"ZCARD"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, - { // + { name: "4. Command too long", - preset: false, presetValue: nil, key: "ZcardKey4", command: []string{"ZCARD", "ZcardKey4", "ZcardKey5"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Return error when not a sorted set", - preset: true, presetValue: "Default value", key: "ZcardKey5", command: []string{"ZCARD", "ZcardKey5"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("value at ZcardKey5 is not a sorted set"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZCARD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d at key \"%s\", got %d", test.expectedResponse, test.key, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleZCOUNT(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string - expectedValue *sorted_set.SortedSet expectedResponse int expectedError error }{ { - name: "1. Get entire count using infinity boundaries", - preset: true, + name: "1. Get entire count using infinity boundaries", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, @@ -501,13 +420,11 @@ func Test_HandleZCOUNT(t *testing.T) { }), key: "ZcountKey1", command: []string{"ZCOUNT", "ZcountKey1", "-inf", "+inf"}, - expectedValue: nil, expectedResponse: 7, expectedError: nil, }, { - name: "2. Get count of sub-set from -inf to limit", - preset: true, + name: "2. Get count of sub-set from -inf to limit", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, @@ -519,13 +436,11 @@ func Test_HandleZCOUNT(t *testing.T) { }), key: "ZcountKey2", command: []string{"ZCOUNT", "ZcountKey2", "-inf", "90"}, - expectedValue: nil, expectedResponse: 5, expectedError: nil, }, { - name: "3. Get count of sub-set from bottom boundary to +inf limit", - preset: true, + name: "3. Get count of sub-set from bottom boundary to +inf limit", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "member1", Score: sorted_set.Score(5.5)}, {Value: "member2", Score: sorted_set.Score(67.77)}, @@ -537,119 +452,133 @@ func Test_HandleZCOUNT(t *testing.T) { }), key: "ZcountKey3", command: []string{"ZCOUNT", "ZcountKey3", "1000", "+inf"}, - expectedValue: nil, expectedResponse: 2, expectedError: nil, }, { name: "4. Return error when bottom boundary is not a valid double/float", - preset: false, presetValue: nil, key: "ZcountKey4", command: []string{"ZCOUNT", "ZcountKey4", "min", "10"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("min constraint must be a double"), }, { name: "5. Return error when top boundary is not a valid double/float", - preset: false, presetValue: nil, key: "ZcountKey5", command: []string{"ZCOUNT", "ZcountKey5", "-10", "max"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("max constraint must be a double"), }, { name: "6. Command is too short", - preset: false, presetValue: nil, key: "ZcountKey6", command: []string{"ZCOUNT"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "7. Command too long", - preset: false, presetValue: nil, key: "ZcountKey7", command: []string{"ZCOUNT", "ZcountKey4", "min", "max", "count"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "8. Throw error when value at the key is not a sorted set", - preset: true, presetValue: "Default value", key: "ZcountKey8", command: []string{"ZCOUNT", "ZcountKey8", "1", "10"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("value at ZcountKey8 is not a sorted set"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZCARD, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d at key \"%s\", got %d", test.expectedResponse, test.key, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleZLEXCOUNT(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string - expectedValue *sorted_set.SortedSet expectedResponse int expectedError error }{ { - name: "1. Get entire count using infinity boundaries", - preset: true, + name: "1. Get entire count using infinity boundaries", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "e", Score: sorted_set.Score(1)}, {Value: "f", Score: sorted_set.Score(1)}, @@ -661,13 +590,11 @@ func Test_HandleZLEXCOUNT(t *testing.T) { }), key: "ZlexCountKey1", command: []string{"ZLEXCOUNT", "ZlexCountKey1", "f", "j"}, - expectedValue: nil, expectedResponse: 5, expectedError: nil, }, { - name: "2. Return 0 when the members do not have the same score", - preset: true, + name: "2. Return 0 when the members do not have the same score", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: sorted_set.Score(5.5)}, {Value: "b", Score: sorted_set.Score(67.77)}, @@ -679,107 +606,124 @@ func Test_HandleZLEXCOUNT(t *testing.T) { }), key: "ZlexCountKey2", command: []string{"ZLEXCOUNT", "ZlexCountKey2", "a", "b"}, - expectedValue: nil, expectedResponse: 0, expectedError: nil, }, { name: "3. Return 0 when the key does not exist", - preset: false, presetValue: nil, key: "ZlexCountKey3", command: []string{"ZLEXCOUNT", "ZlexCountKey3", "a", "z"}, - expectedValue: nil, expectedResponse: 0, expectedError: nil, }, { name: "4. Return error when the value at the key is not a sorted set", - preset: true, presetValue: "Default value", key: "ZlexCountKey4", command: []string{"ZLEXCOUNT", "ZlexCountKey4", "a", "z"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New("value at ZlexCountKey4 is not a sorted set"), }, { name: "5. Command is too short", - preset: false, presetValue: nil, key: "ZlexCountKey5", command: []string{"ZLEXCOUNT"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, presetValue: nil, key: "ZlexCountKey6", command: []string{"ZLEXCOUNT", "ZlexCountKey6", "min", "max", "count"}, - expectedValue: nil, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZLEXCOUNT, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d at key \"%s\", got %d", test.expectedResponse, test.key, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleZDIFF(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse [][]string expectedError error }{ { - name: "1. Get the difference between 2 sorted sets without scores.", - preset: true, + name: "1. Get the difference between 2 sorted sets without scores.", presetValues: map[string]interface{}{ "ZdiffKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, @@ -801,16 +745,15 @@ func Test_HandleZDIFF(t *testing.T) { expectedError: nil, }, { - name: "2. Get the difference between 2 sorted sets with scores.", - preset: true, + name: "2. Get the difference between 2 sorted sets with scores.", presetValues: map[string]interface{}{ - "ZdiffKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, }), - "ZdiffKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, @@ -819,47 +762,45 @@ func Test_HandleZDIFF(t *testing.T) { {Value: "eight", Score: 8}, }), }, - command: []string{"ZDIFF", "ZdiffKey1", "ZdiffKey2", "WITHSCORES"}, + command: []string{"ZDIFF", "ZdiffKey3", "ZdiffKey4", "WITHSCORES"}, expectedResponse: [][]string{{"one", "1"}, {"two", "2"}}, expectedError: nil, }, { - name: "3. Get the difference between 3 sets with scores.", - preset: true, + name: "3. Get the difference between 3 sets with scores.", presetValues: map[string]interface{}{ - "ZdiffKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, }), - "ZdiffKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, {Value: "eleven", Score: 11}, }), - "ZdiffKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "twelve", Score: 12}, }), }, - command: []string{"ZDIFF", "ZdiffKey3", "ZdiffKey4", "ZdiffKey5", "WITHSCORES"}, + command: []string{"ZDIFF", "ZdiffKey5", "ZdiffKey6", "ZdiffKey7", "WITHSCORES"}, expectedResponse: [][]string{{"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, expectedError: nil, }, { - name: "4. Return sorted set if only one key exists and is a sorted set", - preset: true, + name: "4. Return sorted set if only one key exists and is a sorted set", presetValues: map[string]interface{}{ - "ZdiffKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, }), }, - command: []string{"ZDIFF", "ZdiffKey6", "ZdiffKey7", "ZdiffKey8", "WITHSCORES"}, + command: []string{"ZDIFF", "ZdiffKey8", "ZdiffKey9", "ZdiffKey10", "WITHSCORES"}, expectedResponse: [][]string{ {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, {"seven", "7"}, {"eight", "8"}, @@ -867,86 +808,115 @@ func Test_HandleZDIFF(t *testing.T) { expectedError: nil, }, { - name: "5. Throw error when one of the keys is not a sorted set.", - preset: true, + name: "5. Throw error when one of the keys is not a sorted set.", presetValues: map[string]interface{}{ - "ZdiffKey9": "Default value", - "ZdiffKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey11": "Default value", + "ZdiffKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, {Value: "eleven", Score: 11}, }), - "ZdiffKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + "ZdiffKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "twelve", Score: 12}, }), }, - command: []string{"ZDIFF", "ZdiffKey9", "ZdiffKey10", "ZdiffKey11"}, + command: []string{"ZDIFF", "ZdiffKey11", "ZdiffKey12", "ZdiffKey13"}, expectedResponse: nil, - expectedError: errors.New("value at ZdiffKey9 is not a sorted set"), + expectedError: errors.New("value at ZdiffKey11 is not a sorted set"), }, { name: "6. Command too short", - preset: false, command: []string{"ZDIFF"}, expectedResponse: [][]string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZDIFF, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } }) @@ -954,9 +924,14 @@ func Test_HandleZDIFF(t *testing.T) { } func Test_HandleZDIFFSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -965,8 +940,7 @@ func Test_HandleZDIFFSTORE(t *testing.T) { expectedError error }{ { - name: "1. Get the difference between 2 sorted sets.", - preset: true, + name: "1. Get the difference between 2 sorted sets.", presetValues: map[string]interface{}{ "ZdiffStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -986,8 +960,7 @@ func Test_HandleZDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "2. Get the difference between 3 sorted sets.", - preset: true, + name: "2. Get the difference between 3 sorted sets.", presetValues: map[string]interface{}{ "ZdiffStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -1016,8 +989,7 @@ func Test_HandleZDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "3. Return base sorted set element if base set is the only existing key provided and is a valid sorted set", - preset: true, + name: "3. Return base sorted set element if base set is the only existing key provided and is a valid sorted set", presetValues: map[string]interface{}{ "ZdiffStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -1038,8 +1010,7 @@ func Test_HandleZDIFFSTORE(t *testing.T) { expectedError: nil, }, { - name: "4. Throw error when base sorted set is not a set.", - preset: true, + name: "4. Throw error when base sorted set is not a set.", presetValues: map[string]interface{}{ "ZdiffStoreKey9": "Default value", "ZdiffStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ @@ -1060,8 +1031,7 @@ func Test_HandleZDIFFSTORE(t *testing.T) { expectedError: errors.New("value at ZdiffStoreKey9 is not a sorted set"), }, { - name: "5. Throw error when base set is non-existent.", - preset: true, + name: "5. Return 0 when base set is non-existent.", destination: "ZdiffStoreDestinationKey5", presetValues: map[string]interface{}{ "ZdiffStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ @@ -1082,77 +1052,124 @@ func Test_HandleZDIFFSTORE(t *testing.T) { }, { name: "6. Command too short", - preset: false, command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey6"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZDIFFSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.destination), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - set, ok := mockServer.GetValue(ctx, test.destination).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } - for _, elem := range set.GetAll() { - if !test.expectedValue.Contains(elem.Value) { - t.Errorf("could not find element %s in the expected values", elem.Value) - } - } - mockServer.KeyRUnlock(ctx, test.destination) } }) } } func Test_HandleZINCRBY(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValue interface{} key string command []string @@ -1161,8 +1178,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError error }{ { - name: "1. Successfully increment by int. Return the new score", - preset: true, + name: "1. Successfully increment by int. Return the new score", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1179,8 +1195,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: nil, }, { - name: "2. Successfully increment by float. Return new score", - preset: true, + name: "2. Successfully increment by float. Return new score", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1198,7 +1213,6 @@ func Test_HandleZINCRBY(t *testing.T) { }, { name: "3. Increment on non-existent sorted set will create the set with the member and increment as its score", - preset: false, presetValue: nil, key: "ZincrbyKey3", command: []string{"ZINCRBY", "ZincrbyKey3", "346.785", "one"}, @@ -1209,8 +1223,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: nil, }, { - name: "4. Increment score to +inf", - preset: true, + name: "4. Increment score to +inf", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1227,8 +1240,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: nil, }, { - name: "5. Increment score to -inf", - preset: true, + name: "5. Increment score to -inf", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1245,8 +1257,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: nil, }, { - name: "6. Incrementing score by negative increment should lower the score", - preset: true, + name: "6. Incrementing score by negative increment should lower the score", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1264,7 +1275,6 @@ func Test_HandleZINCRBY(t *testing.T) { }, { name: "7. Return error when attempting to increment on a value that is not a valid sorted set", - preset: true, presetValue: "Default value", key: "ZincrbyKey7", command: []string{"ZINCRBY", "ZincrbyKey7", "-2.5", "five"}, @@ -1273,8 +1283,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: errors.New("value at ZincrbyKey7 is not a sorted set"), }, { - name: "8. Return error when trying to increment a member that already has score -inf", - preset: true, + name: "8. Return error when trying to increment a member that already has score -inf", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, }), @@ -1287,8 +1296,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: errors.New("cannot increment -inf or +inf"), }, { - name: "9. Return error when trying to increment a member that already has score +inf", - preset: true, + name: "9. Return error when trying to increment a member that already has score +inf", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: sorted_set.Score(math.Inf(1))}, }), @@ -1301,8 +1309,7 @@ func Test_HandleZINCRBY(t *testing.T) { expectedError: errors.New("cannot increment -inf or +inf"), }, { - name: "10. Return error when increment is not a valid number", - preset: true, + name: "10. Return error when increment is not a valid number", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, }), @@ -1330,71 +1337,115 @@ func Test_HandleZINCRBY(t *testing.T) { }, } - for i, test := range tests { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZINCRBY, %d", i)) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return - } - - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - continue - } - if err != nil { - t.Error(err) - } - - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected response integer %s, got %s", test.expectedResponse, rv.String()) - } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { + + res, _, err = client.ReadValue() + if err != nil { t.Error(err) } - set, ok := mockServer.GetValue(ctx, test.key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.key) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) } - for _, elem := range set.GetAll() { - if !test.expectedValue.Contains(elem.Value) { - t.Errorf("could not find element %s in the expected values", elem.Value) + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if test.expectedValue.Get(elem.Value).Score != elem.Score { - t.Errorf("expected score of element \"%s\" from set at key \"%s\" to be %s, got %s", - elem.Value, test.key, - strconv.FormatFloat(float64(test.expectedValue.Get(elem.Value).Score), 'f', -1, 64), - strconv.FormatFloat(float64(elem.Score), 'f', -1, 64), - ) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } } - mockServer.KeyRUnlock(ctx, test.key) - } + }) } } func Test_HandleZMPOP(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string preset bool @@ -1517,7 +1568,6 @@ func Test_HandleZMPOP(t *testing.T) { name: "6. Successfully pop elements from the first set which is non-empty", preset: true, presetValues: map[string]interface{}{ - "ZmpopKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), "ZmpopKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1538,9 +1588,8 @@ func Test_HandleZMPOP(t *testing.T) { name: "7. Skip the non-set items and pop elements from the first non-empty sorted set found", preset: true, presetValues: map[string]interface{}{ - "ZmpopKey8": "Default value", - "ZmpopKey9": 56, - "ZmpopKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), + "ZmpopKey8": "Default value", + "ZmpopKey9": "56", "ZmpopKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, @@ -1571,70 +1620,129 @@ func Test_HandleZMPOP(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZMPOP, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } + + // Check if the resulting sorted set has the expected members/scores for key, expectedSortedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected key \"%s\" to be a sorted set, got another type", key) + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if !set.Equals(expectedSortedSet) { - t.Errorf("expected sorted set at key \"%s\" %+v, got %+v", key, expectedSortedSet, set) + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } }) @@ -1642,6 +1750,12 @@ func Test_HandleZMPOP(t *testing.T) { } func Test_HandleZPOP(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string preset bool @@ -1762,70 +1876,129 @@ func Test_HandleZPOP(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZPOPMIN/ZPOPMAX, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } + + // Check if the resulting sorted set has the expected members/scores for key, expectedSortedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected key \"%s\" to be a sorted set, got another type", key) + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - if !set.Equals(expectedSortedSet) { - t.Errorf("expected sorted set at key \"%s\" %+v, got %+v", key, expectedSortedSet, set) + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } }) @@ -1833,19 +2006,23 @@ func Test_HandleZPOP(t *testing.T) { } func Test_HandleZMSCORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string - expectedResponse []interface{} + expectedResponse []string expectedError error }{ { // 1. Return multiple scores from the sorted set. // Return nil for elements that do not exist in the sorted set. - name: "Return multiple scores from the sorted set.", - preset: true, + name: "1. Return multiple scores from the sorted set.", presetValues: map[string]interface{}{ "ZmScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, @@ -1854,79 +2031,97 @@ func Test_HandleZMSCORE(t *testing.T) { }), }, command: []string{"ZMSCORE", "ZmScoreKey1", "one", "none", "two", "one", "three", "four", "none", "five"}, - expectedResponse: []interface{}{"1.1", nil, "245", "1.1", "3", "4.055", nil, "5"}, + expectedResponse: []string{"1.1", "", "245", "1.1", "3", "4.055", "", "5"}, expectedError: nil, }, { name: "2. If key does not exist, return empty array", - preset: false, presetValues: nil, command: []string{"ZMSCORE", "ZmScoreKey2", "one", "two", "three", "four"}, - expectedResponse: []interface{}{}, + expectedResponse: []string{}, expectedError: nil, }, { name: "3. Throw error when trying to find scores from elements that are not sorted sets", - preset: true, presetValues: map[string]interface{}{"ZmScoreKey3": "Default value"}, command: []string{"ZMSCORE", "ZmScoreKey3", "one", "two", "three"}, expectedError: errors.New("value at ZmScoreKey3 is not a sorted set"), }, { name: "9. Command too short", - preset: false, command: []string{"ZMSCORE"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZMSCORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - for i := 0; i < len(rv.Array()); i++ { - if rv.Array()[i].IsNull() { - if test.expectedResponse[i] != nil { - t.Errorf("expected element at index %d to be %+v, got %+v", i, test.expectedResponse[i], rv.Array()[i]) - } - continue + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } - if rv.Array()[i].String() != test.expectedResponse[i] { - t.Errorf("expected \"%s\" at index %d, got %s", test.expectedResponse[i], i, rv.Array()[i].String()) + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } + + for i := 0; i < len(res.Array()); i++ { + if test.expectedResponse[i] != res.Array()[i].String() { + t.Errorf("expected element at index %d to be \"%s\", got %s", + i, test.expectedResponse[i], res.Array()[i].String()) } } }) @@ -1934,17 +2129,21 @@ func Test_HandleZMSCORE(t *testing.T) { } func Test_HandleZSCORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string - expectedResponse interface{} + expectedResponse string expectedError error }{ { - name: "1. Return score from a sorted set.", - preset: true, + name: "1. Return score from a sorted set.", presetValues: map[string]interface{}{ "ZscoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, @@ -1958,15 +2157,13 @@ func Test_HandleZSCORE(t *testing.T) { }, { name: "2. If key does not exist, return nil value", - preset: false, presetValues: nil, command: []string{"ZSCORE", "ZscoreKey2", "one"}, - expectedResponse: nil, + expectedResponse: "", expectedError: nil, }, { - name: "3. If key exists and is a sorted set, but the member does not exist, return nil", - preset: true, + name: "3. If key exists and is a sorted set, but the member does not exist, return nil", presetValues: map[string]interface{}{ "ZscoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, @@ -1975,85 +2172,103 @@ func Test_HandleZSCORE(t *testing.T) { }), }, command: []string{"ZSCORE", "ZscoreKey3", "non-existent"}, - expectedResponse: nil, + expectedResponse: "", expectedError: nil, }, { name: "4. Throw error when trying to find scores from elements that are not sorted sets", - preset: true, presetValues: map[string]interface{}{"ZscoreKey4": "Default value"}, command: []string{"ZSCORE", "ZscoreKey4", "one"}, expectedError: errors.New("value at ZscoreKey4 is not a sorted set"), }, { name: "5. Command too short", - preset: false, command: []string{"ZSCORE"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, command: []string{"ZSCORE", "ZscoreKey5", "one", "two"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZSCORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if test.expectedResponse == nil { - if !rv.IsNull() { - t.Errorf("expected nil response, got %+v", rv) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if rv.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got %s", test.expectedResponse, rv.String()) + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } }) } } func Test_HandleZRANDMEMBER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue interface{} command []string @@ -2065,9 +2280,8 @@ func Test_HandleZRANDMEMBER(t *testing.T) { { // 1. Return multiple random elements without removing them. // Count is positive, do not allow repeated elements - name: "1. Return multiple random elements without removing them.", - preset: true, - key: "ZrandMemberKey1", + name: "1. Return multiple random elements without removing them.", + key: "ZrandMemberKey1", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, @@ -2084,9 +2298,8 @@ func Test_HandleZRANDMEMBER(t *testing.T) { { // 2. Return multiple random elements and their scores without removing them. // Count is negative, so allow repeated numbers. - name: "2. Return multiple random elements and their scores without removing them.", - preset: true, - key: "ZrandMemberKey2", + name: "2. Return multiple random elements and their scores without removing them.", + key: "ZrandMemberKey2", presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, @@ -2102,7 +2315,6 @@ func Test_HandleZRANDMEMBER(t *testing.T) { }, { name: "2. Return error when the source key is not a sorted set.", - preset: true, key: "ZrandMemberKey3", presetValue: "Default value", command: []string{"ZRANDMEMBER", "ZrandMemberKey3"}, @@ -2111,119 +2323,129 @@ func Test_HandleZRANDMEMBER(t *testing.T) { }, { name: "5. Command too short", - preset: false, command: []string{"ZRANDMEMBER"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, command: []string{"ZRANDMEMBER", "source5", "source6", "member1", "member2"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "7. Throw error when count is not an integer", - preset: false, command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "count"}, expectedError: errors.New("count must be an integer"), }, { name: "8. Throw error when the fourth argument is not WITHSCORES", - preset: false, command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "8", "ANOTHER"}, expectedError: errors.New("last option must be WITHSCORES"), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZRANDMEMBER, %d", i)) + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - // 1. Check if the response array members are all included in test.expectedResponse. - for _, element := range rv.Array() { + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + // Check that each of the returned elements is in the expected response. + for _, item := range res.Array() { + value := sorted_set.Value(item.Array()[0].String()) if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + return expected[0] == string(value) + }) { + t.Errorf("unexected element \"%s\" in response", value) + } + for _, expected := range test.expectedResponse { + if len(item.Array()) != len(expected) { + t.Errorf("expected response for element \"%s\" to have length %d, got %d", + value, len(expected), len(item.Array())) + } + if expected[0] != string(value) { + continue } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + if len(expected) == 2 { + score := item.Array()[1].String() + if expected[1] != score { + t.Errorf("expected score for memebr \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) - } - } - // 2. Fetch the set and check if its cardinality is what we expect. - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, test.key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected value at key \"%s\" to be a set, got another type", test.key) - } - if set.Cardinality() != test.expectedValue { - t.Errorf("expected cardinality of final set to be %d, got %d", test.expectedValue, set.Cardinality()) - } - // 3. Check if all the returned elements we received are still in the set. - for _, element := range rv.Array() { - if !set.Contains(sorted_set.Value(element.Array()[0].String())) { - t.Errorf("expected element \"%s\" to be in set but it was not found", element.String()) } } - // 4. If allowRepeat is false, check that all the elements make a valid set + + // Check that allowRepeat determines whether elements are repeated or not. if !test.allowRepeat { - var elems []sorted_set.MemberParam - for _, e := range rv.Array() { - if len(e.Array()) == 1 { - elems = append(elems, sorted_set.MemberParam{ - Value: sorted_set.Value(e.Array()[0].String()), - Score: 1, - }) - continue + ss := sorted_set.NewSortedSet([]sorted_set.MemberParam{}) + for _, item := range res.Array() { + member := sorted_set.Value(item.Array()[0].String()) + score := func() sorted_set.Score { + if len(item.Array()) == 2 { + return sorted_set.Score(item.Array()[1].Float()) + } + return sorted_set.Score(0) + }() + _, err = ss.AddOrUpdate( + []sorted_set.MemberParam{{member, score}}, + nil, nil, nil, nil) + if err != nil { + t.Error(err) } - elems = append(elems, sorted_set.MemberParam{ - Value: sorted_set.Value(e.Array()[0].String()), - Score: sorted_set.Score(e.Array()[1].Float()), - }) } - s := sorted_set.NewSortedSet(elems) - if s.Cardinality() != len(elems) { - t.Errorf("expected non-repeating elements for random elements at key \"%s\"", test.key) + if len(res.Array()) != ss.Cardinality() { + t.Error("unexpected repeated elements in response") } } }) @@ -2231,17 +2453,21 @@ func Test_HandleZRANDMEMBER(t *testing.T) { } func Test_HandleZRANK(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse []string expectedError error }{ { - name: "1. Return element's rank from a sorted set.", - preset: true, + name: "1. Return element's rank from a sorted set.", presetValues: map[string]interface{}{ "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2254,8 +2480,7 @@ func Test_HandleZRANK(t *testing.T) { expectedError: nil, }, { - name: "2. Return element's rank from a sorted set with its score.", - preset: true, + name: "2. Return element's rank from a sorted set with its score.", presetValues: map[string]interface{}{ "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100.1}, {Value: "two", Score: 245}, @@ -2269,15 +2494,13 @@ func Test_HandleZRANK(t *testing.T) { }, { name: "3. If key does not exist, return nil value", - preset: false, presetValues: nil, command: []string{"ZRANK", "ZrankKey3", "one"}, expectedResponse: nil, expectedError: nil, }, { - name: "4. If key exists and is a sorted set, but the member does not exist, return nil", - preset: true, + name: "4. If key exists and is a sorted set, but the member does not exist, return nil", presetValues: map[string]interface{}{ "ZrankKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, @@ -2291,75 +2514,90 @@ func Test_HandleZRANK(t *testing.T) { }, { name: "5. Throw error when trying to find scores from elements that are not sorted sets", - preset: true, presetValues: map[string]interface{}{"ZrankKey5": "Default value"}, command: []string{"ZRANK", "ZrankKey5", "one"}, expectedError: errors.New("value at ZrankKey5 is not a sorted set"), }, { name: "5. Command too short", - preset: false, command: []string{"ZRANK"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "6. Command too long", - preset: false, command: []string{"ZRANK", "ZrankKey5", "one", "WITHSCORES", "two"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZRANK, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if test.expectedResponse == nil { - if !rv.IsNull() { - t.Errorf("expected nil response, got %+v", rv) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if len(rv.Array()) != len(test.expectedResponse) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for i := 0; i < len(test.expectedResponse); i++ { - if rv.Array()[i].String() != test.expectedResponse[i] { - t.Errorf("expected element at index %d to be %s, got %s", i, test.expectedResponse[i], rv.Array()[i].String()) + + for i := 0; i < len(res.Array()); i++ { + if test.expectedResponse[i] != res.Array()[i].String() { + t.Errorf("expected element at index %d to be \"%s\", got %s", + i, test.expectedResponse[i], res.Array()[i].String()) } } }) @@ -2367,9 +2605,14 @@ func Test_HandleZRANK(t *testing.T) { } func Test_HandleZREM(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedValues map[string]*sorted_set.SortedSet @@ -2379,8 +2622,7 @@ func Test_HandleZREM(t *testing.T) { { // Successfully remove multiple elements from sorted set, skipping non-existent members. // Return deleted count. - name: "1. Successfully remove multiple elements from sorted set, skipping non-existent members.", - preset: true, + name: "1. Successfully remove multiple elements from sorted set, skipping non-existent members.", presetValues: map[string]interface{}{ "ZremKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2402,7 +2644,6 @@ func Test_HandleZREM(t *testing.T) { }, { name: "2. If key does not exist, return 0", - preset: false, presetValues: nil, command: []string{"ZREM", "ZremKey2", "member"}, expectedValues: nil, @@ -2410,8 +2651,7 @@ func Test_HandleZREM(t *testing.T) { expectedError: nil, }, { - name: "3. Return error key is not a sorted set", - preset: true, + name: "3. Return error key is not a sorted set", presetValues: map[string]interface{}{ "ZremKey3": "Default value", }, @@ -2420,65 +2660,111 @@ func Test_HandleZREM(t *testing.T) { }, { name: "9. Command too short", - preset: false, command: []string{"ZREM"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZREM, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - // Check if the expected sorted set is the same at the current one - if test.expectedValues != nil { - for key, expectedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected value at key \"%s\" to be a sorted set, got another type", key) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if !set.Equals(expectedSet) { - t.Errorf("exptected sorted set %+v, got %+v", expectedSet, set) + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) } } } @@ -2487,9 +2773,14 @@ func Test_HandleZREM(t *testing.T) { } func Test_HandleZREMRANGEBYSCORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedValues map[string]*sorted_set.SortedSet @@ -2497,8 +2788,7 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { expectedError error }{ { - name: "1. Successfully remove multiple elements with scores inside the provided range", - preset: true, + name: "1. Successfully remove multiple elements with scores inside the provided range", presetValues: map[string]interface{}{ "ZremRangeByScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2520,7 +2810,6 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { }, { name: "2. If key does not exist, return 0", - preset: false, presetValues: nil, command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey2", "2", "4"}, expectedValues: nil, @@ -2528,8 +2817,7 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { expectedError: nil, }, { - name: "3. Return error key is not a sorted set", - preset: true, + name: "3. Return error key is not a sorted set", presetValues: map[string]interface{}{ "ZremRangeByScoreKey3": "Default value", }, @@ -2538,71 +2826,116 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey4", "3"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Command too long", - preset: false, command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey5", "4", "5", "8"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZREMRANGEBYSCORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) } - // Check if the expected values are the same - if test.expectedValues != nil { - for key, expectedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected value at key \"%s\" to be a sorted set, got another type", key) + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if !set.Equals(expectedSet) { - t.Errorf("exptected sorted set %+v, got %+v", expectedSet, set) + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) } } } @@ -2611,9 +2944,14 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { } func Test_HandleZREMRANGEBYRANK(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedValues map[string]*sorted_set.SortedSet @@ -2621,8 +2959,7 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { expectedError error }{ { - name: "1. Successfully remove multiple elements within range", - preset: true, + name: "1. Successfully remove multiple elements within range", presetValues: map[string]interface{}{ "ZremRangeByRankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2643,8 +2980,7 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { expectedError: nil, }, { - name: "2. Establish boundaries from the end of the set when negative boundaries are provided", - preset: true, + name: "2. Establish boundaries from the end of the set when negative boundaries are provided", presetValues: map[string]interface{}{ "ZremRangeByRankKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2667,7 +3003,6 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { }, { name: "2. If key does not exist, return 0", - preset: false, presetValues: nil, command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey3", "2", "4"}, expectedValues: nil, @@ -2675,8 +3010,7 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { expectedError: nil, }, { - name: "3. Return error key is not a sorted set", - preset: true, + name: "3. Return error key is not a sorted set", presetValues: map[string]interface{}{ "ZremRangeByRankKey3": "Default value", }, @@ -2685,13 +3019,11 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey4", "3"}, expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "5. Return error when start index is out of bounds", - preset: true, + name: "5. Return error when start index is out of bounds", presetValues: map[string]interface{}{ "ZremRangeByRankKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2707,8 +3039,7 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { expectedError: errors.New("indices out of bounds"), }, { - name: "6. Return error when end index is out of bounds", - preset: true, + name: "6. Return error when end index is out of bounds", presetValues: map[string]interface{}{ "ZremRangeByRankKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2725,65 +3056,111 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { }, { name: "7. Command too long", - preset: false, command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey7", "4", "5", "8"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZREMRANGEBYRANK, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) } - // Check if the expected values are the same - if test.expectedValues != nil { - for key, expectedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected value at key \"%s\" to be a sorted set, got another type", key) + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if !set.Equals(expectedSet) { - t.Errorf("exptected sorted set %+v, got %+v", expectedSet, set) + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) } } } @@ -2792,9 +3169,14 @@ func Test_HandleZREMRANGEBYRANK(t *testing.T) { } func Test_HandleZREMRANGEBYLEX(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedValues map[string]*sorted_set.SortedSet @@ -2802,8 +3184,7 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { expectedError error }{ { - name: "1. Successfully remove multiple elements with scores inside the provided range", - preset: true, + name: "1. Successfully remove multiple elements with scores inside the provided range", presetValues: map[string]interface{}{ "ZremRangeByLexKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 1}, @@ -2825,8 +3206,7 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { expectedError: nil, }, { - name: "2. Return 0 if the members do not have the same score", - preset: true, + name: "2. Return 0 if the members do not have the same score", presetValues: map[string]interface{}{ "ZremRangeByLexKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 2}, @@ -2851,7 +3231,6 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { }, { name: "3. If key does not exist, return 0", - preset: false, presetValues: nil, command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey3", "2", "4"}, expectedValues: nil, @@ -2859,8 +3238,7 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { expectedError: nil, }, { - name: "3. Return error key is not a sorted set", - preset: true, + name: "3. Return error key is not a sorted set", presetValues: map[string]interface{}{ "ZremRangeByLexKey3": "Default value", }, @@ -2869,71 +3247,116 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { }, { name: "4. Command too short", - preset: false, command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey4", "a"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "5. Command too long", - preset: false, command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey5", "a", "b", "c"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZREMRANGEBYLEX, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, rv.Integer()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - // Check if the expected values are the same - if test.expectedValues != nil { - for key, expectedSet := range test.expectedValues { - if _, err = mockServer.KeyRLock(ctx, key); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, key).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected value at key \"%s\" to be a sorted set, got another type", key) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if !set.Equals(expectedSet) { - t.Errorf("exptected sorted set %+v, got %+v", expectedSet, set) + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) } } } @@ -2942,17 +3365,21 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { } func Test_HandleZRANGE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse [][]string expectedError error }{ { - name: "1. Get elements withing score range without score.", - preset: true, + name: "1. Get elements withing score range without score.", presetValues: map[string]interface{}{ "ZrangeKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2966,8 +3393,7 @@ func Test_HandleZRANGE(t *testing.T) { expectedError: nil, }, { - name: "2. Get elements within score range with score.", - preset: true, + name: "2. Get elements within score range with score.", presetValues: map[string]interface{}{ "ZrangeKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -2985,8 +3411,7 @@ func Test_HandleZRANGE(t *testing.T) { { // 3. Get elements within score range with offset and limit. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "3. Get elements within score range with offset and limit.", - preset: true, + name: "3. Get elements within score range with offset and limit.", presetValues: map[string]interface{}{ "ZrangeKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3003,8 +3428,7 @@ func Test_HandleZRANGE(t *testing.T) { // 4. Get elements within score range with offset and limit + reverse the results. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). // REV reverses the original set before getting the range. - name: "4. Get elements within score range with offset and limit + reverse the results.", - preset: true, + name: "4. Get elements within score range with offset and limit + reverse the results.", presetValues: map[string]interface{}{ "ZrangeKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3018,8 +3442,7 @@ func Test_HandleZRANGE(t *testing.T) { expectedError: nil, }, { - name: "5. Get elements within lex range without score.", - preset: true, + name: "5. Get elements within lex range without score.", presetValues: map[string]interface{}{ "ZrangeKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "e", Score: 1}, @@ -3033,8 +3456,7 @@ func Test_HandleZRANGE(t *testing.T) { expectedError: nil, }, { - name: "6. Get elements within lex range with score.", - preset: true, + name: "6. Get elements within lex range with score.", presetValues: map[string]interface{}{ "ZrangeKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "e", Score: 1}, @@ -3052,8 +3474,7 @@ func Test_HandleZRANGE(t *testing.T) { { // 7. Get elements within lex range with offset and limit. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "7. Get elements within lex range with offset and limit.", - preset: true, + name: "7. Get elements within lex range with offset and limit.", presetValues: map[string]interface{}{ "ZrangeKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 1}, @@ -3070,8 +3491,7 @@ func Test_HandleZRANGE(t *testing.T) { // 8. Get elements within lex range with offset and limit + reverse the results. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). // REV reverses the original set before getting the range. - name: "8. Get elements within lex range with offset and limit + reverse the results.", - preset: true, + name: "8. Get elements within lex range with offset and limit + reverse the results.", presetValues: map[string]interface{}{ "ZrangeKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 1}, @@ -3085,8 +3505,7 @@ func Test_HandleZRANGE(t *testing.T) { expectedError: nil, }, { - name: "9. Return an empty slice when we use BYLEX while elements have different scores", - preset: true, + name: "9. Return an empty slice when we use BYLEX while elements have different scores", presetValues: map[string]interface{}{ "ZrangeKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 5}, @@ -3101,7 +3520,6 @@ func Test_HandleZRANGE(t *testing.T) { }, { name: "10. Throw error when limit does not provide both offset and limit", - preset: false, presetValues: nil, command: []string{"ZRANGE", "ZrangeKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, expectedResponse: [][]string{}, @@ -3109,7 +3527,6 @@ func Test_HandleZRANGE(t *testing.T) { }, { name: "11. Throw error when offset is not a valid integer", - preset: false, presetValues: nil, command: []string{"ZRANGE", "ZrangeKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, expectedResponse: [][]string{}, @@ -3117,7 +3534,6 @@ func Test_HandleZRANGE(t *testing.T) { }, { name: "12. Throw error when limit is not a valid integer", - preset: false, presetValues: nil, command: []string{"ZRANGE", "ZrangeKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, expectedResponse: [][]string{}, @@ -3125,15 +3541,13 @@ func Test_HandleZRANGE(t *testing.T) { }, { name: "13. Throw error when offset is negative", - preset: false, presetValues: nil, command: []string{"ZRANGE", "ZrangeKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, expectedResponse: [][]string{}, expectedError: errors.New("limit offset must be >= 0"), }, { - name: "14. Throw error when the key does not hold a sorted set", - preset: true, + name: "14. Throw error when the key does not hold a sorted set", presetValues: map[string]interface{}{ "ZrangeKey14": "Default value", }, @@ -3143,15 +3557,13 @@ func Test_HandleZRANGE(t *testing.T) { }, { name: "15. Command too short", - preset: false, presetValues: nil, command: []string{"ZRANGE", "ZrangeKey15", "1"}, expectedResponse: [][]string{}, expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "16 Command too long", - preset: false, + name: "16. Command too long", presetValues: nil, command: []string{"ZRANGE", "ZrangeKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, expectedResponse: [][]string{}, @@ -3159,61 +3571,89 @@ func Test_HandleZRANGE(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZRANGE, %d", i)) + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - if test.preset { - for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if len(rv.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(rv.Array())) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } }) @@ -3221,9 +3661,14 @@ func Test_HandleZRANGE(t *testing.T) { } func Test_HandleZRANGESTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -3232,8 +3677,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { expectedError error }{ { - name: "1. Get elements withing score range without score.", - preset: true, + name: "1. Get elements withing score range without score.", presetValues: map[string]interface{}{ "ZrangeStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3252,8 +3696,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { expectedError: nil, }, { - name: "2. Get elements within score range with score.", - preset: true, + name: "2. Get elements within score range with score.", presetValues: map[string]interface{}{ "ZrangeStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3274,8 +3717,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { { // 3. Get elements within score range with offset and limit. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "3. Get elements within score range with offset and limit.", - preset: true, + name: "3. Get elements within score range with offset and limit.", presetValues: map[string]interface{}{ "ZrangeStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3296,8 +3738,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { // 4. Get elements within score range with offset and limit + reverse the results. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). // REV reverses the original set before getting the range. - name: "4. Get elements within score range with offset and limit + reverse the results.", - preset: true, + name: "4. Get elements within score range with offset and limit + reverse the results.", presetValues: map[string]interface{}{ "ZrangeStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3315,8 +3756,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { expectedError: nil, }, { - name: "5. Get elements within lex range without score.", - preset: true, + name: "5. Get elements within lex range without score.", presetValues: map[string]interface{}{ "ZrangeStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "e", Score: 1}, @@ -3335,8 +3775,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { expectedError: nil, }, { - name: "6. Get elements within lex range with score.", - preset: true, + name: "6. Get elements within lex range with score.", presetValues: map[string]interface{}{ "ZrangeStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "e", Score: 1}, @@ -3357,8 +3796,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { { // 7. Get elements within lex range with offset and limit. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "7. Get elements within lex range with offset and limit.", - preset: true, + name: "7. Get elements within lex range with offset and limit.", presetValues: map[string]interface{}{ "ZrangeStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 1}, @@ -3379,8 +3817,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { // 8. Get elements within lex range with offset and limit + reverse the results. // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). // REV reverses the original set before getting the range. - name: "8. Get elements within lex range with offset and limit + reverse the results.", - preset: true, + name: "8. Get elements within lex range with offset and limit + reverse the results.", presetValues: map[string]interface{}{ "ZrangeStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 1}, @@ -3398,8 +3835,7 @@ func Test_HandleZRANGESTORE(t *testing.T) { expectedError: nil, }, { - name: "9. Return an empty slice when we use BYLEX while elements have different scores", - preset: true, + name: "9. Return an empty slice when we use BYLEX while elements have different scores", presetValues: map[string]interface{}{ "ZrangeStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "a", Score: 1}, {Value: "b", Score: 5}, @@ -3416,7 +3852,6 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "10. Throw error when limit does not provide both offset and limit", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey10", "ZrangeStoreKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, expectedResponse: 0, @@ -3424,7 +3859,6 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "11. Throw error when offset is not a valid integer", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey11", "ZrangeStoreKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, expectedResponse: 0, @@ -3432,7 +3866,6 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "12. Throw error when limit is not a valid integer", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey12", "ZrangeStoreKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, expectedResponse: 0, @@ -3440,15 +3873,13 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "13. Throw error when offset is negative", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey13", "ZrangeStoreKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, expectedResponse: 0, expectedError: errors.New("limit offset must be >= 0"), }, { - name: "14. Throw error when the key does not hold a sorted set", - preset: true, + name: "14. Throw error when the key does not hold a sorted set", presetValues: map[string]interface{}{ "ZrangeStoreKey14": "Default value", }, @@ -3458,7 +3889,6 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "15. Command too short", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreKey15", "1"}, expectedResponse: 0, @@ -3466,7 +3896,6 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, { name: "16 Command too long", - preset: false, presetValues: nil, command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey16", "ZrangeStoreKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, expectedResponse: 0, @@ -3474,76 +3903,125 @@ func Test_HandleZRANGESTORE(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZRANGESTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.destination), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, test.destination).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - if !set.Equals(test.expectedValue) { - t.Errorf("expected sorted set %+v, got %+v", test.expectedValue, set) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } } func Test_HandleZINTER(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse [][]string expectedError error }{ { - name: "1. Get the intersection between 2 sorted sets.", - preset: true, + name: "1. Get the intersection between 2 sorted sets.", presetValues: map[string]interface{}{ "ZinterKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3563,8 +4041,7 @@ func Test_HandleZINTER(t *testing.T) { { // 2. Get the intersection between 3 sorted sets with scores. // By default, the SUM aggregate will be used. - name: "2. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "2. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3590,8 +4067,7 @@ func Test_HandleZINTER(t *testing.T) { { // 3. Get the intersection between 3 sorted sets with scores. // Use MIN aggregate. - name: "3. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "3. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3617,8 +4093,7 @@ func Test_HandleZINTER(t *testing.T) { { // 4. Get the intersection between 3 sorted sets with scores. // Use MAX aggregate. - name: "4. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "4. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3644,8 +4119,7 @@ func Test_HandleZINTER(t *testing.T) { { // 5. Get the intersection between 3 sorted sets with scores. // Use SUM aggregate with weights modifier. - name: "5. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "5. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3671,8 +4145,7 @@ func Test_HandleZINTER(t *testing.T) { { // 6. Get the intersection between 3 sorted sets with scores. // Use MAX aggregate with added weights. - name: "6. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "6. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3698,8 +4171,7 @@ func Test_HandleZINTER(t *testing.T) { { // 7. Get the intersection between 3 sorted sets with scores. // Use MIN aggregate with added weights. - name: "7. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "7. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3723,8 +4195,7 @@ func Test_HandleZINTER(t *testing.T) { expectedError: nil, }, { - name: "8. Throw an error if there are more weights than keys", - preset: true, + name: "8. Throw an error if there are more weights than keys", presetValues: map[string]interface{}{ "ZinterKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3739,8 +4210,7 @@ func Test_HandleZINTER(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "9. Throw an error if there are fewer weights than keys", - preset: true, + name: "9. Throw an error if there are fewer weights than keys", presetValues: map[string]interface{}{ "ZinterKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3758,8 +4228,7 @@ func Test_HandleZINTER(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "10. Throw an error if there are no keys provided", - preset: true, + name: "10. Throw an error if there are no keys provided", presetValues: map[string]interface{}{ "ZinterKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), "ZinterKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), @@ -3770,8 +4239,7 @@ func Test_HandleZINTER(t *testing.T) { expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "11. Throw an error if any of the provided keys are not sorted sets", - preset: true, + name: "11. Throw an error if any of the provided keys are not sorted sets", presetValues: map[string]interface{}{ "ZinterKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3787,8 +4255,7 @@ func Test_HandleZINTER(t *testing.T) { expectedError: errors.New("value at ZinterKey30 is not a sorted set"), }, { - name: "12. If any of the keys does not exist, return an empty array.", - preset: true, + name: "12. If any of the keys does not exist, return an empty array.", presetValues: map[string]interface{}{ "ZinterKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3807,65 +4274,95 @@ func Test_HandleZINTER(t *testing.T) { }, { name: "13. Command too short", - preset: false, command: []string{"ZINTER"}, expectedResponse: [][]string{}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZINTER, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } }) @@ -3873,9 +4370,14 @@ func Test_HandleZINTER(t *testing.T) { } func Test_HandleZINTERSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} destination string command []string @@ -3884,8 +4386,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError error }{ { - name: "1. Get the intersection between 2 sorted sets.", - preset: true, + name: "1. Get the intersection between 2 sorted sets.", presetValues: map[string]interface{}{ "ZinterStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3901,8 +4402,8 @@ func Test_HandleZINTERSTORE(t *testing.T) { destination: "ZinterStoreDestinationKey1", command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey1", "ZinterStoreKey1", "ZinterStoreKey2"}, expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, + {Value: "three", Score: 6}, {Value: "four", Score: 8}, + {Value: "five", Score: 10}, }), expectedResponse: 3, expectedError: nil, @@ -3910,8 +4411,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 2. Get the intersection between 3 sorted sets with scores. // By default, the SUM aggregate will be used. - name: "2. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "2. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -3931,9 +4431,11 @@ func Test_HandleZINTERSTORE(t *testing.T) { }), }, destination: "ZinterStoreDestinationKey2", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey2", "ZinterStoreKey3", "ZinterStoreKey4", "ZinterStoreKey5", "WITHSCORES"}, + command: []string{ + "ZINTERSTORE", "ZinterStoreDestinationKey2", "ZinterStoreKey3", "ZinterStoreKey4", "ZinterStoreKey5", "WITHSCORES", + }, expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 24}, + {Value: "one", Score: 3}, {Value: "eight", Score: 24}, }), expectedResponse: 2, expectedError: nil, @@ -3941,8 +4443,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 3. Get the intersection between 3 sorted sets with scores. // Use MIN aggregate. - name: "3. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "3. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -3972,8 +4473,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 4. Get the intersection between 3 sorted sets with scores. // Use MAX aggregate. - name: "4. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "4. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4003,8 +4503,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 5. Get the intersection between 3 sorted sets with scores. // Use SUM aggregate with weights modifier. - name: "5. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "5. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4026,7 +4525,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { destination: "ZinterStoreDestinationKey5", command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey5", "ZinterStoreKey12", "ZinterStoreKey13", "ZinterStoreKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "5", "3"}, expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 2808}, + {Value: "one", Score: 3105}, {Value: "eight", Score: 2808}, }), expectedResponse: 2, expectedError: nil, @@ -4034,8 +4533,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 6. Get the intersection between 3 sorted sets with scores. // Use MAX aggregate with added weights. - name: "6. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "6. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4065,8 +4563,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { { // 7. Get the intersection between 3 sorted sets with scores. // Use MIN aggregate with added weights. - name: "7. Get the intersection between 3 sorted sets with scores.", - preset: true, + name: "7. Get the intersection between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZinterStoreKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4094,8 +4591,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError: nil, }, { - name: "8. Throw an error if there are more weights than keys", - preset: true, + name: "8. Throw an error if there are more weights than keys", presetValues: map[string]interface{}{ "ZinterStoreKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4110,8 +4606,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "9. Throw an error if there are fewer weights than keys", - preset: true, + name: "9. Throw an error if there are fewer weights than keys", presetValues: map[string]interface{}{ "ZinterStoreKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4129,8 +4624,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "10. Throw an error if there are no keys provided", - preset: true, + name: "10. Throw an error if there are no keys provided", presetValues: map[string]interface{}{ "ZinterStoreKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), "ZinterStoreKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), @@ -4141,8 +4635,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "11. Throw an error if any of the provided keys are not sorted sets", - preset: true, + name: "11. Throw an error if any of the provided keys are not sorted sets", presetValues: map[string]interface{}{ "ZinterStoreKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4158,8 +4651,7 @@ func Test_HandleZINTERSTORE(t *testing.T) { expectedError: errors.New("value at ZinterStoreKey30 is not a sorted set"), }, { - name: "12. If any of the keys does not exist, return an empty array.", - preset: true, + name: "12. If any of the keys does not exist, return an empty array.", presetValues: map[string]interface{}{ "ZinterStoreKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4178,85 +4670,131 @@ func Test_HandleZINTERSTORE(t *testing.T) { }, { name: "13. Command too short", - preset: false, command: []string{"ZINTERSTORE"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZINTERSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.destination), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, test.destination).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - for _, elem := range set.GetAll() { - if !test.expectedValue.Contains(elem.Value) { - t.Errorf("could not find element %s in the expected values", elem.Value) - } + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } } func Test_HandleZUNION(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool presetValues map[string]interface{} command []string expectedResponse [][]string expectedError error }{ { - name: "1. Get the union between 2 sorted sets.", - preset: true, + name: "1. Get the union between 2 sorted sets.", presetValues: map[string]interface{}{ "ZunionKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4276,8 +4814,7 @@ func Test_HandleZUNION(t *testing.T) { { // 2. Get the union between 3 sorted sets with scores. // By default, the SUM aggregate will be used. - name: "2. Get the union between 3 sorted sets with scores.", - preset: true, + name: "2. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4307,8 +4844,7 @@ func Test_HandleZUNION(t *testing.T) { { // 3. Get the union between 3 sorted sets with scores. // Use MIN aggregate. - name: "3. Get the union between 3 sorted sets with scores.", - preset: true, + name: "3. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4338,8 +4874,7 @@ func Test_HandleZUNION(t *testing.T) { { // 4. Get the union between 3 sorted sets with scores. // Use MAX aggregate. - name: "4. Get the union between 3 sorted sets with scores.", - preset: true, + name: "4. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4369,8 +4904,7 @@ func Test_HandleZUNION(t *testing.T) { { // 5. Get the union between 3 sorted sets with scores. // Use SUM aggregate with weights modifier. - name: "5. Get the union between 3 sorted sets with scores.", - preset: true, + name: "5. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4400,8 +4934,7 @@ func Test_HandleZUNION(t *testing.T) { { // 6. Get the union between 3 sorted sets with scores. // Use MAX aggregate with added weights. - name: "6. Get the union between 3 sorted sets with scores.", - preset: true, + name: "6. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4431,8 +4964,7 @@ func Test_HandleZUNION(t *testing.T) { { // 7. Get the union between 3 sorted sets with scores. // Use MIN aggregate with added weights. - name: "7. Get the union between 3 sorted sets with scores.", - preset: true, + name: "7. Get the union between 3 sorted sets with scores.", presetValues: map[string]interface{}{ "ZunionKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 100}, {Value: "two", Score: 2}, @@ -4459,8 +4991,7 @@ func Test_HandleZUNION(t *testing.T) { expectedError: nil, }, { - name: "8. Throw an error if there are more weights than keys", - preset: true, + name: "8. Throw an error if there are more weights than keys", presetValues: map[string]interface{}{ "ZunionKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4475,8 +5006,7 @@ func Test_HandleZUNION(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "9. Throw an error if there are fewer weights than keys", - preset: true, + name: "9. Throw an error if there are fewer weights than keys", presetValues: map[string]interface{}{ "ZunionKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4494,8 +5024,7 @@ func Test_HandleZUNION(t *testing.T) { expectedError: errors.New("number of weights should match number of keys"), }, { - name: "10. Throw an error if there are no keys provided", - preset: true, + name: "10. Throw an error if there are no keys provided", presetValues: map[string]interface{}{ "ZunionKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), "ZunionKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), @@ -4506,8 +5035,7 @@ func Test_HandleZUNION(t *testing.T) { expectedError: errors.New(constants.WrongArgsResponse), }, { - name: "11. Throw an error if any of the provided keys are not sorted sets", - preset: true, + name: "11. Throw an error if any of the provided keys are not sorted sets", presetValues: map[string]interface{}{ "ZunionKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4523,8 +5051,7 @@ func Test_HandleZUNION(t *testing.T) { expectedError: errors.New("value at ZunionKey30 is not a sorted set"), }, { - name: "12. If any of the keys does not exist, skip it.", - preset: true, + name: "12. If any of the keys does not exist, skip it.", presetValues: map[string]interface{}{ "ZunionKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, @@ -4546,64 +5073,94 @@ func Test_HandleZUNION(t *testing.T) { }, { name: "13. Command too short", - preset: false, command: []string{"ZUNION"}, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZUNION, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } + } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - for _, element := range rv.Array() { - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - // The current sub-slice is a different length, return false because they're not equal - if len(element.Array()) != len(expected) { - return false + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - for i := 0; i < len(expected); i++ { - if element.Array()[i].String() != expected[i] { - return false + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) } } - return true - }) { - t.Errorf("expected response %+v, got %+v", test.expectedResponse, rv.Array()) } } }) @@ -4611,6 +5168,12 @@ func Test_HandleZUNION(t *testing.T) { } func Test_HandleZUNIONSTORE(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error() + } + client := resp.NewConn(conn) + tests := []struct { name string preset bool @@ -4640,8 +5203,8 @@ func Test_HandleZUNIONSTORE(t *testing.T) { command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey1", "ZunionStoreKey1", "ZunionStoreKey2"}, expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "three", Score: 6}, {Value: "four", Score: 8}, + {Value: "five", Score: 10}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, }), expectedResponse: 8, @@ -4947,7 +5510,7 @@ func Test_HandleZUNIONSTORE(t *testing.T) { command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey12", "non-existent", "ZunionStoreKey32", "ZunionStoreKey33"}, expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, {Value: "twelve", Score: 12}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, {Value: "twelve", Score: 24}, {Value: "thirty-six", Score: 36}, }), expectedResponse: 9, @@ -4962,61 +5525,104 @@ func Test_HandleZUNIONSTORE(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("ZUNIONSTORE, %d", i)) - - if test.preset { + if test.presetValues != nil { + var command []resp.Value + var expected string for key, value := range test.presetValues { - if _, err := mockServer.CreateKeyAndLock(ctx, key); err != nil { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, key, value); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, key) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } return } - if err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.destination), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response integer %d, got %d", test.expectedResponse, rv.Integer()) + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } - if test.expectedValue != nil { - if _, err = mockServer.KeyRLock(ctx, test.destination); err != nil { - t.Error(err) - } - set, ok := mockServer.GetValue(ctx, test.destination).(*sorted_set.SortedSet) - if !ok { - t.Errorf("expected vaule at key %s to be set, got another type", test.destination) + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - for _, elem := range set.GetAll() { - if !test.expectedValue.Contains(elem.Value) { - t.Errorf("could not find element %s in the expected values", elem.Value) - } + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } - mockServer.KeyRUnlock(ctx, test.destination) } }) } diff --git a/internal/modules/sorted_set/key_funcs.go b/internal/modules/sorted_set/key_funcs.go index 8c9a5cbf..05ffff60 100644 --- a/internal/modules/sorted_set/key_funcs.go +++ b/internal/modules/sorted_set/key_funcs.go @@ -135,13 +135,11 @@ func zinterstoreKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse) } endIdx := slices.IndexFunc(cmd[1:], func(s string) bool { - if strings.EqualFold(s, "WEIGHTS") || + return strings.EqualFold(s, "WEIGHTS") || strings.EqualFold(s, "AGGREGATE") || - strings.EqualFold(s, "WITHSCORES") { - return true - } - return false + strings.EqualFold(s, "WITHSCORES") }) + if endIdx == -1 { return internal.KeyExtractionFuncResult{ Channels: make([]string, 0), @@ -149,13 +147,15 @@ func zinterstoreKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) WriteKeys: cmd[1:2], }, nil } + if endIdx >= 3 { return internal.KeyExtractionFuncResult{ Channels: make([]string, 0), - ReadKeys: cmd[2:endIdx], + ReadKeys: cmd[2 : endIdx+1], WriteKeys: cmd[1:2], }, nil } + return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse) } @@ -377,7 +377,7 @@ func zunionstoreKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) if endIdx >= 1 { return internal.KeyExtractionFuncResult{ Channels: make([]string, 0), - ReadKeys: cmd[2:endIdx], + ReadKeys: cmd[2 : endIdx+1], WriteKeys: cmd[1:2], }, nil } diff --git a/internal/modules/string/commands.go b/internal/modules/string/commands.go index b4662282..e933b61e 100644 --- a/internal/modules/string/commands.go +++ b/internal/modules/string/commands.go @@ -28,6 +28,7 @@ func handleSetRange(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.WriteKeys[0] + keyExists := params.KeysExist(keys.WriteKeys)[key] offset, ok := internal.AdaptType(params.Command[2]).(int) if !ok { @@ -36,23 +37,11 @@ func handleSetRange(params internal.HandlerFuncParams) ([]byte, error) { newStr := params.Command[3] - if !params.KeyExists(params.Context, key) { - if _, err = params.CreateKeyAndLock(params.Context, key); err != nil { - return nil, err - } - if err = params.SetValue(params.Context, key, newStr); err != nil { - return nil, err - } - params.KeyUnlock(params.Context, key) + if !keyExists { return []byte(fmt.Sprintf(":%d\r\n", len(newStr))), nil } - if _, err := params.KeyLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyUnlock(params.Context, key) - - str, ok := params.GetValue(params.Context, key).(string) + str, ok := params.GetValues(params.Context, []string{key})[key].(string) if !ok { return nil, fmt.Errorf("value at key %s is not a string", key) } @@ -60,7 +49,7 @@ func handleSetRange(params internal.HandlerFuncParams) ([]byte, error) { // If the offset >= length of the string, append the new string to the old one. if offset >= len(str) { newStr = str + newStr - if err = params.SetValue(params.Context, key, newStr); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: newStr}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", len(newStr))), nil @@ -69,7 +58,7 @@ func handleSetRange(params internal.HandlerFuncParams) ([]byte, error) { // If the offset is < 0, prepend the new string to the old one. if offset < 0 { newStr = newStr + str - if err = params.SetValue(params.Context, key, newStr); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: newStr}); err != nil { return nil, err } return []byte(fmt.Sprintf(":%d\r\n", len(newStr))), nil @@ -89,7 +78,7 @@ func handleSetRange(params internal.HandlerFuncParams) ([]byte, error) { break } - if err = params.SetValue(params.Context, key, string(strRunes)); err != nil { + if err = params.SetValues(params.Context, map[string]interface{}{key: string(strRunes)}); err != nil { return nil, err } @@ -103,17 +92,13 @@ func handleStrLen(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] - if !params.KeyExists(params.Context, key) { + if !keyExists { return []byte(":0\r\n"), nil } - if _, err := params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - value, ok := params.GetValue(params.Context, key).(string) + value, ok := params.GetValues(params.Context, []string{key})[key].(string) if !ok { return nil, fmt.Errorf("value at key %s is not a string", key) @@ -129,6 +114,7 @@ func handleSubStr(params internal.HandlerFuncParams) ([]byte, error) { } key := keys.ReadKeys[0] + keyExists := params.KeysExist(keys.ReadKeys)[key] start, startOk := internal.AdaptType(params.Command[2]).(int) end, endOk := internal.AdaptType(params.Command[3]).(int) @@ -138,16 +124,11 @@ func handleSubStr(params internal.HandlerFuncParams) ([]byte, error) { return nil, errors.New("start and end indices must be integers") } - if !params.KeyExists(params.Context, key) { + if !keyExists { return nil, fmt.Errorf("key %s does not exist", key) } - if _, err = params.KeyRLock(params.Context, key); err != nil { - return nil, err - } - defer params.KeyRUnlock(params.Context, key) - - value, ok := params.GetValue(params.Context, key).(string) + value, ok := params.GetValues(params.Context, []string{key})[key].(string) if !ok { return nil, fmt.Errorf("value at key %s is not a string", key) } diff --git a/internal/modules/string/commands_test.go b/internal/modules/string/commands_test.go index a3930087..6acd90ee 100644 --- a/internal/modules/string/commands_test.go +++ b/internal/modules/string/commands_test.go @@ -15,8 +15,6 @@ package str_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -25,71 +23,44 @@ import ( "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" "net" - "reflect" "strconv" "strings" + "sync" "testing" - "unsafe" ) var mockServer *echovault.EchoVault +var addr = "localhost" +var port int func init() { + port, _ = internal.GetFreePort() mockServer, _ = echovault.NewEchoVault( echovault.WithConfig(config.Config{ + BindAddr: addr, + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } +func Test_HandleSetRange(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) } - return nil -} + client := resp.NewConn(conn) -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - KeyExists: mockServer.KeyExists, - CreateKeyAndLock: mockServer.CreateKeyAndLock, - KeyLock: mockServer.KeyLock, - KeyRLock: mockServer.KeyRLock, - KeyUnlock: mockServer.KeyUnlock, - KeyRUnlock: mockServer.KeyRUnlock, - GetValue: mockServer.GetValue, - SetValue: mockServer.SetValue, - } -} - -func Test_HandleSetRange(t *testing.T) { tests := []struct { name string - preset bool key string presetValue string command []string @@ -99,7 +70,6 @@ func Test_HandleSetRange(t *testing.T) { }{ { name: "Test that SETRANGE on non-existent string creates new string", - preset: false, key: "SetRangeKey1", presetValue: "", command: []string{"SETRANGE", "SetRangeKey1", "10", "New String Value"}, @@ -109,7 +79,6 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "Test SETRANGE with an offset that leads to a longer resulting string", - preset: true, key: "SetRangeKey2", presetValue: "Original String Value", command: []string{"SETRANGE", "SetRangeKey2", "16", "Portion Replaced With This New String"}, @@ -119,7 +88,6 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "SETRANGE with negative offset prepends the string", - preset: true, key: "SetRangeKey3", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey3", "-10", "Prepended "}, @@ -129,7 +97,6 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "SETRANGE with offset that embeds new string inside the old string", - preset: true, key: "SetRangeKey4", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey4", "0", "That"}, @@ -139,7 +106,6 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "SETRANGE with offset longer than original lengths appends the string", - preset: true, key: "SetRangeKey5", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey5", "100", " Appended"}, @@ -149,7 +115,6 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "SETRANGE with offset on the last character replaces last character with new string", - preset: true, key: "SetRangeKey6", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey6", strconv.Itoa(len("This is a preset value") - 1), " replaced"}, @@ -159,14 +124,12 @@ func Test_HandleSetRange(t *testing.T) { }, { name: " Offset not integer", - preset: false, command: []string{"SETRANGE", "key", "offset", "value"}, expectedResponse: 0, expectedError: errors.New("offset must be an integer"), }, { name: "SETRANGE target is not a string", - preset: true, key: "test-int", presetValue: "10", command: []string{"SETRANGE", "test-int", "10", "value"}, @@ -175,81 +138,74 @@ func Test_HandleSetRange(t *testing.T) { }, { name: "Command too short", - preset: false, command: []string{"SETRANGE", "key"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "Command too long", - preset: false, command: []string{"SETRANGE", "key", "offset", "value", "value1"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SETRANGE, %d", i)) - - // If there's a preset step, carry it out here - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, internal.AdaptType(test.presetValue)); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) - } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) - - if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err != nil { + + if err = client.WriteArray(command); err != nil { t.Error(err) } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) - } - // Get the value from the echovault and check against the expected value - if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { - t.Error(err) - } - value, ok := mockServer.GetValue(ctx, test.key).(string) - if !ok { - t.Error("expected string data type, got another type") + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - if value != test.expectedValue { - t.Errorf("expected value \"%s\", got \"%s\"", test.expectedValue, value) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } - mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleStrLen(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue string command []string @@ -258,7 +214,6 @@ func Test_HandleStrLen(t *testing.T) { }{ { name: "Return the correct string length for an existing string", - preset: true, key: "StrLenKey1", presetValue: "Test String", command: []string{"STRLEN", "StrLenKey1"}, @@ -267,7 +222,6 @@ func Test_HandleStrLen(t *testing.T) { }, { name: "If the string does not exist, return 0", - preset: false, key: "StrLenKey2", presetValue: "", command: []string{"STRLEN", "StrLenKey2"}, @@ -276,7 +230,6 @@ func Test_HandleStrLen(t *testing.T) { }, { name: "Too few args", - preset: false, key: "StrLenKey3", presetValue: "", command: []string{"STRLEN"}, @@ -285,7 +238,6 @@ func Test_HandleStrLen(t *testing.T) { }, { name: "Too many args", - preset: false, key: "StrLenKey4", presetValue: "", command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"}, @@ -294,51 +246,62 @@ func Test_HandleStrLen(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("STRLEN, %d", i)) - - if test.preset { - _, err := mockServer.CreateKeyAndLock(ctx, test.key) - if err != nil { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != test.expectedResponse { - t.Errorf("expected respons \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } }) } } func Test_HandleSubStr(t *testing.T) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + t.Error(err) + } + client := resp.NewConn(conn) + tests := []struct { name string - preset bool key string presetValue string command []string @@ -347,7 +310,6 @@ func Test_HandleSubStr(t *testing.T) { }{ { name: "Return substring within the range of the string", - preset: true, key: "SubStrKey1", presetValue: "Test String One", command: []string{"SUBSTR", "SubStrKey1", "5", "10"}, @@ -356,7 +318,6 @@ func Test_HandleSubStr(t *testing.T) { }, { name: "Return substring at the end of the string with exact end index", - preset: true, key: "SubStrKey2", presetValue: "Test String Two", command: []string{"SUBSTR", "SubStrKey2", "12", "14"}, @@ -365,7 +326,6 @@ func Test_HandleSubStr(t *testing.T) { }, { name: "Return substring at the end of the string with end index greater than length", - preset: true, key: "SubStrKey3", presetValue: "Test String Three", command: []string{"SUBSTR", "SubStrKey3", "12", "75"}, @@ -374,7 +334,6 @@ func Test_HandleSubStr(t *testing.T) { }, { name: "Return the substring at the start of the string with 0 start index", - preset: true, key: "SubStrKey4", presetValue: "Test String Four", command: []string{"SUBSTR", "SubStrKey4", "0", "3"}, @@ -385,7 +344,6 @@ func Test_HandleSubStr(t *testing.T) { // Return the substring with negative start index. // Substring should begin abs(start) from the end of the string when start is negative. name: "Return the substring with negative start index", - preset: true, key: "SubStrKey5", presetValue: "Test String Five", command: []string{"SUBSTR", "SubStrKey5", "-11", "10"}, @@ -396,7 +354,6 @@ func Test_HandleSubStr(t *testing.T) { // Return reverse substring with end index smaller than start index. // When end index is smaller than start index, the 2 indices are reversed. name: "Return reverse substring with end index smaller than start index", - preset: true, key: "SubStrKey6", presetValue: "Test String Six", command: []string{"SUBSTR", "SubStrKey6", "4", "0"}, @@ -430,42 +387,48 @@ func Test_HandleSubStr(t *testing.T) { }, } - for i, test := range tests { + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SUBSTR, %d", i)) - - if test.preset { - if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { t.Error(err) } - if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { + res, _, err := client.ReadValue() + if err != nil { t.Error(err) } - mockServer.KeyUnlock(ctx, test.key) + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } - handler := getHandler(test.command[0]) - if handler == nil { - t.Errorf("no handler found for command %s", test.command[0]) - return + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } if test.expectedError != nil { - if err.Error() != test.expectedError.Error() { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } - rd := resp.NewReader(bytes.NewBuffer(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, rv.String()) + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } }) } diff --git a/internal/raft/fsm.go b/internal/raft/fsm.go index d736a13f..8e9a0cc4 100644 --- a/internal/raft/fsm.go +++ b/internal/raft/fsm.go @@ -32,11 +32,9 @@ type FSMOpts struct { Config config.Config GetState func() map[string]internal.KeyData GetCommand func(command string) (internal.Command, error) - CreateKeyAndLock func(ctx context.Context, key string) (bool, error) - SetValue func(ctx context.Context, key string, value interface{}) error + SetValues func(ctx context.Context, entries map[string]interface{}) error SetExpiry func(ctx context.Context, key string, expire time.Time, touch bool) - KeyUnlock func(ctx context.Context, key string) - DeleteKey func(ctx context.Context, key string) error + DeleteKey func(key string) error StartSnapshot func() FinishSnapshot func() SetLatestSnapshotTime func(msec int64) @@ -79,7 +77,7 @@ func (fsm *FSM) Apply(log *raft.Log) interface{} { } case "delete-key": - if err := fsm.options.DeleteKey(ctx, request.Key); err != nil { + if err := fsm.options.DeleteKey(request.Key); err != nil { return internal.ApplyResponse{ Error: err, Response: nil, @@ -164,14 +162,10 @@ func (fsm *FSM) Restore(snapshot io.ReadCloser) error { // Set state ctx := context.Background() for k, v := range internal.FilterExpiredKeys(time.Now(), data.State) { - if _, err = fsm.options.CreateKeyAndLock(ctx, k); err != nil { - log.Fatal(err) - } - if err = fsm.options.SetValue(ctx, k, v.Value); err != nil { + if err = fsm.options.SetValues(ctx, map[string]interface{}{k: v.Value}); err != nil { log.Fatal(err) } fsm.options.SetExpiry(ctx, k, v.ExpireAt, false) - fsm.options.KeyUnlock(ctx, k) } // Set latest snapshot milliseconds fsm.options.SetLatestSnapshotTime(data.LatestSnapshotMilliseconds) diff --git a/internal/raft/raft.go b/internal/raft/raft.go index 725446f9..b900fe28 100644 --- a/internal/raft/raft.go +++ b/internal/raft/raft.go @@ -33,13 +33,11 @@ import ( type Opts struct { Config config.Config - CreateKeyAndLock func(ctx context.Context, key string) (bool, error) - SetValue func(ctx context.Context, key string, value interface{}) error + SetValues func(ctx context.Context, entries map[string]interface{}) error SetExpiry func(ctx context.Context, key string, expire time.Time, touch bool) - KeyUnlock func(ctx context.Context, key string) GetState func() map[string]internal.KeyData GetCommand func(command string) (internal.Command, error) - DeleteKey func(ctx context.Context, key string) error + DeleteKey func(key string) error StartSnapshot func() FinishSnapshot func() SetLatestSnapshotTime func(msec int64) @@ -118,10 +116,8 @@ func (r *Raft) RaftInit(ctx context.Context) { Config: r.options.Config, GetState: r.options.GetState, GetCommand: r.options.GetCommand, - CreateKeyAndLock: r.options.CreateKeyAndLock, - SetValue: r.options.SetValue, + SetValues: r.options.SetValues, SetExpiry: r.options.SetExpiry, - KeyUnlock: r.options.KeyUnlock, DeleteKey: r.options.DeleteKey, StartSnapshot: r.options.StartSnapshot, FinishSnapshot: r.options.FinishSnapshot, @@ -226,6 +222,6 @@ func (r *Raft) RaftShutdown() { if err != nil { log.Fatal(err) } - fmt.Println("Leadership transfer successful.") + log.Println("Leadership transfer successful.") } } diff --git a/internal/types.go b/internal/types.go index d17e5c7e..4402acd6 100644 --- a/internal/types.go +++ b/internal/types.go @@ -59,18 +59,12 @@ type HandlerFuncParams struct { Context context.Context Command []string Connection *net.Conn - KeyLock func(ctx context.Context, key string) (bool, error) - KeyUnlock func(ctx context.Context, key string) - KeyRLock func(ctx context.Context, key string) (bool, error) - KeyRUnlock func(ctx context.Context, key string) - KeyExists func(ctx context.Context, key string) bool - CreateKeyAndLock func(ctx context.Context, key string) (bool, error) - GetValue func(ctx context.Context, key string) interface{} - SetValue func(ctx context.Context, key string, value interface{}) error - GetExpiry func(ctx context.Context, key string) time.Time + KeysExist func(keys []string) map[string]bool + GetExpiry func(key string) time.Time + DeleteKey func(key string) error + GetValues func(ctx context.Context, keys []string) map[string]interface{} + SetValues func(ctx context.Context, entries map[string]interface{}) error SetExpiry func(ctx context.Context, key string, expire time.Time, touch bool) - RemoveExpiry func(ctx context.Context, key string) - DeleteKey func(ctx context.Context, key string) error GetClock func() clock.Clock GetAllCommands func() []Command GetACL func() interface{} diff --git a/volumes/modules/module_get/module_get.go b/volumes/modules/module_get/module_get.go index 8e10f1ef..7c0d4b60 100644 --- a/volumes/modules/module_get/module_get.go +++ b/volumes/modules/module_get/module_get.go @@ -40,14 +40,9 @@ func KeyExtractionFunc(cmd []string, args ...string) ([]string, []string, error) func HandlerFunc( ctx context.Context, command []string, - keyExists func(ctx context.Context, key string) bool, - keyLock func(ctx context.Context, key string) (bool, error), - keyUnlock func(ctx context.Context, key string), - keyRLock func(ctx context.Context, key string) (bool, error), - keyRUnlock func(ctx context.Context, key string), - createKeyAndLock func(ctx context.Context, key string) (bool, error), - getValue func(ctx context.Context, key string) interface{}, - setValue func(ctx context.Context, key string, value interface{}) error, + keysExist func(keys []string) map[string]bool, + getValues func(ctx context.Context, keys []string) map[string]interface{}, + setValues func(ctx context.Context, entries map[string]interface{}) error, args ...string) ([]byte, error) { readKeys, _, err := KeyExtractionFunc(command, args...) @@ -55,18 +50,13 @@ func HandlerFunc( return nil, err } key := readKeys[0] + exists := keysExist(readKeys)[key] - if !keyExists(ctx, key) { + if !exists { return []byte(":0\r\n"), nil } - _, err = keyRLock(ctx, key) - if err != nil { - return nil, err - } - defer keyRUnlock(ctx, key) - - val, ok := getValue(ctx, key).(int64) + val, ok := getValues(ctx, []string{key})[key].(int64) if !ok { return nil, fmt.Errorf("value at key %s is not an integer", key) } diff --git a/volumes/modules/module_set/module_set.go b/volumes/modules/module_set/module_set.go index 87bdd111..1fd2333a 100644 --- a/volumes/modules/module_set/module_set.go +++ b/volumes/modules/module_set/module_set.go @@ -40,14 +40,9 @@ func KeyExtractionFunc(cmd []string, args ...string) ([]string, []string, error) func HandlerFunc( ctx context.Context, command []string, - keyExists func(ctx context.Context, key string) bool, - keyLock func(ctx context.Context, key string) (bool, error), - keyUnlock func(ctx context.Context, key string), - keyRLock func(ctx context.Context, key string) (bool, error), - keyRUnlock func(ctx context.Context, key string), - createKeyAndLock func(ctx context.Context, key string) (bool, error), - getValue func(ctx context.Context, key string) interface{}, - setValue func(ctx context.Context, key string, value interface{}) error, + keysExist func(keys []string) map[string]bool, + getValues func(ctx context.Context, keys []string) map[string]interface{}, + setValues func(ctx context.Context, entries map[string]interface{}) error, args ...string) ([]byte, error) { _, writeKeys, err := KeyExtractionFunc(command, args...) @@ -56,25 +51,12 @@ func HandlerFunc( } key := writeKeys[0] - if !keyExists(ctx, key) { - _, err := createKeyAndLock(ctx, key) - if err != nil { - return nil, err - } - } else { - _, err := keyLock(ctx, key) - if err != nil { - return nil, err - } - } - defer keyUnlock(ctx, key) - value, err := strconv.ParseInt(command[2], 10, 64) if err != nil { return nil, err } - err = setValue(ctx, key, value) + err = setValues(ctx, map[string]interface{}{key: value}) if err != nil { return nil, err }